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"
|
||||
path = "src/main/cli.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "sign"
|
||||
path = "src/main/sign.rs"
|
||||
|
||||
[dependencies]
|
||||
dotenv = "0.15.0"
|
||||
env_logger = "0.8.4"
|
||||
|
|
|
@ -27,6 +27,7 @@ mod print;
|
|||
mod scan;
|
||||
mod taddr;
|
||||
mod transaction;
|
||||
mod pay;
|
||||
mod wallet;
|
||||
|
||||
pub use crate::builder::advance_tree;
|
||||
|
@ -37,10 +38,11 @@ pub use crate::chain::{
|
|||
pub use crate::commitment::{CTree, Witness};
|
||||
pub use crate::db::DbAdapter;
|
||||
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::*;
|
||||
pub use crate::mempool::MemPool;
|
||||
pub use crate::print::*;
|
||||
pub use crate::scan::{latest_height, scan_all, sync_async};
|
||||
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);
|
||||
};
|
||||
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("zecpages", &ivk).unwrap();
|
||||
// wallet.new_account_with_key("zecpages", &ivk).unwrap();
|
||||
|
||||
let res = wallet.sync(true, ANCHOR_OFFSET, progress).await;
|
||||
if let Err(err) = res {
|
||||
|
@ -46,7 +46,7 @@ async fn test() -> anyhow::Result<()> {
|
|||
// &address,
|
||||
// 50000,
|
||||
// "test memo",
|
||||
// u64::max_value(),
|
||||
// 0,
|
||||
// 2,
|
||||
// move |progress| {
|
||||
// println!("{}", progress.cur());
|
||||
|
@ -56,6 +56,18 @@ async fn test() -> anyhow::Result<()> {
|
|||
// .unwrap();
|
||||
// println!("TXID = {}", tx_id);
|
||||
|
||||
let tx = wallet
|
||||
.prepare_payment(
|
||||
1,
|
||||
&address,
|
||||
50000,
|
||||
"test memo",
|
||||
0,
|
||||
2)
|
||||
.await
|
||||
.unwrap();
|
||||
println!("TX = {}", tx);
|
||||
|
||||
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::taddr::{get_taddr_balance, shield_taddr};
|
||||
use crate::{connect_lightwalletd, get_latest_height, BlockId, CTree, DbAdapter, NETWORK};
|
||||
use anyhow::Context;
|
||||
use bip39::{Language, Mnemonic};
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::rngs::OsRng;
|
||||
use rand::RngCore;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{mpsc, Arc};
|
||||
use tokio::sync::Mutex;
|
||||
use tonic::Request;
|
||||
|
@ -18,13 +16,18 @@ use zcash_client_backend::encoding::{
|
|||
};
|
||||
use zcash_params::{OUTPUT_PARAMS, SPEND_PARAMS};
|
||||
use zcash_primitives::consensus::{BlockHeight, BranchId, Parameters};
|
||||
use zcash_primitives::memo::Memo;
|
||||
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::zip32::ExtendedFullViewingKey;
|
||||
use zcash_proofs::prover::LocalTxProver;
|
||||
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;
|
||||
|
||||
|
@ -205,6 +208,32 @@ impl Wallet {
|
|||
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(
|
||||
&self,
|
||||
account: u32,
|
||||
|
@ -215,25 +244,7 @@ impl Wallet {
|
|||
anchor_offset: u32,
|
||||
progress_callback: impl Fn(Progress) + Send + 'static,
|
||||
) -> anyhow::Result<String> {
|
||||
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;
|
||||
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;
|
||||
}
|
||||
|
||||
let recipients = Self::_build_recipients(to_address, amount, max_amount_per_note, memo)?;
|
||||
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)?
|
||||
.unwrap();
|
||||
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 mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height));
|
||||
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);
|
||||
let notes = self._get_spendable_notes(account, &extfvk, last_height, anchor_offset)?;
|
||||
log::info!("Spendable notes = {}", notes.len());
|
||||
|
||||
let mut amount = target_amount;
|
||||
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)
|
||||
);
|
||||
}
|
||||
|
||||
let mut builder = Builder::new(NETWORK, BlockHeight::from_u32(last_height));
|
||||
log::info!("Preparing tx");
|
||||
builder.send_change_to(ovk, change_address);
|
||||
|
||||
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 selected_notes = prepare_tx(&mut builder, Some(skey.clone()), ¬es, target_amount, &extfvk, recipients)?;
|
||||
|
||||
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?;
|
||||
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)?;
|
||||
}
|
||||
Ok(tx_id)
|
||||
|
@ -387,6 +335,46 @@ impl Wallet {
|
|||
self.ld_url = ld_url.to_string();
|
||||
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)]
|
||||
|
|
Loading…
Reference in New Issue