2022-11-12 17:39:12 -08:00
|
|
|
use crate::api::account::get_unified_address;
|
2022-11-17 01:13:51 -08:00
|
|
|
use crate::api::recipient::RecipientMemo;
|
2022-11-12 17:39:12 -08:00
|
|
|
use crate::api::sync::get_latest_height;
|
|
|
|
pub use crate::broadcast_tx;
|
2022-11-17 01:13:51 -08:00
|
|
|
use crate::note_selection::{FeeFlat, Order};
|
2022-11-12 17:39:12 -08:00
|
|
|
use crate::{
|
2022-11-15 19:51:52 -08:00
|
|
|
build_tx, fetch_utxos, get_secret_keys, note_selection, AccountData, CoinConfig, DbAdapter,
|
|
|
|
TransactionBuilderConfig, TransactionBuilderError, TransactionPlan, TxBuilderContext,
|
|
|
|
MAX_ATTEMPTS,
|
2022-11-12 17:39:12 -08:00
|
|
|
};
|
|
|
|
use rand::rngs::OsRng;
|
2022-11-06 04:50:51 -08:00
|
|
|
use std::cmp::min;
|
2022-11-15 19:51:52 -08:00
|
|
|
use std::slice;
|
|
|
|
use std::str::FromStr;
|
2022-11-17 01:13:51 -08:00
|
|
|
use zcash_primitives::memo::{Memo, MemoBytes};
|
2022-11-12 17:39:12 -08:00
|
|
|
use zcash_primitives::transaction::builder::Progress;
|
2022-11-04 18:58:35 -07:00
|
|
|
|
2022-11-17 01:13:51 -08:00
|
|
|
#[allow(dead_code)]
|
2022-11-12 17:39:12 -08:00
|
|
|
type PaymentProgressCallback = Box<dyn Fn(Progress) + Send + Sync>;
|
2022-11-06 04:50:51 -08:00
|
|
|
|
|
|
|
pub async fn build_tx_plan(
|
|
|
|
coin: u8,
|
|
|
|
account: u32,
|
|
|
|
last_height: u32,
|
|
|
|
recipients: &[RecipientMemo],
|
2022-11-15 19:51:52 -08:00
|
|
|
excluded_flags: u8,
|
2022-11-06 04:50:51 -08:00
|
|
|
confirmations: u32,
|
2022-11-15 19:51:52 -08:00
|
|
|
) -> note_selection::Result<TransactionPlan> {
|
2022-11-06 04:50:51 -08:00
|
|
|
let c = CoinConfig::get(coin);
|
2022-11-23 00:59:22 -08:00
|
|
|
let network = c.chain.network();
|
2022-11-15 02:21:47 -08:00
|
|
|
let (fvk, checkpoint_height) = {
|
2022-11-06 04:50:51 -08:00
|
|
|
let db = c.db()?;
|
|
|
|
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
2022-11-15 19:51:52 -08:00
|
|
|
let checkpoint_height = get_checkpoint_height(&db, last_height, confirmations)?;
|
2022-11-15 02:21:47 -08:00
|
|
|
(fvk, checkpoint_height)
|
2022-11-06 04:50:51 -08:00
|
|
|
};
|
2022-11-29 07:24:01 -08:00
|
|
|
let change_address = get_unified_address(coin, account, 7)?;
|
2022-11-15 02:21:47 -08:00
|
|
|
let context = TxBuilderContext::from_height(coin, checkpoint_height)?;
|
2022-11-06 04:50:51 -08:00
|
|
|
|
|
|
|
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
|
|
|
|
};
|
2022-11-26 19:48:22 -08:00
|
|
|
loop {
|
2022-11-06 04:50:51 -08:00
|
|
|
let a = min(amount, max_amount_per_note);
|
|
|
|
let memo_bytes: MemoBytes = r.memo.clone().into();
|
2022-11-23 00:59:22 -08:00
|
|
|
let order = Order::new(network, id_order, &r.address, a, memo_bytes);
|
2022-11-06 04:50:51 -08:00
|
|
|
orders.push(order);
|
|
|
|
amount -= a;
|
|
|
|
id_order += 1;
|
2022-11-26 19:48:22 -08:00
|
|
|
if amount == 0 {
|
|
|
|
break;
|
|
|
|
} // at least one note even when amount = 0
|
2022-11-06 04:50:51 -08:00
|
|
|
}
|
|
|
|
}
|
2022-11-15 19:51:52 -08:00
|
|
|
let utxos = fetch_utxos(coin, account, checkpoint_height, excluded_flags).await?;
|
2022-11-06 04:50:51 -08:00
|
|
|
|
2022-11-12 17:39:12 -08:00
|
|
|
let config = TransactionBuilderConfig::new(&change_address);
|
2022-11-15 02:21:47 -08:00
|
|
|
let tx_plan = crate::note_selection::build_tx_plan::<FeeFlat>(
|
2022-11-23 00:59:22 -08:00
|
|
|
network,
|
2022-11-12 17:39:12 -08:00
|
|
|
&fvk,
|
2022-11-15 02:21:47 -08:00
|
|
|
checkpoint_height,
|
|
|
|
&context.orchard_anchor,
|
2022-11-12 17:39:12 -08:00
|
|
|
&utxos,
|
|
|
|
&orders,
|
|
|
|
&config,
|
|
|
|
)?;
|
2022-11-06 04:50:51 -08:00
|
|
|
Ok(tx_plan)
|
|
|
|
}
|
2022-11-12 17:39:12 -08:00
|
|
|
|
|
|
|
pub fn sign_plan(coin: u8, account: u32, tx_plan: &TransactionPlan) -> anyhow::Result<Vec<u8>> {
|
|
|
|
let c = CoinConfig::get(coin);
|
|
|
|
let fvk = {
|
|
|
|
let db = c.db()?;
|
|
|
|
let AccountData { fvk, .. } = db.get_account_info(account)?;
|
|
|
|
fvk
|
|
|
|
};
|
|
|
|
|
|
|
|
if fvk != tx_plan.fvk {
|
|
|
|
return Err(anyhow::anyhow!("Account does not match transaction"));
|
|
|
|
}
|
|
|
|
|
|
|
|
let keys = get_secret_keys(coin, account)?;
|
2022-11-15 02:21:47 -08:00
|
|
|
let tx = build_tx(c.chain.network(), &keys, &tx_plan, OsRng)?;
|
2022-11-12 17:39:12 -08:00
|
|
|
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?;
|
2022-11-13 01:00:21 -08:00
|
|
|
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)?;
|
2022-11-12 17:39:12 -08:00
|
|
|
Ok(txid)
|
|
|
|
}
|
|
|
|
|
2022-11-15 19:51:52 -08:00
|
|
|
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)
|
|
|
|
}
|
|
|
|
|
2022-11-29 07:24:01 -08:00
|
|
|
pub enum AmountOrMax {
|
|
|
|
Amount(u64),
|
|
|
|
Max
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn transfer_pools(coin: u8, account: u32, from_pool: u8, to_pool: u8, amount: AmountOrMax,
|
2022-12-05 09:43:50 -08:00
|
|
|
memo: &str, confirmations: u32) -> anyhow::Result<TransactionPlan> {
|
2022-11-29 07:24:01 -08:00
|
|
|
let address = get_unified_address(coin, account, to_pool)?; // get our own unified address
|
|
|
|
let a = match amount {
|
|
|
|
AmountOrMax::Amount(a) => a,
|
|
|
|
AmountOrMax::Max => 0,
|
|
|
|
};
|
2022-11-15 19:51:52 -08:00
|
|
|
let recipient = RecipientMemo {
|
|
|
|
address,
|
2022-11-29 07:24:01 -08:00
|
|
|
amount: a,
|
2022-12-05 09:43:50 -08:00
|
|
|
memo: Memo::from_str(memo)?,
|
2022-11-15 19:51:52 -08:00
|
|
|
max_amount_per_note: 0,
|
|
|
|
};
|
|
|
|
let last_height = get_latest_height().await?;
|
2022-11-29 07:24:01 -08:00
|
|
|
let tx_plan = match amount {
|
|
|
|
AmountOrMax::Amount(_) => build_tx_plan(coin, account, last_height, slice::from_ref(&recipient),
|
|
|
|
!from_pool, confirmations).await?,
|
|
|
|
AmountOrMax::Max => build_max_tx(coin, account, last_height, &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, confirmations: u32) -> anyhow::Result<String> {
|
2022-12-05 09:43:50 -08:00
|
|
|
let tx_plan = transfer_pools(coin, account, 1, 6, AmountOrMax::Max, "Shield Transparent Balance", confirmations).await?;
|
2022-11-15 19:51:52 -08:00
|
|
|
let tx_id = sign_and_broadcast(coin, account, &tx_plan).await?;
|
|
|
|
log::info!("TXID: {}", tx_id);
|
|
|
|
Ok(tx_id)
|
2022-11-12 17:39:12 -08:00
|
|
|
}
|
2022-11-13 01:00:21 -08:00
|
|
|
|
|
|
|
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(())
|
|
|
|
}
|
2022-11-15 19:51:52 -08:00
|
|
|
|
|
|
|
fn get_checkpoint_height(
|
|
|
|
db: &DbAdapter,
|
|
|
|
last_height: u32,
|
|
|
|
confirmations: u32,
|
|
|
|
) -> anyhow::Result<u32> {
|
|
|
|
let anchor_height = last_height.saturating_sub(confirmations);
|
|
|
|
let checkpoint_height = db
|
|
|
|
.get_checkpoint_height(anchor_height)?
|
|
|
|
.unwrap_or_else(|| db.sapling_activation_height()); // get the latest checkpoint before the requested anchor height
|
|
|
|
Ok(checkpoint_height)
|
|
|
|
}
|