Sender or Recipient pays fee

This commit is contained in:
Hanh 2022-12-11 06:01:10 +08:00
parent a817b05259
commit c269eafe36
9 changed files with 78 additions and 40 deletions

View File

@ -110,11 +110,15 @@ struct CResult_____c_char transfer_pools(uint8_t coin,
uint8_t from_pool, uint8_t from_pool,
uint8_t to_pool, uint8_t to_pool,
uint64_t amount, uint64_t amount,
bool fee_included,
char *memo, char *memo,
uint64_t split_amount, uint64_t split_amount,
uint32_t confirmations); uint32_t confirmations);
struct CResult_____c_char shield_taddr(uint8_t coin, uint32_t account, uint32_t confirmations); struct CResult_____c_char shield_taddr(uint8_t coin,
uint32_t account,
uint64_t amount,
uint32_t confirmations);
void scan_transparent_accounts(uint32_t gap_limit); void scan_transparent_accounts(uint32_t gap_limit);

View File

@ -45,6 +45,7 @@ async fn save_contacts_tx(memos: &[Memo], anchor_offset: u32) -> anyhow::Result<
.map(|m| RecipientMemo { .map(|m| RecipientMemo {
address: address.clone(), address: address.clone(),
amount: 0, amount: 0,
fee_included: false,
memo: m.clone(), memo: m.clone(),
max_amount_per_note: 0, max_amount_per_note: 0,
}) })

View File

