zcash_client_backend: Modify `Proposal` to make multi-step transactions representable.
This commit is contained in:
parent
1db3109cb4
commit
beeea7b44e
|
@ -3062,6 +3062,7 @@ dependencies = [
|
|||
"incrementalmerkletree",
|
||||
"jubjub",
|
||||
"maybe-rayon",
|
||||
"nonempty",
|
||||
"orchard",
|
||||
"proptest",
|
||||
"prost",
|
||||
|
|
|
@ -47,6 +47,7 @@ and this library adheres to Rust's notion of
|
|||
}`
|
||||
- `WalletSummary::next_sapling_subtree_index`
|
||||
- `wallet::propose_standard_transfer_to_address`
|
||||
- `wallet::create_proposed_transactions`
|
||||
- `wallet::input_selection::ShieldedInputs`
|
||||
- `wallet::input_selection::ShieldingSelector` has been
|
||||
factored out from the `InputSelector` trait to separate out transparent
|
||||
|
@ -60,9 +61,10 @@ and this library adheres to Rust's notion of
|
|||
- `wallet::TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
|
||||
- `zcash_client_backend::zip321::TransactionRequest::total`
|
||||
- `zcash_client_backend::zip321::parse::Param::name`
|
||||
- `zcash_client_backend::proposal`
|
||||
- `Proposal::{from_parts, shielded_inputs, payment_pools}`
|
||||
- `zcash_client_backend::proto::`
|
||||
- `zcash_client_backend::proposal`:
|
||||
- `Proposal::{shielded_inputs, payment_pools, single_step, multi_step}`
|
||||
- `Step`
|
||||
- `zcash_client_backend::proto`:
|
||||
- `PROPOSAL_SER_V1`
|
||||
- `ProposalDecodingError`
|
||||
- `proposal` module, for parsing and serializing transaction proposals.
|
||||
|
@ -76,8 +78,8 @@ and this library adheres to Rust's notion of
|
|||
wallet::{ReceivedSaplingNote, WalletTransparentOutput},
|
||||
proposal::{Proposal, SaplingInputs},
|
||||
}`
|
||||
- `zcash_client_backend::zip321
|
||||
` `TransactionRequest::{total, from_indexed}`
|
||||
- `zcash_client_backend::zip321`:
|
||||
- `TransactionRequest::{total, from_indexed}`
|
||||
- `parse::Param::name`
|
||||
|
||||
### Moved
|
||||
|
@ -117,32 +119,44 @@ and this library adheres to Rust's notion of
|
|||
- The `NoteMismatch` variant now wraps a `NoteId` instead of a
|
||||
backend-specific note identifier. The related `NoteRef` type parameter has
|
||||
been removed from `error::Error`.
|
||||
- A new variant `UnsupportedPoolType` has been added.
|
||||
- A new variant `NoSupportedReceivers` has been added.
|
||||
- A new variant `NoSpendingKey` has been added.
|
||||
- A new variant `Proposal` has been added.
|
||||
- New variants have been added:
|
||||
- `Error::UnsupportedPoolType`
|
||||
- `Error::NoSupportedReceivers`
|
||||
- `Error::NoSpendingKey`
|
||||
- `Error::Proposal`
|
||||
- `Error::ProposalNotSupported`
|
||||
- Variant `ChildIndexOutOfRange` has been removed.
|
||||
- `wallet::shield_transparent_funds` no longer takes a `memo` argument;
|
||||
instead, memos to be associated with the shielded outputs should be
|
||||
specified in the construction of the value of the `input_selector`
|
||||
argument, which is used to construct the proposed shielded values as
|
||||
internal "change" outputs.
|
||||
- `wallet::create_proposed_transaction` no longer takes a
|
||||
`change_memo` argument; instead, change memos are represented in the
|
||||
individual values of the `proposed_change` field of the `Proposal`'s
|
||||
`TransactionBalance`.
|
||||
- `wallet::create_proposed_transaction` now takes its `proposal` argument
|
||||
by reference instead of as an owned value.
|
||||
- `wallet::create_proposed_transaction` no longer takes a `min_confirmations`
|
||||
argument. Instead, it uses the anchor height from its `proposal` argument.
|
||||
- `wallet::create_spend_to_address` now takes an additional
|
||||
`change_memo` argument.
|
||||
internal "change" outputs. Also, it returns its result as a `NonEmpty<TxId>`
|
||||
instead of a single `TxId`.
|
||||
- `wallet::create_proposed_transaction` has been replaced by
|
||||
`wallet::create_proposed_transactions`. Relative to the prior method,
|
||||
the new method has the following changes:
|
||||
- It no longer takes a `change_memo` argument; instead, change memos are
|
||||
represented in the individual values of the `proposed_change` field of
|
||||
the `Proposal`'s `TransactionBalance`.
|
||||
- `wallet::create_proposed_transactions` takes its `proposal` argument
|
||||
by reference instead of as an owned value.
|
||||
- `wallet::create_proposed_transactions` no longer takes a `min_confirmations`
|
||||
argument. Instead, it uses the anchor height from its `proposal` argument.
|
||||
- `wallet::create_proposed_transactions` forces implementations to ignore
|
||||
the database identifiers for its contained notes by universally quantifying
|
||||
the `NoteRef` type parameter.
|
||||
- It returns a `NonEmpty<TxId>` instead of a single `TxId` value.
|
||||
- `wallet::create_spend_to_address` now takes an additional `change_memo`
|
||||
argument. It also returns its result as a `NonEmpty<TxId>` instead of a
|
||||
single `TxId`.
|
||||
- `wallet::spend` returns its result as a `NonEmpty<TxId>` instead of a
|
||||
single `TxId`.
|
||||
- The error type of `wallet::create_spend_to_address` has been changed to use
|
||||
`zcash_primitives::transaction::fees::zip317::FeeError` instead of
|
||||
`zcash_primitives::transaction::components::amount::BalanceError`.
|
||||
- The following methods now take `&impl SpendProver, &impl OutputProver`
|
||||
instead of `impl TxProver`:
|
||||
- `wallet::create_proposed_transaction`
|
||||
- `wallet::create_proposed_transactions`
|
||||
- `wallet::create_spend_to_address`
|
||||
- `wallet::shield_transparent_funds`
|
||||
- `wallet::spend`
|
||||
|
@ -162,8 +176,8 @@ and this library adheres to Rust's notion of
|
|||
`data_api::InputSource` must expose.
|
||||
- Changes to the `WalletRead` trait:
|
||||
- `get_checkpoint_depth` has been removed without replacement. This
|
||||
is no longer needed given the change to use the stored anchor height for transaction
|
||||
proposal execution.
|
||||
is no longer needed given the change to use the stored anchor height for
|
||||
transaction proposal execution.
|
||||
- `is_valid_account_extfvk` has been removed; it was unused in
|
||||
the ECC mobile wallet SDKs and has been superseded by `get_account_for_ufvk`.
|
||||
- `get_spendable_sapling_notes`, `select_spendable_sapling_notes`, and
|
||||
|
@ -179,9 +193,6 @@ and this library adheres to Rust's notion of
|
|||
- `wallet::{propose_shielding, shield_transparent_funds}` now takes their
|
||||
`min_confirmations` arguments as `u32` rather than a `NonZeroU32` to permit
|
||||
implmentations to enable zero-conf shielding.
|
||||
- `wallet::create_proposed_transaction` now forces implementations to ignore
|
||||
the database identifiers for its contained notes by universally quantifying
|
||||
the `NoteRef` type parameter.
|
||||
- `wallet::input_selection::GreedyInputSelector` now has relaxed requirements
|
||||
for its `InputSource` associated type.
|
||||
|
||||
|
@ -190,6 +201,11 @@ and this library adheres to Rust's notion of
|
|||
- `Proposal::min_anchor_height` has been removed in favor of storing this
|
||||
value in `SaplingInputs`.
|
||||
- `Proposal::sapling_inputs` has been replaced by `Proposal::shielded_inputs`
|
||||
- In addition to having been moved to the `zcash_client_backend::proposal`
|
||||
module, the `Proposal` type has been substantially modified in order to make
|
||||
it possible to represent multi-step transactions, such as a deshielding
|
||||
transaction followed by a zero-conf transfer as required by ZIP 320. Individual
|
||||
transaction proposals are now represented by the `proposal::Step` type.
|
||||
|
||||
- `zcash_client_backend::fees`:
|
||||
- `ChangeStrategy::compute_balance` arguments have changed.
|
||||
|
@ -245,6 +261,7 @@ and this library adheres to Rust's notion of
|
|||
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
|
||||
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
|
||||
replaced by `into_commitments` which returns a `ScannedBlockCommitments` value.
|
||||
- `zcash_client_backend::data_api::wallet::create_proposed_transaction`
|
||||
|
||||
## [0.10.0] - 2023-09-25
|
||||
|
||||
|
|
|
@ -5,34 +5,41 @@
|
|||
syntax = "proto3";
|
||||
package cash.z.wallet.sdk.ffi;
|
||||
|
||||
// A data structure that describes the inputs to be consumed and outputs to
|
||||
// be produced in a proposed transaction.
|
||||
// A data structure that describes the a series of transactions to be created.
|
||||
message Proposal {
|
||||
// The version of this serialization format.
|
||||
uint32 protoVersion = 1;
|
||||
// ZIP 321 serialized transaction request
|
||||
string transactionRequest = 2;
|
||||
// The vector of selected payment index / output pool mappings. Payment index
|
||||
// 0 corresponds to the payment with no explicit index.
|
||||
repeated PaymentOutputPool paymentOutputPools = 3;
|
||||
// The anchor height to be used in creating the transaction, if any.
|
||||
// Setting the anchor height to zero will disallow the use of any shielded
|
||||
// inputs.
|
||||
uint32 anchorHeight = 4;
|
||||
// The inputs to be used in creating the transaction.
|
||||
repeated ProposedInput inputs = 5;
|
||||
// The total value, fee value, and change outputs of the proposed
|
||||
// transaction
|
||||
TransactionBalance balance = 6;
|
||||
// The fee rule used in constructing this proposal
|
||||
FeeRule feeRule = 7;
|
||||
FeeRule feeRule = 2;
|
||||
// The target height for which the proposal was constructed
|
||||
//
|
||||
// The chain must contain at least this many blocks in order for the proposal to
|
||||
// be executed.
|
||||
uint32 minTargetHeight = 8;
|
||||
// A flag indicating whether the proposal is for a shielding transaction,
|
||||
uint32 minTargetHeight = 3;
|
||||
// The series of transactions to be created.
|
||||
repeated ProposalStep steps = 4;
|
||||
}
|
||||
|
||||
// A data structure that describes the inputs to be consumed and outputs to
|
||||
// be produced in a proposed transaction.
|
||||
message ProposalStep {
|
||||
// ZIP 321 serialized transaction request
|
||||
string transactionRequest = 1;
|
||||
// The vector of selected payment index / output pool mappings. Payment index
|
||||
// 0 corresponds to the payment with no explicit index.
|
||||
repeated PaymentOutputPool paymentOutputPools = 2;
|
||||
// The anchor height to be used in creating the transaction, if any.
|
||||
// Setting the anchor height to zero will disallow the use of any shielded
|
||||
// inputs.
|
||||
uint32 anchorHeight = 3;
|
||||
// The inputs to be used in creating the transaction.
|
||||
repeated ProposedInput inputs = 4;
|
||||
// The total value, fee value, and change outputs of the proposed
|
||||
// transaction
|
||||
TransactionBalance balance = 5;
|
||||
// A flag indicating whether the step is for a shielding transaction,
|
||||
// used for determining which OVK to select for wallet-internal outputs.
|
||||
bool isShielding = 9;
|
||||
bool isShielding = 6;
|
||||
}
|
||||
|
||||
enum ValuePool {
|
||||
|
@ -57,14 +64,37 @@ message PaymentOutputPool {
|
|||
ValuePool valuePool = 2;
|
||||
}
|
||||
|
||||
// The unique identifier and value for each proposed input.
|
||||
message ProposedInput {
|
||||
// The unique identifier and value for each proposed input that does not
|
||||
// require a back-reference to a prior step of the proposal.
|
||||
message ReceivedOutput {
|
||||
bytes txid = 1;
|
||||
ValuePool valuePool = 2;
|
||||
uint32 index = 3;
|
||||
uint64 value = 4;
|
||||
}
|
||||
|
||||
// A reference a payment in a prior step of the proposal. This payment must
|
||||
// belong to the wallet.
|
||||
message PriorStepOutput {
|
||||
uint32 stepIndex = 1;
|
||||
uint32 paymentIndex = 2;
|
||||
}
|
||||
|
||||
// A reference a change output from a prior step of the proposal.
|
||||
message PriorStepChange {
|
||||
uint32 stepIndex = 1;
|
||||
uint32 changeIndex = 2;
|
||||
}
|
||||
|
||||
// The unique identifier and value for an input to be used in the transaction.
|
||||
message ProposedInput {
|
||||
oneof value {
|
||||
ReceivedOutput receivedOutput = 1;
|
||||
PriorStepOutput priorStepOutput = 2;
|
||||
PriorStepChange priorStepChange = 3;
|
||||
}
|
||||
}
|
||||
|
||||
// The fee rule used in constructing a Proposal
|
||||
enum FeeRule {
|
||||
// Protobuf requires that enums have a zero discriminant as the default
|
||||
|
@ -82,15 +112,21 @@ enum FeeRule {
|
|||
|
||||
// The proposed change outputs and fee value.
|
||||
message TransactionBalance {
|
||||
// A list of change output values.
|
||||
repeated ChangeValue proposedChange = 1;
|
||||
// The fee to be paid by the proposed transaction, in zatoshis.
|
||||
uint64 feeRequired = 2;
|
||||
}
|
||||
|
||||
// A proposed change output. If the transparent value pool is selected,
|
||||
// the `memo` field must be null.
|
||||
message ChangeValue {
|
||||
// The value of a change output to be created, in zatoshis.
|
||||
uint64 value = 1;
|
||||
// The value pool in which the change output should be created.
|
||||
ValuePool valuePool = 2;
|
||||
// The optional memo that should be associated with the newly created change output.
|
||||
// Memos must not be present for transparent change outputs.
|
||||
MemoBytes memo = 3;
|
||||
}
|
||||
|
||||
|
|
|
@ -37,6 +37,10 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
|||
/// An error in transaction proposal construction
|
||||
Proposal(ProposalError),
|
||||
|
||||
/// The proposal was structurally valid, but spending shielded outputs of prior multi-step
|
||||
/// transaction steps is not yet supported.
|
||||
ProposalNotSupported,
|
||||
|
||||
/// No account could be found corresponding to a provided spending key.
|
||||
KeyNotRecognized,
|
||||
|
||||
|
@ -107,6 +111,12 @@ where
|
|||
Error::Proposal(e) => {
|
||||
write!(f, "Input selection attempted to construct an invalid proposal: {}", e)
|
||||
}
|
||||
Error::ProposalNotSupported => {
|
||||
write!(
|
||||
f,
|
||||
"The proposal was valid, but spending shielded outputs of prior transaction steps is not yet supported."
|
||||
)
|
||||
}
|
||||
Error::KeyNotRecognized => {
|
||||
write!(
|
||||
f,
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
use std::num::NonZeroU32;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use rand_core::OsRng;
|
||||
use sapling::{
|
||||
note_encryption::{try_sapling_note_decryption, PreparedIncomingViewingKey},
|
||||
prover::{OutputProver, SpendProver},
|
||||
};
|
||||
use zcash_primitives::{
|
||||
consensus::{self, NetworkUpgrade},
|
||||
consensus::{self, BlockHeight, NetworkUpgrade},
|
||||
memo::MemoBytes,
|
||||
transaction::{
|
||||
builder::{BuildConfig, Builder},
|
||||
builder::{BuildConfig, BuildResult, Builder},
|
||||
components::amount::{Amount, NonNegativeAmount},
|
||||
fees::{zip317::FeeError as Zip317FeeError, FeeRule, StandardFeeRule},
|
||||
Transaction, TxId,
|
||||
|
@ -26,7 +27,7 @@ use crate::{
|
|||
decrypt_transaction,
|
||||
fees::{self, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
proposal::Proposal,
|
||||
proposal::{self, Proposal},
|
||||
wallet::{Note, OvkPolicy, Recipient},
|
||||
zip321::{self, Payment},
|
||||
PoolType, ShieldedProtocol,
|
||||
|
@ -207,7 +208,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
|
|||
min_confirmations: NonZeroU32,
|
||||
change_memo: Option<MemoBytes>,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
|
@ -238,7 +239,7 @@ where
|
|||
change_memo,
|
||||
)?;
|
||||
|
||||
create_proposed_transaction(
|
||||
create_proposed_transactions(
|
||||
wallet_db,
|
||||
params,
|
||||
spend_prover,
|
||||
|
@ -316,7 +317,7 @@ pub fn spend<DbT, ParamsT, InputsT>(
|
|||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
|
@ -344,7 +345,7 @@ where
|
|||
min_confirmations,
|
||||
)?;
|
||||
|
||||
create_proposed_transaction(
|
||||
create_proposed_transactions(
|
||||
wallet_db,
|
||||
params,
|
||||
spend_prover,
|
||||
|
@ -521,7 +522,7 @@ where
|
|||
/// to fall back to the transparent receiver until full Orchard support is implemented.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
||||
pub fn create_proposed_transactions<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
|
@ -530,7 +531,7 @@ pub fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
|||
ovk_policy: OvkPolicy,
|
||||
proposal: &Proposal<FeeRuleT, N>,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
|
@ -543,6 +544,83 @@ where
|
|||
ParamsT: consensus::Parameters + Clone,
|
||||
FeeRuleT: FeeRule,
|
||||
{
|
||||
let mut step_results = Vec::with_capacity(proposal.steps().len());
|
||||
for step in proposal.steps() {
|
||||
let step_result = create_proposed_transaction(
|
||||
wallet_db,
|
||||
params,
|
||||
spend_prover,
|
||||
output_prover,
|
||||
usk,
|
||||
ovk_policy.clone(),
|
||||
proposal.fee_rule(),
|
||||
proposal.min_target_height(),
|
||||
&step_results,
|
||||
step,
|
||||
)?;
|
||||
step_results.push((step, step_result));
|
||||
}
|
||||
|
||||
Ok(NonEmpty::from_vec(
|
||||
step_results
|
||||
.iter()
|
||||
.map(|(_, r)| r.transaction().txid())
|
||||
.collect(),
|
||||
)
|
||||
.expect("proposal.steps is NonEmpty"))
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn create_proposed_transaction<DbT, ParamsT, InputsErrT, FeeRuleT, N>(
|
||||
wallet_db: &mut DbT,
|
||||
params: &ParamsT,
|
||||
spend_prover: &impl SpendProver,
|
||||
output_prover: &impl OutputProver,
|
||||
usk: &UnifiedSpendingKey,
|
||||
ovk_policy: OvkPolicy,
|
||||
fee_rule: &FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
prior_step_results: &[(&proposal::Step<N>, BuildResult)],
|
||||
proposal_step: &proposal::Step<N>,
|
||||
) -> Result<
|
||||
BuildResult,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
InputsErrT,
|
||||
FeeRuleT::Error,
|
||||
>,
|
||||
>
|
||||
where
|
||||
DbT: WalletWrite + WalletCommitmentTrees,
|
||||
ParamsT: consensus::Parameters + Clone,
|
||||
FeeRuleT: FeeRule,
|
||||
{
|
||||
// TODO: Spending shielded outputs of prior multi-step transaction steps is not yet
|
||||
// supported. Maybe support this at some point? Doing so would require a higher-level
|
||||
// approach in the wallet that waits for transactions with shielded outputs to be
|
||||
// mined and only then attempts to perform the next step.
|
||||
if proposal_step.prior_step_inputs().iter().any(|s_ref| {
|
||||
prior_step_results.len() <= s_ref.step_index()
|
||||
|| match s_ref.output_index() {
|
||||
proposal::StepOutputIndex::Payment(i) => prior_step_results[s_ref.step_index()]
|
||||
.0
|
||||
.payment_pools()
|
||||
.get(&i)
|
||||
.iter()
|
||||
.all(|pool| matches!(pool, PoolType::Shielded(_))),
|
||||
|
||||
proposal::StepOutputIndex::Change(_) => {
|
||||
// Only shielded change is supported by zcash_client_backend, so multi-step
|
||||
// transactions cannot yet spend prior transactions' change outputs.
|
||||
true
|
||||
}
|
||||
}
|
||||
}) {
|
||||
return Err(Error::ProposalNotSupported);
|
||||
}
|
||||
|
||||
let account = wallet_db
|
||||
.get_account_for_ufvk(&usk.to_unified_full_viewing_key())
|
||||
.map_err(Error::DataSource)?
|
||||
|
@ -559,7 +637,7 @@ where
|
|||
|
||||
let internal_ovk = || {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return if proposal.is_shielding() {
|
||||
return if proposal_step.is_shielding() {
|
||||
Some(OutgoingViewingKey(
|
||||
usk.transparent()
|
||||
.to_account_pubkey()
|
||||
|
@ -574,7 +652,7 @@ where
|
|||
Some(dfvk.to_ovk(Scope::Internal))
|
||||
};
|
||||
|
||||
let (sapling_anchor, sapling_inputs) = proposal.shielded_inputs().map_or_else(
|
||||
let (sapling_anchor, sapling_inputs) = proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((sapling::Anchor::empty_tree(), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
|
||||
|
@ -619,7 +697,7 @@ where
|
|||
// are no possible transparent inputs, so we ignore those
|
||||
let mut builder = Builder::new(
|
||||
params.clone(),
|
||||
proposal.min_target_height(),
|
||||
min_target_height,
|
||||
BuildConfig::Standard {
|
||||
sapling_anchor: Some(sapling_anchor),
|
||||
orchard_anchor: None,
|
||||
|
@ -637,7 +715,7 @@ where
|
|||
.map_err(Error::DataSource)?;
|
||||
|
||||
let mut utxos: Vec<WalletTransparentOutput> = vec![];
|
||||
for utxo in proposal.transparent_inputs() {
|
||||
for utxo in proposal_step.transparent_inputs() {
|
||||
utxos.push(utxo.clone());
|
||||
|
||||
let address_metadata = known_addrs
|
||||
|
@ -662,7 +740,7 @@ where
|
|||
|
||||
let mut sapling_output_meta = vec![];
|
||||
let mut transparent_output_meta = vec![];
|
||||
for payment in proposal.transaction_request().payments().values() {
|
||||
for payment in proposal_step.transaction_request().payments().values() {
|
||||
match &payment.recipient_address {
|
||||
Address::Unified(ua) => {
|
||||
let memo = payment
|
||||
|
@ -716,7 +794,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
for change_value in proposal.balance().proposed_change() {
|
||||
for change_value in proposal_step.balance().proposed_change() {
|
||||
let memo = change_value
|
||||
.memo()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
|
@ -751,7 +829,7 @@ where
|
|||
}
|
||||
|
||||
// Build the transaction with the specified fee rule
|
||||
let build_result = builder.build(OsRng, spend_prover, output_prover, proposal.fee_rule())?;
|
||||
let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?;
|
||||
|
||||
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
|
||||
let sapling_outputs =
|
||||
|
@ -776,10 +854,7 @@ where
|
|||
try_sapling_note_decryption(
|
||||
&internal_ivk,
|
||||
&bundle.shielded_outputs()[output_index],
|
||||
consensus::sapling_zip212_enforcement(
|
||||
params,
|
||||
proposal.min_target_height(),
|
||||
),
|
||||
consensus::sapling_zip212_enforcement(params, min_target_height),
|
||||
)
|
||||
.map(|(note, _, _)| (account, note))
|
||||
})
|
||||
|
@ -819,13 +894,13 @@ where
|
|||
created: time::OffsetDateTime::now_utc(),
|
||||
account,
|
||||
outputs: sapling_outputs.chain(transparent_outputs).collect(),
|
||||
fee_amount: Amount::from(proposal.balance().fee_required()),
|
||||
fee_amount: Amount::from(proposal_step.balance().fee_required()),
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
utxos_spent: utxos.iter().map(|utxo| utxo.outpoint().clone()).collect(),
|
||||
})
|
||||
.map_err(Error::DataSource)?;
|
||||
|
||||
Ok(build_result.transaction().txid())
|
||||
Ok(build_result)
|
||||
}
|
||||
|
||||
/// Constructs a transaction that consumes available transparent UTXOs belonging to
|
||||
|
@ -875,7 +950,7 @@ pub fn shield_transparent_funds<DbT, ParamsT, InputsT>(
|
|||
from_addrs: &[TransparentAddress],
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
<DbT as WalletRead>::Error,
|
||||
<DbT as WalletCommitmentTrees>::Error,
|
||||
|
@ -897,7 +972,7 @@ where
|
|||
min_confirmations,
|
||||
)?;
|
||||
|
||||
create_proposed_transaction(
|
||||
create_proposed_transactions(
|
||||
wallet_db,
|
||||
params,
|
||||
spend_prover,
|
||||
|
|
|
@ -10,7 +10,6 @@ use std::{
|
|||
use nonempty::NonEmpty;
|
||||
use zcash_primitives::{
|
||||
consensus::{self, BlockHeight},
|
||||
legacy::TransparentAddress,
|
||||
transaction::{
|
||||
components::{
|
||||
amount::{BalanceError, NonNegativeAmount},
|
||||
|
@ -31,11 +30,12 @@ use crate::{
|
|||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[cfg(any(feature = "transparent-inputs"))]
|
||||
use std::convert::Infallible;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {std::collections::BTreeSet, zcash_primitives::transaction::components::OutPoint};
|
||||
use {
|
||||
std::collections::BTreeSet, std::convert::Infallible,
|
||||
zcash_primitives::legacy::TransparentAddress,
|
||||
zcash_primitives::transaction::components::OutPoint,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use crate::fees::orchard as orchard_fees;
|
||||
|
@ -433,7 +433,7 @@ where
|
|||
|
||||
match balance {
|
||||
Ok(balance) => {
|
||||
return Proposal::from_parts(
|
||||
return Proposal::single_step(
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
vec![],
|
||||
|
@ -582,7 +582,7 @@ where
|
|||
};
|
||||
|
||||
if balance.total() >= shielding_threshold {
|
||||
Proposal::from_parts(
|
||||
Proposal::single_step(
|
||||
TransactionRequest::empty(),
|
||||
BTreeMap::new(),
|
||||
transparent_inputs,
|
||||
|
|
|
@ -15,7 +15,7 @@ use crate::{
|
|||
PoolType,
|
||||
};
|
||||
|
||||
/// Errors that can occur in construction of a [`Proposal`].
|
||||
/// Errors that can occur in construction of a [`Step`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalError {
|
||||
/// The total output value of the transaction request is not a valid Zcash amount.
|
||||
|
@ -35,6 +35,8 @@ pub enum ProposalError {
|
|||
/// * There provided transaction request is empty; i.e. the only output values specified
|
||||
/// are change and fee amounts.
|
||||
ShieldingInvalid,
|
||||
/// A reference to the output of a prior step is invalid.
|
||||
ReferenceError(StepOutput),
|
||||
}
|
||||
|
||||
impl Display for ProposalError {
|
||||
|
@ -61,6 +63,7 @@ impl Display for ProposalError {
|
|||
f,
|
||||
"The proposal violates the rules for a shielding transaction."
|
||||
),
|
||||
ProposalError::ReferenceError(r) => write!(f, "No prior step output found for {:?}", r),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -98,21 +101,165 @@ impl<NoteRef> ShieldedInputs<NoteRef> {
|
|||
}
|
||||
}
|
||||
|
||||
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
|
||||
/// A proposal for a series of transactions to be created.
|
||||
///
|
||||
/// Each step of the proposal represents a separate transaction to be created. At present, only
|
||||
/// transparent outputs of earlier steps may be spent in later steps; the ability to chain shielded
|
||||
/// transaction steps may be added in a future update.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Proposal<FeeRuleT, NoteRef> {
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
steps: NonEmpty<Step<NoteRef>>,
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a validated multi-step [`Proposal`].
|
||||
///
|
||||
/// This operation validates the proposal for agreement between outputs and inputs
|
||||
/// in the case of multi-step proposals, and ensures that no double-spends are being
|
||||
/// proposed.
|
||||
///
|
||||
/// Parameters:
|
||||
/// * `fee_rule`: The fee rule observed by the proposed transaction.
|
||||
/// * `min_target_height`: The minimum block height at which the transaction may be created.
|
||||
/// * `steps`: A vector of steps that make up the proposal.
|
||||
pub fn multi_step(
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
steps: NonEmpty<Step<NoteRef>>,
|
||||
) -> Result<Self, ProposalError> {
|
||||
// TODO: actually perform the validation described in the documentation.
|
||||
|
||||
Ok(Self {
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
steps,
|
||||
})
|
||||
}
|
||||
|
||||
/// Constructs a validated [`Proposal`] having only a single step from its constituent parts.
|
||||
///
|
||||
/// This operation validates the proposal for balance consistency and agreement between
|
||||
/// the `is_shielding` flag and the structure of the proposal.
|
||||
///
|
||||
/// Parameters:
|
||||
/// * `transaction_request`: The ZIP 321 transaction request describing the payments to be
|
||||
/// made.
|
||||
/// * `payment_pools`: A map from payment index to pool type.
|
||||
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
|
||||
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
|
||||
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
|
||||
/// * `fee_rule`: The fee rule observed by the proposed transaction.
|
||||
/// * `min_target_height`: The minimum block height at which the transaction may be created.
|
||||
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
|
||||
/// transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn single_step(
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
) -> Result<Self, ProposalError> {
|
||||
Ok(Self {
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
steps: NonEmpty::singleton(Step::from_parts(
|
||||
&[],
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
vec![],
|
||||
balance,
|
||||
is_shielding,
|
||||
)?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the fee rule to be used by the transaction builder.
|
||||
pub fn fee_rule(&self) -> &FeeRuleT {
|
||||
&self.fee_rule
|
||||
}
|
||||
|
||||
/// Returns the target height for which the proposal was prepared.
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
pub fn min_target_height(&self) -> BlockHeight {
|
||||
self.min_target_height
|
||||
}
|
||||
|
||||
/// Returns the steps of the proposal. Each step corresponds to an independent transaction to
|
||||
/// be generated as a result of this proposal.
|
||||
pub fn steps(&self) -> &NonEmpty<Step<NoteRef>> {
|
||||
&self.steps
|
||||
}
|
||||
}
|
||||
|
||||
impl<FeeRuleT: Debug, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Proposal")
|
||||
.field("fee_rule", &self.fee_rule)
|
||||
.field("min_target_height", &self.min_target_height)
|
||||
.field("steps", &self.steps)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to either a payment or change output within a step.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum StepOutputIndex {
|
||||
Payment(usize),
|
||||
Change(usize),
|
||||
}
|
||||
|
||||
/// A reference to the output of a step in a proposal.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct StepOutput {
|
||||
step_index: usize,
|
||||
output_index: StepOutputIndex,
|
||||
}
|
||||
|
||||
impl StepOutput {
|
||||
/// Constructs a new [`StepOutput`] from its constituent parts.
|
||||
pub fn new(step_index: usize, output_index: StepOutputIndex) -> Self {
|
||||
Self {
|
||||
step_index,
|
||||
output_index,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the step index to which this reference refers.
|
||||
pub fn step_index(&self) -> usize {
|
||||
self.step_index
|
||||
}
|
||||
|
||||
/// Returns the identifier for the payment or change output within
|
||||
/// the referenced step.
|
||||
pub fn output_index(&self) -> StepOutputIndex {
|
||||
self.output_index
|
||||
}
|
||||
}
|
||||
|
||||
/// The inputs to be consumed and outputs to be produced in a proposed transaction.
|
||||
#[derive(Clone, PartialEq, Eq)]
|
||||
pub struct Step<NoteRef> {
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
prior_step_inputs: Vec<StepOutput>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
||||
/// Constructs a validated [`Proposal`] from its constituent parts.
|
||||
impl<NoteRef> Step<NoteRef> {
|
||||
/// Constructs a validated [`Step`] from its constituent parts.
|
||||
///
|
||||
/// This operation validates the proposal for balance consistency and agreement between
|
||||
/// the `is_shielding` flag and the structure of the proposal.
|
||||
|
@ -124,19 +271,17 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
/// * `transparent_inputs`: The set of previous transparent outputs to be spent.
|
||||
/// * `shielded_inputs`: The sets of previous shielded outputs to be spent.
|
||||
/// * `balance`: The change outputs to be added the transaction and the fee to be paid.
|
||||
/// * `fee_rule`: The fee rule observed by the proposed transaction.
|
||||
/// * `min_target_height`: The minimum block height at which the transaction may be created.
|
||||
/// * `is_shielding`: A flag that identifies whether this is a wallet-internal shielding
|
||||
/// transaction.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn from_parts(
|
||||
prior_steps: &[Step<NoteRef>],
|
||||
transaction_request: TransactionRequest,
|
||||
payment_pools: BTreeMap<usize, PoolType>,
|
||||
transparent_inputs: Vec<WalletTransparentOutput>,
|
||||
shielded_inputs: Option<ShieldedInputs<NoteRef>>,
|
||||
prior_step_inputs: Vec<StepOutput>,
|
||||
balance: TransactionBalance,
|
||||
fee_rule: FeeRuleT,
|
||||
min_target_height: BlockHeight,
|
||||
is_shielding: bool,
|
||||
) -> Result<Self, ProposalError> {
|
||||
let transparent_input_total = transparent_inputs
|
||||
|
@ -145,14 +290,43 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
.fold(Ok(NonNegativeAmount::ZERO), |acc, a| {
|
||||
(acc? + a).ok_or(ProposalError::Overflow)
|
||||
})?;
|
||||
|
||||
let shielded_input_total = shielded_inputs
|
||||
.iter()
|
||||
.flat_map(|s_in| s_in.notes().iter())
|
||||
.map(|out| out.note().value())
|
||||
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
let input_total =
|
||||
(transparent_input_total + shielded_input_total).ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let prior_step_input_total = prior_step_inputs
|
||||
.iter()
|
||||
.map(|s_ref| {
|
||||
let step = prior_steps
|
||||
.get(s_ref.step_index)
|
||||
.ok_or(ProposalError::ReferenceError(*s_ref))?;
|
||||
Ok(match s_ref.output_index {
|
||||
StepOutputIndex::Payment(i) => {
|
||||
step.transaction_request
|
||||
.payments()
|
||||
.get(&i)
|
||||
.ok_or(ProposalError::ReferenceError(*s_ref))?
|
||||
.amount
|
||||
}
|
||||
StepOutputIndex::Change(i) => step
|
||||
.balance
|
||||
.proposed_change()
|
||||
.get(i)
|
||||
.ok_or(ProposalError::ReferenceError(*s_ref))?
|
||||
.value(),
|
||||
})
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
.into_iter()
|
||||
.fold(Some(NonNegativeAmount::ZERO), |acc, a| (acc? + a))
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let input_total = (transparent_input_total + shielded_input_total + prior_step_input_total)
|
||||
.ok_or(ProposalError::Overflow)?;
|
||||
|
||||
let request_total = transaction_request
|
||||
.total()
|
||||
|
@ -173,9 +347,8 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
prior_step_inputs,
|
||||
balance,
|
||||
fee_rule,
|
||||
min_target_height,
|
||||
is_shielding,
|
||||
})
|
||||
} else {
|
||||
|
@ -203,21 +376,15 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
pub fn shielded_inputs(&self) -> Option<&ShieldedInputs<NoteRef>> {
|
||||
self.shielded_inputs.as_ref()
|
||||
}
|
||||
/// Returns the inputs that should be obtained from the outputs of the transaction
|
||||
/// created to satisfy a previous step of the proposal.
|
||||
pub fn prior_step_inputs(&self) -> &[StepOutput] {
|
||||
self.prior_step_inputs.as_ref()
|
||||
}
|
||||
/// Returns the change outputs to be added to the transaction and the fee to be paid.
|
||||
pub fn balance(&self) -> &TransactionBalance {
|
||||
&self.balance
|
||||
}
|
||||
/// Returns the fee rule to be used by the transaction builder.
|
||||
pub fn fee_rule(&self) -> &FeeRuleT {
|
||||
&self.fee_rule
|
||||
}
|
||||
/// Returns the target height for which the proposal was prepared.
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
pub fn min_target_height(&self) -> BlockHeight {
|
||||
self.min_target_height
|
||||
}
|
||||
/// Returns a flag indicating whether or not the proposed transaction
|
||||
/// is exclusively wallet-internal (if it does not involve any external
|
||||
/// recipients).
|
||||
|
@ -226,9 +393,9 @@ impl<FeeRuleT, NoteRef> Proposal<FeeRuleT, NoteRef> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
||||
impl<NoteRef> Debug for Step<NoteRef> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Proposal")
|
||||
f.debug_struct("Step")
|
||||
.field("transaction_request", &self.transaction_request)
|
||||
.field("transparent_inputs", &self.transparent_inputs)
|
||||
.field(
|
||||
|
@ -240,8 +407,6 @@ impl<FeeRuleT, NoteRef> Debug for Proposal<FeeRuleT, NoteRef> {
|
|||
&self.shielded_inputs().map(|i| i.anchor_height),
|
||||
)
|
||||
.field("balance", &self.balance)
|
||||
//.field("fee_rule", &self.fee_rule)
|
||||
.field("min_target_height", &self.min_target_height)
|
||||
.field("is_shielding", &self.is_shielding)
|
||||
.finish_non_exhaustive()
|
||||
}
|
||||
|
|
|
@ -24,7 +24,7 @@ use zcash_note_encryption::{EphemeralKeyBytes, COMPACT_NOTE_SIZE};
|
|||
use crate::{
|
||||
data_api::InputSource,
|
||||
fees::{ChangeValue, TransactionBalance},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs},
|
||||
proposal::{Proposal, ProposalError, ShieldedInputs, Step, StepOutput, StepOutputIndex},
|
||||
zip321::{TransactionRequest, Zip321Error},
|
||||
PoolType, ShieldedProtocol,
|
||||
};
|
||||
|
@ -216,8 +216,12 @@ pub const PROPOSAL_SER_V1: u32 = 1;
|
|||
/// representation.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ProposalDecodingError<DbError> {
|
||||
/// The encoded proposal contained no steps
|
||||
NoSteps,
|
||||
/// The ZIP 321 transaction request URI was invalid.
|
||||
Zip321(Zip321Error),
|
||||
/// A proposed input was null.
|
||||
NullInput(usize),
|
||||
/// A transaction identifier string did not decode to a valid transaction ID.
|
||||
TxIdInvalid(TryFromSliceError),
|
||||
/// An invalid value pool identifier was encountered.
|
||||
|
@ -252,7 +256,11 @@ impl<E> From<Zip321Error> for ProposalDecodingError<E> {
|
|||
impl<E: Display> Display for ProposalDecodingError<E> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
ProposalDecodingError::NoSteps => write!(f, "The proposal had no steps."),
|
||||
ProposalDecodingError::Zip321(err) => write!(f, "Transaction request invalid: {}", err),
|
||||
ProposalDecodingError::NullInput(i) => {
|
||||
write!(f, "Proposed input was null at index {}", i)
|
||||
}
|
||||
ProposalDecodingError::TxIdInvalid(err) => {
|
||||
write!(f, "Invalid transaction id: {:?}", err)
|
||||
}
|
||||
|
@ -317,7 +325,7 @@ fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
|
|||
}
|
||||
}
|
||||
|
||||
impl proposal::ProposedInput {
|
||||
impl proposal::ReceivedOutput {
|
||||
pub fn parse_txid(&self) -> Result<TxId, TryFromSliceError> {
|
||||
Ok(TxId::from_bytes(self.txid[..].try_into()?))
|
||||
}
|
||||
|
@ -359,64 +367,112 @@ impl proposal::Proposal {
|
|||
params: &P,
|
||||
value: &Proposal<StandardFeeRule, NoteRef>,
|
||||
) -> Self {
|
||||
let transaction_request = value.transaction_request().to_uri(params);
|
||||
|
||||
let anchor_height = value
|
||||
.shielded_inputs()
|
||||
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
|
||||
|
||||
let inputs = value
|
||||
.transparent_inputs()
|
||||
use proposal::proposed_input;
|
||||
use proposal::{PriorStepChange, PriorStepOutput, ReceivedOutput};
|
||||
let steps = value
|
||||
.steps()
|
||||
.iter()
|
||||
.map(|utxo| proposal::ProposedInput {
|
||||
txid: utxo.outpoint().hash().to_vec(),
|
||||
value_pool: proposal::ValuePool::Transparent.into(),
|
||||
index: utxo.outpoint().n(),
|
||||
value: utxo.txout().value.into(),
|
||||
})
|
||||
.chain(value.shielded_inputs().iter().flat_map(|s_in| {
|
||||
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
|
||||
txid: rec_note.txid().as_ref().to_vec(),
|
||||
value_pool: proposal::ValuePool::from(rec_note.note().protocol()).into(),
|
||||
index: rec_note.output_index().into(),
|
||||
value: rec_note.note().value().into(),
|
||||
})
|
||||
}))
|
||||
.collect();
|
||||
.map(|step| {
|
||||
let transaction_request = step.transaction_request().to_uri(params);
|
||||
|
||||
let payment_output_pools = value
|
||||
.payment_pools()
|
||||
.iter()
|
||||
.map(|(idx, pool_type)| proposal::PaymentOutputPool {
|
||||
payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
|
||||
value_pool: proposal::ValuePool::from(*pool_type).into(),
|
||||
let anchor_height = step
|
||||
.shielded_inputs()
|
||||
.map_or_else(|| 0, |i| u32::from(i.anchor_height()));
|
||||
|
||||
let inputs = step
|
||||
.transparent_inputs()
|
||||
.iter()
|
||||
.map(|utxo| proposal::ProposedInput {
|
||||
value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
|
||||
txid: utxo.outpoint().hash().to_vec(),
|
||||
value_pool: proposal::ValuePool::Transparent.into(),
|
||||
index: utxo.outpoint().n(),
|
||||
value: utxo.txout().value.into(),
|
||||
})),
|
||||
})
|
||||
.chain(step.shielded_inputs().iter().flat_map(|s_in| {
|
||||
s_in.notes().iter().map(|rec_note| proposal::ProposedInput {
|
||||
value: Some(proposed_input::Value::ReceivedOutput(ReceivedOutput {
|
||||
txid: rec_note.txid().as_ref().to_vec(),
|
||||
value_pool: proposal::ValuePool::from(rec_note.note().protocol())
|
||||
.into(),
|
||||
index: rec_note.output_index().into(),
|
||||
value: rec_note.note().value().into(),
|
||||
})),
|
||||
})
|
||||
}))
|
||||
.chain(step.prior_step_inputs().iter().map(|p_in| {
|
||||
match p_in.output_index() {
|
||||
StepOutputIndex::Payment(i) => proposal::ProposedInput {
|
||||
value: Some(proposed_input::Value::PriorStepOutput(
|
||||
PriorStepOutput {
|
||||
step_index: p_in
|
||||
.step_index()
|
||||
.try_into()
|
||||
.expect("Step index fits into a u32"),
|
||||
payment_index: i
|
||||
.try_into()
|
||||
.expect("Payment index fits into a u32"),
|
||||
},
|
||||
)),
|
||||
},
|
||||
StepOutputIndex::Change(i) => proposal::ProposedInput {
|
||||
value: Some(proposed_input::Value::PriorStepChange(
|
||||
PriorStepChange {
|
||||
step_index: p_in
|
||||
.step_index()
|
||||
.try_into()
|
||||
.expect("Step index fits into a u32"),
|
||||
change_index: i
|
||||
.try_into()
|
||||
.expect("Payment index fits into a u32"),
|
||||
},
|
||||
)),
|
||||
},
|
||||
}
|
||||
}))
|
||||
.collect();
|
||||
|
||||
let payment_output_pools = step
|
||||
.payment_pools()
|
||||
.iter()
|
||||
.map(|(idx, pool_type)| proposal::PaymentOutputPool {
|
||||
payment_index: u32::try_from(*idx).expect("Payment index fits into a u32"),
|
||||
value_pool: proposal::ValuePool::from(*pool_type).into(),
|
||||
})
|
||||
.collect();
|
||||
|
||||
let balance = Some(proposal::TransactionBalance {
|
||||
proposed_change: step
|
||||
.balance()
|
||||
.proposed_change()
|
||||
.iter()
|
||||
.map(|change| proposal::ChangeValue {
|
||||
value: change.value().into(),
|
||||
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
|
||||
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
|
||||
value: memo_bytes.as_slice().to_vec(),
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
fee_required: step.balance().fee_required().into(),
|
||||
});
|
||||
|
||||
#[allow(deprecated)]
|
||||
proposal::ProposalStep {
|
||||
transaction_request,
|
||||
payment_output_pools,
|
||||
anchor_height,
|
||||
inputs,
|
||||
balance,
|
||||
is_shielding: step.is_shielding(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let balance = Some(proposal::TransactionBalance {
|
||||
proposed_change: value
|
||||
.balance()
|
||||
.proposed_change()
|
||||
.iter()
|
||||
.map(|change| proposal::ChangeValue {
|
||||
value: change.value().into(),
|
||||
value_pool: proposal::ValuePool::from(change.output_pool()).into(),
|
||||
memo: change.memo().map(|memo_bytes| proposal::MemoBytes {
|
||||
value: memo_bytes.as_slice().to_vec(),
|
||||
}),
|
||||
})
|
||||
.collect(),
|
||||
fee_required: value.balance().fee_required().into(),
|
||||
});
|
||||
|
||||
#[allow(deprecated)]
|
||||
proposal::Proposal {
|
||||
proto_version: PROPOSAL_SER_V1,
|
||||
transaction_request,
|
||||
payment_output_pools,
|
||||
anchor_height,
|
||||
inputs,
|
||||
balance,
|
||||
fee_rule: match value.fee_rule() {
|
||||
StandardFeeRule::PreZip313 => proposal::FeeRule::PreZip313,
|
||||
StandardFeeRule::Zip313 => proposal::FeeRule::Zip313,
|
||||
|
@ -424,7 +480,7 @@ impl proposal::Proposal {
|
|||
}
|
||||
.into(),
|
||||
min_target_height: value.min_target_height().into(),
|
||||
is_shielding: value.is_shielding(),
|
||||
steps,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -438,6 +494,7 @@ impl proposal::Proposal {
|
|||
where
|
||||
DbT: InputSource<Error = DbError>,
|
||||
{
|
||||
use self::proposal::proposed_input::Value::*;
|
||||
match self.proto_version {
|
||||
PROPOSAL_SER_V1 => {
|
||||
#[allow(deprecated)]
|
||||
|
@ -450,116 +507,166 @@ impl proposal::Proposal {
|
|||
}
|
||||
};
|
||||
|
||||
let transaction_request =
|
||||
TransactionRequest::from_uri(params, &self.transaction_request)?;
|
||||
let mut steps = Vec::with_capacity(self.steps.len());
|
||||
for step in &self.steps {
|
||||
let transaction_request =
|
||||
TransactionRequest::from_uri(params, &step.transaction_request)?;
|
||||
|
||||
let payment_pools = self
|
||||
.payment_output_pools
|
||||
.iter()
|
||||
.map(|pop| {
|
||||
Ok((
|
||||
usize::try_from(pop.payment_index)
|
||||
.expect("Payment index fits into a usize"),
|
||||
pool_type(pop.value_pool)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>(
|
||||
)?;
|
||||
let payment_pools = step
|
||||
.payment_output_pools
|
||||
.iter()
|
||||
.map(|pop| {
|
||||
Ok((
|
||||
usize::try_from(pop.payment_index)
|
||||
.expect("Payment index fits into a usize"),
|
||||
pool_type(pop.value_pool)?,
|
||||
))
|
||||
})
|
||||
.collect::<Result<BTreeMap<usize, PoolType>, ProposalDecodingError<DbError>>>()?;
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_inputs = vec![];
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut transparent_inputs = vec![];
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent_inputs = vec![];
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let mut transparent_inputs = vec![];
|
||||
let mut received_notes = vec![];
|
||||
let mut prior_step_inputs = vec![];
|
||||
for (i, input) in step.inputs.iter().enumerate() {
|
||||
match input
|
||||
.value
|
||||
.as_ref()
|
||||
.ok_or(ProposalDecodingError::NullInput(i))?
|
||||
{
|
||||
ReceivedOutput(out) => {
|
||||
let txid = out
|
||||
.parse_txid()
|
||||
.map_err(ProposalDecodingError::TxIdInvalid)?;
|
||||
|
||||
let mut received_notes = vec![];
|
||||
for input in self.inputs.iter() {
|
||||
let txid = input
|
||||
.parse_txid()
|
||||
.map_err(ProposalDecodingError::TxIdInvalid)?;
|
||||
match out.pool_type()? {
|
||||
PoolType::Transparent => {
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
return Err(ProposalDecodingError::ValuePoolNotSupported(
|
||||
1,
|
||||
));
|
||||
|
||||
match input.pool_type()? {
|
||||
PoolType::Transparent => {
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
return Err(ProposalDecodingError::ValuePoolNotSupported(1));
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let outpoint = OutPoint::new(txid.into(), input.index);
|
||||
transparent_inputs.push(
|
||||
wallet_db
|
||||
.get_unspent_transparent_output(&outpoint)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)?
|
||||
.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
input.index,
|
||||
)
|
||||
})?,
|
||||
);
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
{
|
||||
let outpoint = OutPoint::new(txid.into(), out.index);
|
||||
transparent_inputs.push(
|
||||
wallet_db
|
||||
.get_unspent_transparent_output(&outpoint)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)?
|
||||
.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Transparent,
|
||||
out.index,
|
||||
)
|
||||
})?,
|
||||
);
|
||||
}
|
||||
}
|
||||
PoolType::Shielded(protocol) => received_notes.push(
|
||||
wallet_db
|
||||
.get_spendable_note(&txid, protocol, out.index)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Shielded(protocol),
|
||||
out.index,
|
||||
)
|
||||
})
|
||||
})?,
|
||||
),
|
||||
}
|
||||
}
|
||||
PriorStepOutput(s_ref) => {
|
||||
prior_step_inputs.push(StepOutput::new(
|
||||
s_ref
|
||||
.step_index
|
||||
.try_into()
|
||||
.expect("Step index fits into a usize"),
|
||||
StepOutputIndex::Payment(
|
||||
s_ref
|
||||
.payment_index
|
||||
.try_into()
|
||||
.expect("Payment index fits into a usize"),
|
||||
),
|
||||
));
|
||||
}
|
||||
PriorStepChange(s_ref) => {
|
||||
prior_step_inputs.push(StepOutput::new(
|
||||
s_ref
|
||||
.step_index
|
||||
.try_into()
|
||||
.expect("Step index fits into a usize"),
|
||||
StepOutputIndex::Change(
|
||||
s_ref
|
||||
.change_index
|
||||
.try_into()
|
||||
.expect("Payment index fits into a usize"),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
PoolType::Shielded(protocol) => received_notes.push(
|
||||
wallet_db
|
||||
.get_spendable_note(&txid, protocol, input.index)
|
||||
.map_err(ProposalDecodingError::InputRetrieval)
|
||||
.and_then(|opt| {
|
||||
opt.ok_or({
|
||||
ProposalDecodingError::InputNotFound(
|
||||
txid,
|
||||
PoolType::Shielded(protocol),
|
||||
input.index,
|
||||
)
|
||||
})
|
||||
})?,
|
||||
),
|
||||
}
|
||||
|
||||
let shielded_inputs = NonEmpty::from_vec(received_notes)
|
||||
.map(|notes| ShieldedInputs::from_parts(step.anchor_height.into(), notes));
|
||||
|
||||
let proto_balance = step
|
||||
.balance
|
||||
.as_ref()
|
||||
.ok_or(ProposalDecodingError::BalanceInvalid)?;
|
||||
let balance = TransactionBalance::new(
|
||||
proto_balance
|
||||
.proposed_change
|
||||
.iter()
|
||||
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
|
||||
match cv.pool_type()? {
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => {
|
||||
Ok(ChangeValue::sapling(
|
||||
NonNegativeAmount::from_u64(cv.value).map_err(
|
||||
|_| ProposalDecodingError::BalanceInvalid,
|
||||
)?,
|
||||
cv.memo
|
||||
.as_ref()
|
||||
.map(|bytes| {
|
||||
MemoBytes::from_bytes(&bytes.value)
|
||||
.map_err(ProposalDecodingError::MemoInvalid)
|
||||
})
|
||||
.transpose()?,
|
||||
))
|
||||
}
|
||||
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
NonNegativeAmount::from_u64(proto_balance.fee_required)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
|
||||
)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
|
||||
|
||||
let step = Step::from_parts(
|
||||
&steps,
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
prior_step_inputs,
|
||||
balance,
|
||||
step.is_shielding,
|
||||
)
|
||||
.map_err(ProposalDecodingError::ProposalInvalid)?;
|
||||
|
||||
steps.push(step);
|
||||
}
|
||||
|
||||
let shielded_inputs = NonEmpty::from_vec(received_notes)
|
||||
.map(|notes| ShieldedInputs::from_parts(self.anchor_height.into(), notes));
|
||||
|
||||
let proto_balance = self
|
||||
.balance
|
||||
.as_ref()
|
||||
.ok_or(ProposalDecodingError::BalanceInvalid)?;
|
||||
let balance = TransactionBalance::new(
|
||||
proto_balance
|
||||
.proposed_change
|
||||
.iter()
|
||||
.map(|cv| -> Result<ChangeValue, ProposalDecodingError<_>> {
|
||||
match cv.pool_type()? {
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => {
|
||||
Ok(ChangeValue::sapling(
|
||||
NonNegativeAmount::from_u64(cv.value)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
|
||||
cv.memo
|
||||
.as_ref()
|
||||
.map(|bytes| {
|
||||
MemoBytes::from_bytes(&bytes.value)
|
||||
.map_err(ProposalDecodingError::MemoInvalid)
|
||||
})
|
||||
.transpose()?,
|
||||
))
|
||||
}
|
||||
t => Err(ProposalDecodingError::InvalidChangeRecipient(t)),
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?,
|
||||
NonNegativeAmount::from_u64(proto_balance.fee_required)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?,
|
||||
)
|
||||
.map_err(|_| ProposalDecodingError::BalanceInvalid)?;
|
||||
|
||||
Proposal::from_parts(
|
||||
transaction_request,
|
||||
payment_pools,
|
||||
transparent_inputs,
|
||||
shielded_inputs,
|
||||
balance,
|
||||
Proposal::multi_step(
|
||||
fee_rule,
|
||||
self.min_target_height.into(),
|
||||
self.is_shielding,
|
||||
NonEmpty::from_vec(steps).ok_or(ProposalDecodingError::NoSteps)?,
|
||||
)
|
||||
.map_err(ProposalDecodingError::ProposalInvalid)
|
||||
}
|
||||
|
|
|
@ -1,41 +1,50 @@
|
|||
/// A data structure that describes the inputs to be consumed and outputs to
|
||||
/// be produced in a proposed transaction.
|
||||
/// A data structure that describes the a series of transactions to be created.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct Proposal {
|
||||
/// The version of this serialization format.
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub proto_version: u32,
|
||||
/// ZIP 321 serialized transaction request
|
||||
#[prost(string, tag = "2")]
|
||||
pub transaction_request: ::prost::alloc::string::String,
|
||||
/// The vector of selected payment index / output pool mappings. Payment index
|
||||
/// 0 corresponds to the payment with no explicit index.
|
||||
#[prost(message, repeated, tag = "3")]
|
||||
pub payment_output_pools: ::prost::alloc::vec::Vec<PaymentOutputPool>,
|
||||
/// The anchor height to be used in creating the transaction, if any.
|
||||
/// Setting the anchor height to zero will disallow the use of any shielded
|
||||
/// inputs.
|
||||
#[prost(uint32, tag = "4")]
|
||||
pub anchor_height: u32,
|
||||
/// The inputs to be used in creating the transaction.
|
||||
#[prost(message, repeated, tag = "5")]
|
||||
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
/// The total value, fee value, and change outputs of the proposed
|
||||
/// transaction
|
||||
#[prost(message, optional, tag = "6")]
|
||||
pub balance: ::core::option::Option<TransactionBalance>,
|
||||
/// The fee rule used in constructing this proposal
|
||||
#[prost(enumeration = "FeeRule", tag = "7")]
|
||||
#[prost(enumeration = "FeeRule", tag = "2")]
|
||||
pub fee_rule: i32,
|
||||
/// The target height for which the proposal was constructed
|
||||
///
|
||||
/// The chain must contain at least this many blocks in order for the proposal to
|
||||
/// be executed.
|
||||
#[prost(uint32, tag = "8")]
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub min_target_height: u32,
|
||||
/// A flag indicating whether the proposal is for a shielding transaction,
|
||||
/// The series of transactions to be created.
|
||||
#[prost(message, repeated, tag = "4")]
|
||||
pub steps: ::prost::alloc::vec::Vec<ProposalStep>,
|
||||
}
|
||||
/// A data structure that describes the inputs to be consumed and outputs to
|
||||
/// be produced in a proposed transaction.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProposalStep {
|
||||
/// ZIP 321 serialized transaction request
|
||||
#[prost(string, tag = "1")]
|
||||
pub transaction_request: ::prost::alloc::string::String,
|
||||
/// The vector of selected payment index / output pool mappings. Payment index
|
||||
/// 0 corresponds to the payment with no explicit index.
|
||||
#[prost(message, repeated, tag = "2")]
|
||||
pub payment_output_pools: ::prost::alloc::vec::Vec<PaymentOutputPool>,
|
||||
/// The anchor height to be used in creating the transaction, if any.
|
||||
/// Setting the anchor height to zero will disallow the use of any shielded
|
||||
/// inputs.
|
||||
#[prost(uint32, tag = "3")]
|
||||
pub anchor_height: u32,
|
||||
/// The inputs to be used in creating the transaction.
|
||||
#[prost(message, repeated, tag = "4")]
|
||||
pub inputs: ::prost::alloc::vec::Vec<ProposedInput>,
|
||||
/// The total value, fee value, and change outputs of the proposed
|
||||
/// transaction
|
||||
#[prost(message, optional, tag = "5")]
|
||||
pub balance: ::core::option::Option<TransactionBalance>,
|
||||
/// A flag indicating whether the step is for a shielding transaction,
|
||||
/// used for determining which OVK to select for wallet-internal outputs.
|
||||
#[prost(bool, tag = "9")]
|
||||
#[prost(bool, tag = "6")]
|
||||
pub is_shielding: bool,
|
||||
}
|
||||
/// A mapping from ZIP 321 payment index to the output pool that has been chosen
|
||||
|
@ -49,10 +58,11 @@ pub struct PaymentOutputPool {
|
|||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
pub value_pool: i32,
|
||||
}
|
||||
/// The unique identifier and value for each proposed input.
|
||||
/// The unique identifier and value for each proposed input that does not
|
||||
/// require a back-reference to a prior step of the proposal.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProposedInput {
|
||||
pub struct ReceivedOutput {
|
||||
#[prost(bytes = "vec", tag = "1")]
|
||||
pub txid: ::prost::alloc::vec::Vec<u8>,
|
||||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
|
@ -62,12 +72,53 @@ pub struct ProposedInput {
|
|||
#[prost(uint64, tag = "4")]
|
||||
pub value: u64,
|
||||
}
|
||||
/// A reference a payment in a prior step of the proposal. This payment must
|
||||
/// belong to the wallet.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PriorStepOutput {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub step_index: u32,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub payment_index: u32,
|
||||
}
|
||||
/// A reference a change output from a prior step of the proposal.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct PriorStepChange {
|
||||
#[prost(uint32, tag = "1")]
|
||||
pub step_index: u32,
|
||||
#[prost(uint32, tag = "2")]
|
||||
pub change_index: u32,
|
||||
}
|
||||
/// The unique identifier and value for an input to be used in the transaction.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ProposedInput {
|
||||
#[prost(oneof = "proposed_input::Value", tags = "1, 2, 3")]
|
||||
pub value: ::core::option::Option<proposed_input::Value>,
|
||||
}
|
||||
/// Nested message and enum types in `ProposedInput`.
|
||||
pub mod proposed_input {
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Oneof)]
|
||||
pub enum Value {
|
||||
#[prost(message, tag = "1")]
|
||||
ReceivedOutput(super::ReceivedOutput),
|
||||
#[prost(message, tag = "2")]
|
||||
PriorStepOutput(super::PriorStepOutput),
|
||||
#[prost(message, tag = "3")]
|
||||
PriorStepChange(super::PriorStepChange),
|
||||
}
|
||||
}
|
||||
/// The proposed change outputs and fee value.
|
||||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct TransactionBalance {
|
||||
/// A list of change output values.
|
||||
#[prost(message, repeated, tag = "1")]
|
||||
pub proposed_change: ::prost::alloc::vec::Vec<ChangeValue>,
|
||||
/// The fee to be paid by the proposed transaction, in zatoshis.
|
||||
#[prost(uint64, tag = "2")]
|
||||
pub fee_required: u64,
|
||||
}
|
||||
|
@ -76,10 +127,14 @@ pub struct TransactionBalance {
|
|||
#[allow(clippy::derive_partial_eq_without_eq)]
|
||||
#[derive(Clone, PartialEq, ::prost::Message)]
|
||||
pub struct ChangeValue {
|
||||
/// The value of a change output to be created, in zatoshis.
|
||||
#[prost(uint64, tag = "1")]
|
||||
pub value: u64,
|
||||
/// The value pool in which the change output should be created.
|
||||
#[prost(enumeration = "ValuePool", tag = "2")]
|
||||
pub value_pool: i32,
|
||||
/// The optional memo that should be associated with the newly created change output.
|
||||
/// Memos must not be present for transparent change outputs.
|
||||
#[prost(message, optional, tag = "3")]
|
||||
pub memo: ::core::option::Option<MemoBytes>,
|
||||
}
|
||||
|
|
|
@ -378,6 +378,7 @@ impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard
|
|||
/// viewing key, refer to [ZIP 310].
|
||||
///
|
||||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OvkPolicy {
|
||||
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
|
||||
///
|
||||
|
|
|
@ -70,6 +70,7 @@ maybe-rayon.workspace = true
|
|||
assert_matches.workspace = true
|
||||
incrementalmerkletree = { workspace = true, features = ["test-dependencies"] }
|
||||
shardtree = { workspace = true, features = ["legacy-api", "test-dependencies"] }
|
||||
nonempty.workspace = true
|
||||
proptest.workspace = true
|
||||
rand_core.workspace = true
|
||||
regex = "1.4"
|
||||
|
|
|
@ -5,6 +5,7 @@ use std::num::NonZeroU32;
|
|||
#[cfg(feature = "unstable")]
|
||||
use std::fs::File;
|
||||
|
||||
use nonempty::NonEmpty;
|
||||
use prost::Message;
|
||||
use rand_core::{OsRng, RngCore};
|
||||
use rusqlite::{params, Connection};
|
||||
|
@ -29,7 +30,7 @@ use zcash_client_backend::{
|
|||
self,
|
||||
chain::{scan_cached_blocks, BlockSource, ScanSummary},
|
||||
wallet::{
|
||||
create_proposed_transaction, create_spend_to_address,
|
||||
create_proposed_transactions, create_spend_to_address,
|
||||
input_selection::{GreedyInputSelector, GreedyInputSelectorError, InputSelector},
|
||||
propose_standard_transfer_to_address, propose_transfer, spend,
|
||||
},
|
||||
|
@ -442,7 +443,7 @@ impl<Cache> TestState<Cache> {
|
|||
min_confirmations: NonZeroU32,
|
||||
change_memo: Option<MemoBytes>,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
data_api::error::Error<
|
||||
SqliteClientError,
|
||||
commitment_tree::Error,
|
||||
|
@ -477,7 +478,7 @@ impl<Cache> TestState<Cache> {
|
|||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
data_api::error::Error<
|
||||
SqliteClientError,
|
||||
commitment_tree::Error,
|
||||
|
@ -608,14 +609,14 @@ impl<Cache> TestState<Cache> {
|
|||
)
|
||||
}
|
||||
|
||||
/// Invokes [`create_proposed_transaction`] with the given arguments.
|
||||
pub(crate) fn create_proposed_transaction<InputsErrT, FeeRuleT>(
|
||||
/// Invokes [`create_proposed_transactions`] with the given arguments.
|
||||
pub(crate) fn create_proposed_transactions<InputsErrT, FeeRuleT>(
|
||||
&mut self,
|
||||
usk: &UnifiedSpendingKey,
|
||||
ovk_policy: OvkPolicy,
|
||||
proposal: &Proposal<FeeRuleT, ReceivedNoteId>,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
data_api::error::Error<
|
||||
SqliteClientError,
|
||||
commitment_tree::Error,
|
||||
|
@ -628,7 +629,7 @@ impl<Cache> TestState<Cache> {
|
|||
{
|
||||
let params = self.network();
|
||||
let prover = test_prover();
|
||||
create_proposed_transaction(
|
||||
create_proposed_transactions(
|
||||
&mut self.db_data,
|
||||
¶ms,
|
||||
&prover,
|
||||
|
@ -650,7 +651,7 @@ impl<Cache> TestState<Cache> {
|
|||
from_addrs: &[TransparentAddress],
|
||||
min_confirmations: u32,
|
||||
) -> Result<
|
||||
TxId,
|
||||
NonEmpty<TxId>,
|
||||
data_api::error::Error<
|
||||
SqliteClientError,
|
||||
commitment_tree::Error,
|
||||
|
|
|
@ -2316,7 +2316,7 @@ mod tests {
|
|||
);
|
||||
let txid = st
|
||||
.shield_transparent_funds(&input_selector, value, &usk, &[*taddr], 1)
|
||||
.unwrap();
|
||||
.unwrap()[0];
|
||||
|
||||
// The wallet should have zero transparent balance, because the shielding
|
||||
// transaction can be mined.
|
||||
|
|
|
@ -589,10 +589,10 @@ pub(crate) mod tests {
|
|||
.unwrap();
|
||||
|
||||
let create_proposed_result =
|
||||
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal);
|
||||
assert_matches!(create_proposed_result, Ok(_));
|
||||
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal);
|
||||
assert_matches!(&create_proposed_result, Ok(txids) if txids.len() == 1);
|
||||
|
||||
let sent_tx_id = create_proposed_result.unwrap();
|
||||
let sent_tx_id = create_proposed_result.unwrap()[0];
|
||||
|
||||
// Verify that the sent transaction was stored and that we can decrypt the memos
|
||||
let tx = st
|
||||
|
@ -864,8 +864,8 @@ pub(crate) mod tests {
|
|||
|
||||
// Executing the proposal should succeed
|
||||
let txid = st
|
||||
.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
|
||||
.unwrap();
|
||||
.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
|
||||
.unwrap()[0];
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
@ -921,8 +921,8 @@ pub(crate) mod tests {
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal,),
|
||||
Ok(_)
|
||||
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal,),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
|
||||
// A second proposal fails because there are no usable notes
|
||||
|
@ -1000,8 +1000,8 @@ pub(crate) mod tests {
|
|||
.unwrap();
|
||||
|
||||
let txid2 = st
|
||||
.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
|
||||
.unwrap();
|
||||
.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal)
|
||||
.unwrap()[0];
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid2);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
@ -1066,7 +1066,7 @@ pub(crate) mod tests {
|
|||
)?;
|
||||
|
||||
// Executing the proposal should succeed
|
||||
let txid = st.create_proposed_transaction(&usk, ovk_policy, &proposal)?;
|
||||
let txid = st.create_proposed_transactions(&usk, ovk_policy, &proposal)?[0];
|
||||
|
||||
// Fetch the transaction from the database
|
||||
let raw_tx: Vec<_> = st
|
||||
|
@ -1170,8 +1170,8 @@ pub(crate) mod tests {
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
|
||||
Ok(_)
|
||||
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1232,8 +1232,8 @@ pub(crate) mod tests {
|
|||
|
||||
// Executing the proposal should succeed
|
||||
assert_matches!(
|
||||
st.create_proposed_transaction::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
|
||||
Ok(_)
|
||||
st.create_proposed_transactions::<Infallible, _>(&usk, OvkPolicy::Sender, &proposal),
|
||||
Ok(txids) if txids.len() == 1
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -1310,7 +1310,7 @@ pub(crate) mod tests {
|
|||
OvkPolicy::Sender,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap()[0];
|
||||
|
||||
let amount_left = (value - (amount_sent + fee_rule.fixed_fee()).unwrap()).unwrap();
|
||||
let pending_change = (amount_left - amount_legacy_change).unwrap();
|
||||
|
@ -1441,7 +1441,7 @@ pub(crate) mod tests {
|
|||
OvkPolicy::Sender,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
.unwrap()[0];
|
||||
|
||||
let (h, _) = st.generate_next_block_including(txid);
|
||||
st.scan_cached_blocks(h, 1);
|
||||
|
|
Loading…
Reference in New Issue