zcash_client_backend: Add propose_standard_transfer.

This commit is contained in:
Kris Nuttycombe 2023-10-12 13:49:06 -06:00
parent cc0cc2de84
commit f1c08693a5
3 changed files with 288 additions and 87 deletions

View File

@ -1,6 +1,7 @@
use std::num::NonZeroU32;
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
use zcash_primitives::{
consensus::{self, BlockHeight, NetworkUpgrade},
memo::MemoBytes,
@ -212,29 +213,31 @@ where
DbT: WalletWrite + WalletCommitmentTrees,
DbT::NoteRef: Copy + Eq + Ord,
{
let req = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to.clone(),
amount,
memo,
label: None,
message: None,
other_params: vec![],
}])
.expect(
"It should not be possible for this to violate ZIP 321 request construction invariants.",
);
let account = wallet_db
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
.map_err(Error::DataSource)?
.ok_or(Error::KeyNotRecognized)?;
#[allow(deprecated)]
let fee_rule = StandardFeeRule::PreZip313;
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
spend(
let proposal = propose_standard_transfer_to_address(
wallet_db,
params,
StandardFeeRule::PreZip313,
account,
min_confirmations,
to,
amount,
memo,
change_memo,
)?;
create_proposed_transaction(
wallet_db,
params,
prover,
&GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default()),
usk,
req,
ovk_policy,
proposal,
min_confirmations,
)
}
@ -373,6 +376,77 @@ where
.map_err(Error::from)
}
/// Proposes a transaction paying the specified address from the given account.
///
/// Returns the proposal, which may then be executed using [`create_proposed_transaction`]
///
/// Parameters:
/// * `wallet_db`: A read/write reference to the wallet database
/// * `params`: Consensus parameters
/// * `fee_rule`: The fee rule to use in creating the transaction.
/// * `spend_from_account`: The unified account that controls the funds that will be spent
/// in the resulting transaction. This procedure will return an error if the
/// account ID does not correspond to an account known to the wallet.
/// * `min_confirmations`: The minimum number of confirmations that a previously
/// received note must have in the blockchain in order to be considered for being
/// spent. A value of 10 confirmations is recommended and 0-conf transactions are
/// not supported.
/// * `to`: The address to which `amount` will be paid.
/// * `amount`: The amount to send.
/// * `memo`: A memo to be included in the output to the recipient.
/// * `change_memo`: A memo to be included in any change output that is created.
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]
pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
wallet_db: &mut DbT,
params: &ParamsT,
fee_rule: StandardFeeRule,
spend_from_account: AccountId,
min_confirmations: NonZeroU32,
to: &RecipientAddress,
amount: NonNegativeAmount,
memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>,
) -> Result<
Proposal<StandardFeeRule, DbT::NoteRef>,
Error<
DbT::Error,
CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, DbT::NoteRef>,
Zip317FeeError,
>,
>
where
ParamsT: consensus::Parameters + Clone,
DbT: WalletWrite,
DbT::NoteRef: Copy + Eq + Ord,
{
let request = zip321::TransactionRequest::new(vec![Payment {
recipient_address: to.clone(),
amount,
memo,
label: None,
message: None,
other_params: vec![],
}])
.expect(
"It should not be possible for this to violate ZIP 321 request construction invariants.",
);
let change_strategy = fees::standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
let input_selector =
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
propose_transfer(
wallet_db,
params,
spend_from_account,
&input_selector,
request,
min_confirmations,
)
}
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity)]

View File

