mirror of
https://github.com/Qortal/piratewallet-light-cli.git
synced 2025-07-31 12:21:25 +00:00
Merge
This commit is contained in:
75
lib/Cargo.toml
Normal file
75
lib/Cargo.toml
Normal file
@@ -0,0 +1,75 @@
|
||||
[package]
|
||||
name = "zecwalletlitelib"
|
||||
version = "0.1.0"
|
||||
edition = "2018"
|
||||
|
||||
[dependencies]
|
||||
tower-grpc = { git = "https://github.com/tower-rs/tower-grpc" }
|
||||
futures = "0.1"
|
||||
bytes = "0.4"
|
||||
base58 = "0.1.0"
|
||||
log = "0.4"
|
||||
log4rs = "0.8.3"
|
||||
dirs = "2.0.2"
|
||||
http = "0.1"
|
||||
prost = "0.5"
|
||||
tokio = "0.1"
|
||||
tower-request-modifier = { git = "https://github.com/tower-rs/tower-http" }
|
||||
tower-util = "0.1"
|
||||
hex = "0.3"
|
||||
protobuf = "2"
|
||||
byteorder = "1"
|
||||
rand = "0.5.6"
|
||||
json = "0.12.0"
|
||||
tiny-bip39 = "0.6.2"
|
||||
secp256k1 = "=0.15.0"
|
||||
sha2 = "0.8.0"
|
||||
ripemd160 = "0.8.0"
|
||||
ring = "0.14.0"
|
||||
lazy_static = "1.2.0"
|
||||
tower-service = "0.2"
|
||||
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" }
|
||||
rust-embed = "5.1.0"
|
||||
|
||||
[dependencies.bellman]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
default-features = false
|
||||
features = ["groth16"]
|
||||
|
||||
[dependencies.pairing]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
|
||||
[dependencies.zcash_client_backend]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
default-features = false
|
||||
|
||||
[dependencies.zcash_primitives]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
default-features = false
|
||||
features = ["transparent-inputs"]
|
||||
|
||||
[dependencies.zcash_proofs]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
default-features = false
|
||||
|
||||
[dependencies.ff]
|
||||
git = "https://github.com/adityapk00/librustzcash.git"
|
||||
rev = "0743dadcd017b60a0ac7123d04f0d6e7ce1e8016"
|
||||
features = ["ff_derive"]
|
||||
|
||||
[build-dependencies]
|
||||
tower-grpc-build = { git = "https://github.com/tower-rs/tower-grpc", features = ["tower-hyper"] }
|
||||
|
||||
[dev-dependencies]
|
||||
rand_core = "0.5.1"
|
||||
|
||||
[profile.release]
|
||||
debug = false
|
12
lib/build.rs
Normal file
12
lib/build.rs
Normal file
@@ -0,0 +1,12 @@
|
||||
fn main() {
|
||||
// Build proto files
|
||||
tower_grpc_build::Config::new()
|
||||
.enable_server(false)
|
||||
.enable_client(true)
|
||||
.build(
|
||||
&["proto/service.proto", "proto/compact_formats.proto"],
|
||||
&["proto"],
|
||||
)
|
||||
.unwrap_or_else(|e| panic!("protobuf compilation failed: {}", e));
|
||||
println!("cargo:rerun-if-changed=proto/service.proto");
|
||||
}
|
48
lib/proto/compact_formats.proto
Normal file
48
lib/proto/compact_formats.proto
Normal file
@@ -0,0 +1,48 @@
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "walletrpc";
|
||||
|
||||
// Remember that proto3 fields are all optional. A field that is not present will be set to its zero value.
|
||||
// bytes fields of hashes are in canonical little-endian format.
|
||||
|
||||
// CompactBlock is a packaging of ONLY the data from a block that's needed to:
|
||||
// 1. Detect a payment to your shielded Sapling address
|
||||
// 2. Detect a spend of your shielded Sapling notes
|
||||
// 3. Update your witnesses to generate new Sapling spend proofs.
|
||||
message CompactBlock {
|
||||
uint32 protoVersion = 1; // the version of this wire format, for storage
|
||||
uint64 height = 2; // the height of this block
|
||||
bytes hash = 3;
|
||||
bytes prevHash = 4;
|
||||
uint32 time = 5;
|
||||
bytes header = 6; // (hash, prevHash, and time) OR (full header)
|
||||
repeated CompactTx vtx = 7; // compact transactions from this block
|
||||
}
|
||||
|
||||
message CompactTx {
|
||||
// Index and hash will allow the receiver to call out to chain
|
||||
// explorers or other data structures to retrieve more information
|
||||
// about this transaction.
|
||||
uint64 index = 1;
|
||||
bytes hash = 2;
|
||||
|
||||
// The transaction fee: present if server can provide. In the case of a
|
||||
// stateless server and a transaction with transparent inputs, this will be
|
||||
// unset because the calculation requires reference to prior transactions.
|
||||
// in a pure-Sapling context, the fee will be calculable as:
|
||||
// valueBalance + (sum(vPubNew) - sum(vPubOld) - sum(tOut))
|
||||
uint32 fee = 3;
|
||||
|
||||
repeated CompactSpend spends = 4;
|
||||
repeated CompactOutput outputs = 5;
|
||||
}
|
||||
|
||||
message CompactSpend {
|
||||
bytes nf = 1;
|
||||
}
|
||||
|
||||
message CompactOutput {
|
||||
bytes cmu = 1;
|
||||
bytes epk = 2;
|
||||
bytes ciphertext = 3;
|
||||
}
|
81
lib/proto/service.proto
Normal file
81
lib/proto/service.proto
Normal file
@@ -0,0 +1,81 @@
|
||||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.rpc;
|
||||
option go_package = "walletrpc";
|
||||
|
||||
import "compact_formats.proto";
|
||||
|
||||
// A BlockID message contains identifiers to select a block: a height or a
|
||||
// hash. If the hash is present it takes precedence.
|
||||
message BlockID {
|
||||
uint64 height = 1;
|
||||
bytes hash = 2;
|
||||
}
|
||||
|
||||
// BlockRange technically allows ranging from hash to hash etc but this is not
|
||||
// currently intended for support, though there is no reason you couldn't do
|
||||
// it. Further permutations are left as an exercise.
|
||||
message BlockRange {
|
||||
BlockID start = 1;
|
||||
BlockID end = 2;
|
||||
}
|
||||
|
||||
// A TxFilter contains the information needed to identify a particular
|
||||
// transaction: either a block and an index, or a direct transaction hash.
|
||||
message TxFilter {
|
||||
BlockID block = 1;
|
||||
uint64 index = 2;
|
||||
bytes hash = 3;
|
||||
}
|
||||
|
||||
// RawTransaction contains the complete transaction data. It also optionally includes
|
||||
// the block height in which the transaction was included
|
||||
message RawTransaction {
|
||||
bytes data = 1;
|
||||
uint64 height = 2;
|
||||
}
|
||||
|
||||
message SendResponse {
|
||||
int32 errorCode = 1;
|
||||
string errorMessage = 2;
|
||||
}
|
||||
|
||||
// Empty placeholder. Someday we may want to specify e.g. a particular chain fork.
|
||||
message ChainSpec {}
|
||||
|
||||
message Empty {}
|
||||
|
||||
message LightdInfo {
|
||||
string version = 1;
|
||||
string vendor = 2;
|
||||
bool taddrSupport = 3;
|
||||
string chainName = 4;
|
||||
uint64 saplingActivationHeight = 5;
|
||||
string consensusBranchId = 6; // This should really be u32 or []byte, but string for readability
|
||||
uint64 blockHeight = 7;
|
||||
}
|
||||
|
||||
message TransparentAddress {
|
||||
string address = 1;
|
||||
}
|
||||
|
||||
message TransparentAddressBlockFilter {
|
||||
string address = 1;
|
||||
BlockRange range = 2;
|
||||
}
|
||||
|
||||
service CompactTxStreamer {
|
||||
// Compact Blocks
|
||||
rpc GetLatestBlock(ChainSpec) returns (BlockID) {}
|
||||
rpc GetBlock(BlockID) returns (CompactBlock) {}
|
||||
rpc GetBlockRange(BlockRange) returns (stream CompactBlock) {}
|
||||
|
||||
// Transactions
|
||||
rpc GetTransaction(TxFilter) returns (RawTransaction) {}
|
||||
rpc SendTransaction(RawTransaction) returns (SendResponse) {}
|
||||
|
||||
// t-Address support
|
||||
rpc GetAddressTxids(TransparentAddressBlockFilter) returns (stream RawTransaction) {}
|
||||
|
||||
// Misc
|
||||
rpc GetLightdInfo(Empty) returns (LightdInfo) {}
|
||||
}
|
455
lib/src/commands.rs
Normal file
455
lib/src/commands.rs
Normal file
@@ -0,0 +1,455 @@
|
||||
use std::collections::HashMap;
|
||||
use json::{object};
|
||||
|
||||
use crate::lightclient::LightClient;
|
||||
|
||||
pub trait Command {
|
||||
fn help(&self) -> String;
|
||||
|
||||
fn short_help(&self) -> String;
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String;
|
||||
}
|
||||
|
||||
struct SyncCommand {}
|
||||
impl Command for SyncCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Sync the light client with the server");
|
||||
h.push("Usage:");
|
||||
h.push("sync");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Download CompactBlocks and sync to the server".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_sync(true)
|
||||
}
|
||||
}
|
||||
|
||||
struct RescanCommand {}
|
||||
impl Command for RescanCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Rescan the wallet, rescanning all blocks for new transactions");
|
||||
h.push("Usage:");
|
||||
h.push("rescan");
|
||||
h.push("");
|
||||
h.push("This command will download all blocks since the intial block again from the light client server");
|
||||
h.push("and attempt to scan each block for transactions belonging to the wallet.");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Rescan the wallet, downloading and scanning all blocks and transactions".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_rescan()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct HelpCommand {}
|
||||
impl Command for HelpCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("List all available commands");
|
||||
h.push("Usage:");
|
||||
h.push("help [command_name]");
|
||||
h.push("");
|
||||
h.push("If no \"command_name\" is specified, a list of all available commands is returned");
|
||||
h.push("Example:");
|
||||
h.push("help send");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Lists all available commands".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, args: &[&str], _: &LightClient) -> String {
|
||||
let mut responses = vec![];
|
||||
|
||||
// Print a list of all commands
|
||||
match args.len() {
|
||||
0 => {
|
||||
responses.push(format!("Available commands:"));
|
||||
get_commands().iter().for_each(| (cmd, obj) | {
|
||||
responses.push(format!("{} - {}", cmd, obj.short_help()));
|
||||
});
|
||||
|
||||
responses.join("\n")
|
||||
},
|
||||
1 => {
|
||||
match get_commands().get(args[0]) {
|
||||
Some(cmd) => cmd.help(),
|
||||
None => format!("Command {} not found", args[0])
|
||||
}
|
||||
},
|
||||
_ => self.help()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct InfoCommand {}
|
||||
impl Command for InfoCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Get info about the lightwalletd we're connected to");
|
||||
h.push("Usage:");
|
||||
h.push("info");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Get the lightwalletd server's info".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_sync(true);
|
||||
|
||||
lightclient.do_info()
|
||||
}
|
||||
}
|
||||
|
||||
struct BalanceCommand {}
|
||||
impl Command for BalanceCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Show the current TAZ balance in the wallet");
|
||||
h.push("Usage:");
|
||||
h.push("balance");
|
||||
h.push("");
|
||||
h.push("Transparent and Shielded balances, along with the addresses they belong to are displayed");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Show the current TAZ balance in the wallet".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_sync(true);
|
||||
|
||||
format!("{}", lightclient.do_balance().pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct AddressCommand {}
|
||||
impl Command for AddressCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("List current addresses in the wallet");
|
||||
h.push("Usage:");
|
||||
h.push("address");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"List all addresses in the wallet".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
format!("{}", lightclient.do_address().pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
struct ExportCommand {}
|
||||
impl Command for ExportCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Export private key for an individual wallet addresses.");
|
||||
h.push("Note: To backup the whole wallet, use the 'seed' command insted");
|
||||
h.push("Usage:");
|
||||
h.push("export [t-address or z-address]");
|
||||
h.push("");
|
||||
h.push("If no address is passed, private key for all addresses in the wallet are exported.");
|
||||
h.push("");
|
||||
h.push("Example:");
|
||||
h.push("export ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Export private key for wallet addresses".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
|
||||
if args.len() > 1 {
|
||||
return self.help();
|
||||
}
|
||||
|
||||
let address = if args.is_empty() { None } else { Some(args[0].to_string()) };
|
||||
|
||||
format!("{}", lightclient.do_export(address).pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct SendCommand {}
|
||||
impl Command for SendCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Send TAZ to a given address");
|
||||
h.push("Usage:");
|
||||
h.push("send <address> <amount in tazoshis> \"optional_memo\"");
|
||||
h.push("");
|
||||
h.push("Example:");
|
||||
h.push("send ztestsapling1x65nq4dgp0qfywgxcwk9n0fvm4fysmapgr2q00p85ju252h6l7mmxu2jg9cqqhtvzd69jwhgv8d 200000 \"Hello from the command line\"");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Send TAZ to the given address".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
|
||||
// Parse the args.
|
||||
// 1 - Destination address. T or Z address
|
||||
if args.len() < 2 || args.len() > 3 {
|
||||
return self.help();
|
||||
}
|
||||
|
||||
// Make sure we can parse the amount
|
||||
let value = match args[1].parse::<u64>() {
|
||||
Ok(amt) => amt,
|
||||
Err(e) => {
|
||||
return format!("Couldn't parse amount: {}", e);;
|
||||
}
|
||||
};
|
||||
|
||||
let memo = if args.len() == 3 { Some(args[2].to_string()) } else {None};
|
||||
|
||||
lightclient.do_sync(true);
|
||||
|
||||
lightclient.do_send(args[0], value, memo)
|
||||
}
|
||||
}
|
||||
|
||||
struct SaveCommand {}
|
||||
impl Command for SaveCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Save the wallet to disk");
|
||||
h.push("Usage:");
|
||||
h.push("save");
|
||||
h.push("");
|
||||
h.push("The wallet is saved to disk. The wallet is periodically saved to disk (and also saved upon exit)");
|
||||
h.push("but you can use this command to explicitly save it to disk");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Save wallet file to disk".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_save()
|
||||
}
|
||||
}
|
||||
|
||||
struct SeedCommand {}
|
||||
impl Command for SeedCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Show the wallet's seed phrase");
|
||||
h.push("Usage:");
|
||||
h.push("seed");
|
||||
h.push("");
|
||||
h.push("Your wallet is entirely recoverable from the seed phrase. Please save it carefully and don't share it with anyone");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Display the seed phrase".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
format!("{}", lightclient.do_seed_phrase().pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
struct TransactionsCommand {}
|
||||
impl Command for TransactionsCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("List all incoming and outgoing transactions from this wallet");
|
||||
h.push("Usage:");
|
||||
h.push("list");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"List all transactions in the wallet".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_sync(true);
|
||||
|
||||
format!("{}", lightclient.do_list_transactions().pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
struct HeightCommand {}
|
||||
impl Command for HeightCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Get the latest block height that the wallet is at");
|
||||
h.push("Usage:");
|
||||
h.push("height");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Get the latest block height that the wallet is at".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
format!("{}",
|
||||
object! {
|
||||
"height" => lightclient.last_scanned_height()
|
||||
}.pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct NewAddressCommand {}
|
||||
impl Command for NewAddressCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Create a new address in this wallet");
|
||||
h.push("Usage:");
|
||||
h.push("new [z | t]");
|
||||
h.push("");
|
||||
h.push("Example:");
|
||||
h.push("To create a new z address:");
|
||||
h.push("new z");
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Create a new address in this wallet".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
|
||||
if args.len() != 1 {
|
||||
return format!("No address type specified\n{}", self.help());
|
||||
}
|
||||
|
||||
format!("{}", lightclient.do_new_address(args[0]).pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
struct NotesCommand {}
|
||||
impl Command for NotesCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Show all sapling notes and utxos in this wallet");
|
||||
h.push("Usage:");
|
||||
h.push("notes [all]");
|
||||
h.push("");
|
||||
h.push("If you supply the \"all\" parameter, all previously spent sapling notes and spent utxos are also included");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"List all sapling notes and utxos in the wallet".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, args: &[&str], lightclient: &LightClient) -> String {
|
||||
// Parse the args.
|
||||
if args.len() > 1 {
|
||||
return self.short_help();
|
||||
}
|
||||
|
||||
// Make sure we can parse the amount
|
||||
let all_notes = if args.len() == 1 {
|
||||
match args[0] {
|
||||
"all" => true,
|
||||
a => return format!("Invalid argument \"{}\". Specify 'all' to include unspent notes", a)
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
lightclient.do_sync(true);
|
||||
|
||||
format!("{}", lightclient.do_list_notes(all_notes).pretty(2))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct QuitCommand {}
|
||||
impl Command for QuitCommand {
|
||||
fn help(&self) -> String {
|
||||
let mut h = vec![];
|
||||
h.push("Save the wallet to disk and quit");
|
||||
h.push("Usage:");
|
||||
h.push("quit");
|
||||
h.push("");
|
||||
|
||||
h.join("\n")
|
||||
}
|
||||
|
||||
fn short_help(&self) -> String {
|
||||
"Quit the lightwallet, saving state to disk".to_string()
|
||||
}
|
||||
|
||||
fn exec(&self, _args: &[&str], lightclient: &LightClient) -> String {
|
||||
lightclient.do_save()
|
||||
}
|
||||
}
|
||||
|
||||
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("rescan".to_string(), Box::new(RescanCommand{}));
|
||||
map.insert("help".to_string(), Box::new(HelpCommand{}));
|
||||
map.insert("balance".to_string(), Box::new(BalanceCommand{}));
|
||||
map.insert("addresses".to_string(), Box::new(AddressCommand{}));
|
||||
map.insert("height".to_string(), Box::new(HeightCommand{}));
|
||||
map.insert("export".to_string(), Box::new(ExportCommand{}));
|
||||
map.insert("info".to_string(), Box::new(InfoCommand{}));
|
||||
map.insert("send".to_string(), Box::new(SendCommand{}));
|
||||
map.insert("save".to_string(), Box::new(SaveCommand{}));
|
||||
map.insert("quit".to_string(), Box::new(QuitCommand{}));
|
||||
map.insert("list".to_string(), Box::new(TransactionsCommand{}));
|
||||
map.insert("notes".to_string(), Box::new(NotesCommand{}));
|
||||
map.insert("new".to_string(), Box::new(NewAddressCommand{}));
|
||||
map.insert("seed".to_string(), Box::new(SeedCommand{}));
|
||||
|
||||
Box::new(map)
|
||||
}
|
||||
|
||||
pub fn do_user_command(cmd: &str, args: &Vec<&str>, lightclient: &LightClient) -> String {
|
||||
match get_commands().get(cmd) {
|
||||
Some(cmd) => cmd.exec(args, lightclient),
|
||||
None => format!("Unknown command : {}. Type 'help' for a list of commands", cmd)
|
||||
}
|
||||
}
|
314
lib/src/grpcconnector.rs
Normal file
314
lib/src/grpcconnector.rs
Normal file
@@ -0,0 +1,314 @@
|
||||
|
||||
use log::{error};
|
||||
|
||||
use std::sync::{Arc};
|
||||
use std::net::ToSocketAddrs;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
use futures::{Future};
|
||||
use futures::stream::Stream;
|
||||
|
||||
use tower_h2;
|
||||
use tower_util::MakeService;
|
||||
use tower_grpc::Request;
|
||||
|
||||
use tokio_rustls::client::TlsStream;
|
||||
use tokio_rustls::{rustls::ClientConfig, TlsConnector};
|
||||
|
||||
use tokio::executor::DefaultExecutor;
|
||||
use tokio::net::tcp::TcpStream;
|
||||
|
||||
use zcash_primitives::transaction::{TxId};
|
||||
|
||||
use crate::grpc_client::{ChainSpec, BlockId, BlockRange, RawTransaction,
|
||||
TransparentAddressBlockFilter, TxFilter, Empty, LightdInfo};
|
||||
use crate::grpc_client::client::CompactTxStreamer;
|
||||
|
||||
mod danger {
|
||||
use rustls;
|
||||
use webpki;
|
||||
|
||||
pub struct NoCertificateVerification {}
|
||||
|
||||
impl rustls::ServerCertVerifier for NoCertificateVerification {
|
||||
fn verify_server_cert(&self,
|
||||
_roots: &rustls::RootCertStore,
|
||||
_presented_certs: &[rustls::Certificate],
|
||||
_dns_name: webpki::DNSNameRef<'_>,
|
||||
_ocsp: &[u8]) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
|
||||
Ok(rustls::ServerCertVerified::assertion())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A Secure (https) grpc destination.
|
||||
struct Dst {
|
||||
addr: SocketAddr,
|
||||
host: String,
|
||||
no_cert: bool,
|
||||
}
|
||||
|
||||
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())
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if self.no_cert {
|
||||
config.dangerous()
|
||||
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {}));
|
||||
}
|
||||
|
||||
let config = Arc::new(config);
|
||||
let tls_connector = TlsConnector::from(config);
|
||||
|
||||
let addr_string_local = self.host.clone();
|
||||
|
||||
let domain = match webpki::DNSNameRef::try_from_ascii_str(&addr_string_local) {
|
||||
Ok(d) => d,
|
||||
Err(_) => webpki::DNSNameRef::try_from_ascii_str("localhost").unwrap()
|
||||
};
|
||||
let domain_local = domain.to_owned();
|
||||
|
||||
let stream = TcpStream::connect(&self.addr).and_then(move |sock| {
|
||||
sock.set_nodelay(true).unwrap();
|
||||
tls_connector.connect(domain_local.as_ref(), sock)
|
||||
})
|
||||
.map(move |tcp| tcp);
|
||||
|
||||
Box::new(stream)
|
||||
}
|
||||
}
|
||||
|
||||
// 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 stream = TcpStream::connect(&self.addr)
|
||||
// .and_then(move |sock| {
|
||||
// sock.set_nodelay(true).unwrap();
|
||||
// Ok(sock)
|
||||
// });
|
||||
// Box::new(stream)
|
||||
// }
|
||||
// }
|
||||
|
||||
|
||||
macro_rules! make_grpc_client {
|
||||
($protocol:expr, $host:expr, $port:expr, $nocert:expr) => {{
|
||||
let uri: http::Uri = format!("{}://{}", $protocol, $host).parse().unwrap();
|
||||
|
||||
let addr = format!("{}:{}", $host, $port)
|
||||
.to_socket_addrs()
|
||||
.unwrap()
|
||||
.next()
|
||||
.unwrap();
|
||||
|
||||
let h2_settings = Default::default();
|
||||
let mut make_client = tower_h2::client::Connect::new(Dst {addr, host: $host.to_string(), no_cert: $nocert}, h2_settings, DefaultExecutor::current());
|
||||
|
||||
make_client
|
||||
.make_service(())
|
||||
.map_err(|e| { format!("HTTP/2 connection failed; err={:?}.\nIf you're connecting to a local server, please pass --dangerous to trust the server without checking its TLS certificate", e) })
|
||||
.and_then(move |conn| {
|
||||
let conn = tower_request_modifier::Builder::new()
|
||||
.set_origin(uri)
|
||||
.build(conn)
|
||||
.unwrap();
|
||||
|
||||
CompactTxStreamer::new(conn)
|
||||
// Wait until the client is ready...
|
||||
.ready()
|
||||
.map_err(|e| { format!("client closed: {:?}", e) })
|
||||
})
|
||||
}};
|
||||
}
|
||||
|
||||
|
||||
// ==============
|
||||
// GRPC code
|
||||
// ==============
|
||||
|
||||
pub fn get_info(uri: http::Uri, no_cert: bool) -> Result<LightdInfo, String> {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.and_then(move |mut client| {
|
||||
client.get_lightd_info(Request::new(Empty{}))
|
||||
.map_err(|e| {
|
||||
format!("ERR = {:?}", e)
|
||||
})
|
||||
.and_then(move |response| {
|
||||
Ok(response.into_inner())
|
||||
})
|
||||
.map_err(|e| {
|
||||
format!("ERR = {:?}", e)
|
||||
})
|
||||
});
|
||||
|
||||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner)
|
||||
}
|
||||
|
||||
|
||||
pub fn fetch_blocks<F : 'static + std::marker::Send>(uri: &http::Uri, start_height: u64, end_height: u64, no_cert: bool, mut c: F)
|
||||
where F : FnMut(&[u8], u64) {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.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| {
|
||||
format!("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, b.height);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| format!("gRPC inbound stream error: {:?}", e))
|
||||
})
|
||||
});
|
||||
|
||||
match tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner) {
|
||||
Ok(_) => {}, // The result is processed in callbacks, so nothing to do here
|
||||
Err(e) => {
|
||||
error!("Error while executing fetch_blocks: {}", e);
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fetch_transparent_txids<F : 'static + std::marker::Send>(uri: &http::Uri, address: String,
|
||||
start_height: u64, end_height: u64, no_cert: bool, c: F)
|
||||
where F : Fn(&[u8], u64) {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.and_then(move |mut client| {
|
||||
let start = Some(BlockId{ height: start_height, hash: vec!()});
|
||||
let end = Some(BlockId{ height: end_height, hash: vec!()});
|
||||
|
||||
let br = Request::new(TransparentAddressBlockFilter{ address, range: Some(BlockRange{start, end}) });
|
||||
|
||||
client
|
||||
.get_address_txids(br)
|
||||
.map_err(|e| {
|
||||
format!("RouteChat request failed; err={:?}", e)
|
||||
})
|
||||
.and_then(move |response| {
|
||||
let inbound = response.into_inner();
|
||||
inbound.for_each(move |tx| {
|
||||
//let tx = Transaction::read(&tx.into_inner().data[..]).unwrap();
|
||||
c(&tx.data, tx.height);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| format!("gRPC inbound stream error: {:?}", e))
|
||||
})
|
||||
});
|
||||
|
||||
match tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner) {
|
||||
Ok(_) => {}, // The result is processed in callbacks, so nothing to do here
|
||||
Err(e) => {
|
||||
error!("Error while executing fetch_transparent_txids: {}", e);
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn fetch_full_tx<F : 'static + std::marker::Send>(uri: &http::Uri, txid: TxId, no_cert: bool, c: F)
|
||||
where F : Fn(&[u8]) {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.and_then(move |mut client| {
|
||||
let txfilter = TxFilter { block: None, index: 0, hash: txid.0.to_vec() };
|
||||
client.get_transaction(Request::new(txfilter))
|
||||
.map_err(|e| {
|
||||
format!("RouteChat request failed; err={:?}", e)
|
||||
})
|
||||
.and_then(move |response| {
|
||||
c(&response.into_inner().data);
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| { format!("ERR = {:?}", e) })
|
||||
});
|
||||
|
||||
match tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner) {
|
||||
Ok(_) => {}, // The result is processed in callbacks, so nothing to do here
|
||||
Err(e) => {
|
||||
error!("Error while executing fetch_full_tx: {}", e);
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub fn broadcast_raw_tx(uri: &http::Uri, no_cert: bool, tx_bytes: Box<[u8]>) -> Result<String, String> {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.and_then(move |mut client| {
|
||||
client.send_transaction(Request::new(RawTransaction {data: tx_bytes.to_vec(), height: 0}))
|
||||
.map_err(|e| {
|
||||
format!("ERR = {:?}", e)
|
||||
})
|
||||
.and_then(move |response| {
|
||||
let sendresponse = response.into_inner();
|
||||
if sendresponse.error_code == 0 {
|
||||
Ok(format!("Successfully broadcast Tx: {}", sendresponse.error_message))
|
||||
} else {
|
||||
Err(format!("Error: {:?}", sendresponse))
|
||||
}
|
||||
})
|
||||
.map_err(|e| { format!("ERR = {:?}", e) })
|
||||
});
|
||||
|
||||
tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner)
|
||||
}
|
||||
|
||||
pub fn fetch_latest_block<F : 'static + std::marker::Send>(uri: &http::Uri, no_cert: bool, mut c : F)
|
||||
where F : FnMut(BlockId) {
|
||||
let runner = make_grpc_client!(uri.scheme_str().unwrap(), uri.host().unwrap(), uri.port_part().unwrap(), no_cert)
|
||||
.and_then(|mut client| {
|
||||
client.get_latest_block(Request::new(ChainSpec {}))
|
||||
.map_err(|e| { format!("ERR = {:?}", e) })
|
||||
.and_then(move |response| {
|
||||
c(response.into_inner());
|
||||
Ok(())
|
||||
})
|
||||
.map_err(|e| { format!("ERR = {:?}", e) })
|
||||
});
|
||||
|
||||
match tokio::runtime::current_thread::Runtime::new().unwrap().block_on(runner) {
|
||||
Ok(_) => {}, // The result is processed in callbacks, so nothing to do here
|
||||
Err(e) => {
|
||||
error!("Error while executing fetch_latest_block: {}", e);
|
||||
eprintln!("{}", e);
|
||||
}
|
||||
};
|
||||
}
|
19
lib/src/lib.rs
Normal file
19
lib/src/lib.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
#[macro_use]
|
||||
extern crate rust_embed;
|
||||
|
||||
pub mod lightclient;
|
||||
pub mod grpcconnector;
|
||||
pub mod lightwallet;
|
||||
pub mod commands;
|
||||
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "zcash-params/"]
|
||||
pub struct SaplingParams;
|
||||
|
||||
pub const ANCHOR_OFFSET: u32 = 4;
|
||||
|
||||
|
||||
pub mod grpc_client {
|
||||
include!(concat!(env!("OUT_DIR"), "/cash.z.wallet.sdk.rpc.rs"));
|
||||
}
|
758
lib/src/lightclient.rs
Normal file
758
lib/src/lightclient.rs
Normal file
@@ -0,0 +1,758 @@
|
||||
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;
|
||||
use std::fs::File;
|
||||
use std::io;
|
||||
use std::io::prelude::*;
|
||||
use std::io::{BufReader, BufWriter, Error, ErrorKind};
|
||||
|
||||
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::*;
|
||||
use crate::SaplingParams;
|
||||
|
||||
|
||||
pub const DEFAULT_SERVER: &str = "https://lightd-main.zecwallet.co:443";
|
||||
pub const WALLET_NAME: &str = "zecwallet-light-wallet.dat";
|
||||
pub const LOGFILE_NAME: &str = "zecwallet-light-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,
|
||||
}
|
||||
|
||||
impl LightClientConfig {
|
||||
|
||||
pub fn get_zcash_data_path(&self) -> Box<Path> {
|
||||
let mut zcash_data_location;
|
||||
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("Zcash");
|
||||
} else {
|
||||
zcash_data_location = dirs::home_dir().expect("Couldn't determine home directory!");
|
||||
zcash_data_location.push(".zcash");
|
||||
};
|
||||
|
||||
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 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((600000,
|
||||
"0107385846c7451480912c294b6ce1ee1feba6c2619079fd9104f6e71e4d8fe7",
|
||||
"01690698411e3f8badea7da885e556d7aba365a797e9b20b44ac0946dced14b23c001001ab2a18a5a86aa5d77e43b69071b21770b6fe6b3c26304dcaf7f96c0bb3fed74d000186482712fa0f2e5aa2f2700c4ed49ef360820f323d34e2b447b78df5ec4dfa0401a332e89a21afb073cb1db7d6f07396b56a95e97454b9bca5a63d0ebc575d3a33000000000001c9d3564eff54ebc328eab2e4f1150c3637f4f47516f879a0cfebdf49fe7b1d5201c104705fac60a85596010e41260d07f3a64f38f37a112eaef41cd9d736edc5270145e3d4899fcd7f0f1236ae31eafb3f4b65ad6b11a17eae1729cec09bd3afa01a000000011f8322ef806eb2430dc4a7a41c1b344bea5be946efc7b4349c1c9edb14ff9d39"
|
||||
)),
|
||||
"main" => Some((610000,
|
||||
"000000000218882f481e3b49ca3df819734b8d74aac91f69e848d7499b34b472",
|
||||
"0192943f1eca6525cea7ea8e26b37c792593ed50cfe2be7a1ff551a08dc64b812f001000000001deef7ae5162a9942b4b9aa797137c5bdf60750e9548664127df99d1981dda66901747ad24d5daf294ce2a27aba923e16e52e7348eea3048c5b5654b99ab0a371200149d8aff830305beb3887529f6deb150ab012916c3ce88a6b47b78228f8bfeb3f01ff84a89890cfae65e0852bc44d9aa82be2c5d204f5aebf681c9e966aa46f540e000001d58f1dfaa9db0996996129f8c474acb813bfed452d347fb17ebac2e775e209120000000001319312241b0031e3a255b0d708750b4cb3f3fe79e3503fe488cc8db1dd00753801754bb593ea42d231a7ddf367640f09bbf59dc00f2c1d2003cc340e0c016b5b13"
|
||||
)),
|
||||
_ => 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; 2] {
|
||||
match &self.chain_name[..] {
|
||||
"main" => mainnet::B58_PUBKEY_ADDRESS_PREFIX,
|
||||
"test" => testnet::B58_PUBKEY_ADDRESS_PREFIX,
|
||||
"regtest" => regtest::B58_PUBKEY_ADDRESS_PREFIX,
|
||||
c => panic!("Unknown chain {}", c)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn base58_script_address(&self) -> [u8; 2] {
|
||||
match &self.chain_name[..] {
|
||||
"main" => mainnet::B58_SCRIPT_ADDRESS_PREFIX,
|
||||
"test" => testnet::B58_SCRIPT_ADDRESS_PREFIX,
|
||||
"regtest" => regtest::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<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.set_initial_block(height.try_into().unwrap(), hash, tree),
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn new(seed_phrase: Option<String>, config: &LightClientConfig, latest_block: u64) -> io::Result<Self> {
|
||||
let mut lc = if config.get_wallet_path().exists() {
|
||||
// Make sure that if a wallet exists, there is no seed phrase being attempted
|
||||
if !seed_phrase.is_none() {
|
||||
return Err(Error::new(ErrorKind::AlreadyExists,
|
||||
"Cannot create a new wallet from seed, because a wallet already exists"));
|
||||
}
|
||||
|
||||
let mut file_buffer = BufReader::new(File::open(config.get_wallet_path())?);
|
||||
|
||||
let wallet = LightWallet::read(&mut file_buffer, config)?;
|
||||
LightClient {
|
||||
wallet : Arc::new(wallet),
|
||||
config : config.clone(),
|
||||
sapling_output : vec![],
|
||||
sapling_spend : vec![]
|
||||
}
|
||||
} else {
|
||||
let l = LightClient {
|
||||
wallet : Arc::new(LightWallet::new(seed_phrase, config, latest_block)?),
|
||||
config : config.clone(),
|
||||
sapling_output : vec![],
|
||||
sapling_spend : vec![]
|
||||
};
|
||||
|
||||
l.set_wallet_initial_state();
|
||||
|
||||
l
|
||||
};
|
||||
|
||||
info!("Read wallet with birthday {}", lc.wallet.get_first_tx_block());
|
||||
|
||||
// Read Sapling Params
|
||||
lc.sapling_output.extend_from_slice(SaplingParams::get("sapling-output.params").unwrap().as_ref());
|
||||
lc.sapling_spend.extend_from_slice(SaplingParams::get("sapling-spend.params").unwrap().as_ref());
|
||||
|
||||
info!("Created LightClient to {}", &config.server);
|
||||
|
||||
Ok(lc)
|
||||
}
|
||||
|
||||
pub fn last_scanned_height(&self) -> u64 {
|
||||
self.wallet.last_scanned_height() as u64
|
||||
}
|
||||
|
||||
// Export private keys
|
||||
pub fn do_export(&self, addr: Option<String>) -> JsonValue {
|
||||
// Clone address so it can be moved into the closure
|
||||
let address = addr.clone();
|
||||
|
||||
// Go over all z addresses
|
||||
let z_keys = self.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 = self.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);
|
||||
|
||||
all_keys.into()
|
||||
}
|
||||
|
||||
pub fn do_address(&self) -> JsonValue {
|
||||
// Collect z addresses
|
||||
let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| {
|
||||
encode_payment_address(self.config.hrp_sapling_address(), &ad)
|
||||
}).collect::<Vec<String>>();
|
||||
|
||||
// Collect t addresses
|
||||
let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| {
|
||||
self.wallet.address_from_sk(&sk)
|
||||
}).collect::<Vec<String>>();
|
||||
|
||||
object!{
|
||||
"z_addresses" => z_addresses,
|
||||
"t_addresses" => t_addresses,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_balance(&self) -> JsonValue {
|
||||
// Collect z addresses
|
||||
let z_addresses = self.wallet.address.read().unwrap().iter().map( |ad| {
|
||||
let address = encode_payment_address(self.config.hrp_sapling_address(), &ad);
|
||||
object!{
|
||||
"address" => address.clone(),
|
||||
"zbalance" => self.wallet.zbalance(Some(address.clone())),
|
||||
"verified_zbalance" => self.wallet.verified_zbalance(Some(address)),
|
||||
}
|
||||
}).collect::<Vec<JsonValue>>();
|
||||
|
||||
// Collect t addresses
|
||||
let t_addresses = self.wallet.tkeys.read().unwrap().iter().map( |sk| {
|
||||
let address = self.wallet.address_from_sk(&sk);
|
||||
|
||||
// Get the balance for this address
|
||||
let balance = self.wallet.tbalance(Some(address.clone()));
|
||||
|
||||
object!{
|
||||
"address" => address,
|
||||
"balance" => balance,
|
||||
}
|
||||
}).collect::<Vec<JsonValue>>();
|
||||
|
||||
object!{
|
||||
"zbalance" => self.wallet.zbalance(None),
|
||||
"verified_zbalance" => self.wallet.verified_zbalance(None),
|
||||
"tbalance" => self.wallet.tbalance(None),
|
||||
"z_addresses" => z_addresses,
|
||||
"t_addresses" => t_addresses,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn do_save(&self) -> String {
|
||||
let mut file_buffer = BufWriter::with_capacity(
|
||||
1_000_000, // 1 MB write buffer
|
||||
File::create(self.config.get_wallet_path()).unwrap());
|
||||
|
||||
self.wallet.write(&mut file_buffer).unwrap();
|
||||
info!("Saved wallet");
|
||||
|
||||
format!("Saved Wallet")
|
||||
}
|
||||
|
||||
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) -> JsonValue {
|
||||
object!{
|
||||
"seed" => self.wallet.get_seed_phrase(),
|
||||
"birthday" => self.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
|
||||
self.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,
|
||||
"created_in_txid" => format!("{}", txid),
|
||||
"value" => nd.note.value,
|
||||
"is_change" => nd.is_change,
|
||||
"address" => self.wallet.note_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);
|
||||
}
|
||||
});
|
||||
|
||||
// Collect UTXOs
|
||||
let utxos = self.wallet.get_utxos().iter()
|
||||
.filter(|utxo| utxo.unconfirmed_spent.is_none()) // Filter out unconfirmed from the list of utxos
|
||||
.map(|utxo| {
|
||||
object!{
|
||||
"created_in_block" => utxo.height,
|
||||
"created_in_txid" => format!("{}", utxo.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)),
|
||||
}
|
||||
})
|
||||
.collect::<Vec<JsonValue>>();
|
||||
|
||||
// Collect pending UTXOs
|
||||
let pending_utxos = self.wallet.get_utxos().iter()
|
||||
.filter(|utxo| utxo.unconfirmed_spent.is_some()) // Filter to include only unconfirmed utxos
|
||||
.map(|utxo|
|
||||
object!{
|
||||
"created_in_block" => utxo.height,
|
||||
"created_in_txid" => format!("{}", utxo.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)),
|
||||
}
|
||||
)
|
||||
.collect::<Vec<JsonValue>>();;
|
||||
|
||||
let mut res = object!{
|
||||
"unspent_notes" => unspent_notes,
|
||||
"pending_notes" => pending_notes,
|
||||
"utxos" => utxos,
|
||||
"pending_utxos" => pending_utxos,
|
||||
};
|
||||
|
||||
if all_notes {
|
||||
res["spent_notes"] = JsonValue::Array(spent_notes);
|
||||
}
|
||||
|
||||
// If all notes, also add historical utxos
|
||||
if all_notes {
|
||||
res["spent_utxos"] = JsonValue::Array(self.wallet.txs.read().unwrap().values()
|
||||
.flat_map(|wtx| {
|
||||
wtx.utxos.iter()
|
||||
.filter(|utxo| utxo.spent.is_some())
|
||||
.map(|utxo| {
|
||||
object!{
|
||||
"created_in_block" => wtx.block,
|
||||
"created_in_txid" => format!("{}", utxo.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)),
|
||||
}
|
||||
}).collect::<Vec<JsonValue>>()
|
||||
}).collect::<Vec<JsonValue>>()
|
||||
);
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
pub fn do_list_transactions(&self) -> JsonValue {
|
||||
// Create a list of TransactionItems
|
||||
let mut tx_list = self.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,
|
||||
"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,
|
||||
"txid" => format!("{}", v.txid),
|
||||
"amount" => nd.note.value as i64,
|
||||
"address" => self.wallet.note_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,
|
||||
"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) -> JsonValue {
|
||||
let new_address = match addr_type {
|
||||
"z" => self.wallet.add_zaddr(),
|
||||
"t" => self.wallet.add_taddr(),
|
||||
_ => {
|
||||
let e = format!("Unrecognized address type: {}", addr_type);
|
||||
error!("{}", e);
|
||||
return object!{
|
||||
"error" => e
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
array![new_address]
|
||||
}
|
||||
|
||||
pub fn do_rescan(&self) -> String {
|
||||
info!("Rescan starting");
|
||||
// First, clear the state from the wallet
|
||||
self.wallet.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.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 {
|
||||
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 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;
|
||||
}
|
||||
|
||||
match local_light_wallet.scan_block(encoded_block) {
|
||||
Ok(block_txns) => {
|
||||
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.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
|
||||
// TODO: Use for all t addresses
|
||||
let address = self.wallet.address_from_sk(&self.wallet.tkeys.read().unwrap()[0]);
|
||||
let wallet = self.wallet.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
|
||||
wallet.scan_full_tx(&tx, height as i32);
|
||||
}
|
||||
);
|
||||
|
||||
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.txs.read().unwrap().values()
|
||||
.filter(|wtx| wtx.full_tx_scanned == false)
|
||||
.map(|wtx| (wtx.txid, 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.scan_full_tx(&tx, height);
|
||||
});
|
||||
};
|
||||
|
||||
responses.join("\n")
|
||||
}
|
||||
|
||||
pub fn do_send(&self, addr: &str, value: u64, memo: Option<String>) -> String {
|
||||
info!("Creating transaction");
|
||||
let rawtx = self.wallet.send_to_address(
|
||||
u32::from_str_radix(&self.config.consensus_branch_id, 16).unwrap(), // Blossom ID
|
||||
&self.sapling_spend, &self.sapling_output,
|
||||
vec![(&addr, value, memo)]
|
||||
);
|
||||
|
||||
match rawtx {
|
||||
Ok(txbytes) => match broadcast_raw_tx(&self.get_server_uri(), self.config.no_cert_verification, txbytes) {
|
||||
Ok(k) => k,
|
||||
Err(e) => e,
|
||||
},
|
||||
Err(e) => format!("No Tx to broadcast. Error was: {}", e)
|
||||
}
|
||||
}
|
||||
}
|
2908
lib/src/lightwallet.rs
Normal file
2908
lib/src/lightwallet.rs
Normal file
File diff suppressed because it is too large
Load Diff
46
lib/src/lightwallet/address.rs
Normal file
46
lib/src/lightwallet/address.rs
Normal file
@@ -0,0 +1,46 @@
|
||||
//! Structs for handling supported address types.
|
||||
|
||||
use pairing::bls12_381::Bls12;
|
||||
use zcash_primitives::primitives::PaymentAddress;
|
||||
use zcash_client_backend::encoding::{decode_payment_address, decode_transparent_address};
|
||||
use zcash_primitives::legacy::TransparentAddress;
|
||||
|
||||
/// An address that funds can be sent to.
|
||||
pub enum RecipientAddress {
|
||||
Shielded(PaymentAddress<Bls12>),
|
||||
Transparent(TransparentAddress),
|
||||
}
|
||||
|
||||
impl From<PaymentAddress<Bls12>> for RecipientAddress {
|
||||
fn from(addr: PaymentAddress<Bls12>) -> Self {
|
||||
RecipientAddress::Shielded(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<TransparentAddress> for RecipientAddress {
|
||||
fn from(addr: TransparentAddress) -> Self {
|
||||
RecipientAddress::Transparent(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl RecipientAddress {
|
||||
pub fn from_str(s: &str, hrp_sapling_address: &str, b58_pubkey_address: [u8; 2], b58_script_address: [u8; 2]) -> Option<Self> {
|
||||
// Try to match a sapling z address
|
||||
if let Some(pa) = match decode_payment_address(hrp_sapling_address, s) {
|
||||
Ok(ret) => ret,
|
||||
Err(_) => None
|
||||
}
|
||||
{
|
||||
Some(RecipientAddress::Shielded(pa)) // Matched a shielded address
|
||||
} else if let Some(addr) = match decode_transparent_address(
|
||||
&b58_pubkey_address, &b58_script_address, s) {
|
||||
Ok(ret) => ret,
|
||||
Err(_) => None
|
||||
}
|
||||
{
|
||||
Some(RecipientAddress::Transparent(addr)) // Matched a transparent address
|
||||
} else {
|
||||
None // Didn't match anything
|
||||
}
|
||||
}
|
||||
}
|
493
lib/src/lightwallet/data.rs
Normal file
493
lib/src/lightwallet/data.rs
Normal file
@@ -0,0 +1,493 @@
|
||||
use std::io::{self, Read, Write};
|
||||
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
use pairing::bls12_381::{Bls12};
|
||||
use ff::{PrimeField, PrimeFieldRepr};
|
||||
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
merkle_tree::{CommitmentTree, IncrementalWitness},
|
||||
sapling::Node,
|
||||
serialize::{Vector, Optional},
|
||||
transaction::{
|
||||
components::{OutPoint},
|
||||
TxId,
|
||||
},
|
||||
note_encryption::{Memo,},
|
||||
zip32::{ExtendedFullViewingKey,},
|
||||
JUBJUB,
|
||||
primitives::{Diversifier, Note,},
|
||||
jubjub::{
|
||||
JubjubEngine,
|
||||
fs::{Fs, FsRepr},
|
||||
}
|
||||
};
|
||||
use zcash_primitives::zip32::ExtendedSpendingKey;
|
||||
|
||||
|
||||
pub struct BlockData {
|
||||
pub height: i32,
|
||||
pub hash: BlockHash,
|
||||
pub tree: CommitmentTree<Node>,
|
||||
}
|
||||
|
||||
impl BlockData {
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let height = reader.read_i32::<LittleEndian>()?;
|
||||
|
||||
let mut hash_bytes = [0; 32];
|
||||
reader.read_exact(&mut hash_bytes)?;
|
||||
|
||||
let tree = CommitmentTree::<Node>::read(&mut reader)?;
|
||||
|
||||
let endtag = reader.read_u64::<LittleEndian>()?;
|
||||
if endtag != 11 {
|
||||
println!("End tag for blockdata {}", endtag);
|
||||
}
|
||||
|
||||
|
||||
Ok(BlockData{
|
||||
height,
|
||||
hash: BlockHash{ 0: hash_bytes },
|
||||
tree
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
writer.write_i32::<LittleEndian>(self.height)?;
|
||||
writer.write_all(&self.hash.0)?;
|
||||
self.tree.write(&mut writer)?;
|
||||
writer.write_u64::<LittleEndian>(11)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SaplingNoteData {
|
||||
pub(super) account: usize,
|
||||
pub(super) extfvk: ExtendedFullViewingKey, // Technically, this should be recoverable from the account number, but we're going to refactor this in the future, so I'll write it again here.
|
||||
pub diversifier: Diversifier,
|
||||
pub note: Note<Bls12>,
|
||||
pub(super) witnesses: Vec<IncrementalWitness<Node>>,
|
||||
pub(super) nullifier: [u8; 32],
|
||||
pub spent: Option<TxId>, // If this note was confirmed spent
|
||||
pub unconfirmed_spent: Option<TxId>, // If this note was spent in a send, but has not yet been confirmed.
|
||||
pub memo: Option<Memo>,
|
||||
pub is_change: bool,
|
||||
// TODO: We need to remove the unconfirmed_spent (i.e., set it to None) if the Tx has expired
|
||||
}
|
||||
|
||||
|
||||
/// Reads an FsRepr from [u8] of length 32
|
||||
/// This will panic (abort) if length provided is
|
||||
/// not correct
|
||||
/// TODO: This is duplicate from rustzcash.rs
|
||||
fn read_fs(from: &[u8]) -> FsRepr {
|
||||
assert_eq!(from.len(), 32);
|
||||
|
||||
let mut f = <<Bls12 as JubjubEngine>::Fs as PrimeField>::Repr::default();
|
||||
f.read_le(from).expect("length is 32 bytes");
|
||||
|
||||
f
|
||||
}
|
||||
|
||||
// Reading a note also needs the corresponding address to read from.
|
||||
pub fn read_note<R: Read>(mut reader: R) -> io::Result<(u64, Fs)> {
|
||||
let value = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
let mut r_bytes: [u8; 32] = [0; 32];
|
||||
reader.read_exact(&mut r_bytes)?;
|
||||
|
||||
let r = match Fs::from_repr(read_fs(&r_bytes)) {
|
||||
Ok(r) => r,
|
||||
Err(_) => return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidInput, "Couldn't parse randomness"))
|
||||
};
|
||||
|
||||
Ok((value, r))
|
||||
}
|
||||
|
||||
impl SaplingNoteData {
|
||||
fn serialized_version() -> u64 {
|
||||
1
|
||||
}
|
||||
|
||||
pub 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,
|
||||
extfvk: extfvk.clone(),
|
||||
diversifier: output.to.diversifier,
|
||||
note: output.note,
|
||||
witnesses: vec![witness],
|
||||
nullifier: nf,
|
||||
spent: None,
|
||||
unconfirmed_spent: None,
|
||||
memo: None,
|
||||
is_change: output.is_change,
|
||||
}
|
||||
}
|
||||
|
||||
// Reading a note also needs the corresponding address to read from.
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let version = reader.read_u64::<LittleEndian>()?;
|
||||
assert_eq!(version, SaplingNoteData::serialized_version());
|
||||
|
||||
let account = reader.read_u64::<LittleEndian>()? as usize;
|
||||
|
||||
let extfvk = ExtendedFullViewingKey::read(&mut reader)?;
|
||||
|
||||
let mut diversifier_bytes = [0u8; 11];
|
||||
reader.read_exact(&mut diversifier_bytes)?;
|
||||
let diversifier = Diversifier{0: diversifier_bytes};
|
||||
|
||||
// To recover the note, read the value and r, and then use the payment address
|
||||
// to recreate the note
|
||||
let (value, r) = read_note(&mut reader)?; // TODO: This method is in a different package, because of some fields that are private
|
||||
|
||||
let maybe_note = extfvk.fvk.vk.into_payment_address(diversifier, &JUBJUB).unwrap().create_note(value, r, &JUBJUB);
|
||||
|
||||
let note = match maybe_note {
|
||||
Some(n) => Ok(n),
|
||||
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the note for the address"))
|
||||
}?;
|
||||
|
||||
let witnesses = Vector::read(&mut reader, |r| IncrementalWitness::<Node>::read(r))?;
|
||||
|
||||
let mut nullifier = [0u8; 32];
|
||||
reader.read_exact(&mut nullifier)?;
|
||||
|
||||
// Note that this is only the spent field, we ignore the unconfirmed_spent field.
|
||||
// The reason is that unconfirmed spents are only in memory, and we need to get the actual value of spent
|
||||
// from the blockchain anyway.
|
||||
let spent = Optional::read(&mut reader, |r| {
|
||||
let mut txid_bytes = [0u8; 32];
|
||||
r.read_exact(&mut txid_bytes)?;
|
||||
Ok(TxId{0: txid_bytes})
|
||||
})?;
|
||||
|
||||
let memo = Optional::read(&mut reader, |r| {
|
||||
let mut memo_bytes = [0u8; 512];
|
||||
r.read_exact(&mut memo_bytes)?;
|
||||
match Memo::from_bytes(&memo_bytes) {
|
||||
Some(m) => Ok(m),
|
||||
None => Err(io::Error::new(io::ErrorKind::InvalidInput, "Couldn't create the memo"))
|
||||
}
|
||||
})?;
|
||||
|
||||
let is_change: bool = reader.read_u8()? > 0;
|
||||
|
||||
Ok(SaplingNoteData {
|
||||
account,
|
||||
extfvk,
|
||||
diversifier,
|
||||
note,
|
||||
witnesses,
|
||||
nullifier,
|
||||
spent,
|
||||
unconfirmed_spent: None,
|
||||
memo,
|
||||
is_change,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
// Write a version number first, so we can later upgrade this if needed.
|
||||
writer.write_u64::<LittleEndian>(SaplingNoteData::serialized_version())?;
|
||||
|
||||
writer.write_u64::<LittleEndian>(self.account as u64)?;
|
||||
|
||||
self.extfvk.write(&mut writer)?;
|
||||
|
||||
writer.write_all(&self.diversifier.0)?;
|
||||
|
||||
// Writing the note means writing the note.value and note.r. The Note is recoverable
|
||||
// from these 2 values and the Payment address.
|
||||
writer.write_u64::<LittleEndian>(self.note.value)?;
|
||||
|
||||
let mut rcm = [0; 32];
|
||||
self.note.r.into_repr().write_le(&mut rcm[..])?;
|
||||
writer.write_all(&rcm)?;
|
||||
|
||||
Vector::write(&mut writer, &self.witnesses, |wr, wi| wi.write(wr) )?;
|
||||
|
||||
writer.write_all(&self.nullifier)?;
|
||||
Optional::write(&mut writer, &self.spent, |w, t| w.write_all(&t.0))?;
|
||||
|
||||
Optional::write(&mut writer, &self.memo, |w, m| w.write_all(m.as_bytes()))?;
|
||||
|
||||
writer.write_u8(if self.is_change {1} else {0})?;
|
||||
|
||||
// Note that we don't write the unconfirmed_spent field, because if the wallet is restarted,
|
||||
// we don't want to be beholden to any expired txns
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Utxo {
|
||||
pub address: String,
|
||||
pub txid: TxId,
|
||||
pub output_index: u64,
|
||||
pub script: Vec<u8>,
|
||||
pub value: u64,
|
||||
pub height: i32,
|
||||
|
||||
pub spent: Option<TxId>, // If this utxo was confirmed spent
|
||||
pub unconfirmed_spent: Option<TxId>, // If this utxo was spent in a send, but has not yet been confirmed.
|
||||
}
|
||||
|
||||
impl Utxo {
|
||||
pub fn serialized_version() -> u64 {
|
||||
return 1;
|
||||
}
|
||||
|
||||
pub fn to_outpoint(&self) -> OutPoint {
|
||||
OutPoint { hash: self.txid.0, n: self.output_index as u32 }
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let version = reader.read_u64::<LittleEndian>()?;
|
||||
assert_eq!(version, Utxo::serialized_version());
|
||||
|
||||
let address_len = reader.read_i32::<LittleEndian>()?;
|
||||
let mut address_bytes = vec![0; address_len as usize];
|
||||
reader.read_exact(&mut address_bytes)?;
|
||||
let address = String::from_utf8(address_bytes).unwrap();
|
||||
assert_eq!(address.chars().take(1).collect::<Vec<char>>()[0], 't');
|
||||
|
||||
let mut txid_bytes = [0; 32];
|
||||
reader.read_exact(&mut txid_bytes)?;
|
||||
let txid = TxId { 0: txid_bytes };
|
||||
|
||||
let output_index = reader.read_u64::<LittleEndian>()?;
|
||||
let value = reader.read_u64::<LittleEndian>()?;
|
||||
let height = reader.read_i32::<LittleEndian>()?;
|
||||
|
||||
let script = Vector::read(&mut reader, |r| {
|
||||
let mut byte = [0; 1];
|
||||
r.read_exact(&mut byte)?;
|
||||
Ok(byte[0])
|
||||
})?;
|
||||
|
||||
let spent = Optional::read(&mut reader, |r| {
|
||||
let mut txbytes = [0u8; 32];
|
||||
r.read_exact(&mut txbytes)?;
|
||||
Ok(TxId{0: txbytes})
|
||||
})?;
|
||||
|
||||
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
|
||||
|
||||
Ok(Utxo {
|
||||
address,
|
||||
txid,
|
||||
output_index,
|
||||
script,
|
||||
value,
|
||||
height,
|
||||
spent,
|
||||
unconfirmed_spent: None::<TxId>,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
writer.write_u64::<LittleEndian>(Utxo::serialized_version())?;
|
||||
|
||||
writer.write_u32::<LittleEndian>(self.address.as_bytes().len() as u32)?;
|
||||
writer.write_all(self.address.as_bytes())?;
|
||||
|
||||
writer.write_all(&self.txid.0)?;
|
||||
|
||||
writer.write_u64::<LittleEndian>(self.output_index)?;
|
||||
writer.write_u64::<LittleEndian>(self.value)?;
|
||||
writer.write_i32::<LittleEndian>(self.height)?;
|
||||
|
||||
Vector::write(&mut writer, &self.script, |w, b| w.write_all(&[*b]))?;
|
||||
|
||||
Optional::write(&mut writer, &self.spent, |w, txid| w.write_all(&txid.0))?;
|
||||
|
||||
// Note that we don't write the unconfirmed spent field, because if the wallet is restarted, we'll reset any unconfirmed stuff.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct OutgoingTxMetadata {
|
||||
pub address: String,
|
||||
pub value : u64,
|
||||
pub memo : Memo,
|
||||
}
|
||||
|
||||
impl OutgoingTxMetadata {
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let address_len = reader.read_u64::<LittleEndian>()?;
|
||||
let mut address_bytes = vec![0; address_len as usize];
|
||||
reader.read_exact(&mut address_bytes)?;
|
||||
let address = String::from_utf8(address_bytes).unwrap();
|
||||
|
||||
let value = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
let mut memo_bytes = [0u8; 512];
|
||||
reader.read_exact(&mut memo_bytes)?;
|
||||
let memo = Memo::from_bytes(&memo_bytes).unwrap();
|
||||
|
||||
Ok(OutgoingTxMetadata{
|
||||
address,
|
||||
value,
|
||||
memo,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
// Strings are written as len + utf8
|
||||
writer.write_u64::<LittleEndian>(self.address.as_bytes().len() as u64)?;
|
||||
writer.write_all(self.address.as_bytes())?;
|
||||
|
||||
writer.write_u64::<LittleEndian>(self.value)?;
|
||||
writer.write_all(self.memo.as_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct WalletTx {
|
||||
pub block: i32,
|
||||
|
||||
// Txid of this transaction. It's duplicated here (It is also the Key in the HashMap that points to this
|
||||
// WalletTx in LightWallet::txs)
|
||||
pub txid: TxId,
|
||||
|
||||
// List of all notes received in this tx. Some of these might be change notes.
|
||||
pub notes: Vec<SaplingNoteData>,
|
||||
|
||||
// List of all Utxos received in this Tx. Some of these might be change notes
|
||||
pub utxos: Vec<Utxo>,
|
||||
|
||||
// Total shielded value spent in this Tx. Note that this is the value of the wallet's notes spent.
|
||||
// Some change may be returned in one of the notes above. Subtract the two to get the actual value spent.
|
||||
// Also note that even after subtraction, you might need to account for transparent inputs and outputs
|
||||
// to make sure the value is accurate.
|
||||
pub total_shielded_value_spent: u64,
|
||||
|
||||
// Total amount of transparent funds that belong to us that were spent in this Tx.
|
||||
pub total_transparent_value_spent : u64,
|
||||
|
||||
// All outgoing sapling sends to addresses outside this wallet
|
||||
pub outgoing_metadata: Vec<OutgoingTxMetadata>,
|
||||
|
||||
pub full_tx_scanned: bool,
|
||||
}
|
||||
|
||||
impl WalletTx {
|
||||
pub fn serialized_version() -> u64 {
|
||||
return 3;
|
||||
}
|
||||
|
||||
pub fn new(height: i32, txid: &TxId) -> Self {
|
||||
WalletTx {
|
||||
block: height,
|
||||
txid: txid.clone(),
|
||||
notes: vec![],
|
||||
utxos: vec![],
|
||||
total_shielded_value_spent: 0,
|
||||
total_transparent_value_spent: 0,
|
||||
outgoing_metadata: vec![],
|
||||
full_tx_scanned: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn read<R: Read>(mut reader: R) -> io::Result<Self> {
|
||||
let version = reader.read_u64::<LittleEndian>()?;
|
||||
assert!(version <= WalletTx::serialized_version());
|
||||
|
||||
let block = reader.read_i32::<LittleEndian>()?;
|
||||
|
||||
let mut txid_bytes = [0u8; 32];
|
||||
reader.read_exact(&mut txid_bytes)?;
|
||||
|
||||
let txid = TxId{0: txid_bytes};
|
||||
|
||||
let notes = Vector::read(&mut reader, |r| SaplingNoteData::read(r))?;
|
||||
let utxos = Vector::read(&mut reader, |r| Utxo::read(r))?;
|
||||
|
||||
let total_shielded_value_spent = reader.read_u64::<LittleEndian>()?;
|
||||
let total_transparent_value_spent = reader.read_u64::<LittleEndian>()?;
|
||||
|
||||
// Outgoing metadata was only added in version 2
|
||||
let outgoing_metadata = Vector::read(&mut reader, |r| OutgoingTxMetadata::read(r))?;
|
||||
|
||||
let full_tx_scanned = reader.read_u8()? > 0;
|
||||
|
||||
Ok(WalletTx{
|
||||
block,
|
||||
txid,
|
||||
notes,
|
||||
utxos,
|
||||
total_shielded_value_spent,
|
||||
total_transparent_value_spent,
|
||||
outgoing_metadata,
|
||||
full_tx_scanned
|
||||
})
|
||||
}
|
||||
|
||||
pub fn write<W: Write>(&self, mut writer: W) -> io::Result<()> {
|
||||
writer.write_u64::<LittleEndian>(WalletTx::serialized_version())?;
|
||||
|
||||
writer.write_i32::<LittleEndian>(self.block)?;
|
||||
|
||||
writer.write_all(&self.txid.0)?;
|
||||
|
||||
Vector::write(&mut writer, &self.notes, |w, nd| nd.write(w))?;
|
||||
Vector::write(&mut writer, &self.utxos, |w, u| u.write(w))?;
|
||||
|
||||
writer.write_u64::<LittleEndian>(self.total_shielded_value_spent)?;
|
||||
writer.write_u64::<LittleEndian>(self.total_transparent_value_spent)?;
|
||||
|
||||
// Write the outgoing metadata
|
||||
Vector::write(&mut writer, &self.outgoing_metadata, |w, om| om.write(w))?;
|
||||
|
||||
writer.write_u8(if self.full_tx_scanned {1} else {0})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct SpendableNote {
|
||||
pub txid: TxId,
|
||||
pub nullifier: [u8; 32],
|
||||
pub diversifier: Diversifier,
|
||||
pub note: Note<Bls12>,
|
||||
pub witness: IncrementalWitness<Node>,
|
||||
pub extsk: ExtendedSpendingKey,
|
||||
}
|
||||
|
||||
impl SpendableNote {
|
||||
pub fn from(txid: TxId, nd: &SaplingNoteData, anchor_offset: usize, extsk: &ExtendedSpendingKey) -> Option<Self> {
|
||||
// Include only notes that haven't been spent, or haven't been included in an unconfirmed spend yet.
|
||||
if nd.spent.is_none() && nd.unconfirmed_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(),
|
||||
extsk: extsk.clone(),
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
127
lib/src/lightwallet/extended_key.rs
Normal file
127
lib/src/lightwallet/extended_key.rs
Normal file
@@ -0,0 +1,127 @@
|
||||
use ring::{
|
||||
digest,
|
||||
hmac::{SigningContext, SigningKey},
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
use secp256k1::{PublicKey, Secp256k1, SecretKey, SignOnly, VerifyOnly, Error};
|
||||
|
||||
lazy_static! {
|
||||
static ref SECP256K1_SIGN_ONLY: Secp256k1<SignOnly> = Secp256k1::signing_only();
|
||||
static ref SECP256K1_VERIFY_ONLY: Secp256k1<VerifyOnly> = Secp256k1::verification_only();
|
||||
}
|
||||
/// Random entropy, part of extended key.
|
||||
type ChainCode = Vec<u8>;
|
||||
|
||||
|
||||
const HARDENED_KEY_START_INDEX: u32 = 2_147_483_648; // 2 ** 31
|
||||
|
||||
/// KeyIndex indicates the key type and index of a child key.
|
||||
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
|
||||
pub enum KeyIndex {
|
||||
/// Normal key, index range is from 0 to 2 ** 31 - 1
|
||||
Normal(u32),
|
||||
/// Hardened key, index range is from 2 ** 31 to 2 ** 32 - 1
|
||||
Hardened(u32),
|
||||
}
|
||||
|
||||
impl KeyIndex {
|
||||
|
||||
/// Check index range.
|
||||
pub fn is_valid(self) -> bool {
|
||||
match self {
|
||||
KeyIndex::Normal(i) => i < HARDENED_KEY_START_INDEX,
|
||||
KeyIndex::Hardened(i) => i >= HARDENED_KEY_START_INDEX,
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate Hardened KeyIndex from normalize index value.
|
||||
pub fn hardened_from_normalize_index(i: u32) -> Result<KeyIndex, Error> {
|
||||
if i < HARDENED_KEY_START_INDEX {
|
||||
Ok(KeyIndex::Hardened(HARDENED_KEY_START_INDEX + i))
|
||||
} else {
|
||||
Ok(KeyIndex::Hardened(i))
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate KeyIndex from raw index value.
|
||||
pub fn from_index(i: u32) -> Result<Self, Error> {
|
||||
if i < HARDENED_KEY_START_INDEX {
|
||||
Ok(KeyIndex::Normal(i))
|
||||
} else {
|
||||
Ok(KeyIndex::Hardened(i))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<u32> for KeyIndex {
|
||||
fn from(index: u32) -> Self {
|
||||
KeyIndex::from_index(index).expect("KeyIndex")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// ExtendedPrivKey is used for child key derivation.
|
||||
/// See [secp256k1 crate documentation](https://docs.rs/secp256k1) for SecretKey signatures usage.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ExtendedPrivKey {
|
||||
pub private_key: SecretKey,
|
||||
pub chain_code: ChainCode,
|
||||
}
|
||||
|
||||
|
||||
impl ExtendedPrivKey {
|
||||
|
||||
/// Generate an ExtendedPrivKey from seed
|
||||
pub fn with_seed(seed: &[u8]) -> Result<ExtendedPrivKey, Error> {
|
||||
let signature = {
|
||||
let signing_key = SigningKey::new(&digest::SHA512, b"Bitcoin seed");
|
||||
let mut h = SigningContext::with_key(&signing_key);
|
||||
h.update(&seed);
|
||||
h.sign()
|
||||
};
|
||||
let sig_bytes = signature.as_ref();
|
||||
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
|
||||
let private_key = SecretKey::from_slice(key)?;
|
||||
Ok(ExtendedPrivKey {
|
||||
private_key,
|
||||
chain_code: chain_code.to_vec(),
|
||||
})
|
||||
}
|
||||
|
||||
fn sign_hardended_key(&self, index: u32) -> ring::hmac::Signature {
|
||||
let signing_key = SigningKey::new(&digest::SHA512, &self.chain_code);
|
||||
let mut h = SigningContext::with_key(&signing_key);
|
||||
h.update(&[0x00]);
|
||||
h.update(&self.private_key[..]);
|
||||
h.update(&index.to_be_bytes());
|
||||
h.sign()
|
||||
}
|
||||
|
||||
fn sign_normal_key(&self, index: u32) -> ring::hmac::Signature {
|
||||
let signing_key = SigningKey::new(&digest::SHA512, &self.chain_code);
|
||||
let mut h = SigningContext::with_key(&signing_key);
|
||||
let public_key = PublicKey::from_secret_key(&SECP256K1_SIGN_ONLY, &self.private_key);
|
||||
h.update(&public_key.serialize());
|
||||
h.update(&index.to_be_bytes());
|
||||
h.sign()
|
||||
}
|
||||
|
||||
/// Derive a child key from ExtendedPrivKey.
|
||||
pub fn derive_private_key(&self, key_index: KeyIndex) -> Result<ExtendedPrivKey, Error> {
|
||||
if !key_index.is_valid() {
|
||||
return Err(Error::InvalidTweak);
|
||||
}
|
||||
let signature = match key_index {
|
||||
KeyIndex::Hardened(index) => self.sign_hardended_key(index),
|
||||
KeyIndex::Normal(index) => self.sign_normal_key(index),
|
||||
};
|
||||
let sig_bytes = signature.as_ref();
|
||||
let (key, chain_code) = sig_bytes.split_at(sig_bytes.len() / 2);
|
||||
let mut private_key = SecretKey::from_slice(key)?;
|
||||
private_key.add_assign(&self.private_key[..])?;
|
||||
Ok(ExtendedPrivKey {
|
||||
private_key,
|
||||
chain_code: chain_code.to_vec(),
|
||||
})
|
||||
}
|
||||
}
|
123
lib/src/lightwallet/prover.rs
Normal file
123
lib/src/lightwallet/prover.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
//! Abstractions over the proving system and parameters for ease of use.
|
||||
|
||||
use bellman::groth16::{prepare_verifying_key, Parameters, PreparedVerifyingKey};
|
||||
use pairing::bls12_381::{Bls12, Fr};
|
||||
use zcash_primitives::{
|
||||
jubjub::{edwards, fs::Fs, Unknown},
|
||||
primitives::{Diversifier, PaymentAddress, ProofGenerationKey},
|
||||
redjubjub::{PublicKey, Signature},
|
||||
transaction::components::Amount
|
||||
};
|
||||
use zcash_primitives::{
|
||||
merkle_tree::CommitmentTreeWitness, prover::TxProver, sapling::Node,
|
||||
transaction::components::GROTH_PROOF_SIZE, JUBJUB,
|
||||
};
|
||||
use zcash_proofs::sapling::SaplingProvingContext;
|
||||
|
||||
/// An implementation of [`TxProver`] using Sapling Spend and Output parameters provided
|
||||
/// in-memory.
|
||||
pub struct InMemTxProver {
|
||||
spend_params: Parameters<Bls12>,
|
||||
spend_vk: PreparedVerifyingKey<Bls12>,
|
||||
output_params: Parameters<Bls12>,
|
||||
}
|
||||
|
||||
impl InMemTxProver {
|
||||
pub fn new(spend_params: &[u8], output_params: &[u8]) -> Self {
|
||||
// Deserialize params
|
||||
let spend_params = Parameters::<Bls12>::read(spend_params, false)
|
||||
.expect("couldn't deserialize Sapling spend parameters file");
|
||||
let output_params = Parameters::<Bls12>::read(output_params, false)
|
||||
.expect("couldn't deserialize Sapling spend parameters file");
|
||||
|
||||
// Prepare verifying keys
|
||||
let spend_vk = prepare_verifying_key(&spend_params.vk);
|
||||
|
||||
InMemTxProver {
|
||||
spend_params,
|
||||
spend_vk,
|
||||
output_params,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TxProver for InMemTxProver {
|
||||
type SaplingProvingContext = SaplingProvingContext;
|
||||
|
||||
fn new_sapling_proving_context(&self) -> Self::SaplingProvingContext {
|
||||
SaplingProvingContext::new()
|
||||
}
|
||||
|
||||
fn spend_proof(
|
||||
&self,
|
||||
ctx: &mut Self::SaplingProvingContext,
|
||||
proof_generation_key: ProofGenerationKey<Bls12>,
|
||||
diversifier: Diversifier,
|
||||
rcm: Fs,
|
||||
ar: Fs,
|
||||
value: u64,
|
||||
anchor: Fr,
|
||||
witness: CommitmentTreeWitness<Node>,
|
||||
) -> Result<
|
||||
(
|
||||
[u8; GROTH_PROOF_SIZE],
|
||||
edwards::Point<Bls12, Unknown>,
|
||||
PublicKey<Bls12>,
|
||||
),
|
||||
(),
|
||||
> {
|
||||
let (proof, cv, rk) = ctx.spend_proof(
|
||||
proof_generation_key,
|
||||
diversifier,
|
||||
rcm,
|
||||
ar,
|
||||
value,
|
||||
anchor,
|
||||
witness,
|
||||
&self.spend_params,
|
||||
&self.spend_vk,
|
||||
&JUBJUB,
|
||||
)?;
|
||||
|
||||
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
|
||||
proof
|
||||
.write(&mut zkproof[..])
|
||||
.expect("should be able to serialize a proof");
|
||||
|
||||
Ok((zkproof, cv, rk))
|
||||
}
|
||||
|
||||
fn output_proof(
|
||||
&self,
|
||||
ctx: &mut Self::SaplingProvingContext,
|
||||
esk: Fs,
|
||||
payment_address: PaymentAddress<Bls12>,
|
||||
rcm: Fs,
|
||||
value: u64,
|
||||
) -> ([u8; GROTH_PROOF_SIZE], edwards::Point<Bls12, Unknown>) {
|
||||
let (proof, cv) = ctx.output_proof(
|
||||
esk,
|
||||
payment_address,
|
||||
rcm,
|
||||
value,
|
||||
&self.output_params,
|
||||
&JUBJUB,
|
||||
);
|
||||
|
||||
let mut zkproof = [0u8; GROTH_PROOF_SIZE];
|
||||
proof
|
||||
.write(&mut zkproof[..])
|
||||
.expect("should be able to serialize a proof");
|
||||
|
||||
(zkproof, cv)
|
||||
}
|
||||
|
||||
fn binding_sig(
|
||||
&self,
|
||||
ctx: &mut Self::SaplingProvingContext,
|
||||
value_balance: Amount,
|
||||
sighash: &[u8; 32],
|
||||
) -> Result<Signature, ()> {
|
||||
ctx.binding_sig(value_balance, sighash, &JUBJUB)
|
||||
}
|
||||
}
|
21
lib/src/lightwallet/utils.rs
Normal file
21
lib/src/lightwallet/utils.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
use std::io::{self, Read, Write};
|
||||
use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
|
||||
|
||||
pub fn read_string<R: Read>(mut reader: R) -> io::Result<String> {
|
||||
// Strings are written as <littleendian> len + bytes
|
||||
let str_len = reader.read_u64::<LittleEndian>()?;
|
||||
let mut str_bytes = vec![0; str_len as usize];
|
||||
reader.read_exact(&mut str_bytes)?;
|
||||
|
||||
let str = String::from_utf8(str_bytes).map_err(|e| {
|
||||
io::Error::new(io::ErrorKind::InvalidData, e.to_string())
|
||||
})?;
|
||||
|
||||
Ok(str)
|
||||
}
|
||||
|
||||
pub fn write_string<W: Write>(mut writer: W, s: &String) -> io::Result<()> {
|
||||
// Strings are written as len + utf8
|
||||
writer.write_u64::<LittleEndian>(s.as_bytes().len() as u64)?;
|
||||
writer.write_all(s.as_bytes())
|
||||
}
|
BIN
lib/zcash-params/sapling-output.params
Normal file
BIN
lib/zcash-params/sapling-output.params
Normal file
Binary file not shown.
BIN
lib/zcash-params/sapling-spend.params
Normal file
BIN
lib/zcash-params/sapling-spend.params
Normal file
Binary file not shown.
Reference in New Issue
Block a user