use crate ::lightwallet ::LightWallet ;
use log ::{ info , warn , error } ;
use rand ::{ rngs ::OsRng , seq ::SliceRandom } ;
use std ::sync ::{ Arc , RwLock } ;
use std ::sync ::atomic ::{ AtomicU64 , AtomicI32 , AtomicUsize , Ordering } ;
use std ::path ::{ Path , PathBuf } ;
use std ::fs ::File ;
use std ::collections ::HashMap ;
use std ::io ;
use std ::io ::prelude ::* ;
use std ::io ::{ BufReader , BufWriter , Error , ErrorKind } ;
use protobuf ::parse_from_bytes ;
use json ::{ object , array , JsonValue } ;
use zcash_primitives ::transaction ::{ TxId , Transaction } ;
use zcash_client_backend ::{
constants ::testnet , constants ::mainnet , constants ::regtest , encoding ::encode_payment_address ,
} ;
use crate ::grpc_client ::{ BlockId } ;
use crate ::grpcconnector ::{ self , * } ;
use crate ::SaplingParams ;
use crate ::ANCHOR_OFFSET ;
pub const DEFAULT_SERVER : & str = "https://" ;
pub const WALLET_NAME : & str = "silentdragonlite-cli-wallet.dat" ;
pub const LOGFILE_NAME : & str = "silentdragonlite-cli-wallet.debug.log" ;
#[ derive(Clone, Debug) ]
pub struct LightClientConfig {
pub server : http ::Uri ,
pub chain_name : String ,
pub sapling_activation_height : u64 ,
pub consensus_branch_id : String ,
pub anchor_offset : u32 ,
pub no_cert_verification : bool ,
pub data_dir : Option < String >
}
impl LightClientConfig {
// Create an unconnected (to any server) config to test for local wallet etc...
pub fn create_unconnected ( chain_name : String , dir : Option < String > ) -> LightClientConfig {
LightClientConfig {
server : http ::Uri ::default ( ) ,
chain_name : chain_name ,
sapling_activation_height : 0 ,
consensus_branch_id : "" . to_string ( ) ,
anchor_offset : ANCHOR_OFFSET ,
no_cert_verification : false ,
data_dir : dir ,
}
}
pub fn create ( server : http ::Uri , dangerous : bool ) -> io ::Result < ( LightClientConfig , u64 ) > {
// Do a getinfo first, before opening the wallet
let info = grpcconnector ::get_info ( server . clone ( ) , dangerous )
. map_err ( | e | std ::io ::Error ::new ( ErrorKind ::ConnectionRefused , e ) ) ? ;
// Create a Light Client Config
let config = LightClientConfig {
server ,
chain_name : info . chain_name ,
sapling_activation_height : info . sapling_activation_height ,
consensus_branch_id : info . consensus_branch_id ,
anchor_offset : ANCHOR_OFFSET ,
no_cert_verification : dangerous ,
data_dir : None ,
} ;
Ok ( ( config , info . block_height ) )
}
pub fn get_zcash_data_path ( & self ) -> Box < Path > {
let mut zcash_data_location ;
if self . data_dir . is_some ( ) {
zcash_data_location = PathBuf ::from ( & self . data_dir . as_ref ( ) . unwrap ( ) ) ;
} else {
if cfg ! ( target_os = "macos" ) | | cfg ! ( target_os = "windows" ) {
zcash_data_location = dirs ::data_dir ( ) . expect ( "Couldn't determine app data directory!" ) ;
zcash_data_location . push ( "HUSH3" ) ;
} else {
zcash_data_location = dirs ::home_dir ( ) . expect ( "Couldn't determine home directory!" ) ;
zcash_data_location . push ( ".komodo/HUSH3/" ) ;
} ;
match & self . chain_name [ . . ] {
"main" = > { } ,
"test" = > zcash_data_location . push ( "testnet3" ) ,
"regtest" = > zcash_data_location . push ( "regtest" ) ,
c = > panic ! ( "Unknown chain {}" , c ) ,
} ;
}
zcash_data_location . into_boxed_path ( )
}
pub fn get_wallet_path ( & self ) -> Box < Path > {
let mut wallet_location = self . get_zcash_data_path ( ) . into_path_buf ( ) ;
wallet_location . push ( WALLET_NAME ) ;
wallet_location . into_boxed_path ( )
}
pub fn wallet_exists ( & self ) -> bool {
return self . get_wallet_path ( ) . exists ( )
}
pub fn get_log_path ( & self ) -> Box < Path > {
let mut log_path = self . get_zcash_data_path ( ) . into_path_buf ( ) ;
log_path . push ( LOGFILE_NAME ) ;
log_path . into_boxed_path ( )
}
pub fn get_initial_state ( & self ) -> Option < ( u64 , & str , & str ) > {
match & self . chain_name [ . . ] {
"test" = > Some ( ( 105942 ,
"00000001c0199f329ee03379bf1387856dbab23765da508bf9b9d8d544f212c0" ,
""
) ) ,
"main" = > Some ( ( 105944 ,
"0000000313b0ec7c5a1e9b997ce44a7763b56c5505526c36634a004ed52d7787" ,
""
) ) ,
_ = > None
}
}
pub fn get_server_or_default ( server : Option < String > ) -> http ::Uri {
match server {
Some ( s ) = > {
let mut s = if s . starts_with ( "http" ) { s } else { "http://" . to_string ( ) + & s } ;
let uri : http ::Uri = s . parse ( ) . unwrap ( ) ;
if uri . port_part ( ) . is_none ( ) {
s = s + ":443" ;
}
s
}
None = > DEFAULT_SERVER . to_string ( )
} . parse ( ) . unwrap ( )
}
pub fn get_coin_type ( & self ) -> u32 {
match & self . chain_name [ . . ] {
"main" = > mainnet ::COIN_TYPE ,
"test" = > testnet ::COIN_TYPE ,
"regtest" = > regtest ::COIN_TYPE ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
pub fn hrp_sapling_address ( & self ) -> & str {
match & self . chain_name [ . . ] {
"main" = > mainnet ::HRP_SAPLING_PAYMENT_ADDRESS ,
"test" = > testnet ::HRP_SAPLING_PAYMENT_ADDRESS ,
"regtest" = > regtest ::HRP_SAPLING_PAYMENT_ADDRESS ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
pub fn hrp_sapling_private_key ( & self ) -> & str {
match & self . chain_name [ . . ] {
"main" = > mainnet ::HRP_SAPLING_EXTENDED_SPENDING_KEY ,
"test" = > testnet ::HRP_SAPLING_EXTENDED_SPENDING_KEY ,
"regtest" = > regtest ::HRP_SAPLING_EXTENDED_SPENDING_KEY ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
pub fn base58_pubkey_address ( & self ) -> [ u8 ; 1 ] {
match & self . chain_name [ . . ] {
"main" = > mainnet ::B58_PUBKEY_ADDRESS_PREFIX ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
pub fn base58_script_address ( & self ) -> [ u8 ; 1 ] {
match & self . chain_name [ . . ] {
"main" = > mainnet ::B58_SCRIPT_ADDRESS_PREFIX ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
pub fn base58_secretkey_prefix ( & self ) -> [ u8 ; 1 ] {
match & self . chain_name [ . . ] {
"main" = > [ 0x80 ] ,
"test" = > [ 0xEF ] ,
"regtest" = > [ 0xEF ] ,
c = > panic ! ( "Unknown chain {}" , c )
}
}
}
pub struct LightClient {
pub wallet : Arc < RwLock < LightWallet > > ,
pub config : LightClientConfig ,
// zcash-params
pub sapling_output : Vec < u8 > ,
pub sapling_spend : Vec < u8 > ,
}
impl LightClient {
pub fn set_wallet_initial_state ( & self ) {
use std ::convert ::TryInto ;
let state = self . config . get_initial_state ( ) ;
match state {
Some ( ( height , hash , tree ) ) = > self . wallet . read ( ) . unwrap ( ) . set_initial_block ( height . try_into ( ) . unwrap ( ) , hash , tree ) ,
_ = > true ,
} ;
}
fn read_sapling_params ( & mut self ) {
// Read Sapling Params
self . sapling_output . extend_from_slice ( SaplingParams ::get ( "sapling-output.params" ) . unwrap ( ) . as_ref ( ) ) ;
self . sapling_spend . extend_from_slice ( SaplingParams ::get ( "sapling-spend.params" ) . unwrap ( ) . as_ref ( ) ) ;
}
/// Method to create a test-only version of the LightClient
#[ allow(dead_code) ]
fn unconnected ( seed_phrase : String , dir : Option < String > ) -> io ::Result < Self > {
let config = LightClientConfig ::create_unconnected ( "test" . to_string ( ) , dir ) ;
let mut l = LightClient {
wallet : Arc ::new ( RwLock ::new ( LightWallet ::new ( Some ( seed_phrase ) , & config , 0 ) ? ) ) ,
config : config . clone ( ) ,
sapling_output : vec ! [ ] ,
sapling_spend : vec ! [ ]
} ;
l . set_wallet_initial_state ( ) ;
l . read_sapling_params ( ) ;
info ! ( "Created new wallet!" ) ;
info ! ( "Created LightClient to {}" , & config . server ) ;
Ok ( l )
}
/// Create a brand new wallet with a new seed phrase. Will fail if a wallet file
/// already exists on disk
pub fn new ( config : & LightClientConfig , latest_block : u64 ) -> io ::Result < Self > {
if config . wallet_exists ( ) {
return Err ( Error ::new ( ErrorKind ::AlreadyExists ,
"Cannot create a new wallet from seed, because a wallet already exists" ) ) ;
}
let mut l = LightClient {
wallet : Arc ::new ( RwLock ::new ( LightWallet ::new ( None , config , latest_block ) ? ) ) ,
config : config . clone ( ) ,
sapling_output : vec ! [ ] ,
sapling_spend : vec ! [ ]
} ;
l . set_wallet_initial_state ( ) ;
l . read_sapling_params ( ) ;
info ! ( "Created new wallet with a new seed!" ) ;
info ! ( "Created LightClient to {}" , & config . server ) ;
Ok ( l )
}
pub fn new_from_phrase ( seed_phrase : String , config : & LightClientConfig , latest_block : u64 ) -> io ::Result < Self > {
if config . wallet_exists ( ) {
return Err ( Error ::new ( ErrorKind ::AlreadyExists ,
"Cannot create a new wallet from seed, because a wallet already exists" ) ) ;
}
let mut l = LightClient {
wallet : Arc ::new ( RwLock ::new ( LightWallet ::new ( Some ( seed_phrase ) , config , latest_block ) ? ) ) ,
config : config . clone ( ) ,
sapling_output : vec ! [ ] ,
sapling_spend : vec ! [ ]
} ;
l . set_wallet_initial_state ( ) ;
l . read_sapling_params ( ) ;
info ! ( "Created new wallet!" ) ;
info ! ( "Created LightClient to {}" , & config . server ) ;
Ok ( l )
}
pub fn read_from_disk ( config : & LightClientConfig ) -> io ::Result < Self > {
if ! config . wallet_exists ( ) {
return Err ( Error ::new ( ErrorKind ::AlreadyExists ,
format ! ( "Cannot read wallet. No file at {}" , config . get_wallet_path ( ) . display ( ) ) ) ) ;
}
let mut file_buffer = BufReader ::new ( File ::open ( config . get_wallet_path ( ) ) ? ) ;
let wallet = LightWallet ::read ( & mut file_buffer , config ) ? ;
let mut lc = LightClient {
wallet : Arc ::new ( RwLock ::new ( wallet ) ) ,
config : config . clone ( ) ,
sapling_output : vec ! [ ] ,
sapling_spend : vec ! [ ]
} ;
lc . read_sapling_params ( ) ;
info ! ( "Read wallet with birthday {}" , lc . wallet . read ( ) . unwrap ( ) . get_first_tx_block ( ) ) ;
info ! ( "Created LightClient to {}" , & config . server ) ;
if crate ::lightwallet ::bugs ::BugBip39Derivation ::has_bug ( & lc ) {
let m = format ! ( "WARNING!!!\nYour wallet has a bip39derivation bug that's showing incorrect addresses.\nPlease run 'fixbip39bug' to automatically fix the address derivation in your wallet!\nPlease see: https://github.com/adityapk00/silentdragonlite-light-cli/blob/master/bip39bug.md" ) ;
info ! ( "{}" , m ) ;
println ! ( "{}" , m ) ;
}
Ok ( lc )
}
pub fn attempt_recover_seed ( config : & LightClientConfig ) -> Result < String , String > {
use std ::io ::prelude ::* ;
use byteorder ::{ LittleEndian , ReadBytesExt , } ;
use bip39 ::{ Mnemonic , Language } ;
use zcash_primitives ::serialize ::Vector ;
let mut reader = BufReader ::new ( File ::open ( config . get_wallet_path ( ) ) . unwrap ( ) ) ;
let version = reader . read_u64 ::< LittleEndian > ( ) . unwrap ( ) ;
println ! ( "Reading wallet version {}" , version ) ;
let encrypted = if version > = 4 {
reader . read_u8 ( ) . unwrap ( ) > 0
} else {
false
} ;
if encrypted {
return Err ( "The wallet is encrypted!" . to_string ( ) ) ;
}
let mut enc_seed = [ 0 u8 ; 48 ] ;
if version > = 4 {
reader . read_exact ( & mut enc_seed ) . unwrap ( ) ;
}
let _nonce = if version > = 4 {
Vector ::read ( & mut reader , | r | r . read_u8 ( ) ) . unwrap ( )
} else {
vec ! [ ]
} ;
// Seed
let mut seed_bytes = [ 0 u8 ; 32 ] ;
reader . read_exact ( & mut seed_bytes ) . unwrap ( ) ;
let phrase = Mnemonic ::from_entropy ( & seed_bytes , Language ::English , ) . unwrap ( ) . phrase ( ) . to_string ( ) ;
Ok ( phrase )
}
pub fn last_scanned_height ( & self ) -> u64 {
self . wallet . read ( ) . unwrap ( ) . last_scanned_height ( ) as u64
}
// Export private keys
pub fn do_export ( & self , addr : Option < String > ) -> Result < JsonValue , & str > {
if ! self . wallet . read ( ) . unwrap ( ) . is_unlocked_for_spending ( ) {
error ! ( "Wallet is locked" ) ;
return Err ( "Wallet is locked" ) ;
}
// Clone address so it can be moved into the closure
let address = addr . clone ( ) ;
let wallet = self . wallet . read ( ) . unwrap ( ) ;
// Go over all z addresses
let z_keys = wallet . get_z_private_keys ( ) . iter ( )
. filter ( move | ( addr , _ ) | address . is_none ( ) | | address . as_ref ( ) = = Some ( addr ) )
. map ( | ( addr , pk ) |
object ! {
"address" = > addr . clone ( ) ,
"private_key" = > pk . clone ( )
}
) . collect ::< Vec < JsonValue > > ( ) ;
// Clone address so it can be moved into the closure
let address = addr . clone ( ) ;
// Go over all t addresses
let t_keys = wallet . get_t_secret_keys ( ) . iter ( )
. filter ( move | ( addr , _ ) | address . is_none ( ) | | address . as_ref ( ) = = Some ( addr ) )
. map ( | ( addr , sk ) |
object ! {
"address" = > addr . clone ( ) ,
"private_key" = > sk . clone ( ) ,
}
) . collect ::< Vec < JsonValue > > ( ) ;
let mut all_keys = vec ! [ ] ;
all_keys . extend_from_slice ( & z_keys ) ;
all_keys . extend_from_slice ( & t_keys ) ;
Ok ( all_keys . into ( ) )
}
pub fn do_address ( & self ) -> JsonValue {
let wallet = self . wallet . read ( ) . unwrap ( ) ;
// Collect z addresses
let z_addresses = wallet . zaddress . read ( ) . unwrap ( ) . iter ( ) . map ( | ad | {
encode_payment_address ( self . config . hrp_sapling_address ( ) , & ad )
} ) . collect ::< Vec < String > > ( ) ;
// Collect t addresses
let t_addresses = wallet . taddresses . read ( ) . unwrap ( ) . iter ( ) . map ( | a | a . clone ( ) )
. collect ::< Vec < String > > ( ) ;
object ! {
"z_addresses" = > z_addresses ,
"t_addresses" = > t_addresses ,
}
}
pub fn do_balance ( & self ) -> JsonValue {
let wallet = self . wallet . read ( ) . unwrap ( ) ;
// Collect z addresses
let z_addresses = wallet . zaddress . read ( ) . unwrap ( ) . iter ( ) . map ( | ad | {
let address = encode_payment_address ( self . config . hrp_sapling_address ( ) , & ad ) ;
object ! {
"address" = > address . clone ( ) ,
"zbalance" = > wallet . zbalance ( Some ( address . clone ( ) ) ) ,
"verified_zbalance" = > wallet . verified_zbalance ( Some ( address ) ) ,
}
} ) . collect ::< Vec < JsonValue > > ( ) ;
// Collect t addresses
let t_addresses = wallet . taddresses . read ( ) . unwrap ( ) . iter ( ) . map ( | address | {
// Get the balance for this address
let balance = wallet . tbalance ( Some ( address . clone ( ) ) ) ;
object ! {
"address" = > address . clone ( ) ,
"balance" = > balance ,
}
} ) . collect ::< Vec < JsonValue > > ( ) ;
object ! {
"zbalance" = > wallet . zbalance ( None ) ,
"verified_zbalance" = > wallet . verified_zbalance ( None ) ,
"tbalance" = > wallet . tbalance ( None ) ,
"z_addresses" = > z_addresses ,
"t_addresses" = > t_addresses ,
}
}
pub fn do_save ( & self ) -> Result < ( ) , String > {
// If the wallet is encrypted but unlocked, lock it again.
{
let mut wallet = self . wallet . write ( ) . unwrap ( ) ;
if wallet . is_encrypted ( ) & & wallet . is_unlocked_for_spending ( ) {
match wallet . lock ( ) {
Ok ( _ ) = > { } ,
Err ( e ) = > {
let err = format ! ( "ERR: {}" , e ) ;
error ! ( "{}" , err ) ;
return Err ( e . to_string ( ) ) ;
}
}
}
}
let mut file_buffer = BufWriter ::with_capacity (
1_000_000 , // 1 MB write buffer
File ::create ( self . config . get_wallet_path ( ) ) . unwrap ( ) ) ;
match self . wallet . write ( ) . unwrap ( ) . write ( & mut file_buffer ) {
Ok ( _ ) = > Ok ( ( ) ) ,
Err ( e ) = > {
let err = format ! ( "ERR: {}" , e ) ;
error ! ( "{}" , err ) ;
Err ( e . to_string ( ) )
}
}
}
pub fn get_server_uri ( & self ) -> http ::Uri {
self . config . server . clone ( )
}
pub fn do_info ( & self ) -> String {
match get_info ( self . get_server_uri ( ) , self . config . no_cert_verification ) {
Ok ( i ) = > {
let o = object ! {
"version" = > i . version ,
"vendor" = > i . vendor ,
"taddr_support" = > i . taddr_support ,
"chain_name" = > i . chain_name ,
"sapling_activation_height" = > i . sapling_activation_height ,
"consensus_branch_id" = > i . consensus_branch_id ,
"latest_block_height" = > i . block_height
} ;
o . pretty ( 2 )
} ,
Err ( e ) = > e
}
}
pub fn do_seed_phrase ( & self ) -> Result < JsonValue , & str > {
if ! self . wallet . read ( ) . unwrap ( ) . is_unlocked_for_spending ( ) {
error ! ( "Wallet is locked" ) ;
return Err ( "Wallet is locked" ) ;
}
let wallet = self . wallet . read ( ) . unwrap ( ) ;
Ok ( object ! {
"seed" = > wallet . get_seed_phrase ( ) ,
"birthday" = > wallet . get_birthday ( )
} )
}
// Return a list of all notes, spent and unspent
pub fn do_list_notes ( & self , all_notes : bool ) -> JsonValue {
let mut unspent_notes : Vec < JsonValue > = vec ! [ ] ;
let mut spent_notes : Vec < JsonValue > = vec ! [ ] ;
let mut pending_notes : Vec < JsonValue > = vec ! [ ] ;
{
// Collect Sapling notes
let wallet = self . wallet . read ( ) . unwrap ( ) ;
wallet . txs . read ( ) . unwrap ( ) . iter ( )
. flat_map ( | ( txid , wtx ) | {
wtx . notes . iter ( ) . filter_map ( move | nd |
if ! all_notes & & nd . spent . is_some ( ) {
None
} else {
Some ( object ! {
"created_in_block" = > wtx . block ,
"datetime" = > wtx . datetime ,
"created_in_txid" = > format ! ( "{}" , txid ) ,
"value" = > nd . note . value ,
"is_change" = > nd . is_change ,
"address" = > LightWallet ::note_address ( self . config . hrp_sapling_address ( ) , nd ) ,
"spent" = > nd . spent . map ( | spent_txid | format ! ( "{}" , spent_txid ) ) ,
"unconfirmed_spent" = > nd . unconfirmed_spent . map ( | spent_txid | format ! ( "{}" , spent_txid ) ) ,
} )
}
)
} )
. for_each ( | note | {
if note [ "spent" ] . is_null ( ) & & note [ "unconfirmed_spent" ] . is_null ( ) {
unspent_notes . push ( note ) ;
} else if ! note [ "spent" ] . is_null ( ) {
spent_notes . push ( note ) ;
} else {
pending_notes . push ( note ) ;
}
} ) ;
}
let mut unspent_utxos : Vec < JsonValue > = vec ! [ ] ;
let mut spent_utxos : Vec < JsonValue > = vec ! [ ] ;
let mut pending_utxos : Vec < JsonValue > = vec ! [ ] ;
{
let wallet = self . wallet . read ( ) . unwrap ( ) ;
wallet . txs . read ( ) . unwrap ( ) . iter ( )
. flat_map ( | ( txid , wtx ) | {
wtx . utxos . iter ( ) . filter_map ( move | utxo |
if ! all_notes & & utxo . spent . is_some ( ) {
None
} else {
Some ( object ! {
"created_in_block" = > wtx . block ,
"datetime" = > wtx . datetime ,
"created_in_txid" = > format ! ( "{}" , txid ) ,
"value" = > utxo . value ,
"scriptkey" = > hex ::encode ( utxo . script . clone ( ) ) ,
"is_change" = > false , // TODO: Identify notes as change if we send change to taddrs
"address" = > utxo . address . clone ( ) ,
"spent" = > utxo . spent . map ( | spent_txid | format ! ( "{}" , spent_txid ) ) ,
"unconfirmed_spent" = > utxo . unconfirmed_spent . map ( | spent_txid | format ! ( "{}" , spent_txid ) ) ,
} )
}
)
} )
. for_each ( | utxo | {
if utxo [ "spent" ] . is_null ( ) & & utxo [ "unconfirmed_spent" ] . is_null ( ) {
unspent_utxos . push ( utxo ) ;
} else if ! utxo [ "spent" ] . is_null ( ) {
spent_utxos . push ( utxo ) ;
} else {
pending_utxos . push ( utxo ) ;
}
} ) ;
}
let mut res = object ! {
"unspent_notes" = > unspent_notes ,
"pending_notes" = > pending_notes ,
"utxos" = > unspent_utxos ,
"pending_utxos" = > pending_utxos ,
} ;
if all_notes {
res [ "spent_notes" ] = JsonValue ::Array ( spent_notes ) ;
res [ "spent_utxos" ] = JsonValue ::Array ( spent_utxos ) ;
}
res
}
pub fn do_list_transactions ( & self ) -> JsonValue {
let wallet = self . wallet . read ( ) . unwrap ( ) ;
// Create a list of TransactionItems
let mut tx_list = wallet . txs . read ( ) . unwrap ( ) . iter ( )
. flat_map ( | ( _k , v ) | {
let mut txns : Vec < JsonValue > = vec ! [ ] ;
if v . total_shielded_value_spent > 0 {
// If money was spent, create a transaction. For this, we'll subtract
// all the change notes. TODO: Add transparent change here to subtract it also
let total_change : u64 = v . notes . iter ( )
. filter ( | nd | nd . is_change )
. map ( | nd | nd . note . value )
. sum ( ) ;
// TODO: What happens if change is > than sent ?
// Collect outgoing metadata
let outgoing_json = v . outgoing_metadata . iter ( )
. map ( | om |
object ! {
"address" = > om . address . clone ( ) ,
"value" = > om . value ,
"memo" = > LightWallet ::memo_str ( & Some ( om . memo . clone ( ) ) ) ,
} )
. collect ::< Vec < JsonValue > > ( ) ;
txns . push ( object ! {
"block_height" = > v . block ,
"datetime" = > v . datetime ,
"txid" = > format ! ( "{}" , v . txid ) ,
"amount" = > total_change as i64
- v . total_shielded_value_spent as i64
- v . total_transparent_value_spent as i64 ,
"outgoing_metadata" = > outgoing_json ,
} ) ;
}
// For each sapling note that is not a change, add a Tx.
txns . extend ( v . notes . iter ( )
. filter ( | nd | ! nd . is_change )
. map ( | nd |
object ! {
"block_height" = > v . block ,
"datetime" = > v . datetime ,
"txid" = > format ! ( "{}" , v . txid ) ,
"amount" = > nd . note . value as i64 ,
"address" = > LightWallet ::note_address ( self . config . hrp_sapling_address ( ) , nd ) ,
"memo" = > LightWallet ::memo_str ( & nd . memo ) ,
} )
) ;
// Get the total transparent received
let total_transparent_received = v . utxos . iter ( ) . map ( | u | u . value ) . sum ::< u64 > ( ) ;
if total_transparent_received > v . total_transparent_value_spent {
// Create an input transaction for the transparent value as well.
txns . push ( object ! {
"block_height" = > v . block ,
"datetime" = > v . datetime ,
"txid" = > format ! ( "{}" , v . txid ) ,
"amount" = > total_transparent_received as i64 - v . total_transparent_value_spent as i64 ,
"address" = > v . utxos . iter ( ) . map ( | u | u . address . clone ( ) ) . collect ::< Vec < String > > ( ) . join ( "," ) ,
"memo" = > None ::< String >
} )
}
txns
} )
. collect ::< Vec < JsonValue > > ( ) ;
tx_list . sort_by ( | a , b | if a [ "block_height" ] = = b [ "block_height" ] {
a [ "txid" ] . as_str ( ) . cmp ( & b [ "txid" ] . as_str ( ) )
} else {
a [ "block_height" ] . as_i32 ( ) . cmp ( & b [ "block_height" ] . as_i32 ( ) )
}
) ;
JsonValue ::Array ( tx_list )
}
/// Create a new address, deriving it from the seed.
pub fn do_new_address ( & self , addr_type : & str ) -> Result < JsonValue , String > {
if ! self . wallet . read ( ) . unwrap ( ) . is_unlocked_for_spending ( ) {
error ! ( "Wallet is locked" ) ;
return Err ( "Wallet is locked" . to_string ( ) ) ;
}
let wallet = self . wallet . write ( ) . unwrap ( ) ;
let new_address = match addr_type {
"z" = > wallet . add_zaddr ( ) ,
"t" = > wallet . add_taddr ( ) ,
_ = > {
let e = format ! ( "Unrecognized address type: {}" , addr_type ) ;
error ! ( "{}" , e ) ;
return Err ( e ) ;
}
} ;
Ok ( array ! [ new_address ] )
}
pub fn do_rescan ( & self ) -> String {
info ! ( "Rescan starting" ) ;
// First, clear the state from the wallet
self . wallet . read ( ) . unwrap ( ) . clear_blocks ( ) ;
// Then set the initial block
self . set_wallet_initial_state ( ) ;
// Then, do a sync, which will force a full rescan from the initial state
let response = self . do_sync ( true ) ;
info ! ( "Rescan finished" ) ;
response
}
pub fn do_sync ( & self , print_updates : bool ) -> String {
// 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 . read ( ) . unwrap ( ) . last_scanned_height ( ) as u64 ;
// This will hold the latest block fetched from the RPC
let latest_block_height = Arc ::new ( AtomicU64 ::new ( 0 ) ) ;
let lbh = latest_block_height . clone ( ) ;
fetch_latest_block ( & self . get_server_uri ( ) , self . config . no_cert_verification , move | block : BlockId | {
lbh . store ( block . height , Ordering ::SeqCst ) ;
} ) ;
let latest_block = latest_block_height . load ( Ordering ::SeqCst ) ;
if latest_block < last_scanned_height {
let w = format ! ( "Server's latest block({}) is behind ours({})" , latest_block , last_scanned_height ) ;
warn ! ( "{}" , w ) ;
return w ;
}
info ! ( "Latest block is {}" , latest_block ) ;
// Get the end height to scan to.
let mut end_height = std ::cmp ::min ( last_scanned_height + 1000 , latest_block ) ;
// If there's nothing to scan, just return
if last_scanned_height = = latest_block {
info ! ( "Nothing to sync, returning" ) ;
return "" . to_string ( ) ;
}
// Count how many bytes we've downloaded
let bytes_downloaded = Arc ::new ( AtomicUsize ::new ( 0 ) ) ;
let mut total_reorg = 0 ;
// Collect all txns in blocks that we have a tx in. We'll fetch all these
// txs along with our own, so that the server doesn't learn which ones
// belong to us.
let all_new_txs = Arc ::new ( RwLock ::new ( vec ! [ ] ) ) ;
// Fetch CompactBlocks in increments
loop {
// Collect all block times, because we'll need to update transparent tx
// datetime via the block height timestamp
let block_times = Arc ::new ( RwLock ::new ( HashMap ::new ( ) ) ) ;
let local_light_wallet = self . wallet . clone ( ) ;
let local_bytes_downloaded = bytes_downloaded . clone ( ) ;
let start_height = last_scanned_height + 1 ;
info ! ( "Start height is {}" , start_height ) ;
// Show updates only if we're syncing a lot of blocks
if print_updates & & end_height - start_height > 100 {
print ! ( "Syncing {}/{}\r" , start_height , latest_block ) ;
io ::stdout ( ) . flush ( ) . ok ( ) . expect ( "Could not flush stdout" ) ;
}
// Fetch compact blocks
info ! ( "Fetching blocks {}-{}" , start_height , end_height ) ;
let all_txs = all_new_txs . clone ( ) ;
let block_times_inner = block_times . clone ( ) ;
let last_invalid_height = Arc ::new ( AtomicI32 ::new ( 0 ) ) ;
let last_invalid_height_inner = last_invalid_height . clone ( ) ;
fetch_blocks ( & self . get_server_uri ( ) , start_height , end_height , self . config . no_cert_verification ,
move | encoded_block : & [ u8 ] , height : u64 | {
// Process the block only if there were no previous errors
if last_invalid_height_inner . load ( Ordering ::SeqCst ) > 0 {
return ;
}
// Parse the block and save it's time. We'll use this timestamp for
// transactions in this block that might belong to us.
let block : Result < zcash_client_backend ::proto ::compact_formats ::CompactBlock , _ >
= parse_from_bytes ( encoded_block ) ;
match block {
Ok ( b ) = > {
block_times_inner . write ( ) . unwrap ( ) . insert ( b . height , b . time ) ;
} ,
Err ( _ ) = > { }
}
match local_light_wallet . read ( ) . unwrap ( ) . scan_block ( encoded_block ) {
Ok ( block_txns ) = > {
// Add to global tx list
all_txs . write ( ) . unwrap ( ) . extend_from_slice ( & block_txns . iter ( ) . map ( | txid | ( txid . clone ( ) , height as i32 ) ) . collect ::< Vec < _ > > ( ) [ . . ] ) ;
} ,
Err ( invalid_height ) = > {
// Block at this height seems to be invalid, so invalidate up till that point
last_invalid_height_inner . store ( invalid_height , Ordering ::SeqCst ) ;
}
} ;
local_bytes_downloaded . fetch_add ( encoded_block . len ( ) , Ordering ::SeqCst ) ;
} ) ;
// Check if there was any invalid block, which means we might have to do a reorg
let invalid_height = last_invalid_height . load ( Ordering ::SeqCst ) ;
if invalid_height > 0 {
total_reorg + = self . wallet . read ( ) . unwrap ( ) . invalidate_block ( invalid_height ) ;
warn ! ( "Invalidated block at height {}. Total reorg is now {}" , invalid_height , total_reorg ) ;
}
// Make sure we're not re-orging too much!
if total_reorg > ( crate ::lightwallet ::MAX_REORG - 1 ) as u64 {
error ! ( "Reorg has now exceeded {} blocks!" , crate ::lightwallet ::MAX_REORG ) ;
return format ! ( "Reorg has exceeded {} blocks. Aborting." , crate ::lightwallet ::MAX_REORG ) ;
}
if invalid_height > 0 {
// Reset the scanning heights
last_scanned_height = ( invalid_height - 1 ) as u64 ;
end_height = std ::cmp ::min ( last_scanned_height + 1000 , latest_block ) ;
warn ! ( "Reorg: reset scanning from {} to {}" , last_scanned_height , end_height ) ;
continue ;
}
// If it got here, that means the blocks are scanning properly now.
// So, reset the total_reorg
total_reorg = 0 ;
// We'll also fetch all the txids that our transparent addresses are involved with
{
// Copy over addresses so as to not lock up the wallet, which we'll use inside the callback below.
let addresses = self . wallet . read ( ) . unwrap ( )
. taddresses . read ( ) . unwrap ( ) . iter ( ) . map ( | a | a . clone ( ) )
. collect ::< Vec < String > > ( ) ;
for address in addresses {
let wallet = self . wallet . clone ( ) ;
let block_times_inner = block_times . clone ( ) ;
fetch_transparent_txids ( & self . get_server_uri ( ) , address , start_height , end_height , self . config . no_cert_verification ,
move | tx_bytes : & [ u8 ] , height : u64 | {
let tx = Transaction ::read ( tx_bytes ) . unwrap ( ) ;
// Scan this Tx for transparent inputs and outputs
let datetime = block_times_inner . read ( ) . unwrap ( ) . get ( & height ) . map ( | v | * v ) . unwrap_or ( 0 ) ;
wallet . read ( ) . unwrap ( ) . scan_full_tx ( & tx , height as i32 , datetime as u64 ) ;
}
) ;
}
}
// Do block height accounting
last_scanned_height = end_height ;
end_height = last_scanned_height + 1000 ;
if last_scanned_height > = latest_block {
break ;
} else if end_height > latest_block {
end_height = latest_block ;
}
}
if print_updates {
println ! ( "" ) ; // New line to finish up the updates
}
let mut responses = vec ! [ ] ;
info ! ( "Synced to {}, Downloaded {} kB" , latest_block , bytes_downloaded . load ( Ordering ::SeqCst ) / 1024 ) ;
responses . push ( format ! ( "Synced to {}, Downloaded {} kB" , latest_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 mut txids_to_fetch : Vec < ( TxId , i32 ) > = self . wallet . read ( ) . unwrap ( ) . txs . read ( ) . unwrap ( ) . values ( )
. filter ( | wtx | wtx . full_tx_scanned = = false )
. map ( | wtx | ( wtx . txid . clone ( ) , wtx . block ) )
. collect ::< Vec < ( TxId , i32 ) > > ( ) ;
info ! ( "Fetching {} new txids, total {} with decoy" , txids_to_fetch . len ( ) , all_new_txs . read ( ) . unwrap ( ) . len ( ) ) ;
txids_to_fetch . extend_from_slice ( & all_new_txs . read ( ) . unwrap ( ) [ . . ] ) ;
txids_to_fetch . sort ( ) ;
txids_to_fetch . dedup ( ) ;
let mut rng = OsRng ;
txids_to_fetch . shuffle ( & mut rng ) ;
// And go and fetch the txids, getting the full transaction, so we can
// read the memos
for ( txid , height ) in txids_to_fetch {
let light_wallet_clone = self . wallet . clone ( ) ;
info ! ( "Fetching full Tx: {}" , txid ) ;
fetch_full_tx ( & self . get_server_uri ( ) , txid , self . config . no_cert_verification , move | tx_bytes : & [ u8 ] | {
let tx = Transaction ::read ( tx_bytes ) . unwrap ( ) ;
light_wallet_clone . read ( ) . unwrap ( ) . scan_full_tx ( & tx , height , 0 ) ;
} ) ;
} ;
responses . join ( "\n" )
}
pub fn do_send ( & self , addrs : Vec < ( & str , u64 , Option < String > ) > ) -> Result < String , String > {
if ! self . wallet . read ( ) . unwrap ( ) . is_unlocked_for_spending ( ) {
error ! ( "Wallet is locked" ) ;
return Err ( "Wallet is locked" . to_string ( ) ) ;
}
info ! ( "Creating transaction" ) ;
let rawtx = self . wallet . write ( ) . unwrap ( ) . send_to_address (
u32 ::from_str_radix ( & self . config . consensus_branch_id , 16 ) . unwrap ( ) ,
& self . sapling_spend , & self . sapling_output ,
addrs
) ;
match rawtx {
Ok ( txbytes ) = > broadcast_raw_tx ( & self . get_server_uri ( ) , self . config . no_cert_verification , txbytes ) ,
Err ( e ) = > Err ( format ! ( "Error: No Tx to broadcast. Error was: {}" , e ) )
}
}
}
#[ cfg(test) ]
pub mod tests {
use lazy_static ::lazy_static ;
use tempdir ::TempDir ;
use super ::{ LightClient , LightClientConfig } ;
lazy_static ! {
static ref TEST_SEED : String = "youth strong sweet gorilla hammer unhappy congress stamp left stereo riot salute road tag clean toilet artefact fork certain leopard entire civil degree wonder" . to_string ( ) ;
}
#[ test ]
pub fn test_encrypt_decrypt ( ) {
let lc = super ::LightClient ::unconnected ( TEST_SEED . to_string ( ) , None ) . unwrap ( ) ;
assert ! ( ! lc . do_export ( None ) . is_err ( ) ) ;
assert ! ( ! lc . do_new_address ( "z" ) . is_err ( ) ) ;
assert ! ( ! lc . do_new_address ( "t" ) . is_err ( ) ) ;
assert_eq ! ( lc . do_seed_phrase ( ) . unwrap ( ) [ "seed" ] , TEST_SEED . to_string ( ) ) ;
// Encrypt and Lock the wallet
lc . wallet . write ( ) . unwrap ( ) . encrypt ( "password" . to_string ( ) ) . unwrap ( ) ;
assert ! ( lc . do_export ( None ) . is_err ( ) ) ;
assert ! ( lc . do_seed_phrase ( ) . is_err ( ) ) ;
assert ! ( lc . do_new_address ( "t" ) . is_err ( ) ) ;
assert ! ( lc . do_new_address ( "z" ) . is_err ( ) ) ;
assert ! ( lc . do_send ( vec ! [ ( "z" , 0 , None ) ] ) . is_err ( ) ) ;
// Do a unlock, and make sure it all works now
lc . wallet . write ( ) . unwrap ( ) . unlock ( "password" . to_string ( ) ) . unwrap ( ) ;
assert ! ( ! lc . do_export ( None ) . is_err ( ) ) ;
assert ! ( ! lc . do_seed_phrase ( ) . is_err ( ) ) ;
assert ! ( ! lc . do_new_address ( "t" ) . is_err ( ) ) ;
assert ! ( ! lc . do_new_address ( "z" ) . is_err ( ) ) ;
}
#[ test ]
pub fn test_addresses ( ) {
let lc = super ::LightClient ::unconnected ( TEST_SEED . to_string ( ) , None ) . unwrap ( ) ;
// Add new z and t addresses
let taddr1 = lc . do_new_address ( "t" ) . unwrap ( ) [ 0 ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let taddr2 = lc . do_new_address ( "t" ) . unwrap ( ) [ 0 ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let zaddr1 = lc . do_new_address ( "z" ) . unwrap ( ) [ 0 ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let zaddr2 = lc . do_new_address ( "z" ) . unwrap ( ) [ 0 ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
let addresses = lc . do_address ( ) ;
assert_eq ! ( addresses [ "z_addresses" ] . len ( ) , 3 ) ;
assert_eq ! ( addresses [ "z_addresses" ] [ 1 ] , zaddr1 ) ;
assert_eq ! ( addresses [ "z_addresses" ] [ 2 ] , zaddr2 ) ;
assert_eq ! ( addresses [ "t_addresses" ] . len ( ) , 3 ) ;
assert_eq ! ( addresses [ "t_addresses" ] [ 1 ] , taddr1 ) ;
assert_eq ! ( addresses [ "t_addresses" ] [ 2 ] , taddr2 ) ;
}
#[ test ]
pub fn test_wallet_creation ( ) {
// Create a new tmp director
{
let tmp = TempDir ::new ( "lctest" ) . unwrap ( ) ;
let dir_name = tmp . path ( ) . to_str ( ) . map ( | s | s . to_string ( ) ) ;
// A lightclient to a new, empty directory works.
let config = LightClientConfig ::create_unconnected ( "test" . to_string ( ) , dir_name ) ;
let lc = LightClient ::new ( & config , 0 ) . unwrap ( ) ;
let seed = lc . do_seed_phrase ( ) . unwrap ( ) [ "seed" ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
lc . do_save ( ) . unwrap ( ) ;
// Doing another new will fail, because the wallet file now already exists
assert ! ( LightClient ::new ( & config , 0 ) . is_err ( ) ) ;
// new_from_phrase will not work either, again, because wallet file exists
assert ! ( LightClient ::new_from_phrase ( TEST_SEED . to_string ( ) , & config , 0 ) . is_err ( ) ) ;
// Creating a lightclient to the same dir without a seed should re-read the same wallet
// file and therefore the same seed phrase
let lc2 = LightClient ::read_from_disk ( & config ) . unwrap ( ) ;
assert_eq ! ( seed , lc2 . do_seed_phrase ( ) . unwrap ( ) [ "seed" ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ;
}
// Now, get a new directory, and try to read from phrase
{
let tmp = TempDir ::new ( "lctest" ) . unwrap ( ) ;
let dir_name = tmp . path ( ) . to_str ( ) . map ( | s | s . to_string ( ) ) ;
let config = LightClientConfig ::create_unconnected ( "test" . to_string ( ) , dir_name ) ;
// read_from_disk will fail, because the dir doesn't exist
assert ! ( LightClient ::read_from_disk ( & config ) . is_err ( ) ) ;
// New from phrase should work becase a file doesn't exist already
let lc = LightClient ::new_from_phrase ( TEST_SEED . to_string ( ) , & config , 0 ) . unwrap ( ) ;
assert_eq ! ( TEST_SEED . to_string ( ) , lc . do_seed_phrase ( ) . unwrap ( ) [ "seed" ] . as_str ( ) . unwrap ( ) . to_string ( ) ) ;
lc . do_save ( ) . unwrap ( ) ;
// Now a new will fail because wallet exists
assert ! ( LightClient ::new ( & config , 0 ) . is_err ( ) ) ;
}
}
#[ test ]
pub fn test_recover_seed ( ) {
// Create a new tmp director
{
let tmp = TempDir ::new ( "lctest" ) . unwrap ( ) ;
let dir_name = tmp . path ( ) . to_str ( ) . map ( | s | s . to_string ( ) ) ;
// A lightclient to a new, empty directory works.
let config = LightClientConfig ::create_unconnected ( "test" . to_string ( ) , dir_name ) ;
let lc = LightClient ::new ( & config , 0 ) . unwrap ( ) ;
let seed = lc . do_seed_phrase ( ) . unwrap ( ) [ "seed" ] . as_str ( ) . unwrap ( ) . to_string ( ) ;
lc . do_save ( ) . unwrap ( ) ;
assert_eq ! ( seed , LightClient ::attempt_recover_seed ( & config ) . unwrap ( ) ) ;
// Now encrypt and save the file
lc . wallet . write ( ) . unwrap ( ) . encrypt ( "password" . to_string ( ) ) . unwrap ( ) ;
lc . do_save ( ) . unwrap ( ) ;
assert ! ( LightClient ::attempt_recover_seed ( & config ) . is_err ( ) ) ;
}
}
}