@ -14,6 +14,7 @@ use tempfile::NamedTempFile;
#[cfg(feature = "unstable")]
use tempfile::TempDir;
use zcash_client_backend::fees::{standard, DustOutputPolicy};
#[allow(deprecated)]
use zcash_client_backend::{
address::RecipientAddress,
@ -22,8 +23,10 @@ use zcash_client_backend::{
chain::{scan_cached_blocks, BlockSource, ScanSummary},
wallet::{
create_proposed_transaction, create_spend_to_address,
input_selection::{GreedyInputSelectorError, InputSelector, Proposal},
propose_transfer, spend,
input_selection::{
GreedyInputSelector, GreedyInputSelectorError, InputSelector, Proposal,
},
propose_standard_transfer_to_address, propose_transfer, spend,
},
AccountBalance, AccountBirthday, WalletRead, WalletSummary, WalletWrite,
},
@ -38,7 +41,7 @@ use zcash_note_encryption::Domain;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight, Network, NetworkUpgrade, Parameters},
memo::MemoBytes,
memo::{Memo, MemoBytes},
sapling::{
note_encryption::{sapling_note_encryption, SaplingDomain},
util::generate_random_rseed,
@ -47,7 +50,7 @@ use zcash_primitives::{
},
transaction::{
components::amount::NonNegativeAmount,
fees::{zip317::FeeError as Zip317FeeError, FeeRule},
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
Transaction, TxId,
},
zip32::{sapling::DiversifiableFullViewingKey, DiversifierIndex},
@ -524,6 +527,43 @@ impl<Cache> TestState<Cache> {
)
}
/// Invokes [`propose_standard_transfer`] with the given arguments.
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
pub(crate) fn propose_standard_transfer<CommitmentTreeErrT>(
&mut self,
spend_from_account: AccountId,
fee_rule: StandardFeeRule,
min_confirmations: NonZeroU32,
to: &RecipientAddress,
amount: NonNegativeAmount,
memo: Option<MemoBytes>,
change_memo: Option<MemoBytes>,
) -> Result<
Proposal<StandardFeeRule, ReceivedNoteId>,
data_api::error::Error<
SqliteClientError,
CommitmentTreeErrT,
GreedyInputSelectorError<Zip317FeeError, ReceivedNoteId>,
Zip317FeeError,
>,
> {
let params = self.network();
let result = propose_standard_transfer_to_address::<_, _, CommitmentTreeErrT>(
&mut self.db_data,
&params,
fee_rule,
spend_from_account,
min_confirmations,
to,
amount,
memo,
change_memo,
);
result
}
/// Invokes [`propose_shielding`] with the given arguments.
#[cfg(feature = "transparent-inputs")]
#[allow(clippy::type_complexity)]
@ -993,3 +1033,15 @@ impl TestCache for FsBlockCache {
meta
}
}
pub(crate) fn input_selector(
fee_rule: StandardFeeRule,
change_memo: Option<&str>,
) -> GreedyInputSelector<
WalletDb<rusqlite::Connection, Network>,
standard::SingleOutputChangeStrategy,
> {
let change_memo = change_memo.map(|m| MemoBytes::from(m.parse::<Memo>().unwrap()));
let change_strategy = standard::SingleOutputChangeStrategy::new(fee_rule, change_memo);
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
}

View File

