264 lines
7.6 KiB
Rust
264 lines
7.6 KiB
Rust
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<dyn Fn(Progress) + Send + Sync>;
|
|
|
|
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<TransactionPlan> {
|
|
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::<FeeFlat>(
|
|
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<TransactionPlan> {
|
|
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<Vec<u8>> {
|
|
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<String> {
|
|
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<TransactionPlan> {
|
|
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<TransactionPlan> {
|
|
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<TransactionPlan> {
|
|
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(())
|
|
}
|