Cold wallet prepare/sign
This commit is contained in:
parent
d51c15c571
commit
af51bf664f
|
@ -18,6 +18,10 @@ path = "src/main/warp_cli.rs"
|
||||||
name = "cli"
|
name = "cli"
|
||||||
path = "src/main/cli.rs"
|
path = "src/main/cli.rs"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "sign"
|
||||||
|
path = "src/main/sign.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
dotenv = "0.15.0"
|
dotenv = "0.15.0"
|
||||||
env_logger = "0.8.4"
|
env_logger = "0.8.4"
|
||||||
|
|
|
@ -27,6 +27,7 @@ mod print;
|
||||||
mod scan;
|
mod scan;
|
||||||
mod taddr;
|
mod taddr;
|
||||||
mod transaction;
|
mod transaction;
|
||||||
|
mod pay;
|
||||||
mod wallet;
|
mod wallet;
|
||||||
|
|
||||||
pub use crate::builder::advance_tree;
|
pub use crate::builder::advance_tree;
|
||||||
|
@ -37,10 +38,11 @@ pub use crate::chain::{
|
||||||
pub use crate::commitment::{CTree, Witness};
|
pub use crate::commitment::{CTree, Witness};
|
||||||
pub use crate::db::DbAdapter;
|
pub use crate::db::DbAdapter;
|
||||||
pub use crate::hash::pedersen_hash;
|
pub use crate::hash::pedersen_hash;
|
||||||
pub use crate::key::is_valid_key;
|
pub use crate::key::{is_valid_key, decode_key};
|
||||||
pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
|
pub use crate::lw_rpc::compact_tx_streamer_client::CompactTxStreamerClient;
|
||||||
pub use crate::lw_rpc::*;
|
pub use crate::lw_rpc::*;
|
||||||
pub use crate::mempool::MemPool;
|
pub use crate::mempool::MemPool;
|
||||||
pub use crate::print::*;
|
pub use crate::print::*;
|
||||||
pub use crate::scan::{latest_height, scan_all, sync_async};
|
pub use crate::scan::{latest_height, scan_all, sync_async};
|
||||||
pub use crate::wallet::{Wallet, WalletBalance};
|
pub use crate::wallet::{Wallet, WalletBalance};
|
||||||
|
pub use crate::pay::{sign_offline_tx, broadcast_tx, Tx};
|
||||||
|
|
|
@ -0,0 +1,32 @@
|
||||||
|
use clap::Clap;
|
||||||
|
use sync::{decode_key, Tx, sign_offline_tx, NETWORK};
|
||||||
|
use std::fs::File;
|
||||||
|
use std::io::{Read, Write};
|
||||||
|
use zcash_client_backend::encoding::decode_extended_spending_key;
|
||||||
|
use zcash_primitives::consensus::Parameters;
|
||||||
|
|
||||||
|
#[derive(Clap, Debug)]
|
||||||
|
struct SignArgs {
|
||||||
|
tx_filename: String,
|
||||||
|
out_filename: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> anyhow::Result<()> {
|
||||||
|
let key = dotenv::var("KEY").unwrap();
|
||||||
|
let (_seed, sk, _ivk, _address) = decode_key(&key)?;
|
||||||
|
|
||||||
|
let opts: SignArgs = SignArgs::parse();
|
||||||
|
let sk = sk.unwrap();
|
||||||
|
let sk = decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &sk)?.unwrap();
|
||||||
|
|
||||||
|
let file_name = opts.tx_filename;
|
||||||
|
let mut file = File::open(file_name)?;
|
||||||
|
let mut s = String::new();
|
||||||
|
file.read_to_string(&mut s).unwrap();
|
||||||
|
let tx: Tx = serde_json::from_str(&s)?;
|
||||||
|
let raw_tx = sign_offline_tx(&tx, &sk)?;
|
||||||
|
|
||||||
|
let mut out_file = File::create(opts.out_filename)?;
|
||||||
|
writeln!(out_file, "{}", hex::encode(&raw_tx))?;
|
||||||
|
Ok(())
|
||||||
|
}
|
|
@ -28,9 +28,9 @@ async fn test() -> anyhow::Result<()> {
|
||||||
log::info!("Height = {}", height);
|
log::info!("Height = {}", height);
|
||||||
};
|
};
|
||||||
let wallet = Wallet::new(DB_NAME, LWD_URL);
|
let wallet = Wallet::new(DB_NAME, LWD_URL);
|
||||||
// wallet.new_account_with_key("main", &seed).unwrap();
|
wallet.new_account_with_key("main", &seed).unwrap();
|
||||||
// wallet.new_account_with_key("test", &seed2).unwrap();
|
// wallet.new_account_with_key("test", &seed2).unwrap();
|
||||||
wallet.new_account_with_key("zecpages", &ivk).unwrap();
|
// wallet.new_account_with_key("zecpages", &ivk).unwrap();
|
||||||
|
|
||||||
let res = wallet.sync(true, ANCHOR_OFFSET, progress).await;
|
let res = wallet.sync(true, ANCHOR_OFFSET, progress).await;
|
||||||
if let Err(err) = res {
|
if let Err(err) = res {
|
||||||
|
@ -46,7 +46,7 @@ async fn test() -> anyhow::Result<()> {
|
||||||
// &address,
|
// &address,
|
||||||
// 50000,
|
// 50000,
|
||||||
// "test memo",
|
// "test memo",
|
||||||
// u64::max_value(),
|
// 0,
|
||||||
// 2,
|
// 2,
|
||||||
// move |progress| {
|
// move |progress| {
|
||||||
// println!("{}", progress.cur());
|
// println!("{}", progress.cur());
|
||||||
|
@ -56,6 +56,18 @@ async fn test() -> anyhow::Result<()> {
|
||||||
// .unwrap();
|
// .unwrap();
|
||||||
// println!("TXID = {}", tx_id);
|
// println!("TXID = {}", tx_id);
|
||||||
|
|
||||||
|
let tx = wallet
|
||||||
|
.prepare_payment(
|
||||||
|
1,
|
||||||
|
&address,
|
||||||
|
50000,
|
||||||
|
"test memo",
|
||||||
|
0,
|
||||||
|
2)
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("TX = {}", tx);
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,273 @@
|
||||||
|
use crate::{NETWORK, connect_lightwalletd, RawTransaction, get_latest_height};
|
||||||
|
use zcash_primitives::zip32::{ExtendedSpendingKey, ExtendedFullViewingKey};
|
||||||
|
use zcash_primitives::sapling::{Diversifier, Rseed, Node};
|
||||||
|
use zcash_primitives::transaction::components::Amount;
|
||||||
|
use zcash_primitives::sapling::keys::OutgoingViewingKey;
|
||||||
|
use zcash_primitives::memo::MemoBytes;
|
||||||
|
use zcash_client_backend::encoding::{encode_extended_full_viewing_key, decode_extended_full_viewing_key};
|
||||||
|
use zcash_primitives::consensus::{Parameters, Network, BlockHeight, BranchId};
|
||||||
|
use zcash_primitives::transaction::builder::Builder;
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
use jubjub::Fr;
|
||||||
|
use zcash_primitives::merkle_tree::IncrementalWitness;
|
||||||
|
use zcash_client_backend::address::RecipientAddress;
|
||||||
|
use crate::db::SpendableNote;
|
||||||
|
use crate::wallet::Recipient;
|
||||||
|
use zcash_primitives::transaction::components::amount::DEFAULT_FEE;
|
||||||
|
use serde::{Serialize, Deserialize};
|
||||||
|
use zcash_proofs::prover::LocalTxProver;
|
||||||
|
use tonic::Request;
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct Tx {
|
||||||
|
height: u32,
|
||||||
|
inputs: Vec<TxIn>,
|
||||||
|
outputs: Vec<TxOut>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Tx {
|
||||||
|
pub fn new(height: u32) -> Self {
|
||||||
|
Tx {
|
||||||
|
height,
|
||||||
|
inputs: vec![],
|
||||||
|
outputs: vec![],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
pub struct TxIn {
|
||||||
|
diversifier: String,
|
||||||
|
fvk: String,
|
||||||
|
amount: u64,
|
||||||
|
rseed: String,
|
||||||
|
witness: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||||
|
pub struct TxOut {
|
||||||
|
addr: String,
|
||||||
|
amount: u64,
|
||||||
|
ovk: String,
|
||||||
|
memo: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait TxBuilder {
|
||||||
|
fn add_input(
|
||||||
|
&mut self,
|
||||||
|
skey: Option<ExtendedSpendingKey>,
|
||||||
|
diversifier: &Diversifier,
|
||||||
|
fvk: &ExtendedFullViewingKey,
|
||||||
|
amount: Amount,
|
||||||
|
rseed: &[u8],
|
||||||
|
witness: &[u8]
|
||||||
|
) -> anyhow::Result<()>;
|
||||||
|
fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()>;
|
||||||
|
fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()>;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ColdTxBuilder {
|
||||||
|
pub tx: Tx,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ColdTxBuilder {
|
||||||
|
pub fn new(height: u32) -> Self {
|
||||||
|
ColdTxBuilder {
|
||||||
|
tx: Tx::new(height),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TxBuilder for ColdTxBuilder {
|
||||||
|
fn add_input(&mut self, _skey: Option<ExtendedSpendingKey>, diversifier: &Diversifier, fvk: &ExtendedFullViewingKey, amount: Amount, rseed: &[u8], witness: &[u8]) -> anyhow::Result<()> {
|
||||||
|
let tx_in = TxIn {
|
||||||
|
diversifier: hex::encode(diversifier.0),
|
||||||
|
fvk: encode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &fvk),
|
||||||
|
amount: u64::from(amount),
|
||||||
|
rseed: hex::encode(rseed),
|
||||||
|
witness: hex::encode(witness),
|
||||||
|
};
|
||||||
|
self.tx.inputs.push(tx_in);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()> {
|
||||||
|
let tx_out = TxOut {
|
||||||
|
addr: address.to_string(),
|
||||||
|
amount: u64::from(amount),
|
||||||
|
ovk: String::new(),
|
||||||
|
memo: String::new(),
|
||||||
|
};
|
||||||
|
self.tx.outputs.push(tx_out);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()> {
|
||||||
|
let tx_out = TxOut {
|
||||||
|
addr: address.to_string(),
|
||||||
|
amount: u64::from(amount),
|
||||||
|
ovk: hex::encode(ovk.0),
|
||||||
|
memo: hex::encode(memo.as_slice()),
|
||||||
|
};
|
||||||
|
self.tx.outputs.push(tx_out);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TxBuilder for Builder<'_, Network, OsRng> {
|
||||||
|
fn add_input(&mut self, skey: Option<ExtendedSpendingKey>, diversifier: &Diversifier, fvk: &ExtendedFullViewingKey, amount: Amount, rseed: &[u8], witness: &[u8]) -> anyhow::Result<()> {
|
||||||
|
let pa = fvk.fvk.vk.to_payment_address(diversifier.clone()).unwrap();
|
||||||
|
let mut rseed_bytes = [0u8; 32];
|
||||||
|
rseed_bytes.copy_from_slice(rseed);
|
||||||
|
let fr = Fr::from_bytes(&rseed_bytes).unwrap();
|
||||||
|
let note = pa.create_note(u64::from(amount), Rseed::BeforeZip212(fr)).unwrap();
|
||||||
|
let witness = IncrementalWitness::<Node>::read(&*witness).unwrap();
|
||||||
|
let merkle_path = witness.path().unwrap();
|
||||||
|
self.add_sapling_spend(skey.unwrap(), diversifier.clone(), note, merkle_path)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_t_output(&mut self, address: &str, amount: Amount) -> anyhow::Result<()> {
|
||||||
|
let to_addr = RecipientAddress::decode(&NETWORK, address).ok_or(anyhow::anyhow!("Not a valid address"))?;
|
||||||
|
if let RecipientAddress::Transparent(t_address) = to_addr {
|
||||||
|
self.add_transparent_output(&t_address, amount)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_z_output(&mut self, address: &str, ovk: &OutgoingViewingKey, amount: Amount, memo: &MemoBytes) -> anyhow::Result<()> {
|
||||||
|
let to_addr = RecipientAddress::decode(&NETWORK, address).ok_or(anyhow::anyhow!("Not a valid address"))?;
|
||||||
|
if let RecipientAddress::Shielded(pa) = to_addr {
|
||||||
|
self.add_sapling_output(
|
||||||
|
Some(ovk.clone()),
|
||||||
|
pa.clone(),
|
||||||
|
amount,
|
||||||
|
Some(memo.clone()),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn prepare_tx<B: TxBuilder>(builder: &mut B, skey: Option<ExtendedSpendingKey>, notes: &[SpendableNote], target_amount: Amount, fvk: &ExtendedFullViewingKey, recipients: &[Recipient]) -> anyhow::Result<Vec<u32>> {
|
||||||
|
let mut amount = target_amount;
|
||||||
|
amount += DEFAULT_FEE;
|
||||||
|
let target_amount_with_fee = amount;
|
||||||
|
let mut selected_notes: Vec<u32> = vec![];
|
||||||
|
for n in notes.iter() {
|
||||||
|
if amount.is_positive() {
|
||||||
|
let a = amount.min(
|
||||||
|
Amount::from_u64(n.note.value)
|
||||||
|
.map_err(|_| anyhow::anyhow!("Invalid amount"))?,
|
||||||
|
);
|
||||||
|
amount -= a;
|
||||||
|
let mut witness_bytes: Vec<u8> = vec![];
|
||||||
|
n.witness.write(&mut witness_bytes)?;
|
||||||
|
if let Rseed::BeforeZip212(rseed) = n.note.rseed { // rseed are stored as pre-zip212
|
||||||
|
builder.add_input(
|
||||||
|
skey.clone(),
|
||||||
|
&n.diversifier,
|
||||||
|
fvk,
|
||||||
|
Amount::from_u64(n.note.value).unwrap(),
|
||||||
|
&rseed.to_bytes(),
|
||||||
|
&witness_bytes,
|
||||||
|
)?;
|
||||||
|
selected_notes.push(n.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if amount.is_positive() {
|
||||||
|
log::info!("Not enough balance");
|
||||||
|
anyhow::bail!(
|
||||||
|
"Not enough balance, need {} zats, missing {} zats",
|
||||||
|
u64::from(target_amount_with_fee),
|
||||||
|
u64::from(amount)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
log::info!("Preparing tx");
|
||||||
|
let ovk = &fvk.fvk.ovk;
|
||||||
|
|
||||||
|
for r in recipients.iter() {
|
||||||
|
let to_addr = RecipientAddress::decode(&NETWORK, &r.address)
|
||||||
|
.ok_or(anyhow::anyhow!("Invalid address"))?;
|
||||||
|
let amount = Amount::from_u64(r.amount).unwrap();
|
||||||
|
match &to_addr {
|
||||||
|
RecipientAddress::Shielded(_pa) => {
|
||||||
|
log::info!("Sapling output: {}", r.amount);
|
||||||
|
let memo_bytes = hex::decode(&r.memo).unwrap();
|
||||||
|
let memo = MemoBytes::from_bytes(&memo_bytes)?;
|
||||||
|
builder.add_z_output(
|
||||||
|
&r.address,
|
||||||
|
ovk,
|
||||||
|
amount,
|
||||||
|
&memo,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
RecipientAddress::Transparent(_address) => {
|
||||||
|
builder.add_t_output(&r.address, amount)
|
||||||
|
}
|
||||||
|
}?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(selected_notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sign_offline_tx(tx: &Tx, sk: &ExtendedSpendingKey) -> anyhow::Result<Vec<u8>> {
|
||||||
|
let last_height = BlockHeight::from_u32(tx.height as u32);
|
||||||
|
let mut builder = Builder::new(NETWORK, last_height);
|
||||||
|
for txin in tx.inputs.iter() {
|
||||||
|
let mut diversifier = [0u8; 11];
|
||||||
|
hex::decode_to_slice(&txin.diversifier, &mut diversifier)?;
|
||||||
|
let diversifier = Diversifier(diversifier);
|
||||||
|
let fvk = decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &txin.fvk)?.unwrap();
|
||||||
|
let pa = fvk.fvk.vk.to_payment_address(diversifier).unwrap();
|
||||||
|
let mut rseed_bytes = [0u8; 32];
|
||||||
|
hex::decode_to_slice(&txin.rseed, &mut rseed_bytes)?;
|
||||||
|
let rseed = Fr::from_bytes(&rseed_bytes).unwrap();
|
||||||
|
let note = pa.create_note(txin.amount, Rseed::BeforeZip212(rseed)).unwrap();
|
||||||
|
let w = hex::decode(&txin.witness)?;
|
||||||
|
let witness = IncrementalWitness::<Node>::read(&*w)?;
|
||||||
|
let merkle_path = witness.path().unwrap();
|
||||||
|
|
||||||
|
builder.add_sapling_spend(sk.clone(), diversifier, note, merkle_path)?;
|
||||||
|
}
|
||||||
|
for txout in tx.outputs.iter() {
|
||||||
|
let recipient = RecipientAddress::decode(&NETWORK, &txout.addr).unwrap();
|
||||||
|
let amount = Amount::from_u64(txout.amount).unwrap();
|
||||||
|
match recipient {
|
||||||
|
RecipientAddress::Transparent(ta) => {
|
||||||
|
builder.add_transparent_output(&ta, amount)?;
|
||||||
|
}
|
||||||
|
RecipientAddress::Shielded(pa) => {
|
||||||
|
let mut ovk = [0u8; 32];
|
||||||
|
hex::decode_to_slice(&txout.ovk, &mut ovk)?;
|
||||||
|
let ovk = OutgoingViewingKey(ovk);
|
||||||
|
let mut memo = vec![0; 512];
|
||||||
|
let m = hex::decode(&txout.memo)?;
|
||||||
|
memo[..m.len()].copy_from_slice(&m);
|
||||||
|
let memo = MemoBytes::from_bytes(&memo)?;
|
||||||
|
builder.add_sapling_output(Some(ovk), pa, amount, Some(memo))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let prover = LocalTxProver::with_default_location().unwrap();
|
||||||
|
let consensus_branch_id = BranchId::for_height(&NETWORK, last_height);
|
||||||
|
let (tx, _) = builder.build(consensus_branch_id, &prover)?;
|
||||||
|
let mut raw_tx = vec![];
|
||||||
|
tx.write(&mut raw_tx)?;
|
||||||
|
|
||||||
|
Ok(raw_tx)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn broadcast_tx(tx: &[u8], ld_url: &str) -> anyhow::Result<String> {
|
||||||
|
let mut client = connect_lightwalletd(ld_url).await?;
|
||||||
|
let latest_height = get_latest_height(&mut client).await?;
|
||||||
|
let raw_tx = RawTransaction {
|
||||||
|
data: tx.to_vec(),
|
||||||
|
height: latest_height as u64,
|
||||||
|
};
|
||||||
|
let rep = client.send_transaction(Request::new(raw_tx)).await?.into_inner();
|
||||||
|
Ok(rep.error_message)
|
||||||
|
}
|
168
src/wallet.rs
168
src/wallet.rs
|
@ -3,12 +3,10 @@ use crate::key::{decode_key, is_valid_key};
|
||||||
use crate::scan::ProgressCallback;
|
use crate::scan::ProgressCallback;
|
||||||
use crate::taddr::{get_taddr_balance, shield_taddr};
|
use crate::taddr::{get_taddr_balance, shield_taddr};
|
||||||
use crate::{connect_lightwalletd, get_latest_height, BlockId, CTree, DbAdapter, NETWORK};
|
use crate::{connect_lightwalletd, get_latest_height, BlockId, CTree, DbAdapter, NETWORK};
|
||||||
use anyhow::Context;
|
|
||||||
use bip39::{Language, Mnemonic};
|
use bip39::{Language, Mnemonic};
|
||||||
use rand::prelude::SliceRandom;
|
use rand::prelude::SliceRandom;
|
||||||
use rand::rngs::OsRng;
|
use rand::rngs::OsRng;
|
||||||
use rand::RngCore;
|
use rand::RngCore;
|
||||||
use std::str::FromStr;
|
|
||||||
use std::sync::{mpsc, Arc};
|
use std::sync::{mpsc, Arc};
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
use tonic::Request;
|
use tonic::Request;
|
||||||
|
@ -18,13 +16,18 @@ use zcash_client_backend::encoding::{
|
||||||
};
|
};
|
||||||
use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS};
|
use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS};
|
||||||
use zcash_primitives::consensus::{BlockHeight, BranchId, Parameters};
|
use zcash_primitives::consensus::{BlockHeight, BranchId, Parameters};
|
||||||
use zcash_primitives::memo::Memo;
|
|
||||||
use zcash_primitives::transaction::builder::{Builder, Progress};
|
use zcash_primitives::transaction::builder::{Builder, Progress};
|
||||||
use zcash_primitives::transaction::components::amount::{DEFAULT_FEE, MAX_MONEY};
|
use zcash_primitives::transaction::components::amount::MAX_MONEY;
|
||||||
use zcash_primitives::transaction::components::Amount;
|
use zcash_primitives::transaction::components::Amount;
|
||||||
use zcash_primitives::zip32::ExtendedFullViewingKey;
|
use zcash_primitives::zip32::ExtendedFullViewingKey;
|
||||||
use zcash_proofs::prover::LocalTxProver;
|
use zcash_proofs::prover::LocalTxProver;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
use crate::pay::prepare_tx;
|
||||||
|
use zcash_primitives::memo::{Memo, MemoBytes};
|
||||||
|
use std::convert::TryFrom;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use crate::db::SpendableNote;
|
||||||
|
use crate::pay::{ColdTxBuilder, Tx};
|
||||||
|
|
||||||
const DEFAULT_CHUNK_SIZE: u32 = 100_000;
|
const DEFAULT_CHUNK_SIZE: u32 = 100_000;
|
||||||
|
|
||||||
|
@ -205,6 +208,32 @@ impl Wallet {
|
||||||
self._send_payment(account, &recipients, anchor_offset, progress_callback).await
|
self._send_payment(account, &recipients, anchor_offset, progress_callback).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn prepare_payment(
|
||||||
|
&self,
|
||||||
|
account: u32,
|
||||||
|
to_address: &str,
|
||||||
|
amount: u64,
|
||||||
|
memo: &str,
|
||||||
|
max_amount_per_note: u64,
|
||||||
|
anchor_offset: u32,
|
||||||
|
) -> anyhow::Result<String> {
|
||||||
|
let last_height = self.get_latest_height().await?;
|
||||||
|
let recipients = Self::_build_recipients(to_address, amount, max_amount_per_note, memo)?;
|
||||||
|
let tx = self._prepare_payment(account, amount, last_height, &recipients, anchor_offset)?;
|
||||||
|
let tx_str = serde_json::to_string(&tx)?;
|
||||||
|
Ok(tx_str)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _prepare_payment(&self, account: u32, amount: u64, last_height: u32, recipients: &Vec<Recipient>, anchor_offset: u32) -> anyhow::Result<Tx> {
|
||||||
|
let amount = Amount::from_u64(amount).unwrap();
|
||||||
|
let ivk = self.db.get_ivk(account)?;
|
||||||
|
let extfvk = decode_extended_full_viewing_key(NETWORK.hrp_sapling_extended_full_viewing_key(), &ivk)?.unwrap();
|
||||||
|
let notes = self._get_spendable_notes(account, &extfvk, last_height, anchor_offset)?;
|
||||||
|
let mut builder = ColdTxBuilder::new(last_height);
|
||||||
|
prepare_tx(&mut builder, None, ¬es, amount, &extfvk, recipients)?;
|
||||||
|
Ok(builder.tx)
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn send_payment(
|
pub async fn send_payment(
|
||||||
&self,
|
&self,
|
||||||
account: u32,
|
account: u32,
|
||||||
|
@ -215,25 +244,7 @@ impl Wallet {
|
||||||
anchor_offset: u32,
|
anchor_offset: u32,
|
||||||
progress_callback: impl Fn(Progress) + Send + 'static,
|
progress_callback: impl Fn(Progress) + Send + 'static,
|
||||||
) -> anyhow::Result<String> {
|
) -> anyhow::Result<String> {
|
||||||
let mut recipients: Vec<Recipient> = vec![];
|
let recipients = Self::_build_recipients(to_address, amount, max_amount_per_note, memo)?;
|
||||||
let target_amount = Amount::from_u64(amount).unwrap();
|
|
||||||
let max_amount_per_note = if max_amount_per_note != 0 {
|
|
||||||
Amount::from_u64(max_amount_per_note).unwrap()
|
|
||||||
} else {
|
|
||||||
Amount::from_i64(MAX_MONEY).unwrap()
|
|
||||||
};
|
|
||||||
let mut remaining_amount = target_amount;
|
|
||||||
while remaining_amount.is_positive() {
|
|
||||||
let note_amount = remaining_amount.min(max_amount_per_note);
|
|
||||||
let recipient = Recipient {
|
|
||||||
address: to_address.to_string(),
|
|
||||||
amount: u64::from(note_amount),
|
|
||||||
memo: memo.to_string(),
|
|
||||||
};
|
|
||||||
recipients.push(recipient);
|
|
||||||
remaining_amount -= note_amount;
|
|
||||||
}
|
|
||||||
|
|
||||||
self._send_payment(account, &recipients, anchor_offset, progress_callback).await
|
self._send_payment(account, &recipients, anchor_offset, progress_callback).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -250,76 +261,13 @@ impl Wallet {
|
||||||
decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &secret_key)?
|
decode_extended_spending_key(NETWORK.hrp_sapling_extended_spending_key(), &secret_key)?
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let extfvk = ExtendedFullViewingKey::from(&skey);
|
let extfvk = ExtendedFullViewingKey::from(&skey);
|
||||||
let (_, change_address) = extfvk.default_address().unwrap();
|
|
||||||
let ovk = extfvk.fvk.ovk;
|
|
||||||
let last_height = self.get_latest_height().await?;
|
let last_height = self.get_latest_height().await?;
|
||||||
let mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height));
|
let notes = self._get_spendable_notes(account, &extfvk, last_height, anchor_offset)?;
|
||||||
let anchor_height = self
|
|
||||||
.db
|
|
||||||
.get_last_sync_height()?
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("No spendable notes"))?;
|
|
||||||
let anchor_height = anchor_height.min(last_height - anchor_offset);
|
|
||||||
log::info!("Anchor = {}", anchor_height);
|
|
||||||
let mut notes = self
|
|
||||||
.db
|
|
||||||
.get_spendable_notes(account, anchor_height, &extfvk)?;
|
|
||||||
notes.shuffle(&mut OsRng);
|
|
||||||
log::info!("Spendable notes = {}", notes.len());
|
log::info!("Spendable notes = {}", notes.len());
|
||||||
|
|
||||||
let mut amount = target_amount;
|
let mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height));
|
||||||
amount += DEFAULT_FEE;
|
|
||||||
let target_amount_with_fee = amount;
|
|
||||||
let mut selected_note: Vec<u32> = vec![];
|
|
||||||
for n in notes.iter() {
|
|
||||||
if amount.is_positive() {
|
|
||||||
let a = amount.min(
|
|
||||||
Amount::from_u64(n.note.value)
|
|
||||||
.map_err(|_| anyhow::anyhow!("Invalid amount"))?,
|
|
||||||
);
|
|
||||||
amount -= a;
|
|
||||||
let merkle_path = n.witness.path().context("Invalid Merkle Path")?;
|
|
||||||
let mut witness_bytes: Vec<u8> = vec![];
|
|
||||||
n.witness.write(&mut witness_bytes)?;
|
|
||||||
builder.add_sapling_spend(
|
|
||||||
skey.clone(),
|
|
||||||
n.diversifier,
|
|
||||||
n.note.clone(),
|
|
||||||
merkle_path,
|
|
||||||
)?;
|
|
||||||
selected_note.push(n.id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if amount.is_positive() {
|
|
||||||
log::info!("Not enough balance");
|
|
||||||
anyhow::bail!(
|
|
||||||
"Not enough balance, need {} zats, missing {} zats",
|
|
||||||
u64::from(target_amount_with_fee),
|
|
||||||
u64::from(amount)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Preparing tx");
|
log::info!("Preparing tx");
|
||||||
builder.send_change_to(ovk, change_address);
|
let selected_notes = prepare_tx(&mut builder, Some(skey.clone()), ¬es, target_amount, &extfvk, recipients)?;
|
||||||
|
|
||||||
for r in recipients.iter() {
|
|
||||||
let to_addr = RecipientAddress::decode(&NETWORK, &r.address)
|
|
||||||
.ok_or(anyhow::anyhow!("Invalid address"))?;
|
|
||||||
let amount = Amount::from_u64(r.amount).unwrap();
|
|
||||||
match &to_addr {
|
|
||||||
RecipientAddress::Shielded(pa) => {
|
|
||||||
log::info!("Sapling output: {}", r.amount);
|
|
||||||
builder.add_sapling_output(
|
|
||||||
Some(ovk),
|
|
||||||
pa.clone(),
|
|
||||||
amount,
|
|
||||||
Some(Memo::from_str(&r.memo)?.into()),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
RecipientAddress::Transparent(t_address) => {
|
|
||||||
builder.add_transparent_output(&t_address, amount)
|
|
||||||
}
|
|
||||||
}?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let (progress_tx, progress_rx) = mpsc::channel::<Progress>();
|
let (progress_tx, progress_rx) = mpsc::channel::<Progress>();
|
||||||
|
|
||||||
|
@ -342,7 +290,7 @@ impl Wallet {
|
||||||
let tx_id = send_transaction(&mut client, &raw_tx, last_height).await?;
|
let tx_id = send_transaction(&mut client, &raw_tx, last_height).await?;
|
||||||
log::info!("Tx ID = {}", tx_id);
|
log::info!("Tx ID = {}", tx_id);
|
||||||
|
|
||||||
for id_note in selected_note.iter() {
|
for id_note in selected_notes.iter() {
|
||||||
self.db.mark_spent(*id_note, 0)?;
|
self.db.mark_spent(*id_note, 0)?;
|
||||||
}
|
}
|
||||||
Ok(tx_id)
|
Ok(tx_id)
|
||||||
|
@ -387,6 +335,46 @@ impl Wallet {
|
||||||
self.ld_url = ld_url.to_string();
|
self.ld_url = ld_url.to_string();
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn _get_spendable_notes(&self, account: u32, extfvk: &ExtendedFullViewingKey, last_height: u32, anchor_offset: u32) -> anyhow::Result<Vec<SpendableNote>> {
|
||||||
|
let anchor_height = self
|
||||||
|
.db
|
||||||
|
.get_last_sync_height()?
|
||||||
|
.ok_or_else(|| anyhow::anyhow!("No spendable notes"))?;
|
||||||
|
let anchor_height = anchor_height.min(last_height - anchor_offset);
|
||||||
|
log::info!("Anchor = {}", anchor_height);
|
||||||
|
let mut notes = self
|
||||||
|
.db
|
||||||
|
.get_spendable_notes(account, anchor_height, extfvk)?;
|
||||||
|
notes.shuffle(&mut OsRng);
|
||||||
|
log::info!("Spendable notes = {}", notes.len());
|
||||||
|
|
||||||
|
Ok(notes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn _build_recipients(to_address: &str, amount: u64, max_amount_per_note: u64, memo: &str) -> anyhow::Result<Vec<Recipient>> {
|
||||||
|
let mut recipients: Vec<Recipient> = vec![];
|
||||||
|
let target_amount = Amount::from_u64(amount).unwrap();
|
||||||
|
let max_amount_per_note = if max_amount_per_note != 0 {
|
||||||
|
Amount::from_u64(max_amount_per_note).unwrap()
|
||||||
|
} else {
|
||||||
|
Amount::from_i64(MAX_MONEY).unwrap()
|
||||||
|
};
|
||||||
|
let mut remaining_amount = target_amount;
|
||||||
|
let memo = Memo::from_str(memo)?;
|
||||||
|
let memo_bytes = MemoBytes::try_from(memo)?;
|
||||||
|
while remaining_amount.is_positive() {
|
||||||
|
let note_amount = remaining_amount.min(max_amount_per_note);
|
||||||
|
let recipient = Recipient {
|
||||||
|
address: to_address.to_string(),
|
||||||
|
amount: u64::from(note_amount),
|
||||||
|
memo: hex::encode(memo_bytes.as_slice()),
|
||||||
|
};
|
||||||
|
recipients.push(recipient);
|
||||||
|
remaining_amount -= note_amount;
|
||||||
|
}
|
||||||
|
Ok(recipients)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
|
Loading…
Reference in New Issue