Merge pull request #1105 from nuttycom/wallet/orchard_spends_and_outputs
zcash_client_backend: add Orchard spends and outputs to transaction construction
This commit is contained in:
commit
04343e16f1
|
@ -17,6 +17,8 @@ and this library adheres to Rust's notion of
|
|||
- `BlockMetadata::orchard_tree_size`
|
||||
- `ScannedBlock::orchard`
|
||||
- `ScannedBlockCommitments::orchard`
|
||||
- `ORCHARD_SHARD_HEIGHT`
|
||||
- `BlockMetadata::orchard_tree_size`
|
||||
- `zcash_client_backend::fees::orchard`
|
||||
- `zcash_client_backend::fees::ChangeValue::orchard`
|
||||
- `zcash_client_backend::wallet`:
|
||||
|
@ -29,6 +31,10 @@ and this library adheres to Rust's notion of
|
|||
- Changes to the `WalletRead` trait:
|
||||
- Added `get_orchard_nullifiers`
|
||||
- `ShieldedProtocol` has a new `Orchard` variant.
|
||||
- `WalletCommitmentTrees` has new members when the `orchard` feature is enabled:
|
||||
- `type OrchardShardStore`
|
||||
- `fn with_orchard_tree_mut`
|
||||
- `fn put_orchard_subtree_roots`
|
||||
- `zcash_client_backend::fees`:
|
||||
- Arguments to `ChangeStrategy::compute_balance` have changed.
|
||||
|
||||
|
@ -59,6 +65,7 @@ and this library adheres to Rust's notion of
|
|||
- `zcash_client_backend::wallet`:
|
||||
- `Note`
|
||||
- `ReceivedNote`
|
||||
- `Recipient::{map_internal_account, internal_account_transpose_option}`
|
||||
- `WalletSaplingOutput::recipient_key_scope`
|
||||
- `TransparentAddressMetadata` (which replaces `zcash_keys::address::AddressMetadata`).
|
||||
- `impl {Debug, Clone} for OvkPolicy`
|
||||
|
@ -121,7 +128,7 @@ and this library adheres to Rust's notion of
|
|||
backend-specific note identifier. The related `NoteRef` type parameter has
|
||||
been removed from `error::Error`.
|
||||
- New variants have been added:
|
||||
- `Error::UnsupportedPoolType`
|
||||
- `Error::UnsupportedChangeType`
|
||||
- `Error::NoSupportedReceivers`
|
||||
- `Error::NoSpendingKey`
|
||||
- `Error::Proposal`
|
||||
|
@ -147,9 +154,9 @@ and this library adheres to Rust's notion of
|
|||
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::create_spend_to_address` now takes additional `change_memo` and
|
||||
`fallback_change_pool` arguments. 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
|
||||
|
@ -219,13 +226,20 @@ and this library adheres to Rust's notion of
|
|||
now also provides the output pool to which change should be sent and an
|
||||
optional memo to be associated with the change output.
|
||||
- `ChangeError` has a new `BundleError` variant.
|
||||
- `fixed::SingleOutputChangeStrategy::new` and
|
||||
`zip317::SingleOutputChangeStrategy::new` each now accept an additional
|
||||
`change_memo` argument.
|
||||
- `fixed::SingleOutputChangeStrategy::new`,
|
||||
`zip317::SingleOutputChangeStrategy::new`, and
|
||||
`standard::SingleOutputChangeStrategy::new` each now accept additional
|
||||
`change_memo` and `fallback_change_pool` arguments.
|
||||
- `zcash_client_backend::wallet`:
|
||||
- The fields of `ReceivedSaplingNote` are now private. Use
|
||||
`ReceivedSaplingNote::from_parts` for construction instead. Accessor methods
|
||||
are provided for each previously public field.
|
||||
- `Recipient` is now polymorphic in the type of the payload for wallet-internal
|
||||
recipients. This simplifies the handling of wallet-internal outputs.
|
||||
- `SentTransactionOutput::from_parts` now takes a `Recipient<Note>`.
|
||||
- `SentTransactionOutput::recipient` now returns a `Recipient<Note>`.
|
||||
- `OvkPolicy::Custom` is now a structured variant that can contain independent
|
||||
Sapling and Orchard `OutgoingViewingKey`s.
|
||||
- `zcash_client_backend::scanning::ScanError` has a new variant, `TreeSizeInvalid`.
|
||||
- `zcash_client_backend::zip321::TransactionRequest::payments` now returns a
|
||||
`BTreeMap<usize, Payment>` instead of `&[Payment]` so that parameter
|
||||
|
@ -267,6 +281,8 @@ and this library adheres to Rust's notion of
|
|||
`zcash_client_backend::ReceivedNote`.
|
||||
- `zcash_client_backend::::wallet::input_selection::{Proposal, ShieldedInputs, ProposalError}`
|
||||
have been moved to `zcash_client_backend::proposal`.
|
||||
- `zcash_client_backend::wallet::SentTransactionOutput::sapling_change_to` - the
|
||||
note created by an internal transfer is now conveyed in the `recipient` field.
|
||||
- `zcash_client_backend::data_api`
|
||||
- `zcash_client_backend::data_api::ScannedBlock::from_parts` has been made crate-private.
|
||||
- `zcash_client_backend::data_api::ScannedBlock::into_sapling_commitments` has been
|
||||
|
|
|
@ -8,7 +8,6 @@ use std::{
|
|||
};
|
||||
|
||||
use incrementalmerkletree::{frontier::Frontier, Retention};
|
||||
use sapling::{Node, NOTE_COMMITMENT_TREE_DEPTH};
|
||||
use secrecy::SecretVec;
|
||||
use shardtree::{error::ShardTreeError, store::ShardStore, ShardTree};
|
||||
use zcash_primitives::{
|
||||
|
@ -51,6 +50,13 @@ pub mod wallet;
|
|||
/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
|
||||
pub const SAPLING_SHARD_HEIGHT: u8 = sapling::NOTE_COMMITMENT_TREE_DEPTH / 2;
|
||||
|
||||
/// The height of subtree roots in the Orchard note commitment tree.
|
||||
///
|
||||
/// This conforms to the structure of subtree data returned by
|
||||
/// `lightwalletd` when using the `GetSubtreeRoots` GRPC call.
|
||||
#[cfg(feature = "orchard")]
|
||||
pub const ORCHARD_SHARD_HEIGHT: u8 = { orchard::NOTE_COMMITMENT_TREE_DEPTH as u8 } / 2;
|
||||
|
||||
/// An enumeration of constraints that can be applied when querying for nullifiers for notes
|
||||
/// belonging to the wallet.
|
||||
pub enum NullifierQuery {
|
||||
|
@ -808,29 +814,37 @@ pub struct SentTransaction<'a> {
|
|||
pub utxos_spent: Vec<OutPoint>,
|
||||
}
|
||||
|
||||
/// A type that represents an output (either Sapling or transparent) that was sent by the wallet.
|
||||
/// An output of a transaction generated by the wallet.
|
||||
///
|
||||
/// This type is capable of representing both shielded and transparent outputs.
|
||||
pub struct SentTransactionOutput {
|
||||
output_index: usize,
|
||||
recipient: Recipient,
|
||||
recipient: Recipient<Note>,
|
||||
value: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
sapling_change_to: Option<(AccountId, sapling::Note)>,
|
||||
}
|
||||
|
||||
impl SentTransactionOutput {
|
||||
/// Constructs a new [`SentTransactionOutput`] from its constituent parts.
|
||||
///
|
||||
/// ### Fields:
|
||||
/// * `output_index` - the index of the output or action in the sent transaction
|
||||
/// * `recipient` - the recipient of the output, either a Zcash address or a
|
||||
/// wallet-internal account and the note belonging to the wallet created by
|
||||
/// the output
|
||||
/// * `value` - the value of the output, in zatoshis
|
||||
/// * `memo` - the memo that was sent with this output
|
||||
pub fn from_parts(
|
||||
output_index: usize,
|
||||
recipient: Recipient,
|
||||
recipient: Recipient<Note>,
|
||||
value: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
sapling_change_to: Option<(AccountId, sapling::Note)>,
|
||||
) -> Self {
|
||||
Self {
|
||||
output_index,
|
||||
recipient,
|
||||
value,
|
||||
memo,
|
||||
sapling_change_to,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -843,9 +857,9 @@ impl SentTransactionOutput {
|
|||
pub fn output_index(&self) -> usize {
|
||||
self.output_index
|
||||
}
|
||||
/// Returns the recipient address of the transaction, or the account id for wallet-internal
|
||||
/// transactions.
|
||||
pub fn recipient(&self) -> &Recipient {
|
||||
/// Returns the recipient address of the transaction, or the account id and
|
||||
/// resulting note for wallet-internal outputs.
|
||||
pub fn recipient(&self) -> &Recipient<Note> {
|
||||
&self.recipient
|
||||
}
|
||||
/// Returns the value of the newly created output.
|
||||
|
@ -857,12 +871,6 @@ impl SentTransactionOutput {
|
|||
pub fn memo(&self) -> Option<&MemoBytes> {
|
||||
self.memo.as_ref()
|
||||
}
|
||||
|
||||
/// Returns the account to which change (or wallet-internal value in the case of a shielding
|
||||
/// transaction) was sent, along with the change note.
|
||||
pub fn sapling_change_to(&self) -> Option<&(AccountId, sapling::Note)> {
|
||||
self.sapling_change_to.as_ref()
|
||||
}
|
||||
}
|
||||
|
||||
/// A data structure used to set the birthday height for an account, and ensure that the initial
|
||||
|
@ -870,7 +878,7 @@ impl SentTransactionOutput {
|
|||
#[derive(Clone, Debug)]
|
||||
pub struct AccountBirthday {
|
||||
height: BlockHeight,
|
||||
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>,
|
||||
sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
|
||||
recover_until: Option<BlockHeight>,
|
||||
}
|
||||
|
||||
|
@ -911,7 +919,7 @@ impl AccountBirthday {
|
|||
#[cfg(feature = "test-dependencies")]
|
||||
pub fn from_parts(
|
||||
height: BlockHeight,
|
||||
sapling_frontier: Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH>,
|
||||
sapling_frontier: Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }>,
|
||||
recover_until: Option<BlockHeight>,
|
||||
) -> Self {
|
||||
Self {
|
||||
|
@ -944,7 +952,9 @@ impl AccountBirthday {
|
|||
|
||||
/// Returns the Sapling note commitment tree frontier as of the end of the block at
|
||||
/// [`Self::height`].
|
||||
pub fn sapling_frontier(&self) -> &Frontier<Node, NOTE_COMMITMENT_TREE_DEPTH> {
|
||||
pub fn sapling_frontier(
|
||||
&self,
|
||||
) -> &Frontier<sapling::Node, { sapling::NOTE_COMMITMENT_TREE_DEPTH }> {
|
||||
&self.sapling_frontier
|
||||
}
|
||||
|
||||
|
@ -1081,13 +1091,15 @@ pub trait WalletWrite: WalletRead {
|
|||
/// also provide operations related to Orchard note commitment trees in the future.
|
||||
pub trait WalletCommitmentTrees {
|
||||
type Error;
|
||||
/// The type of the backing [`ShardStore`] for the Sapling note commitment tree.
|
||||
type SaplingShardStore<'a>: ShardStore<
|
||||
H = sapling::Node,
|
||||
CheckpointId = BlockHeight,
|
||||
Error = Self::Error,
|
||||
>;
|
||||
|
||||
///
|
||||
/// Evaluates the given callback function with a reference to the Sapling
|
||||
/// note commitment tree maintained by the wallet.
|
||||
fn with_sapling_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
|
||||
where
|
||||
for<'a> F: FnMut(
|
||||
|
@ -1099,12 +1111,48 @@ pub trait WalletCommitmentTrees {
|
|||
) -> Result<A, E>,
|
||||
E: From<ShardTreeError<Self::Error>>;
|
||||
|
||||
/// Adds a sequence of note commitment tree subtree roots to the data store.
|
||||
/// Adds a sequence of Sapling note commitment tree subtree roots to the data store.
|
||||
///
|
||||
/// Each such value should be the Merkle root of a subtree of the Sapling note commitment tree
|
||||
/// containing 2^[`SAPLING_SHARD_HEIGHT`] note commitments.
|
||||
fn put_sapling_subtree_roots(
|
||||
&mut self,
|
||||
start_index: u64,
|
||||
roots: &[CommitmentTreeRoot<sapling::Node>],
|
||||
) -> Result<(), ShardTreeError<Self::Error>>;
|
||||
|
||||
/// The type of the backing [`ShardStore`] for the Orchard note commitment tree.
|
||||
#[cfg(feature = "orchard")]
|
||||
type OrchardShardStore<'a>: ShardStore<
|
||||
H = orchard::tree::MerkleHashOrchard,
|
||||
CheckpointId = BlockHeight,
|
||||
Error = Self::Error,
|
||||
>;
|
||||
|
||||
/// Evaluates the given callback function with a reference to the Orchard
|
||||
/// note commitment tree maintained by the wallet.
|
||||
#[cfg(feature = "orchard")]
|
||||
fn with_orchard_tree_mut<F, A, E>(&mut self, callback: F) -> Result<A, E>
|
||||
where
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
Self::OrchardShardStore<'a>,
|
||||
{ ORCHARD_SHARD_HEIGHT * 2 },
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>,
|
||||
) -> Result<A, E>,
|
||||
E: From<ShardTreeError<Self::Error>>;
|
||||
|
||||
/// Adds a sequence of Orchard note commitment tree subtree roots to the data store.
|
||||
///
|
||||
/// Each such value should be the Merkle root of a subtree of the Orchard note commitment tree
|
||||
/// containing 2^[`ORCHARD_SHARD_HEIGHT`] note commitments.
|
||||
#[cfg(feature = "orchard")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
start_index: u64,
|
||||
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> Result<(), ShardTreeError<Self::Error>>;
|
||||
}
|
||||
|
||||
#[cfg(feature = "test-dependencies")]
|
||||
|
@ -1138,6 +1186,9 @@ pub mod testing {
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
use {crate::wallet::TransparentAddressMetadata, zcash_primitives::legacy::TransparentAddress};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use super::ORCHARD_SHARD_HEIGHT;
|
||||
|
||||
pub struct MockWalletDb {
|
||||
pub network: Network,
|
||||
pub sapling_tree: ShardTree<
|
||||
|
@ -1145,6 +1196,12 @@ pub mod testing {
|
|||
{ SAPLING_SHARD_HEIGHT * 2 },
|
||||
SAPLING_SHARD_HEIGHT,
|
||||
>,
|
||||
#[cfg(feature = "orchard")]
|
||||
pub orchard_tree: ShardTree<
|
||||
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>,
|
||||
{ ORCHARD_SHARD_HEIGHT * 2 },
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>,
|
||||
}
|
||||
|
||||
impl MockWalletDb {
|
||||
|
@ -1152,6 +1209,8 @@ pub mod testing {
|
|||
Self {
|
||||
network,
|
||||
sapling_tree: ShardTree::new(MemoryShardStore::empty(), 100),
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard_tree: ShardTree::new(MemoryShardStore::empty(), 100),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1406,5 +1465,43 @@ pub mod testing {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
type OrchardShardStore<'a> =
|
||||
MemoryShardStore<orchard::tree::MerkleHashOrchard, BlockHeight>;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn with_orchard_tree_mut<F, A, E>(&mut self, mut callback: F) -> Result<A, E>
|
||||
where
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
Self::OrchardShardStore<'a>,
|
||||
{ ORCHARD_SHARD_HEIGHT * 2 },
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>,
|
||||
) -> Result<A, E>,
|
||||
E: From<ShardTreeError<Self::Error>>,
|
||||
{
|
||||
callback(&mut self.orchard_tree)
|
||||
}
|
||||
|
||||
/// Adds a sequence of note commitment tree subtree roots to the data store.
|
||||
#[cfg(feature = "orchard")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
start_index: u64,
|
||||
roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> Result<(), ShardTreeError<Self::Error>> {
|
||||
self.with_orchard_tree_mut(|t| {
|
||||
for (root, i) in roots.iter().zip(0u64..) {
|
||||
let root_addr =
|
||||
Address::from_parts(ORCHARD_SHARD_HEIGHT.into(), start_index + i);
|
||||
t.insert(root_addr, *root.root_hash())?;
|
||||
}
|
||||
Ok::<_, ShardTreeError<Self::Error>>(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -13,6 +13,7 @@ use zcash_primitives::{
|
|||
zip32::AccountId,
|
||||
};
|
||||
|
||||
use crate::address::UnifiedAddress;
|
||||
use crate::data_api::wallet::input_selection::InputSelectorError;
|
||||
use crate::proposal::ProposalError;
|
||||
use crate::PoolType;
|
||||
|
@ -66,11 +67,15 @@ pub enum Error<DataSourceError, CommitmentTreeError, SelectionError, FeeError> {
|
|||
/// It is forbidden to provide a memo when constructing a transparent output.
|
||||
MemoForbidden,
|
||||
|
||||
/// Attempted to create a spend to an unsupported pool type (currently, Orchard).
|
||||
UnsupportedPoolType(PoolType),
|
||||
/// Attempted to send change to an unsupported pool.
|
||||
///
|
||||
/// This is indicative of a programming error; execution of a transaction proposal that
|
||||
/// presumes support for the specified pool was performed using an application that does not
|
||||
/// provide such support.
|
||||
UnsupportedChangeType(PoolType),
|
||||
|
||||
/// Attempted to create a spend to an unsupported Unified Address receiver
|
||||
NoSupportedReceivers(Vec<u32>),
|
||||
NoSupportedReceivers(Box<UnifiedAddress>),
|
||||
|
||||
/// A proposed transaction cannot be built because it requires spending an input
|
||||
/// for which no spending key is available.
|
||||
|
@ -140,8 +145,12 @@ where
|
|||
Error::ScanRequired => write!(f, "Must scan blocks first"),
|
||||
Error::Builder(e) => write!(f, "An error occurred building the transaction: {}", e),
|
||||
Error::MemoForbidden => write!(f, "It is not possible to send a memo to a transparent address."),
|
||||
Error::UnsupportedPoolType(t) => write!(f, "Attempted to send to an unsupported pool: {}", t),
|
||||
Error::NoSupportedReceivers(t) => write!(f, "Unified address contained only unsupported receiver types: {:?}", &t[..]),
|
||||
Error::UnsupportedChangeType(t) => write!(f, "Attempted to send change to an unsupported pool type: {}", t),
|
||||
Error::NoSupportedReceivers(ua) => write!(
|
||||
f,
|
||||
"A recipient's unified address does not contain any receivers to which the wallet can send funds; required one of {}",
|
||||
ua.receiver_types().iter().enumerate().map(|(i, tc)| format!("{}{:?}", if i > 0 { ", " } else { "" }, tc)).collect::<String>()
|
||||
),
|
||||
Error::NoSpendingKey(addr) => write!(f, "No spending key available for address: {}", addr),
|
||||
Error::NoteMismatch(n) => write!(f, "A note being spent ({:?}) does not correspond to either the internal or external full viewing key for the provided spending key.", n),
|
||||
|
||||
|
|
|
@ -27,8 +27,7 @@ use crate::{
|
|||
decrypt_transaction,
|
||||
fees::{self, DustOutputPolicy},
|
||||
keys::UnifiedSpendingKey,
|
||||
proposal::ProposalError,
|
||||
proposal::{self, Proposal},
|
||||
proposal::{self, Proposal, ProposalError},
|
||||
wallet::{Note, OvkPolicy, Recipient},
|
||||
zip321::{self, Payment},
|
||||
PoolType, ShieldedProtocol,
|
||||
|
@ -44,7 +43,6 @@ use super::InputSource;
|
|||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
input_selection::ShieldingSelector,
|
||||
sapling::keys::OutgoingViewingKey,
|
||||
std::convert::Infallible,
|
||||
zcash_keys::encoding::AddressCodec,
|
||||
zcash_primitives::legacy::TransparentAddress,
|
||||
|
@ -211,6 +209,7 @@ pub fn create_spend_to_address<DbT, ParamsT>(
|
|||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
Error<
|
||||
|
@ -241,6 +240,7 @@ where
|
|||
amount,
|
||||
memo,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
)?;
|
||||
|
||||
create_proposed_transactions(
|
||||
|
@ -359,7 +359,7 @@ where
|
|||
|
||||
/// Select transaction inputs, compute fees, and construct a proposal for a transaction or series
|
||||
/// of transactions that can then be authorized and made ready for submission to the network with
|
||||
/// [`create_proposed_transaction`].
|
||||
/// [`create_proposed_transactions`].
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn propose_transfer<DbT, ParamsT, InputsT, CommitmentTreeErrT>(
|
||||
|
@ -403,7 +403,7 @@ where
|
|||
|
||||
/// Proposes making a payment to the specified address from the given account.
|
||||
///
|
||||
/// Returns the proposal, which may then be executed using [`create_proposed_transaction`].
|
||||
/// Returns the proposal, which may then be executed using [`create_proposed_transactions`].
|
||||
/// Depending upon the recipient address, more than one transaction may be constructed
|
||||
/// in the execution of the returned proposal.
|
||||
///
|
||||
|
@ -424,6 +424,8 @@ where
|
|||
/// * `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.
|
||||
/// * `fallback_change_pool`: The shielded pool to which change should be sent if
|
||||
/// automatic change pool determination fails.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::type_complexity)]
|
||||
pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
|
||||
|
@ -436,6 +438,7 @@ pub fn propose_standard_transfer_to_address<DbT, ParamsT, CommitmentTreeErrT>(
|
|||
amount: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
Proposal<StandardFeeRule, DbT::NoteRef>,
|
||||
Error<
|
||||
|
@ -462,7 +465,11 @@ where
|
|||
"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 change_strategy = fees::standard::SingleOutputChangeStrategy::new(
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
);
|
||||
let input_selector =
|
||||
GreedyInputSelector::<DbT, _>::new(change_strategy, DustOutputPolicy::default());
|
||||
|
||||
|
@ -645,72 +652,85 @@ where
|
|||
.map_err(Error::DataSource)?
|
||||
.ok_or(Error::KeyNotRecognized)?;
|
||||
|
||||
let dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
let (sapling_anchor, sapling_inputs) =
|
||||
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Sapling)) {
|
||||
proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((Some(sapling::Anchor::empty_tree()), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_sapling_tree_mut::<_, _, Error<_, _, _, _>>(|sapling_tree| {
|
||||
let anchor = sapling_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.into();
|
||||
|
||||
// Apply the outgoing viewing key policy.
|
||||
let external_ovk = match ovk_policy {
|
||||
OvkPolicy::Sender => Some(dfvk.to_ovk(Scope::External)),
|
||||
OvkPolicy::Custom(ovk) => Some(ovk),
|
||||
OvkPolicy::Discard => None,
|
||||
};
|
||||
let sapling_inputs = inputs
|
||||
.notes()
|
||||
.iter()
|
||||
.filter_map(|selected| match selected.note() {
|
||||
Note::Sapling(note) => {
|
||||
let key = match selected.spending_key_scope() {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
|
||||
let internal_ovk = || {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
return if proposal_step.is_shielding() {
|
||||
Some(OutgoingViewingKey(
|
||||
usk.transparent()
|
||||
.to_account_pubkey()
|
||||
.internal_ovk()
|
||||
.as_bytes(),
|
||||
))
|
||||
sapling_tree
|
||||
.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&inputs.anchor_height(),
|
||||
)
|
||||
.map(|merkle_path| Some((key, note, merkle_path)))
|
||||
.map_err(Error::from)
|
||||
.transpose()
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
|
||||
|
||||
Ok((Some(anchor), sapling_inputs))
|
||||
})
|
||||
},
|
||||
)?
|
||||
} else {
|
||||
Some(dfvk.to_ovk(Scope::Internal))
|
||||
(None, vec![])
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
Some(dfvk.to_ovk(Scope::Internal))
|
||||
};
|
||||
#[cfg(feature = "orchard")]
|
||||
let (orchard_anchor, orchard_inputs) =
|
||||
if proposal_step.involves(PoolType::Shielded(ShieldedProtocol::Orchard)) {
|
||||
proposal_step.shielded_inputs().map_or_else(
|
||||
|| Ok((Some(orchard::Anchor::empty_tree()), vec![])),
|
||||
|inputs| {
|
||||
wallet_db.with_orchard_tree_mut::<_, _, Error<_, _, _, _>>(|orchard_tree| {
|
||||
let anchor = orchard_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.into();
|
||||
|
||||
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| {
|
||||
let anchor = sapling_tree
|
||||
.root_at_checkpoint_id(&inputs.anchor_height())?
|
||||
.into();
|
||||
let orchard_inputs = inputs
|
||||
.notes()
|
||||
.iter()
|
||||
.filter_map(|selected| match selected.note() {
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(note) => orchard_tree
|
||||
.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&inputs.anchor_height(),
|
||||
)
|
||||
.map(|merkle_path| Some((note, merkle_path)))
|
||||
.map_err(Error::from)
|
||||
.transpose(),
|
||||
Note::Sapling(_) => None,
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
|
||||
|
||||
let sapling_inputs = inputs
|
||||
.notes()
|
||||
.iter()
|
||||
.map(|selected| {
|
||||
match selected.note() {
|
||||
Note::Sapling(note) => {
|
||||
let key = match selected.spending_key_scope() {
|
||||
Scope::External => usk.sapling().clone(),
|
||||
Scope::Internal => usk.sapling().derive_internal(),
|
||||
};
|
||||
|
||||
let merkle_path = sapling_tree.witness_at_checkpoint_id_caching(
|
||||
selected.note_commitment_tree_position(),
|
||||
&inputs.anchor_height(),
|
||||
)?;
|
||||
|
||||
Ok((key, note, merkle_path))
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
Note::Orchard(_) => {
|
||||
// FIXME: Implement this once `Proposal` has been refactored to
|
||||
// include Orchard notes.
|
||||
panic!("Orchard spends are not yet supported");
|
||||
}
|
||||
}
|
||||
Ok((Some(anchor), orchard_inputs))
|
||||
})
|
||||
.collect::<Result<Vec<_>, Error<_, _, _, _>>>()?;
|
||||
|
||||
Ok((anchor, sapling_inputs))
|
||||
})
|
||||
},
|
||||
)?;
|
||||
},
|
||||
)?
|
||||
} else {
|
||||
(None, vec![])
|
||||
};
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_anchor = None;
|
||||
|
||||
// Create the transaction. The type of the proposal ensures that there
|
||||
// are no possible transparent inputs, so we ignore those
|
||||
|
@ -718,13 +738,18 @@ where
|
|||
params.clone(),
|
||||
min_target_height,
|
||||
BuildConfig::Standard {
|
||||
sapling_anchor: Some(sapling_anchor),
|
||||
orchard_anchor: None,
|
||||
sapling_anchor,
|
||||
orchard_anchor,
|
||||
},
|
||||
);
|
||||
|
||||
for (key, note, merkle_path) in sapling_inputs.into_iter() {
|
||||
builder.add_sapling_spend(&key, note.clone(), merkle_path)?;
|
||||
for (sapling_key, sapling_note, merkle_path) in sapling_inputs.into_iter() {
|
||||
builder.add_sapling_spend(&sapling_key, sapling_note.clone(), merkle_path)?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
for (orchard_note, merkle_path) in orchard_inputs.into_iter() {
|
||||
builder.add_orchard_spend(usk.orchard(), *orchard_note, merkle_path.into())?;
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
@ -817,9 +842,72 @@ where
|
|||
utxos_spent
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_fvk: orchard::keys::FullViewingKey = usk.orchard().into();
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_external_ovk = match &ovk_policy {
|
||||
OvkPolicy::Sender => Some(orchard_fvk.to_ovk(orchard::keys::Scope::External)),
|
||||
OvkPolicy::Custom { orchard, .. } => Some(orchard.clone()),
|
||||
OvkPolicy::Discard => None,
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_internal_ovk = || {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
if proposal_step.is_shielding() {
|
||||
return Some(orchard::keys::OutgoingViewingKey::from(
|
||||
usk.transparent()
|
||||
.to_account_pubkey()
|
||||
.internal_ovk()
|
||||
.as_bytes(),
|
||||
));
|
||||
}
|
||||
|
||||
Some(orchard_fvk.to_ovk(Scope::Internal))
|
||||
};
|
||||
|
||||
let sapling_dfvk = usk.sapling().to_diversifiable_full_viewing_key();
|
||||
|
||||
// Apply the outgoing viewing key policy.
|
||||
let sapling_external_ovk = match &ovk_policy {
|
||||
OvkPolicy::Sender => Some(sapling_dfvk.to_ovk(Scope::External)),
|
||||
OvkPolicy::Custom { sapling, .. } => Some(*sapling),
|
||||
OvkPolicy::Discard => None,
|
||||
};
|
||||
|
||||
let sapling_internal_ovk = || {
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
if proposal_step.is_shielding() {
|
||||
return Some(sapling::keys::OutgoingViewingKey(
|
||||
usk.transparent()
|
||||
.to_account_pubkey()
|
||||
.internal_ovk()
|
||||
.as_bytes(),
|
||||
));
|
||||
}
|
||||
|
||||
Some(sapling_dfvk.to_ovk(Scope::Internal))
|
||||
};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
let mut orchard_output_meta = vec![];
|
||||
let mut sapling_output_meta = vec![];
|
||||
let mut transparent_output_meta = vec![];
|
||||
for payment in proposal_step.transaction_request().payments().values() {
|
||||
for (payment, output_pool) in proposal_step
|
||||
.payment_pools()
|
||||
.iter()
|
||||
.map(|(idx, output_pool)| {
|
||||
let payment = proposal_step
|
||||
.transaction_request()
|
||||
.payments()
|
||||
.get(idx)
|
||||
.expect(
|
||||
"The mapping between payment index and payment is checked in step construction",
|
||||
);
|
||||
(payment, output_pool)
|
||||
})
|
||||
{
|
||||
match &payment.recipient_address {
|
||||
Address::Unified(ua) => {
|
||||
let memo = payment
|
||||
|
@ -827,31 +915,56 @@ where
|
|||
.as_ref()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
|
||||
if let Some(sapling_receiver) = ua.sapling() {
|
||||
builder.add_sapling_output(
|
||||
external_ovk,
|
||||
*sapling_receiver,
|
||||
payment.amount,
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((
|
||||
Recipient::Unified(
|
||||
ua.clone(),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
payment.amount,
|
||||
Some(memo),
|
||||
));
|
||||
} else if let Some(taddr) = ua.transparent() {
|
||||
if payment.memo.is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
} else {
|
||||
builder.add_transparent_output(taddr, payment.amount)?;
|
||||
match output_pool {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => {
|
||||
return Err(Error::ProposalNotSupported);
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => {
|
||||
builder.add_orchard_output(
|
||||
orchard_external_ovk.clone(),
|
||||
*ua.orchard().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||
payment.amount.into(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
orchard_output_meta.push((
|
||||
Recipient::Unified(
|
||||
ua.clone(),
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||
),
|
||||
payment.amount,
|
||||
Some(memo),
|
||||
));
|
||||
}
|
||||
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => {
|
||||
builder.add_sapling_output(
|
||||
sapling_external_ovk,
|
||||
*ua.sapling().expect("The mapping between payment pool and receiver is checked in step construction"),
|
||||
payment.amount,
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((
|
||||
Recipient::Unified(
|
||||
ua.clone(),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
payment.amount,
|
||||
Some(memo),
|
||||
));
|
||||
}
|
||||
|
||||
PoolType::Transparent => {
|
||||
if payment.memo.is_some() {
|
||||
return Err(Error::MemoForbidden);
|
||||
} else {
|
||||
builder.add_transparent_output(
|
||||
ua.transparent().expect("The mapping between payment pool and receiver is checked in step construction."),
|
||||
payment.amount
|
||||
)?;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return Err(Error::NoSupportedReceivers(
|
||||
ua.unknown().iter().map(|(tc, _)| *tc).collect(),
|
||||
));
|
||||
}
|
||||
}
|
||||
Address::Sapling(addr) => {
|
||||
|
@ -859,7 +972,12 @@ where
|
|||
.memo
|
||||
.as_ref()
|
||||
.map_or_else(MemoBytes::empty, |m| m.clone());
|
||||
builder.add_sapling_output(external_ovk, *addr, payment.amount, memo.clone())?;
|
||||
builder.add_sapling_output(
|
||||
sapling_external_ovk,
|
||||
*addr,
|
||||
payment.amount,
|
||||
memo.clone(),
|
||||
)?;
|
||||
sapling_output_meta.push((Recipient::Sapling(*addr), payment.amount, Some(memo)));
|
||||
}
|
||||
Address::Transparent(to) => {
|
||||
|
@ -880,8 +998,8 @@ where
|
|||
match change_value.output_pool() {
|
||||
ShieldedProtocol::Sapling => {
|
||||
builder.add_sapling_output(
|
||||
internal_ovk(),
|
||||
dfvk.change_address().1,
|
||||
sapling_internal_ovk(),
|
||||
sapling_dfvk.change_address().1,
|
||||
change_value.value(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
|
@ -894,15 +1012,29 @@ where
|
|||
Some(memo),
|
||||
))
|
||||
}
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
ShieldedProtocol::Orchard => {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
return Err(Error::UnsupportedPoolType(PoolType::Shielded(
|
||||
return Err(Error::UnsupportedChangeType(PoolType::Shielded(
|
||||
ShieldedProtocol::Orchard,
|
||||
)));
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
unimplemented!("FIXME: implement Orchard change output creation.")
|
||||
{
|
||||
builder.add_orchard_output(
|
||||
orchard_internal_ovk(),
|
||||
orchard_fvk.address_at(0u32, orchard::keys::Scope::Internal),
|
||||
change_value.value().into(),
|
||||
memo.clone(),
|
||||
)?;
|
||||
orchard_output_meta.push((
|
||||
Recipient::InternalAccount(
|
||||
account,
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard),
|
||||
),
|
||||
change_value.value(),
|
||||
Some(memo),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -910,7 +1042,39 @@ where
|
|||
// Build the transaction with the specified fee rule
|
||||
let build_result = builder.build(OsRng, spend_prover, output_prover, fee_rule)?;
|
||||
|
||||
let internal_ivk = PreparedIncomingViewingKey::new(&dfvk.to_ivk(Scope::Internal));
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_internal_ivk = orchard_fvk.to_ivk(orchard::keys::Scope::Internal);
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_outputs =
|
||||
orchard_output_meta
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, (recipient, value, memo))| {
|
||||
let output_index = build_result
|
||||
.orchard_meta()
|
||||
.output_action_index(i)
|
||||
.expect("An action should exist in the transaction for each Orchard output.");
|
||||
|
||||
let recipient = recipient
|
||||
.map_internal_account(|pool| {
|
||||
assert!(pool == PoolType::Shielded(ShieldedProtocol::Orchard));
|
||||
build_result
|
||||
.transaction()
|
||||
.orchard_bundle()
|
||||
.and_then(|bundle| {
|
||||
bundle
|
||||
.decrypt_output_with_key(output_index, &orchard_internal_ivk)
|
||||
.map(|(note, _, _)| Note::Orchard(note))
|
||||
})
|
||||
})
|
||||
.internal_account_transpose_option()
|
||||
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK");
|
||||
|
||||
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
|
||||
});
|
||||
|
||||
let sapling_internal_ivk =
|
||||
PreparedIncomingViewingKey::new(&sapling_dfvk.to_ivk(Scope::Internal));
|
||||
let sapling_outputs =
|
||||
sapling_output_meta
|
||||
.into_iter()
|
||||
|
@ -921,27 +1085,28 @@ where
|
|||
.output_index(i)
|
||||
.expect("An output should exist in the transaction for each Sapling payment.");
|
||||
|
||||
let received_as = if let Recipient::InternalAccount(
|
||||
account,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
) = recipient
|
||||
{
|
||||
build_result
|
||||
.transaction()
|
||||
.sapling_bundle()
|
||||
.and_then(|bundle| {
|
||||
try_sapling_note_decryption(
|
||||
&internal_ivk,
|
||||
&bundle.shielded_outputs()[output_index],
|
||||
consensus::sapling_zip212_enforcement(params, min_target_height),
|
||||
)
|
||||
.map(|(note, _, _)| (account, note))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let recipient = recipient
|
||||
.map_internal_account(|pool| {
|
||||
assert!(pool == PoolType::Shielded(ShieldedProtocol::Sapling));
|
||||
build_result
|
||||
.transaction()
|
||||
.sapling_bundle()
|
||||
.and_then(|bundle| {
|
||||
try_sapling_note_decryption(
|
||||
&sapling_internal_ivk,
|
||||
&bundle.shielded_outputs()[output_index],
|
||||
consensus::sapling_zip212_enforcement(
|
||||
params,
|
||||
min_target_height,
|
||||
),
|
||||
)
|
||||
.map(|(note, _, _)| Note::Sapling(note))
|
||||
})
|
||||
})
|
||||
.internal_account_transpose_option()
|
||||
.expect("Wallet-internal outputs must be decryptable with the wallet's IVK");
|
||||
|
||||
SentTransactionOutput::from_parts(output_index, recipient, value, memo, received_as)
|
||||
SentTransactionOutput::from_parts(output_index, recipient, value, memo)
|
||||
});
|
||||
|
||||
let transparent_outputs = transparent_output_meta.into_iter().map(|(addr, value)| {
|
||||
|
@ -958,21 +1123,21 @@ where
|
|||
.map(|(index, _)| index)
|
||||
.expect("An output should exist in the transaction for each transparent payment.");
|
||||
|
||||
SentTransactionOutput::from_parts(
|
||||
output_index,
|
||||
Recipient::Transparent(*addr),
|
||||
value,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
SentTransactionOutput::from_parts(output_index, Recipient::Transparent(*addr), value, None)
|
||||
});
|
||||
|
||||
let mut outputs = vec![];
|
||||
#[cfg(feature = "orchard")]
|
||||
outputs.extend(orchard_outputs);
|
||||
outputs.extend(sapling_outputs);
|
||||
outputs.extend(transparent_outputs);
|
||||
|
||||
wallet_db
|
||||
.store_sent_tx(&SentTransaction {
|
||||
tx: build_result.transaction(),
|
||||
created: time::OffsetDateTime::now_utc(),
|
||||
account,
|
||||
outputs: sapling_outputs.chain(transparent_outputs).collect(),
|
||||
outputs,
|
||||
fee_amount: Amount::from(proposal_step.balance().fee_required()),
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
utxos_spent,
|
||||
|
|
|
@ -455,9 +455,9 @@ where
|
|||
Err(other) => return Err(other.into()),
|
||||
}
|
||||
|
||||
#[cfg(not(zcash_unstable = "orchard"))]
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let selectable_pools = &[ShieldedProtocol::Sapling];
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
#[cfg(feature = "orchard")]
|
||||
let selectable_pools = &[ShieldedProtocol::Sapling, ShieldedProtocol::Orchard];
|
||||
|
||||
shielded_inputs = wallet_db
|
||||
|
|
|
@ -34,6 +34,7 @@ pub(crate) fn single_change_output_balance<
|
|||
dust_output_policy: &DustOutputPolicy,
|
||||
default_dust_threshold: NonNegativeAmount,
|
||||
change_memo: Option<MemoBytes>,
|
||||
_fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<TransactionBalance, ChangeError<E, NoteRefT>>
|
||||
where
|
||||
E: From<F::Error> + From<BalanceError>,
|
||||
|
@ -86,7 +87,6 @@ where
|
|||
|
||||
// TODO: implement a less naive strategy for selecting the pool to which change will be sent.
|
||||
#[cfg(feature = "orchard")]
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
let (change_pool, sapling_change, orchard_change) =
|
||||
if orchard_in.is_positive() || orchard_out.is_positive() {
|
||||
// Send change to Orchard if we're spending any Orchard inputs or creating any Orchard outputs
|
||||
|
@ -96,9 +96,12 @@ where
|
|||
// Sapling outputs, so that we avoid pool-crossing.
|
||||
(ShieldedProtocol::Sapling, 1, 0)
|
||||
} else {
|
||||
// For all other transactions, send change to Sapling.
|
||||
// FIXME: Change this to Orchard once Orchard outputs are enabled.
|
||||
(ShieldedProtocol::Sapling, 1, 0)
|
||||
// This is a fully-transparent transaction, so the caller gets to decide
|
||||
// where to shield change.
|
||||
match _fallback_change_pool {
|
||||
ShieldedProtocol::Orchard => (_fallback_change_pool, 0, 1),
|
||||
ShieldedProtocol::Sapling => (_fallback_change_pool, 1, 0),
|
||||
}
|
||||
};
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let (change_pool, sapling_change) = (ShieldedProtocol::Sapling, 1);
|
||||
|
|
|
@ -9,6 +9,8 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
DustOutputPolicy, TransactionBalance,
|
||||
|
@ -22,15 +24,24 @@ use super::orchard as orchard_fees;
|
|||
pub struct SingleOutputChangeStrategy {
|
||||
fee_rule: FixedFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified fee rule
|
||||
/// and change memo.
|
||||
pub fn new(fee_rule: FixedFeeRule, change_memo: Option<MemoBytes>) -> Self {
|
||||
///
|
||||
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
|
||||
/// feature flags, and the transaction has no shielded inputs.
|
||||
pub fn new(
|
||||
fee_rule: FixedFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -65,6 +76,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
dust_output_policy,
|
||||
self.fee_rule().fixed_fee(),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -89,13 +101,15 @@ mod tests {
|
|||
tests::{TestSaplingInput, TestTransparentInput},
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||
},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn change_without_dust() {
|
||||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None);
|
||||
let change_strategy =
|
||||
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -136,7 +150,8 @@ mod tests {
|
|||
fn dust_change() {
|
||||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let change_strategy = SingleOutputChangeStrategy::new(fee_rule, None);
|
||||
let change_strategy =
|
||||
SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
|
|
@ -14,6 +14,8 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
|
||||
use super::{
|
||||
fixed, sapling as sapling_fees, zip317, ChangeError, ChangeStrategy, DustOutputPolicy,
|
||||
TransactionBalance,
|
||||
|
@ -27,15 +29,24 @@ use super::orchard as orchard_fees;
|
|||
pub struct SingleOutputChangeStrategy {
|
||||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
|
||||
/// fee parameters.
|
||||
pub fn new(fee_rule: StandardFeeRule, change_memo: Option<MemoBytes>) -> Self {
|
||||
///
|
||||
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
|
||||
/// feature flags, and the transaction has no shielded inputs.
|
||||
pub fn new(
|
||||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -63,6 +74,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
StandardFeeRule::PreZip313 => fixed::SingleOutputChangeStrategy::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(10000)),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
@ -78,6 +90,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
StandardFeeRule::Zip313 => fixed::SingleOutputChangeStrategy::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::const_from_u64(1000)),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
@ -93,6 +106,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
StandardFeeRule::Zip317 => zip317::SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
)
|
||||
.compute_balance(
|
||||
params,
|
||||
|
|
|
@ -13,6 +13,8 @@ use zcash_primitives::{
|
|||
},
|
||||
};
|
||||
|
||||
use crate::ShieldedProtocol;
|
||||
|
||||
use super::{
|
||||
common::single_change_output_balance, sapling as sapling_fees, ChangeError, ChangeStrategy,
|
||||
DustOutputPolicy, TransactionBalance,
|
||||
|
@ -26,15 +28,24 @@ use super::orchard as orchard_fees;
|
|||
pub struct SingleOutputChangeStrategy {
|
||||
fee_rule: Zip317FeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
}
|
||||
|
||||
impl SingleOutputChangeStrategy {
|
||||
/// Constructs a new [`SingleOutputChangeStrategy`] with the specified ZIP 317
|
||||
/// fee parameters and change memo.
|
||||
pub fn new(fee_rule: Zip317FeeRule, change_memo: Option<MemoBytes>) -> Self {
|
||||
///
|
||||
/// `fallback_change_pool` is used when more than one shielded pool is enabled via
|
||||
/// feature flags, and the transaction has no shielded inputs.
|
||||
pub fn new(
|
||||
fee_rule: Zip317FeeRule,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Self {
|
||||
Self {
|
||||
fee_rule,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -145,6 +156,7 @@ impl ChangeStrategy for SingleOutputChangeStrategy {
|
|||
dust_output_policy,
|
||||
self.fee_rule.marginal_fee(),
|
||||
self.change_memo.clone(),
|
||||
self.fallback_change_pool,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -170,11 +182,16 @@ mod tests {
|
|||
tests::{TestSaplingInput, TestTransparentInput},
|
||||
ChangeError, ChangeStrategy, ChangeValue, DustOutputPolicy,
|
||||
},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn change_without_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None);
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -213,7 +230,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_transparent_payments() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None);
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -252,7 +273,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_allowable_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None);
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
@ -296,7 +321,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn change_with_disallowed_dust() {
|
||||
let change_strategy = SingleOutputChangeStrategy::new(Zip317FeeRule::standard(), None);
|
||||
let change_strategy = SingleOutputChangeStrategy::new(
|
||||
Zip317FeeRule::standard(),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
|
||||
// spend a single Sapling note that is sufficient to pay the fee
|
||||
let result = change_strategy.compute_balance(
|
||||
|
|
|
@ -97,7 +97,6 @@ pub enum ShieldedProtocol {
|
|||
/// The Sapling protocol
|
||||
Sapling,
|
||||
/// The Orchard protocol
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
Orchard,
|
||||
}
|
||||
|
||||
|
@ -118,7 +117,6 @@ impl PoolType {
|
|||
Address::Unified(ua) => match self {
|
||||
PoolType::Transparent => ua.transparent().is_some(),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => ua.sapling().is_some(),
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => {
|
||||
#[cfg(feature = "orchard")]
|
||||
return ua.orchard().is_some();
|
||||
|
@ -136,7 +134,6 @@ impl fmt::Display for PoolType {
|
|||
match self {
|
||||
PoolType::Transparent => f.write_str("Transparent"),
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => f.write_str("Sapling"),
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => f.write_str("Orchard"),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -490,6 +490,59 @@ impl<NoteRef> Step<NoteRef> {
|
|||
pub fn is_shielding(&self) -> bool {
|
||||
self.is_shielding
|
||||
}
|
||||
|
||||
/// Returns whether or not this proposal requires interaction with the specified pool
|
||||
pub fn involves(&self, pool_type: PoolType) -> bool {
|
||||
match pool_type {
|
||||
PoolType::Transparent => {
|
||||
self.is_shielding
|
||||
|| !self.transparent_inputs.is_empty()
|
||||
|| self
|
||||
.payment_pools()
|
||||
.values()
|
||||
.any(|pool| matches!(pool, PoolType::Transparent))
|
||||
}
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => {
|
||||
let sapling_in = self.shielded_inputs.iter().any(|s_in| {
|
||||
s_in.notes()
|
||||
.iter()
|
||||
.any(|note| matches!(note.note(), Note::Sapling(_)))
|
||||
});
|
||||
let sapling_out = self
|
||||
.payment_pools()
|
||||
.values()
|
||||
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Sapling)));
|
||||
let sapling_change = self
|
||||
.balance
|
||||
.proposed_change()
|
||||
.iter()
|
||||
.any(|c| c.output_pool() == ShieldedProtocol::Sapling);
|
||||
|
||||
sapling_in || sapling_out || sapling_change
|
||||
}
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
let orchard_in = false;
|
||||
#[cfg(feature = "orchard")]
|
||||
let orchard_in = self.shielded_inputs.iter().any(|s_in| {
|
||||
s_in.notes()
|
||||
.iter()
|
||||
.any(|note| matches!(note.note(), Note::Orchard(_)))
|
||||
});
|
||||
let orchard_out = self
|
||||
.payment_pools()
|
||||
.values()
|
||||
.any(|pool| matches!(pool, PoolType::Shielded(ShieldedProtocol::Orchard)));
|
||||
let orchard_change = self
|
||||
.balance
|
||||
.proposed_change()
|
||||
.iter()
|
||||
.any(|c| c.output_pool() == ShieldedProtocol::Orchard);
|
||||
|
||||
orchard_in || orchard_out || orchard_change
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<NoteRef> Debug for Step<NoteRef> {
|
||||
|
@ -501,6 +554,7 @@ impl<NoteRef> Debug for Step<NoteRef> {
|
|||
"shielded_inputs",
|
||||
&self.shielded_inputs().map(|i| i.notes.len()),
|
||||
)
|
||||
.field("prior_step_inputs", &self.prior_step_inputs)
|
||||
.field(
|
||||
"anchor_height",
|
||||
&self.shielded_inputs().map(|i| i.anchor_height),
|
||||
|
|
|
@ -319,7 +319,6 @@ fn pool_type<T>(pool_id: i32) -> Result<PoolType, ProposalDecodingError<T>> {
|
|||
match proposal::ValuePool::try_from(pool_id) {
|
||||
Ok(proposal::ValuePool::Transparent) => Ok(PoolType::Transparent),
|
||||
Ok(proposal::ValuePool::Sapling) => Ok(PoolType::Shielded(ShieldedProtocol::Sapling)),
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
Ok(proposal::ValuePool::Orchard) => Ok(PoolType::Shielded(ShieldedProtocol::Orchard)),
|
||||
_ => Err(ProposalDecodingError::ValuePoolNotSupported(pool_id)),
|
||||
}
|
||||
|
@ -354,7 +353,6 @@ impl From<ShieldedProtocol> for proposal::ValuePool {
|
|||
fn from(value: ShieldedProtocol) -> Self {
|
||||
match value {
|
||||
ShieldedProtocol::Sapling => proposal::ValuePool::Sapling,
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
ShieldedProtocol::Orchard => proposal::ValuePool::Orchard,
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,11 +65,33 @@ impl NoteId {
|
|||
/// internal account ID and the pool to which funds were sent in the case of a wallet-internal
|
||||
/// output.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Recipient {
|
||||
pub enum Recipient<N> {
|
||||
Transparent(TransparentAddress),
|
||||
Sapling(sapling::PaymentAddress),
|
||||
Unified(UnifiedAddress, PoolType),
|
||||
InternalAccount(AccountId, PoolType),
|
||||
InternalAccount(AccountId, N),
|
||||
}
|
||||
|
||||
impl<N> Recipient<N> {
|
||||
pub fn map_internal_account<B, F: FnOnce(N) -> B>(self, f: F) -> Recipient<B> {
|
||||
match self {
|
||||
Recipient::Transparent(t) => Recipient::Transparent(t),
|
||||
Recipient::Sapling(s) => Recipient::Sapling(s),
|
||||
Recipient::Unified(u, p) => Recipient::Unified(u, p),
|
||||
Recipient::InternalAccount(a, n) => Recipient::InternalAccount(a, f(n)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<N> Recipient<Option<N>> {
|
||||
pub fn internal_account_transpose_option(self) -> Option<Recipient<N>> {
|
||||
match self {
|
||||
Recipient::Transparent(t) => Some(Recipient::Transparent(t)),
|
||||
Recipient::Sapling(s) => Some(Recipient::Sapling(s)),
|
||||
Recipient::Unified(u, p) => Some(Recipient::Unified(u, p)),
|
||||
Recipient::InternalAccount(a, n) => n.map(|n0| Recipient::InternalAccount(a, n0)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A subset of a [`Transaction`] relevant to wallets and light clients.
|
||||
|
@ -380,26 +402,43 @@ impl<NoteRef> orchard_fees::InputView<NoteRef> for ReceivedNote<NoteRef, orchard
|
|||
/// [ZIP 310]: https://zips.z.cash/zip-0310
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum OvkPolicy {
|
||||
/// Use the outgoing viewing key from the sender's [`ExtendedFullViewingKey`].
|
||||
/// Use the outgoing viewing key from the sender's [`UnifiedFullViewingKey`].
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the sender, in addition to the
|
||||
/// recipients.
|
||||
///
|
||||
/// [`ExtendedFullViewingKey`]: sapling::zip32::ExtendedFullViewingKey
|
||||
/// [`UnifiedFullViewingKey`]: zcash_keys::keys::UnifiedFullViewingKey
|
||||
Sender,
|
||||
|
||||
/// Use a custom outgoing viewing key. This might for instance be derived from a
|
||||
/// separate seed than the wallet's spending keys.
|
||||
/// Use custom outgoing viewing keys. These might for instance be derived from a
|
||||
/// different seed than the wallet's spending keys.
|
||||
///
|
||||
/// Transaction outputs will be decryptable by the recipients, and whoever controls
|
||||
/// the provided outgoing viewing key.
|
||||
Custom(sapling::keys::OutgoingViewingKey),
|
||||
|
||||
/// Use no outgoing viewing key. Transaction outputs will be decryptable by their
|
||||
/// the provided outgoing viewing keys.
|
||||
Custom {
|
||||
sapling: sapling::keys::OutgoingViewingKey,
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: orchard::keys::OutgoingViewingKey,
|
||||
},
|
||||
/// Use no outgoing viewing keys. Transaction outputs will be decryptable by their
|
||||
/// recipients, but not by the sender.
|
||||
Discard,
|
||||
}
|
||||
|
||||
impl OvkPolicy {
|
||||
/// Constructs an [`OvkPolicy::Custom`] value from a single arbitrary 32-byte key.
|
||||
///
|
||||
/// Outputs of transactions created with this OVK policy will be recoverable using
|
||||
/// this key irrespective of the output pool.
|
||||
pub fn custom_from_common_bytes(key: &[u8; 32]) -> Self {
|
||||
OvkPolicy::Custom {
|
||||
sapling: sapling::keys::OutgoingViewingKey(*key),
|
||||
#[cfg(feature = "orchard")]
|
||||
orchard: orchard::keys::OutgoingViewingKey::from(*key),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Metadata related to the ZIP 32 derivation of a transparent address.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
|
|
|
@ -44,7 +44,7 @@ jubjub.workspace = true
|
|||
secrecy.workspace = true
|
||||
|
||||
# - Shielded protocols
|
||||
orchard.workspace = true
|
||||
orchard = { workspace = true, optional = true }
|
||||
sapling.workspace = true
|
||||
|
||||
# - Note commitment trees
|
||||
|
@ -90,7 +90,7 @@ multicore = ["maybe-rayon/threads", "zcash_primitives/multicore"]
|
|||
|
||||
## Enables support for storing data related to the sending and receiving of
|
||||
## Orchard funds.
|
||||
orchard = ["zcash_client_backend/orchard"]
|
||||
orchard = ["dep:orchard", "zcash_client_backend/orchard"]
|
||||
|
||||
## Exposes APIs that are useful for testing, such as `proptest` strategies.
|
||||
test-dependencies = [
|
||||
|
|
|
@ -340,6 +340,7 @@ mod tests {
|
|||
scanning::ScanError,
|
||||
wallet::OvkPolicy,
|
||||
zip321::{Payment, TransactionRequest},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
|
@ -529,7 +530,7 @@ mod tests {
|
|||
}])
|
||||
.unwrap();
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
SingleOutputChangeStrategy::new(FeeRule::standard(), None),
|
||||
SingleOutputChangeStrategy::new(FeeRule::standard(), None, ShieldedProtocol::Sapling),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
assert_matches!(
|
||||
|
|
|
@ -69,11 +69,14 @@ use zcash_client_backend::{
|
|||
keys::{UnifiedAddressRequest, UnifiedFullViewingKey, UnifiedSpendingKey},
|
||||
proto::compact_formats::CompactBlock,
|
||||
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
|
||||
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
|
||||
DecryptedOutput, ShieldedProtocol, TransferType,
|
||||
};
|
||||
|
||||
use crate::{error::SqliteClientError, wallet::commitment_tree::SqliteShardStore};
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
use zcash_client_backend::data_api::ORCHARD_SHARD_HEIGHT;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use {
|
||||
zcash_client_backend::wallet::TransparentAddressMetadata,
|
||||
|
@ -180,10 +183,18 @@ impl<C: Borrow<rusqlite::Connection>, P: consensus::Parameters> InputSource for
|
|||
fn get_spendable_note(
|
||||
&self,
|
||||
txid: &TxId,
|
||||
_protocol: ShieldedProtocol,
|
||||
protocol: ShieldedProtocol,
|
||||
index: u32,
|
||||
) -> Result<Option<ReceivedNote<Self::NoteRef, Note>>, Self::Error> {
|
||||
wallet::sapling::get_spendable_sapling_note(self.conn.borrow(), &self.params, txid, index)
|
||||
match protocol {
|
||||
ShieldedProtocol::Sapling => wallet::sapling::get_spendable_sapling_note(
|
||||
self.conn.borrow(),
|
||||
&self.params,
|
||||
txid,
|
||||
index,
|
||||
),
|
||||
ShieldedProtocol::Orchard => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn select_spendable_notes(
|
||||
|
@ -588,12 +599,13 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
for output in d_tx.sapling_outputs {
|
||||
match output.transfer_type {
|
||||
TransferType::Outgoing | TransferType::WalletInternal => {
|
||||
let value = output.note.value();
|
||||
let recipient = if output.transfer_type == TransferType::Outgoing {
|
||||
Recipient::Sapling(output.note.recipient())
|
||||
} else {
|
||||
Recipient::InternalAccount(
|
||||
output.account,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling)
|
||||
Note::Sapling(output.note.clone()),
|
||||
)
|
||||
};
|
||||
|
||||
|
@ -604,7 +616,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
tx_ref,
|
||||
output.index,
|
||||
&recipient,
|
||||
NonNegativeAmount::from_u64(output.note.value().inner()).map_err(|_| {
|
||||
NonNegativeAmount::from_u64(value.inner()).map_err(|_| {
|
||||
SqliteClientError::CorruptedData(
|
||||
"Note value is not a valid Zcash amount.".to_string(),
|
||||
)
|
||||
|
@ -712,21 +724,28 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
|
|||
output,
|
||||
)?;
|
||||
|
||||
if let Some((account, note)) = output.sapling_change_to() {
|
||||
wallet::sapling::put_received_note(
|
||||
wdb.conn.0,
|
||||
&DecryptedOutput {
|
||||
index: output.output_index(),
|
||||
note: note.clone(),
|
||||
account: *account,
|
||||
memo: output
|
||||
.memo()
|
||||
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
|
||||
transfer_type: TransferType::WalletInternal,
|
||||
},
|
||||
tx_ref,
|
||||
None,
|
||||
)?;
|
||||
match output.recipient() {
|
||||
Recipient::InternalAccount(account, Note::Sapling(note)) => {
|
||||
wallet::sapling::put_received_note(
|
||||
wdb.conn.0,
|
||||
&DecryptedOutput {
|
||||
index: output.output_index(),
|
||||
note: note.clone(),
|
||||
account: *account,
|
||||
memo: output
|
||||
.memo()
|
||||
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
|
||||
transfer_type: TransferType::WalletInternal,
|
||||
},
|
||||
tx_ref,
|
||||
None,
|
||||
)?;
|
||||
}
|
||||
#[cfg(feature = "orchard")]
|
||||
Recipient::InternalAccount(_account, Note::Orchard(_note)) => {
|
||||
todo!();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -805,6 +824,37 @@ impl<P: consensus::Parameters> WalletCommitmentTrees for WalletDb<rusqlite::Conn
|
|||
.map_err(|e| ShardTreeError::Storage(commitment_tree::Error::Query(e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
type OrchardShardStore<'a> = SqliteShardStore<
|
||||
&'a rusqlite::Transaction<'a>,
|
||||
orchard::tree::MerkleHashOrchard,
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
|
||||
where
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
Self::OrchardShardStore<'a>,
|
||||
{ ORCHARD_SHARD_HEIGHT * 2 },
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>,
|
||||
) -> Result<A, E>,
|
||||
E: From<ShardTreeError<Self::Error>>,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
_start_index: u64,
|
||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> Result<(), ShardTreeError<Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTransaction<'conn>, P> {
|
||||
|
@ -845,6 +895,37 @@ impl<'conn, P: consensus::Parameters> WalletCommitmentTrees for WalletDb<SqlTran
|
|||
roots,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
type OrchardShardStore<'a> = SqliteShardStore<
|
||||
&'a rusqlite::Transaction<'a>,
|
||||
orchard::tree::MerkleHashOrchard,
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>;
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn with_orchard_tree_mut<F, A, E>(&mut self, _callback: F) -> Result<A, E>
|
||||
where
|
||||
for<'a> F: FnMut(
|
||||
&'a mut ShardTree<
|
||||
Self::OrchardShardStore<'a>,
|
||||
{ ORCHARD_SHARD_HEIGHT * 2 },
|
||||
ORCHARD_SHARD_HEIGHT,
|
||||
>,
|
||||
) -> Result<A, E>,
|
||||
E: From<ShardTreeError<Self::Error>>,
|
||||
{
|
||||
todo!()
|
||||
}
|
||||
|
||||
#[cfg(feature = "orchard")]
|
||||
fn put_orchard_subtree_roots(
|
||||
&mut self,
|
||||
_start_index: u64,
|
||||
_roots: &[CommitmentTreeRoot<orchard::tree::MerkleHashOrchard>],
|
||||
) -> Result<(), ShardTreeError<Self::Error>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
|
||||
/// A handle for the SQLite block source.
|
||||
|
|
|
@ -22,7 +22,6 @@ use sapling::{
|
|||
zip32::DiversifiableFullViewingKey,
|
||||
Note, Nullifier, PaymentAddress,
|
||||
};
|
||||
use zcash_client_backend::fees::{standard, DustOutputPolicy};
|
||||
#[allow(deprecated)]
|
||||
use zcash_client_backend::{
|
||||
address::Address,
|
||||
|
@ -45,6 +44,10 @@ use zcash_client_backend::{
|
|||
wallet::OvkPolicy,
|
||||
zip321,
|
||||
};
|
||||
use zcash_client_backend::{
|
||||
fees::{standard, DustOutputPolicy},
|
||||
ShieldedProtocol,
|
||||
};
|
||||
use zcash_note_encryption::Domain;
|
||||
use zcash_primitives::{
|
||||
block::BlockHash,
|
||||
|
@ -442,6 +445,7 @@ impl<Cache> TestState<Cache> {
|
|||
ovk_policy: OvkPolicy,
|
||||
min_confirmations: NonZeroU32,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
NonEmpty<TxId>,
|
||||
data_api::error::Error<
|
||||
|
@ -465,6 +469,7 @@ impl<Cache> TestState<Cache> {
|
|||
ovk_policy,
|
||||
min_confirmations,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -547,6 +552,7 @@ impl<Cache> TestState<Cache> {
|
|||
amount: NonNegativeAmount,
|
||||
memo: Option<MemoBytes>,
|
||||
change_memo: Option<MemoBytes>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> Result<
|
||||
Proposal<StandardFeeRule, ReceivedNoteId>,
|
||||
data_api::error::Error<
|
||||
|
@ -567,6 +573,7 @@ impl<Cache> TestState<Cache> {
|
|||
amount,
|
||||
memo,
|
||||
change_memo,
|
||||
fallback_change_pool,
|
||||
);
|
||||
|
||||
if let Ok(proposal) = &result {
|
||||
|
@ -1053,12 +1060,14 @@ impl TestCache for FsBlockCache {
|
|||
pub(crate) fn input_selector(
|
||||
fee_rule: StandardFeeRule,
|
||||
change_memo: Option<&str>,
|
||||
fallback_change_pool: ShieldedProtocol,
|
||||
) -> 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);
|
||||
let change_strategy =
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, change_memo, fallback_change_pool);
|
||||
GreedyInputSelector::new(change_strategy, DustOutputPolicy::default())
|
||||
}
|
||||
|
||||
|
|
|
@ -74,6 +74,7 @@ use std::num::NonZeroU32;
|
|||
use std::ops::RangeInclusive;
|
||||
use tracing::debug;
|
||||
use zcash_client_backend::data_api::{AccountBalance, Ratio, WalletSummary};
|
||||
use zcash_client_backend::wallet::Note;
|
||||
use zcash_primitives::transaction::components::amount::NonNegativeAmount;
|
||||
use zcash_primitives::zip32::Scope;
|
||||
|
||||
|
@ -136,7 +137,6 @@ pub(crate) fn pool_code(pool_type: PoolType) -> i64 {
|
|||
match pool_type {
|
||||
PoolType::Transparent => 0i64,
|
||||
PoolType::Shielded(ShieldedProtocol::Sapling) => 2i64,
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
PoolType::Shielded(ShieldedProtocol::Orchard) => 3i64,
|
||||
}
|
||||
}
|
||||
|
@ -817,7 +817,6 @@ pub(crate) fn get_received_memo(
|
|||
)
|
||||
.optional()?
|
||||
.flatten(),
|
||||
#[cfg(zcash_unstable = "orchard")]
|
||||
_ => {
|
||||
return Err(SqliteClientError::UnsupportedPoolType(PoolType::Shielded(
|
||||
note_id.protocol(),
|
||||
|
@ -1796,7 +1795,7 @@ pub(crate) fn update_expired_notes(
|
|||
// and `put_sent_output`
|
||||
fn recipient_params<P: consensus::Parameters>(
|
||||
params: &P,
|
||||
to: &Recipient,
|
||||
to: &Recipient<Note>,
|
||||
) -> (Option<String>, Option<u32>, PoolType) {
|
||||
match to {
|
||||
Recipient::Transparent(addr) => (Some(addr.encode(params)), None, PoolType::Transparent),
|
||||
|
@ -1806,7 +1805,11 @@ fn recipient_params<P: consensus::Parameters>(
|
|||
PoolType::Shielded(ShieldedProtocol::Sapling),
|
||||
),
|
||||
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
|
||||
Recipient::InternalAccount(id, pool) => (None, Some(u32::from(*id)), *pool),
|
||||
Recipient::InternalAccount(id, note) => (
|
||||
None,
|
||||
Some(u32::from(*id)),
|
||||
PoolType::Shielded(note.protocol()),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1862,7 +1865,7 @@ pub(crate) fn put_sent_output<P: consensus::Parameters>(
|
|||
from_account: AccountId,
|
||||
tx_ref: i64,
|
||||
output_index: usize,
|
||||
recipient: &Recipient,
|
||||
recipient: &Recipient<Note>,
|
||||
value: NonNegativeAmount,
|
||||
memo: Option<&MemoBytes>,
|
||||
) -> Result<(), SqliteClientError> {
|
||||
|
@ -2227,6 +2230,8 @@ mod tests {
|
|||
#[test]
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn transparent_balance_across_shielding() {
|
||||
use zcash_client_backend::ShieldedProtocol;
|
||||
|
||||
let mut st = TestBuilder::new()
|
||||
.with_block_cache()
|
||||
.with_test_account(AccountBirthday::from_sapling_activation)
|
||||
|
@ -2311,6 +2316,7 @@ mod tests {
|
|||
fixed::SingleOutputChangeStrategy::new(
|
||||
FixedFeeRule::non_standard(NonNegativeAmount::ZERO),
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
|
|
@ -579,8 +579,11 @@ pub(crate) mod tests {
|
|||
let fee_rule = StandardFeeRule::PreZip313;
|
||||
|
||||
let change_memo = "Test change memo".parse::<Memo>().unwrap();
|
||||
let change_strategy =
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, Some(change_memo.clone().into()));
|
||||
let change_strategy = standard::SingleOutputChangeStrategy::new(
|
||||
fee_rule,
|
||||
Some(change_memo.clone().into()),
|
||||
ShieldedProtocol::Sapling,
|
||||
);
|
||||
let input_selector =
|
||||
&GreedyInputSelector::new(change_strategy, DustOutputPolicy::default());
|
||||
|
||||
|
@ -731,7 +734,7 @@ pub(crate) mod tests {
|
|||
|
||||
let fee_rule = StandardFeeRule::Zip317;
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None),
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
let proposal0 = st
|
||||
|
@ -856,7 +859,8 @@ pub(crate) mod tests {
|
|||
None,
|
||||
OvkPolicy::Sender,
|
||||
NonZeroU32::new(1).unwrap(),
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::KeyNotRecognized)
|
||||
);
|
||||
|
@ -885,7 +889,8 @@ pub(crate) mod tests {
|
|||
&to,
|
||||
NonNegativeAmount::const_from_u64(1),
|
||||
None,
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::ScanRequired)
|
||||
);
|
||||
|
@ -952,7 +957,8 @@ pub(crate) mod tests {
|
|||
&to,
|
||||
NonNegativeAmount::const_from_u64(70000),
|
||||
None,
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::InsufficientFunds {
|
||||
available,
|
||||
|
@ -981,7 +987,8 @@ pub(crate) mod tests {
|
|||
&to,
|
||||
NonNegativeAmount::const_from_u64(70000),
|
||||
None,
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::InsufficientFunds {
|
||||
available,
|
||||
|
@ -1016,6 +1023,7 @@ pub(crate) mod tests {
|
|||
amount_sent,
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1073,6 +1081,7 @@ pub(crate) mod tests {
|
|||
NonNegativeAmount::const_from_u64(15000),
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1091,7 +1100,8 @@ pub(crate) mod tests {
|
|||
&to,
|
||||
NonNegativeAmount::const_from_u64(2000),
|
||||
None,
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::InsufficientFunds {
|
||||
available,
|
||||
|
@ -1120,7 +1130,8 @@ pub(crate) mod tests {
|
|||
&to,
|
||||
NonNegativeAmount::const_from_u64(2000),
|
||||
None,
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Err(data_api::error::Error::InsufficientFunds {
|
||||
available,
|
||||
|
@ -1153,6 +1164,7 @@ pub(crate) mod tests {
|
|||
amount_sent2,
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1220,6 +1232,7 @@ pub(crate) mod tests {
|
|||
NonNegativeAmount::const_from_u64(15000),
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)?;
|
||||
|
||||
// Executing the proposal should succeed
|
||||
|
@ -1322,6 +1335,7 @@ pub(crate) mod tests {
|
|||
NonNegativeAmount::const_from_u64(50000),
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1384,6 +1398,7 @@ pub(crate) mod tests {
|
|||
NonNegativeAmount::const_from_u64(50000),
|
||||
None,
|
||||
None,
|
||||
ShieldedProtocol::Sapling,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
|
@ -1455,7 +1470,7 @@ pub(crate) mod tests {
|
|||
#[allow(deprecated)]
|
||||
let fee_rule = FixedFeeRule::standard();
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
fixed::SingleOutputChangeStrategy::new(fee_rule, None),
|
||||
fixed::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -1552,7 +1567,8 @@ pub(crate) mod tests {
|
|||
assert_eq!(st.get_total_balance(account), total);
|
||||
assert_eq!(st.get_spendable_balance(account, 1), total);
|
||||
|
||||
let input_selector = input_selector(StandardFeeRule::Zip317, None);
|
||||
let input_selector =
|
||||
input_selector(StandardFeeRule::Zip317, None, ShieldedProtocol::Sapling);
|
||||
|
||||
// This first request will fail due to insufficient non-dust funds
|
||||
let req = TransactionRequest::new(vec![Payment {
|
||||
|
@ -1657,7 +1673,7 @@ pub(crate) mod tests {
|
|||
let fee_rule = StandardFeeRule::PreZip313;
|
||||
|
||||
let input_selector = GreedyInputSelector::new(
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None),
|
||||
standard::SingleOutputChangeStrategy::new(fee_rule, None, ShieldedProtocol::Sapling),
|
||||
DustOutputPolicy::default(),
|
||||
);
|
||||
|
||||
|
@ -1825,7 +1841,8 @@ pub(crate) mod tests {
|
|||
None,
|
||||
OvkPolicy::Sender,
|
||||
NonZeroU32::new(5).unwrap(),
|
||||
None
|
||||
None,
|
||||
ShieldedProtocol::Sapling
|
||||
),
|
||||
Ok(_)
|
||||
);
|
||||
|
|
|
@ -14,7 +14,8 @@ The entries below are relative to the `zcash_client_backend` crate as of
|
|||
- `address`
|
||||
- `encoding`
|
||||
- `keys`
|
||||
- `zcash_keys::address::UnifiedAddress::unknown`:
|
||||
- `zcash_keys::address::UnifiedAddress::{unknown, has_orchard, has_sapling,
|
||||
has_transparent, receiver_types}`:
|
||||
- `zcash_keys::keys`:
|
||||
- `AddressGenerationError`
|
||||
- `UnifiedAddressRequest`
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
//! Structs for handling supported address types.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use sapling::PaymentAddress;
|
||||
use zcash_address::{
|
||||
unified::{self, Container, Encoding},
|
||||
unified::{self, Container, Encoding, Typecode},
|
||||
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
|
||||
};
|
||||
use zcash_primitives::{consensus, legacy::TransparentAddress};
|
||||
|
@ -108,17 +106,37 @@ impl UnifiedAddress {
|
|||
}
|
||||
}
|
||||
|
||||
/// Returns whether this address has an Orchard receiver.
|
||||
///
|
||||
/// This method is available irrespective of whether the `orchard` feature flag is enabled.
|
||||
pub fn has_orchard(&self) -> bool {
|
||||
#[cfg(not(feature = "orchard"))]
|
||||
return false;
|
||||
#[cfg(feature = "orchard")]
|
||||
return self.orchard.is_some();
|
||||
}
|
||||
|
||||
/// Returns the Orchard receiver within this Unified Address, if any.
|
||||
#[cfg(feature = "orchard")]
|
||||
pub fn orchard(&self) -> Option<&orchard::Address> {
|
||||
self.orchard.as_ref()
|
||||
}
|
||||
|
||||
/// Returns whether this address has a Sapling receiver.
|
||||
pub fn has_sapling(&self) -> bool {
|
||||
self.sapling.is_some()
|
||||
}
|
||||
|
||||
/// Returns the Sapling receiver within this Unified Address, if any.
|
||||
pub fn sapling(&self) -> Option<&PaymentAddress> {
|
||||
self.sapling.as_ref()
|
||||
}
|
||||
|
||||
/// Returns whether this address has a Transparent receiver.
|
||||
pub fn has_transparent(&self) -> bool {
|
||||
self.transparent.is_some()
|
||||
}
|
||||
|
||||
/// Returns the transparent receiver within this Unified Address, if any.
|
||||
pub fn transparent(&self) -> Option<&TransparentAddress> {
|
||||
self.transparent.as_ref()
|
||||
|
@ -168,6 +186,24 @@ impl UnifiedAddress {
|
|||
self.to_address(params.address_network().expect("Unrecognized network"))
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Returns the set of receiver typecodes.
|
||||
pub fn receiver_types(&self) -> Vec<Typecode> {
|
||||
let result = std::iter::empty();
|
||||
#[cfg(feature = "orchard")]
|
||||
let result = result.chain(self.orchard.map(|_| Typecode::Orchard));
|
||||
let result = result.chain(self.sapling.map(|_| Typecode::Sapling));
|
||||
let result = result.chain(self.transparent.map(|taddr| match taddr {
|
||||
TransparentAddress::PublicKeyHash(_) => Typecode::P2pkh,
|
||||
TransparentAddress::ScriptHash(_) => Typecode::P2sh,
|
||||
}));
|
||||
let result = result.chain(
|
||||
self.unknown()
|
||||
.iter()
|
||||
.map(|(typecode, _)| Typecode::Unknown(*typecode)),
|
||||
);
|
||||
result.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// An address that funds can be sent to.
|
||||
|
|
Loading…
Reference in New Issue