@ -458,9 +458,7 @@ pub(crate) mod tests {
transaction::{
components::{amount::NonNegativeAmount, Amount},
fees::{
fixed::FeeRule as FixedFeeRule,
zip317::{FeeError as Zip317FeeError, FeeRule as Zip317FeeRule},
StandardFeeRule,
fixed::FeeRule as FixedFeeRule, zip317::FeeError as Zip317FeeError, StandardFeeRule,
},
Transaction,
},
@ -478,7 +476,7 @@ pub(crate) mod tests {
WalletWrite,
},
decrypt_transaction,
fees::{fixed, standard, zip317, DustOutputPolicy},
fees::{fixed, standard, DustOutputPolicy},
keys::UnifiedSpendingKey,
wallet::OvkPolicy,
zip321::{self, Payment, TransactionRequest},
@ -486,7 +484,7 @@ pub(crate) mod tests {
use crate::{
error::SqliteClientError,
testing::{AddressType, BlockCache, TestBuilder, TestState},
testing::{input_selector, AddressType, BlockCache, TestBuilder, TestState},
wallet::{
block_max_scanned, commitment_tree, sapling::select_spendable_sapling_notes,
scanning::tests::test_with_canopy_birthday,
@ -689,7 +687,7 @@ pub(crate) mod tests {
.with_test_account(AccountBirthday::from_sapling_activation)
.build();
let (_, usk, _) = st.test_account().unwrap();
let (account, _, _) = st.test_account().unwrap();
let dfvk = st.test_account_sapling().unwrap();
let to = dfvk.default_address().1.into();
@ -698,13 +696,13 @@ pub(crate) mod tests {
// We cannot do anything if we aren't synchronised
assert_matches!(
st.create_spend_to_address(
&usk,
st.propose_standard_transfer::<Infallible>(
account,
StandardFeeRule::PreZip313,
NonZeroU32::new(1).unwrap(),
&to,
NonNegativeAmount::const_from_u64(1),
None,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
),
Err(data_api::error::Error::ScanRequired)
@ -712,7 +710,7 @@ pub(crate) mod tests {
}
#[test]
fn create_to_address_fails_on_unverified_notes() {
fn spend_fails_on_unverified_notes() {
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
@ -765,13 +763,13 @@ pub(crate) mod tests {
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into();
assert_matches!(
st.create_spend_to_address(
&usk,
st.propose_standard_transfer::<Infallible>(
account,
StandardFeeRule::Zip317,
NonZeroU32::new(10).unwrap(),
&to,
NonNegativeAmount::const_from_u64(70000),
None,
OvkPolicy::Sender,
NonZeroU32::new(10).unwrap(),
None
),
Err(data_api::error::Error::InsufficientFunds {
@ -794,13 +792,13 @@ pub(crate) mod tests {
// Spend still fails
assert_matches!(
st.create_spend_to_address(
&usk,
st.propose_standard_transfer::<Infallible>(
account,
StandardFeeRule::Zip317,
NonZeroU32::new(10).unwrap(),
&to,
NonNegativeAmount::const_from_u64(70000),
None,
OvkPolicy::Sender,
NonZeroU32::new(10).unwrap(),
None
),
Err(data_api::error::Error::InsufficientFunds {
@ -824,20 +822,31 @@ pub(crate) mod tests {
(value * 9).unwrap()
);
// Spend should now succeed
// Should now be able to generate a proposal
let amount_sent = NonNegativeAmount::from_u64(70000).unwrap();
let txid = st
.create_spend_to_address(
&usk,
let min_confirmations = NonZeroU32::new(10).unwrap();
let proposal = st
.propose_standard_transfer::<Infallible>(
account,
StandardFeeRule::Zip317,
min_confirmations,
&to,
amount_sent,
None,
OvkPolicy::Sender,
NonZeroU32::new(10).unwrap(),
None,
)
.unwrap();
// Executing the proposal should succeed
let txid = st
.create_proposed_transaction::<Infallible, _>(
&usk,
OvkPolicy::Sender,
proposal,
min_confirmations,
)
.unwrap();
let (h, _) = st.generate_next_block_including(txid);
st.scan_cached_blocks(h, 1);
@ -851,7 +860,7 @@ pub(crate) mod tests {
}
#[test]
fn create_to_address_fails_on_locked_notes() {
fn spend_fails_on_locked_notes() {
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
@ -860,6 +869,11 @@ pub(crate) mod tests {
let (account, usk, _) = st.test_account().unwrap();
let dfvk = st.test_account_sapling().unwrap();
// TODO: This test was originally written to use the pre-zip-313 fee rule
// and has not yet been updated.
#[allow(deprecated)]
let fee_rule = StandardFeeRule::PreZip313;
// Add funds to the wallet in a single note
let value = NonNegativeAmount::const_from_u64(50000);
let (h1, _, _) = st.generate_next_block(&dfvk, AddressType::DefaultExternal, value);
@ -872,28 +886,39 @@ pub(crate) mod tests {
// Send some of the funds to another address, but don't mine the tx.
let extsk2 = ExtendedSpendingKey::master(&[]);
let to = extsk2.default_address().1.into();
assert_matches!(
st.create_spend_to_address(
&usk,
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st
.propose_standard_transfer::<Infallible>(
account,
fee_rule,
min_confirmations,
&to,
NonNegativeAmount::const_from_u64(15000),
None,
None,
)
.unwrap();
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(
&usk,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
proposal,
min_confirmations
),
Ok(_)
);
// A second spend fails because there are no usable notes
// A second proposal fails because there are no usable notes
assert_matches!(
st.create_spend_to_address(
&usk,
st.propose_standard_transfer::<Infallible>(
account,
fee_rule,
NonZeroU32::new(1).unwrap(),
&to,
NonNegativeAmount::const_from_u64(2000),
None,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
),
Err(data_api::error::Error::InsufficientFunds {
@ -914,15 +939,15 @@ pub(crate) mod tests {
}
st.scan_cached_blocks(h1 + 1, 41);
// Second spend still fails
// Second proposal still fails
assert_matches!(
st.create_spend_to_address(
&usk,
st.propose_standard_transfer::<Infallible>(
account,
fee_rule,
NonZeroU32::new(1).unwrap(),
&to,
NonNegativeAmount::const_from_u64(2000),
None,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
),
Err(data_api::error::Error::InsufficientFunds {
@ -945,19 +970,29 @@ pub(crate) mod tests {
assert_eq!(st.get_spendable_balance(account, 1), value);
// Second spend should now succeed
let amount_sent2 = NonNegativeAmount::from_u64(2000).unwrap();
let txid2 = st
.create_spend_to_address(
&usk,
let amount_sent2 = NonNegativeAmount::const_from_u64(2000);
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st
.propose_standard_transfer::<Infallible>(
account,
fee_rule,
min_confirmations,
&to,
amount_sent2,
None,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None,
)
.unwrap();
let txid2 = st
.create_proposed_transaction::<Infallible, _>(
&usk,
OvkPolicy::Sender,
proposal,
min_confirmations,
)
.unwrap();
let (h, _) = st.generate_next_block_including(txid2);
st.scan_cached_blocks(h, 1);
@ -992,6 +1027,11 @@ pub(crate) mod tests {
let addr2 = extsk2.default_address().1;
let to = addr2.into();
// TODO: This test was originally written to use the pre-zip-313 fee rule
// and has not yet been updated.
#[allow(deprecated)]
let fee_rule = StandardFeeRule::PreZip313;
#[allow(clippy::type_complexity)]
let send_and_recover_with_policy = |st: &mut TestState<BlockCache>,
ovk_policy|
@ -1004,16 +1044,21 @@ pub(crate) mod tests {
Zip317FeeError,
>,
> {
let txid = st.create_spend_to_address(
&usk,
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st.propose_standard_transfer(
account,
fee_rule,
min_confirmations,
&to,
NonNegativeAmount::const_from_u64(15000),
None,
ovk_policy,
NonZeroU32::new(1).unwrap(),
None,
)?;
// Executing the proposal should succeed
let txid =
st.create_proposed_transaction(&usk, ovk_policy, proposal, min_confirmations)?;
// Fetch the transaction from the database
let raw_tx: Vec<_> = st
.wallet()
@ -1071,7 +1116,7 @@ pub(crate) mod tests {
}
#[test]
fn create_to_address_succeeds_to_t_addr_zero_change() {
fn spend_succeeds_to_t_addr_zero_change() {
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
@ -1089,24 +1134,40 @@ pub(crate) mod tests {
assert_eq!(st.get_total_balance(account), value);
assert_eq!(st.get_spendable_balance(account, 1), value);
// TODO: This test was originally written to use the pre-zip-313 fee rule
// and has not yet been updated.
#[allow(deprecated)]
let fee_rule = StandardFeeRule::PreZip313;
// TODO: generate_next_block_from_tx does not currently support transparent outputs.
let to = TransparentAddress::PublicKey([7; 20]).into();
assert_matches!(
st.create_spend_to_address(
&usk,
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st
.propose_standard_transfer::<Infallible>(
account,
fee_rule,
min_confirmations,
&to,
NonNegativeAmount::const_from_u64(50000),
None,
None,
)
.unwrap();
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(
&usk,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
proposal,
min_confirmations
),
Ok(_)
);
}
#[test]
fn create_to_address_spends_a_change_note() {
fn change_note_spends_succeed() {
let mut st = TestBuilder::new()
.with_block_cache()
.with_test_account(AccountBirthday::from_sapling_activation)
@ -1131,17 +1192,33 @@ pub(crate) mod tests {
NonNegativeAmount::ZERO
);
// TODO: This test was originally written to use the pre-zip-313 fee rule
// and has not yet been updated.
#[allow(deprecated)]
let fee_rule = StandardFeeRule::PreZip313;
// TODO: generate_next_block_from_tx does not currently support transparent outputs.
let to = TransparentAddress::PublicKey([7; 20]).into();
assert_matches!(
st.create_spend_to_address(
&usk,
let min_confirmations = NonZeroU32::new(1).unwrap();
let proposal = st
.propose_standard_transfer::<Infallible>(
account,
fee_rule,
min_confirmations,
&to,
NonNegativeAmount::const_from_u64(50000),
None,
None,
)
.unwrap();
// Executing the proposal should succeed
assert_matches!(
st.create_proposed_transaction::<Infallible, _>(
&usk,
OvkPolicy::Sender,
NonZeroU32::new(1).unwrap(),
None
proposal,
min_confirmations
),
Ok(_)
);
@ -1205,6 +1282,7 @@ pub(crate) mod tests {
])
.unwrap();
#[allow(deprecated)]
let fee_rule = FixedFeeRule::standard();
let input_selector = GreedyInputSelector::new(
fixed::SingleOutputChangeStrategy::new(fee_rule, None),
@ -1298,10 +1376,7 @@ pub(crate) mod tests {
assert_eq!(st.get_total_balance(account), total);
assert_eq!(st.get_spendable_balance(account, 1), total);
let input_selector = GreedyInputSelector::new(
zip317::SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None),
DustOutputPolicy::default(),
);
let input_selector = input_selector(StandardFeeRule::Zip317, None);
// This first request will fail due to insufficient non-dust funds
let req = TransactionRequest::new(vec![Payment {