use crate::api::account::get_unified_address; use crate::api::recipient::RecipientMemo; use crate::api::sync::get_latest_height; pub use crate::broadcast_tx; use crate::chain::{get_checkpoint_height, EXPIRY_HEIGHT_OFFSET}; use crate::note_selection::{FeeFlat, Order, UTXO}; use crate::{ build_tx, fetch_utxos, get_secret_keys, note_selection, AccountData, CoinConfig, TransactionBuilderConfig, TransactionBuilderError, TransactionPlan, TxBuilderContext, MAX_ATTEMPTS, }; use rand::rngs::OsRng; use std::cmp::min; use std::slice; use std::str::FromStr; use zcash_client_backend::encoding::decode_extended_full_viewing_key; use zcash_primitives::consensus::Parameters; use zcash_primitives::memo::{Memo, MemoBytes}; use zcash_primitives::transaction::builder::Progress; #[allow(dead_code)] type PaymentProgressCallback = Box; pub async fn build_tx_plan_with_utxos( coin: u8, account: u32, checkpoint_height: u32, expiry_height: u32, recipients: &[RecipientMemo], utxos: &[UTXO], ) -> note_selection::Result { let c = CoinConfig::get(coin); let network = c.chain.network(); let mut recipient_fee = false; for r in recipients { if r.fee_included { if recipient_fee { return Err(TransactionBuilderError::DuplicateRecipientFee); } recipient_fee = true; } } let (fvk, taddr, orchard_fvk) = { let db = c.db()?; let AccountData { fvk, .. } = db.get_account_info(account)?; let taddr = db.get_taddr(account)?.unwrap_or_default(); let orchard_fvk = db.get_orchard(account)?.map(|o| hex::encode(&o.fvk)).unwrap_or_default(); (fvk, taddr, orchard_fvk) }; let change_address = get_unified_address(coin, account, 7)?; let context = TxBuilderContext::from_height(coin, checkpoint_height)?; let mut orders = vec![]; let mut id_order = 0; for r in recipients { let mut amount = r.amount; let max_amount_per_note = if r.max_amount_per_note == 0 { u64::MAX } else { r.max_amount_per_note }; loop { let a = min(amount, max_amount_per_note); let memo_bytes: MemoBytes = r.memo.clone().into(); let order = Order::new(network, id_order, &r.address, a, false, memo_bytes); orders.push(order); amount -= a; id_order += 1; if amount == 0 { break; } // at least one note even when amount = 0 } orders.last_mut().unwrap().take_fee = r.fee_included; } let config = TransactionBuilderConfig::new(&change_address); let tx_plan = note_selection::build_tx_plan::( network, &fvk, &taddr, &orchard_fvk, checkpoint_height, expiry_height, &context.orchard_anchor, &utxos, &orders, &config, )?; Ok(tx_plan) } pub async fn build_tx_plan( coin: u8, account: u32, last_height: u32, recipients: &[RecipientMemo], excluded_flags: u8, confirmations: u32, ) -> note_selection::Result { let checkpoint_height = { let c = CoinConfig::get(coin); let db = c.db()?; let checkpoint_height = get_checkpoint_height(&db, last_height, confirmations)?; checkpoint_height }; let expiry_height = get_latest_height().await? + EXPIRY_HEIGHT_OFFSET; let utxos = fetch_utxos(coin, account, checkpoint_height, excluded_flags).await?; let tx_plan = build_tx_plan_with_utxos( coin, account, checkpoint_height, expiry_height, recipients, &utxos, ) .await?; Ok(tx_plan) } pub fn sign_plan(coin: u8, account: u32, tx_plan: &TransactionPlan) -> anyhow::Result> { let c = CoinConfig::get(coin); let network = c.chain.network(); let fvk = { let db = c.db()?; let AccountData { fvk, .. } = db.get_account_info(account)?; fvk }; let fvk = decode_extended_full_viewing_key(network.hrp_sapling_extended_full_viewing_key(), &fvk) .unwrap() .to_diversifiable_full_viewing_key(); let tx_plan_fvk = decode_extended_full_viewing_key( network.hrp_sapling_extended_full_viewing_key(), &tx_plan.fvk, ) .unwrap() .to_diversifiable_full_viewing_key(); if fvk.to_bytes() != tx_plan_fvk.to_bytes() { return Err(anyhow::anyhow!("Account does not match transaction")); } let keys = get_secret_keys(coin, account)?; let tx = build_tx(c.chain.network(), &keys, &tx_plan, OsRng)?; Ok(tx) } pub async fn sign_and_broadcast( coin: u8, account: u32, tx_plan: &TransactionPlan, ) -> anyhow::Result { let tx = sign_plan(coin, account, tx_plan)?; let txid = broadcast_tx(&tx).await?; let id_notes: Vec<_> = tx_plan .spends .iter() .filter_map(|n| if n.id != 0 { Some(n.id) } else { None }) .collect(); mark_spent(coin, &id_notes)?; Ok(txid) } pub async fn build_max_tx( coin: u8, account: u32, last_height: u32, recipient: &RecipientMemo, // amount & max_amount per note are ignored excluded_flags: u8, confirmations: u32, ) -> note_selection::Result { let mut recipient = recipient.clone(); let checkpoint_height = { let c = CoinConfig::get(coin); let db = c.db()?; get_checkpoint_height(&db, last_height, confirmations)? }; let utxos = fetch_utxos(coin, account, checkpoint_height, excluded_flags).await?; let available_funds: u64 = utxos.iter().map(|n| n.amount).sum(); recipient.amount = available_funds; for _ in 0..MAX_ATTEMPTS { // this will fail at least once because of the fees let result = build_tx_plan( coin, account, last_height, slice::from_ref(&recipient), excluded_flags, confirmations, ) .await; match result { Err(TransactionBuilderError::NotEnoughFunds(missing)) => { recipient.amount -= missing; // reduce the amount and retry } _ => return result, } } Err(TransactionBuilderError::TxTooComplex) } pub async fn transfer_pools( coin: u8, account: u32, from_pool: u8, to_pool: u8, amount: u64, fee_included: bool, memo: &str, split_amount: u64, confirmations: u32, ) -> anyhow::Result { let address = get_unified_address(coin, account, to_pool)?; // get our own unified address let recipient = RecipientMemo { address, amount, fee_included, memo: Memo::from_str(memo)?, max_amount_per_note: split_amount, }; let last_height = get_latest_height().await?; let tx_plan = build_tx_plan( coin, account, last_height, slice::from_ref(&recipient), !from_pool, confirmations, ) .await?; Ok(tx_plan) } /// Make a transaction that shields the transparent balance pub async fn shield_taddr( coin: u8, account: u32, amount: u64, confirmations: u32, ) -> anyhow::Result { let tx_plan = transfer_pools( coin, account, 1, 6, amount, true, "Shield Transparent Balance", 0, confirmations, ) .await?; Ok(tx_plan) } fn mark_spent(coin: u8, ids: &[u32]) -> anyhow::Result<()> { let c = CoinConfig::get(coin); let mut db = c.db()?; db.tx_mark_spend(ids)?; Ok(()) }