@ -12,7 +12,6 @@ use std::path::Path;
use std::sync::Mutex; use std::sync::Mutex;
use tokio::sync::Semaphore; use tokio::sync::Semaphore;
use zcash_primitives::transaction::builder::Progress; use zcash_primitives::transaction::builder::Progress;
use crate::api::payment_v2::AmountOrMax;
static mut POST_COBJ: Option<ffi::DartPostCObjectFnType> = None; static mut POST_COBJ: Option<ffi::DartPostCObjectFnType> = None;
@ -424,6 +423,7 @@ pub async unsafe extern "C" fn transfer_pools(
account: u32, account: u32,
from_pool: u8, to_pool: u8, from_pool: u8, to_pool: u8,
amount: u64, amount: u64,
fee_included: bool,
memo: *mut c_char, memo: *mut c_char,
split_amount: u64, split_amount: u64,
confirmations: u32, confirmations: u32,
@ -431,7 +431,7 @@ pub async unsafe extern "C" fn transfer_pools(
from_c_str!(memo); from_c_str!(memo);
let res = async move { let res = async move {
let tx_plan = crate::api::payment_v2::transfer_pools(coin, account, from_pool, to_pool, let tx_plan = crate::api::payment_v2::transfer_pools(coin, account, from_pool, to_pool,
AmountOrMax::Amount(amount), amount, fee_included,
&memo, split_amount, confirmations).await?; &memo, split_amount, confirmations).await?;
let tx_plan = serde_json::to_string(&tx_plan)?; let tx_plan = serde_json::to_string(&tx_plan)?;
Ok::<_, anyhow::Error>(tx_plan) Ok::<_, anyhow::Error>(tx_plan)
@ -444,9 +444,10 @@ pub async unsafe extern "C" fn transfer_pools(
pub async unsafe extern "C" fn shield_taddr( pub async unsafe extern "C" fn shield_taddr(
coin: u8, coin: u8,
account: u32, account: u32,
amount: u64,
confirmations: u32, confirmations: u32,
) -> CResult<*mut c_char> { ) -> CResult<*mut c_char> {
let res = crate::api::payment_v2::shield_taddr(coin, account, confirmations).await; let res = crate::api::payment_v2::shield_taddr(coin, account, amount, confirmations).await;
to_cresult_str(res) to_cresult_str(res)
} }

View File

@ -28,6 +28,15 @@ pub async fn build_tx_plan(
) -> note_selection::Result<TransactionPlan> { ) -> note_selection::Result<TransactionPlan> {
let c = CoinConfig::get(coin); let c = CoinConfig::get(coin);
let network = c.chain.network(); 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, checkpoint_height) = { let (fvk, checkpoint_height) = {
let db = c.db()?; let db = c.db()?;
let AccountData { fvk, .. } = db.get_account_info(account)?; let AccountData { fvk, .. } = db.get_account_info(account)?;
@ -49,7 +58,7 @@ pub async fn build_tx_plan(
loop { loop {
let a = min(amount, max_amount_per_note); let a = min(amount, max_amount_per_note);
let memo_bytes: MemoBytes = r.memo.clone().into(); let memo_bytes: MemoBytes = r.memo.clone().into();
let order = Order::new(network, id_order, &r.address, a, memo_bytes); let order = Order::new(network, id_order, &r.address, a, false, memo_bytes);
orders.push(order); orders.push(order);
amount -= a; amount -= a;
id_order += 1; id_order += 1;
@ -57,6 +66,7 @@ pub async fn build_tx_plan(
break; break;
} // at least one note even when amount = 0 } // at least one note even when amount = 0
} }
orders.last_mut().unwrap().take_fee = r.fee_included;
} }
let utxos = fetch_utxos(coin, account, checkpoint_height, excluded_flags).await?; let utxos = fetch_utxos(coin, account, checkpoint_height, excluded_flags).await?;
@ -144,36 +154,25 @@ pub async fn build_max_tx(
Err(TransactionBuilderError::TxTooComplex) Err(TransactionBuilderError::TxTooComplex)
} }
pub enum AmountOrMax { pub async fn transfer_pools(coin: u8, account: u32, from_pool: u8, to_pool: u8, amount: u64,
Amount(u64), fee_included: bool, memo: &str, split_amount: u64, confirmations: u32) -> anyhow::Result<TransactionPlan> {
Max
}
pub async fn transfer_pools(coin: u8, account: u32, from_pool: u8, to_pool: u8, amount: AmountOrMax,
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 address = get_unified_address(coin, account, to_pool)?; // get our own unified address
let a = match amount {
AmountOrMax::Amount(a) => a,
AmountOrMax::Max => 0,
};
let recipient = RecipientMemo { let recipient = RecipientMemo {
address, address,
amount: a, amount,
fee_included,
memo: Memo::from_str(memo)?, memo: Memo::from_str(memo)?,
max_amount_per_note: split_amount, max_amount_per_note: split_amount,
}; };
let last_height = get_latest_height().await?; let last_height = get_latest_height().await?;
let tx_plan = match amount { let tx_plan = build_tx_plan(coin, account, last_height, slice::from_ref(&recipient),
AmountOrMax::Amount(_) => build_tx_plan(coin, account, last_height, slice::from_ref(&recipient), !from_pool, confirmations).await?;
!from_pool, confirmations).await?,
AmountOrMax::Max => build_max_tx(coin, account, last_height, &recipient, !from_pool, confirmations).await?,
};
Ok(tx_plan) Ok(tx_plan)
} }
/// Make a transaction that shields the transparent balance /// Make a transaction that shields the transparent balance
pub async fn shield_taddr(coin: u8, account: u32, confirmations: u32) -> anyhow::Result<String> { pub async fn shield_taddr(coin: u8, account: u32, amount: u64, confirmations: u32) -> anyhow::Result<String> {
let tx_plan = transfer_pools(coin, account, 1, 6, AmountOrMax::Max, "Shield Transparent Balance", 0, confirmations).await?; let tx_plan = transfer_pools(coin, account, 1, 6, amount, true, "Shield Transparent Balance", 0, confirmations).await?;
let tx_id = sign_and_broadcast(coin, account, &tx_plan).await?; let tx_id = sign_and_broadcast(coin, account, &tx_plan).await?;
log::info!("TXID: {}", tx_id); log::info!("TXID: {}", tx_id);
Ok(tx_id) Ok(tx_id)

View File

@ -8,6 +8,7 @@ use zcash_primitives::memo::Memo;
pub struct Recipient { pub struct Recipient {
pub address: String, pub address: String,
pub amount: u64, pub amount: u64,
pub fee_included: bool,
pub reply_to: bool, pub reply_to: bool,
pub subject: String, pub subject: String,
pub memo: String, pub memo: String,
@ -24,6 +25,7 @@ pub struct RecipientShort {
pub struct RecipientMemo { pub struct RecipientMemo {
pub address: String, pub address: String,
pub amount: u64, pub amount: u64,
pub fee_included: bool,
pub memo: Memo, pub memo: Memo,
pub max_amount_per_note: u64, pub max_amount_per_note: u64,
} }
@ -38,6 +40,7 @@ impl RecipientMemo {
Ok(RecipientMemo { Ok(RecipientMemo {
address: r.address.clone(), address: r.address.clone(),
amount: r.amount, amount: r.amount,
fee_included: r.fee_included,
memo: Memo::from_str(&memo)?, memo: Memo::from_str(&memo)?,
max_amount_per_note: r.max_amount_per_note, max_amount_per_note: r.max_amount_per_note,
}) })
@ -49,6 +52,7 @@ impl From<RecipientShort> for RecipientMemo {
RecipientMemo { RecipientMemo {
address: r.address, address: r.address,
amount: r.amount, amount: r.amount,
fee_included: false,
memo: Memo::Empty, memo: Memo::Empty,
max_amount_per_note: 0, max_amount_per_note: 0,
} }

View File

@ -1,10 +1,7 @@
use crate::coinconfig::CoinConfig; use crate::coinconfig::CoinConfig;
use anyhow::anyhow; use anyhow::anyhow;
use bip39::{Language, Mnemonic, Seed}; use bip39::{Language, Mnemonic, Seed};
use zcash_client_backend::encoding::{ use zcash_client_backend::encoding::{decode_extended_full_viewing_key, decode_extended_spending_key, decode_payment_address, decode_transparent_address, encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address};
decode_extended_full_viewing_key, decode_extended_spending_key,
encode_extended_full_viewing_key, encode_extended_spending_key, encode_payment_address,
};
use zcash_client_backend::keys::UnifiedFullViewingKey; use zcash_client_backend::keys::UnifiedFullViewingKey;
use zcash_primitives::consensus::{Network, Parameters}; use zcash_primitives::consensus::{Network, Parameters};
use zcash_primitives::zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey}; use zcash_primitives::zip32::{ChildIndex, ExtendedFullViewingKey, ExtendedSpendingKey};
@ -80,8 +77,21 @@ pub fn is_valid_key(coin: u8, key: &str) -> i8 {
pub fn is_valid_address(coin: u8, address: &str) -> bool { pub fn is_valid_address(coin: u8, address: &str) -> bool {
let c = CoinConfig::get(coin); let c = CoinConfig::get(coin);
let network = c.chain.network(); let network = c.chain.network();
let recipient = zcash_client_backend::address::RecipientAddress::decode(network, address); if decode_payment_address(network.hrp_sapling_payment_address(), address).is_ok() {
recipient.is_some() true
}
else if let Ok(Some(_)) = decode_transparent_address(
&network.b58_pubkey_address_prefix(),
&network.b58_script_address_prefix(),
address) {
true
}
else if zcash_client_backend::address::RecipientAddress::decode(network, address).is_some() {
true
}
else {
false
}
} }
fn derive_secret_key( fn derive_secret_key(

View File

@ -19,6 +19,10 @@ use zcash_primitives::memo::Memo;
pub enum TransactionBuilderError { pub enum TransactionBuilderError {
#[error("Not enough funds: Missing {0} zats")] #[error("Not enough funds: Missing {0} zats")]
NotEnoughFunds(u64), NotEnoughFunds(u64),
#[error("Only one recipient can pay for the fees")]
DuplicateRecipientFee,
#[error("Recipient amount too low for the fees")]
RecipientCannotPayFee,
#[error("Tx too complex")] #[error("Tx too complex")]
TxTooComplex, TxTooComplex,
#[error(transparent)] #[error(transparent)]
@ -47,7 +51,8 @@ pub fn recipients_to_orders(network: &Network, recipients: &[Recipient]) -> Resu
Ok::<_, TransactionBuilderError>(Order { Ok::<_, TransactionBuilderError>(Order {
id: i as u32, id: i as u32,
destinations, destinations,
amount: r.amount, raw_amount: r.amount,
take_fee: r.fee_included, // Caller must make sure that at most one recipient pays for the fees
memo: Memo::from_str(&r.memo).unwrap().into(), memo: Memo::from_str(&r.memo).unwrap().into(),
}) })
}) })

View File

@ -34,9 +34,10 @@ pub fn group_orders(orders: &[Order], fee: u64) -> Result<(Vec<OrderInfo>, Order
group_type |= 1 << i; group_type |= 1 << i;
} }
} }
let amount = order.amount(fee)?;
order_info.push(OrderInfo { order_info.push(OrderInfo {
group_type, group_type,
amount: order.amount, amount,
}); });
} }
@ -189,6 +190,7 @@ pub fn fill(
order_infos: &[OrderInfo], order_infos: &[OrderInfo],
amounts: &OrderGroupAmounts, amounts: &OrderGroupAmounts,
allocation: &FundAllocation, allocation: &FundAllocation,
fee: u64
) -> Result<Vec<Fill>> { ) -> Result<Vec<Fill>> {
assert_eq!(orders.len(), order_infos.len()); assert_eq!(orders.len(), order_infos.len());
let mut fills = vec![]; let mut fills = vec![];
@ -202,7 +204,7 @@ pub fn fill(
let fill = Fill { let fill = Fill {
id_order: Some(order.id), id_order: Some(order.id),
destination: order.destinations[ilog2(info.group_type)].unwrap(), destination: order.destinations[ilog2(info.group_type)].unwrap(),
amount: order.amount, amount: order.amount(fee)?,
memo: order.memo.clone(), memo: order.memo.clone(),
}; };
fills.push(fill); fills.push(fill);
@ -211,13 +213,13 @@ pub fn fill(
let fill1 = Fill { let fill1 = Fill {
id_order: Some(order.id), id_order: Some(order.id),
destination: order.destinations[1].unwrap(), destination: order.destinations[1].unwrap(),
amount: (order.amount as f64 * f).round() as u64, amount: (order.amount(fee)? as f64 * f).round() as u64,
memo: order.memo.clone(), memo: order.memo.clone(),
}; };
let fill2 = Fill { let fill2 = Fill {
id_order: Some(order.id), id_order: Some(order.id),
destination: order.destinations[2].unwrap(), destination: order.destinations[2].unwrap(),
amount: order.amount - fill1.amount, amount: order.amount(fee)? - fill1.amount,
memo: order.memo.clone(), memo: order.memo.clone(),
}; };
if fill1.amount != 0 { if fill1.amount != 0 {
@ -310,7 +312,7 @@ pub fn build_tx_plan<F: FeeCalculator>(
); );
let net_chg = [s0 + s1 - s2, o0 + o1 - o2]; let net_chg = [s0 + s1 - s2, o0 + o1 - o2];
let mut fills = fill(&orders, &groups, &amounts, &allocation)?; let mut fills = fill(&orders, &groups, &amounts, &allocation, fee)?;
let (notes, change) = select_inputs(&utxos, &allocation)?; let (notes, change) = select_inputs(&utxos, &allocation)?;
let change_destinations = decode(network, &config.change_address)?; let change_destinations = decode(network, &config.change_address)?;

View File

@ -1,7 +1,7 @@
use super::ser::MemoBytesProxy; use super::ser::MemoBytesProxy;
use crate::note_selection::ua::decode; use crate::note_selection::ua::decode;
use crate::unified::orchard_as_unified; use crate::unified::orchard_as_unified;
use crate::Hash; use crate::{Hash, TransactionBuilderError};
use orchard::Address; use orchard::Address;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_hex::{SerHex, Strict}; use serde_hex::{SerHex, Strict};
@ -103,7 +103,8 @@ pub struct UTXO {
pub struct Order { pub struct Order {
pub id: u32, pub id: u32,
pub destinations: [Option<Destination>; 3], pub destinations: [Option<Destination>; 3],
pub amount: u64, pub raw_amount: u64,
pub take_fee: bool,
#[serde(with = "MemoBytesProxy")] #[serde(with = "MemoBytesProxy")]
pub memo: MemoBytes, pub memo: MemoBytes,
} }
@ -200,13 +201,24 @@ impl Destination {
} }
impl Order { impl Order {
pub fn new(network: &Network, id: u32, address: &str, amount: u64, memo: MemoBytes) -> Self { pub fn new(network: &Network, id: u32, address: &str, amount: u64, take_fee: bool, memo: MemoBytes) -> Self {
let destinations = decode(network, address).unwrap(); let destinations = decode(network, address).unwrap();
Order { Order {
id, id,
destinations, destinations,
amount, raw_amount: amount,
take_fee,
memo, memo,
} }
} }
pub fn amount(&self, fee: u64) -> Result<u64, TransactionBuilderError> {
if self.take_fee {
if self.raw_amount < fee { return Err(TransactionBuilderError::RecipientCannotPayFee) }
Ok(self.raw_amount - fee)
}
else {
Ok(self.raw_amount)
}
}
} }