Merge pull request #568 from zcash/387-unified-address
Add `RecipientAddress::Unified`
This commit is contained in:
commit
7f59b588be
|
@ -25,6 +25,8 @@ and this library adheres to Rust's notion of
|
|||
of a `zcash_client_backend::zip321::TransactionRequest` value.
|
||||
This facilitates the implementation of ZIP 321 support in wallets and
|
||||
provides substantially greater flexibility in transaction creation.
|
||||
- `zcash_client_backend::address`:
|
||||
- `RecipientAddress::Unified`
|
||||
- `zcash_client_backend::proto`:
|
||||
- `actions` field on `compact_formats::CompactTx`
|
||||
- `compact_formats::CompactOrchardAction`
|
||||
|
@ -35,6 +37,7 @@ and this library adheres to Rust's notion of
|
|||
- New experimental APIs that should be considered unstable, and are
|
||||
likely to be modified and/or moved to a different module in a future
|
||||
release:
|
||||
- `zcash_client_backend::address::UnifiedAddress`
|
||||
- `zcash_client_backend::keys::{UnifiedSpendingKey`, `UnifiedFullViewingKey`}
|
||||
- `zcash_client_backend::encoding::AddressCodec`
|
||||
- `zcash_client_backend::encoding::encode_payment_address`
|
||||
|
|
|
@ -24,6 +24,7 @@ hdwallet = { version = "0.3.1", optional = true }
|
|||
jubjub = "0.9"
|
||||
log = "0.4"
|
||||
nom = "7"
|
||||
orchard = "0.1"
|
||||
percent-encoding = "2.1.0"
|
||||
proptest = { version = "1.0.0", optional = true }
|
||||
protobuf = "~2.27.1" # MSRV 1.52.1
|
||||
|
@ -33,6 +34,7 @@ secp256k1 = { version = "0.21", optional = true }
|
|||
sha2 = { version = "0.10.1", optional = true }
|
||||
subtle = "2.2.3"
|
||||
time = "0.2"
|
||||
zcash_address = { version = "0.1", path = "../components/zcash_address" }
|
||||
zcash_note_encryption = { version = "0.1", path = "../components/zcash_note_encryption" }
|
||||
zcash_primitives = { version = "0.6", path = "../zcash_primitives" }
|
||||
|
||||
|
@ -47,7 +49,11 @@ zcash_proofs = { version = "0.6", path = "../zcash_proofs" }
|
|||
|
||||
[features]
|
||||
transparent-inputs = ["ripemd", "hdwallet", "sha2", "secp256k1", "zcash_primitives/transparent-inputs"]
|
||||
test-dependencies = ["proptest", "zcash_primitives/test-dependencies"]
|
||||
test-dependencies = [
|
||||
"proptest",
|
||||
"orchard/test-dependencies",
|
||||
"zcash_primitives/test-dependencies",
|
||||
]
|
||||
|
||||
[lib]
|
||||
bench = false
|
||||
|
|
|
@ -1,11 +1,143 @@
|
|||
//! Structs for handling supported address types.
|
||||
|
||||
use zcash_primitives::{consensus, legacy::TransparentAddress, sapling::PaymentAddress};
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use crate::encoding::{
|
||||
decode_payment_address, decode_transparent_address, encode_payment_address_p,
|
||||
encode_transparent_address_p,
|
||||
use zcash_address::{
|
||||
unified::{self, Container, Encoding},
|
||||
ConversionError, Network, ToAddress, TryFromRawAddress, ZcashAddress,
|
||||
};
|
||||
use zcash_primitives::{consensus, constants, legacy::TransparentAddress, sapling::PaymentAddress};
|
||||
|
||||
fn params_to_network<P: consensus::Parameters>(params: &P) -> Network {
|
||||
// Use the Sapling HRP as an indicator of network.
|
||||
match params.hrp_sapling_payment_address() {
|
||||
constants::mainnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Main,
|
||||
constants::testnet::HRP_SAPLING_PAYMENT_ADDRESS => Network::Test,
|
||||
constants::regtest::HRP_SAPLING_PAYMENT_ADDRESS => Network::Regtest,
|
||||
_ => panic!("Unsupported network kind"),
|
||||
}
|
||||
}
|
||||
|
||||
/// A Unified Address.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct UnifiedAddress {
|
||||
orchard: Option<orchard::Address>,
|
||||
sapling: Option<PaymentAddress>,
|
||||
transparent: Option<TransparentAddress>,
|
||||
unknown: Vec<(u32, Vec<u8>)>,
|
||||
}
|
||||
|
||||
impl TryFrom<unified::Address> for UnifiedAddress {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from(ua: unified::Address) -> Result<Self, Self::Error> {
|
||||
let mut orchard = None;
|
||||
let mut sapling = None;
|
||||
let mut transparent = None;
|
||||
|
||||
// We can use as-parsed order here for efficiency, because we're breaking out the
|
||||
// receivers we support from the unknown receivers.
|
||||
let unknown = ua
|
||||
.items_as_parsed()
|
||||
.iter()
|
||||
.filter_map(|receiver| match receiver {
|
||||
unified::Receiver::Orchard(data) => {
|
||||
Option::from(orchard::Address::from_raw_address_bytes(data))
|
||||
.ok_or("Invalid Orchard receiver in Unified Address")
|
||||
.map(|addr| {
|
||||
orchard = Some(addr);
|
||||
None
|
||||
})
|
||||
.transpose()
|
||||
}
|
||||
unified::Receiver::Sapling(data) => PaymentAddress::from_bytes(data)
|
||||
.ok_or("Invalid Sapling receiver in Unified Address")
|
||||
.map(|pa| {
|
||||
sapling = Some(pa);
|
||||
None
|
||||
})
|
||||
.transpose(),
|
||||
unified::Receiver::P2pkh(data) => {
|
||||
transparent = Some(TransparentAddress::PublicKey(*data));
|
||||
None
|
||||
}
|
||||
unified::Receiver::P2sh(data) => {
|
||||
transparent = Some(TransparentAddress::Script(*data));
|
||||
None
|
||||
}
|
||||
unified::Receiver::Unknown { typecode, data } => {
|
||||
Some(Ok((*typecode, data.clone())))
|
||||
}
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
Ok(Self {
|
||||
orchard,
|
||||
sapling,
|
||||
transparent,
|
||||
unknown,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl UnifiedAddress {
|
||||
/// Constructs a Unified Address from a given set of receivers.
|
||||
///
|
||||
/// Returns `None` if the receivers would produce an invalid Unified Address (namely,
|
||||
/// if no shielded receiver is provided).
|
||||
pub fn from_receivers(
|
||||
orchard: Option<orchard::Address>,
|
||||
sapling: Option<PaymentAddress>,
|
||||
transparent: Option<TransparentAddress>,
|
||||
) -> Option<Self> {
|
||||
if orchard.is_some() || sapling.is_some() {
|
||||
Some(Self {
|
||||
orchard,
|
||||
sapling,
|
||||
transparent,
|
||||
unknown: vec![],
|
||||
})
|
||||
} else {
|
||||
// UAs require at least one shielded receiver.
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Sapling receiver within this Unified Address, if any.
|
||||
pub fn sapling(&self) -> Option<&PaymentAddress> {
|
||||
self.sapling.as_ref()
|
||||
}
|
||||
|
||||
fn to_address(&self, net: Network) -> ZcashAddress {
|
||||
let ua = unified::Address::try_from_items(
|
||||
self.unknown
|
||||
.iter()
|
||||
.map(|(typecode, data)| unified::Receiver::Unknown {
|
||||
typecode: *typecode,
|
||||
data: data.clone(),
|
||||
})
|
||||
.chain(self.transparent.as_ref().map(|taddr| match taddr {
|
||||
TransparentAddress::PublicKey(data) => unified::Receiver::P2pkh(*data),
|
||||
TransparentAddress::Script(data) => unified::Receiver::P2sh(*data),
|
||||
}))
|
||||
.chain(
|
||||
self.sapling
|
||||
.as_ref()
|
||||
.map(|pa| pa.to_bytes())
|
||||
.map(unified::Receiver::Sapling),
|
||||
)
|
||||
.chain(
|
||||
self.orchard
|
||||
.as_ref()
|
||||
.map(|addr| addr.to_raw_address_bytes())
|
||||
.map(unified::Receiver::Orchard),
|
||||
)
|
||||
.collect(),
|
||||
)
|
||||
.expect("UnifiedAddress should only be constructed safely");
|
||||
ZcashAddress::from_unified(net, ua)
|
||||
}
|
||||
}
|
||||
|
||||
/// An address that funds can be sent to.
|
||||
// TODO: rename to ParsedAddress
|
||||
|
@ -13,6 +145,7 @@ use crate::encoding::{
|
|||
pub enum RecipientAddress {
|
||||
Shielded(PaymentAddress),
|
||||
Transparent(TransparentAddress),
|
||||
Unified(UnifiedAddress),
|
||||
}
|
||||
|
||||
impl From<PaymentAddress> for RecipientAddress {
|
||||
|
@ -27,25 +160,92 @@ impl From<TransparentAddress> for RecipientAddress {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<UnifiedAddress> for RecipientAddress {
|
||||
fn from(addr: UnifiedAddress) -> Self {
|
||||
RecipientAddress::Unified(addr)
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFromRawAddress for RecipientAddress {
|
||||
type Error = &'static str;
|
||||
|
||||
fn try_from_raw_sapling(data: [u8; 43]) -> Result<Self, ConversionError<Self::Error>> {
|
||||
let pa = PaymentAddress::from_bytes(&data).ok_or("Invalid Sapling payment address")?;
|
||||
Ok(pa.into())
|
||||
}
|
||||
|
||||
fn try_from_raw_unified(
|
||||
ua: zcash_address::unified::Address,
|
||||
) -> Result<Self, ConversionError<Self::Error>> {
|
||||
UnifiedAddress::try_from(ua)
|
||||
.map_err(ConversionError::User)
|
||||
.map(RecipientAddress::from)
|
||||
}
|
||||
|
||||
fn try_from_raw_transparent_p2pkh(
|
||||
data: [u8; 20],
|
||||
) -> Result<Self, ConversionError<Self::Error>> {
|
||||
Ok(TransparentAddress::PublicKey(data).into())
|
||||
}
|
||||
|
||||
fn try_from_raw_transparent_p2sh(data: [u8; 20]) -> Result<Self, ConversionError<Self::Error>> {
|
||||
Ok(TransparentAddress::Script(data).into())
|
||||
}
|
||||
}
|
||||
|
||||
impl RecipientAddress {
|
||||
pub fn decode<P: consensus::Parameters>(params: &P, s: &str) -> Option<Self> {
|
||||
if let Ok(Some(pa)) = decode_payment_address(params.hrp_sapling_payment_address(), s) {
|
||||
Some(pa.into())
|
||||
} else if let Ok(Some(addr)) = decode_transparent_address(
|
||||
¶ms.b58_pubkey_address_prefix(),
|
||||
¶ms.b58_script_address_prefix(),
|
||||
s,
|
||||
) {
|
||||
Some(addr.into())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
let addr = ZcashAddress::try_from_encoded(s).ok()?;
|
||||
addr.convert_if_network(params_to_network(params)).ok()
|
||||
}
|
||||
|
||||
pub fn encode<P: consensus::Parameters>(&self, params: &P) -> String {
|
||||
let net = params_to_network(params);
|
||||
|
||||
match self {
|
||||
RecipientAddress::Shielded(pa) => encode_payment_address_p(params, pa),
|
||||
RecipientAddress::Transparent(addr) => encode_transparent_address_p(params, addr),
|
||||
RecipientAddress::Shielded(pa) => ZcashAddress::from_sapling(net, pa.to_bytes()),
|
||||
RecipientAddress::Transparent(addr) => match addr {
|
||||
TransparentAddress::PublicKey(data) => {
|
||||
ZcashAddress::from_transparent_p2pkh(net, *data)
|
||||
}
|
||||
TransparentAddress::Script(data) => ZcashAddress::from_transparent_p2sh(net, *data),
|
||||
},
|
||||
RecipientAddress::Unified(ua) => ua.to_address(net),
|
||||
}
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use zcash_primitives::{consensus::MAIN_NETWORK, zip32::ExtendedFullViewingKey};
|
||||
|
||||
use super::{RecipientAddress, UnifiedAddress};
|
||||
use crate::keys::sapling;
|
||||
|
||||
#[test]
|
||||
fn ua_round_trip() {
|
||||
let orchard = {
|
||||
let sk = orchard::keys::SpendingKey::from_zip32_seed(&[0; 32], 0, 0).unwrap();
|
||||
let fvk = orchard::keys::FullViewingKey::from(&sk);
|
||||
Some(fvk.address_at(0u32, orchard::keys::Scope::External))
|
||||
};
|
||||
|
||||
let sapling = {
|
||||
let extsk = sapling::spending_key(&[0; 32], 0, 0.into());
|
||||
let extfvk = ExtendedFullViewingKey::from(&extsk);
|
||||
Some(extfvk.default_address().1)
|
||||
};
|
||||
|
||||
let transparent = { None };
|
||||
|
||||
let ua = UnifiedAddress::from_receivers(orchard, sapling, transparent).unwrap();
|
||||
|
||||
let addr = RecipientAddress::Unified(ua);
|
||||
let addr_str = addr.encode(&MAIN_NETWORK);
|
||||
assert_eq!(
|
||||
RecipientAddress::decode(&MAIN_NETWORK, &addr_str),
|
||||
Some(addr)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -334,6 +334,16 @@ where
|
|||
|
||||
for payment in request.payments() {
|
||||
match &payment.recipient_address {
|
||||
RecipientAddress::Unified(ua) => builder
|
||||
.add_sapling_output(
|
||||
ovk,
|
||||
ua.sapling()
|
||||
.expect("TODO: Add Orchard support to builder")
|
||||
.clone(),
|
||||
payment.amount,
|
||||
payment.memo.clone().unwrap_or_else(MemoBytes::empty),
|
||||
)
|
||||
.map_err(Error::Builder),
|
||||
RecipientAddress::Shielded(to) => builder
|
||||
.add_sapling_output(
|
||||
ovk,
|
||||
|
@ -359,7 +369,8 @@ where
|
|||
let sent_outputs = request.payments().iter().enumerate().map(|(i, payment)| {
|
||||
let idx = match &payment.recipient_address {
|
||||
// Sapling outputs are shuffled, so we need to look up where the output ended up.
|
||||
RecipientAddress::Shielded(_) =>
|
||||
// TODO: When we add Orchard support, we will need to trial-decrypt to find them.
|
||||
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) =>
|
||||
tx_metadata.output_index(i).expect("An output should exist in the transaction for each shielded payment."),
|
||||
RecipientAddress::Transparent(addr) => {
|
||||
let script = addr.script();
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
//! Helper functions for managing light client key material.
|
||||
use zcash_primitives::{consensus, zip32::AccountId};
|
||||
use zcash_primitives::{
|
||||
consensus,
|
||||
zip32::{AccountId, DiversifierIndex},
|
||||
};
|
||||
|
||||
use crate::address::UnifiedAddress;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use zcash_primitives::legacy::keys as legacy;
|
||||
use std::convert::TryInto;
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
use zcash_primitives::legacy::keys::{self as legacy, IncomingViewingKey};
|
||||
|
||||
pub mod sapling {
|
||||
use zcash_primitives::zip32::{AccountId, ChildIndex};
|
||||
|
@ -45,6 +53,17 @@ pub mod sapling {
|
|||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
fn to_transparent_child_index(j: DiversifierIndex) -> Option<u32> {
|
||||
let (low_4_bytes, rest) = j.0.split_at(4);
|
||||
let transparent_j = u32::from_le_bytes(low_4_bytes.try_into().unwrap());
|
||||
if transparent_j > (0x7FFFFFFF) || rest.iter().any(|b| b != &0) {
|
||||
None
|
||||
} else {
|
||||
Some(transparent_j)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[doc(hidden)]
|
||||
pub enum DerivationError {
|
||||
|
@ -164,6 +183,75 @@ impl UnifiedFullViewingKey {
|
|||
pub fn sapling(&self) -> Option<&sapling::ExtendedFullViewingKey> {
|
||||
self.sapling.as_ref()
|
||||
}
|
||||
|
||||
/// Attempts to derive the Unified Address for the given diversifier index.
|
||||
///
|
||||
/// Returns `None` if the specified index does not produce a valid diversifier.
|
||||
// TODO: Allow filtering down by receiver types?
|
||||
pub fn address(&self, j: DiversifierIndex) -> Option<UnifiedAddress> {
|
||||
let sapling = if let Some(extfvk) = self.sapling.as_ref() {
|
||||
Some(extfvk.address(j)?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
let transparent = if let Some(tfvk) = self.transparent.as_ref() {
|
||||
match to_transparent_child_index(j) {
|
||||
Some(transparent_j) => match tfvk
|
||||
.derive_external_ivk()
|
||||
.and_then(|tivk| tivk.derive_address(transparent_j))
|
||||
{
|
||||
Ok(taddr) => Some(taddr),
|
||||
Err(_) => return None,
|
||||
},
|
||||
// Diversifier doesn't generate a valid transparent child index.
|
||||
None => return None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
#[cfg(not(feature = "transparent-inputs"))]
|
||||
let transparent = None;
|
||||
|
||||
UnifiedAddress::from_receivers(None, sapling, transparent)
|
||||
}
|
||||
|
||||
/// Searches the diversifier space starting at diversifier index `j` for one which will
|
||||
/// produce a valid diversifier, and return the Unified Address constructed using that
|
||||
/// diversifier along with the index at which the valid diversifier was found.
|
||||
///
|
||||
/// Returns `None` if no valid diversifier exists
|
||||
pub fn find_address(
|
||||
&self,
|
||||
mut j: DiversifierIndex,
|
||||
) -> Option<(UnifiedAddress, DiversifierIndex)> {
|
||||
// If we need to generate a transparent receiver, check that the user has not
|
||||
// specified an invalid transparent child index, from which we can never search to
|
||||
// find a valid index.
|
||||
#[cfg(feature = "transparent-inputs")]
|
||||
if self.transparent.is_some() && to_transparent_child_index(j).is_none() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find a working diversifier and construct the associated address.
|
||||
loop {
|
||||
let res = self.address(j);
|
||||
if let Some(ua) = res {
|
||||
break Some((ua, j));
|
||||
}
|
||||
if j.increment().is_err() {
|
||||
break None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the Unified Address corresponding to the smallest valid diversifier index,
|
||||
/// along with that index.
|
||||
pub fn default_address(&self) -> (UnifiedAddress, DiversifierIndex) {
|
||||
self.find_address(DiversifierIndex::new())
|
||||
.expect("UFVK should have at least one valid diversifier")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
|
|
@ -393,7 +393,7 @@ mod parse {
|
|||
/// ZIP 321 URI.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum Param {
|
||||
Addr(RecipientAddress),
|
||||
Addr(Box<RecipientAddress>),
|
||||
Amount(Amount),
|
||||
Memo(MemoBytes),
|
||||
Label(String),
|
||||
|
@ -440,7 +440,7 @@ mod parse {
|
|||
});
|
||||
|
||||
let mut payment = Payment {
|
||||
recipient_address: addr.ok_or(Zip321Error::RecipientMissing(i))?,
|
||||
recipient_address: *addr.ok_or(Zip321Error::RecipientMissing(i))?,
|
||||
amount: Amount::zero(),
|
||||
memo: None,
|
||||
label: None,
|
||||
|
@ -452,7 +452,9 @@ mod parse {
|
|||
match v {
|
||||
Param::Amount(a) => payment.amount = a,
|
||||
Param::Memo(m) => match payment.recipient_address {
|
||||
RecipientAddress::Shielded(_) => payment.memo = Some(m),
|
||||
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => {
|
||||
payment.memo = Some(m)
|
||||
}
|
||||
RecipientAddress::Transparent(_) => {
|
||||
return Err(Zip321Error::TransparentMemo(i))
|
||||
}
|
||||
|
@ -483,7 +485,7 @@ mod parse {
|
|||
// then cause `map_opt` to fail.
|
||||
RecipientAddress::decode(params, addr_str).map(|a| {
|
||||
Some(IndexedParam {
|
||||
param: Param::Addr(a),
|
||||
param: Param::Addr(Box::new(a)),
|
||||
payment_index: 0,
|
||||
})
|
||||
})
|
||||
|
@ -587,6 +589,7 @@ mod parse {
|
|||
) -> Result<IndexedParam, String> {
|
||||
let param = match name {
|
||||
"address" => RecipientAddress::decode(params, value)
|
||||
.map(Box::new)
|
||||
.map(Param::Addr)
|
||||
.ok_or(format!(
|
||||
"Could not interpret {} as a valid Zcash address.",
|
||||
|
@ -646,14 +649,24 @@ pub mod testing {
|
|||
transaction::components::amount::testing::arb_nonnegative_amount,
|
||||
};
|
||||
|
||||
use crate::address::RecipientAddress;
|
||||
use crate::address::{RecipientAddress, UnifiedAddress};
|
||||
|
||||
use super::{MemoBytes, Payment, TransactionRequest};
|
||||
|
||||
prop_compose! {
|
||||
fn arb_unified_addr()(
|
||||
sapling in arb_shielded_addr(),
|
||||
transparent in option::of(arb_transparent_addr()),
|
||||
) -> UnifiedAddress {
|
||||
UnifiedAddress::from_receivers(None, Some(sapling), transparent).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn arb_addr() -> impl Strategy<Value = RecipientAddress> {
|
||||
prop_oneof![
|
||||
arb_shielded_addr().prop_map(RecipientAddress::Shielded),
|
||||
arb_transparent_addr().prop_map(RecipientAddress::Transparent),
|
||||
arb_unified_addr().prop_map(RecipientAddress::Unified),
|
||||
]
|
||||
}
|
||||
|
||||
|
@ -676,15 +689,15 @@ pub mod testing {
|
|||
other_params in btree_map(VALID_PARAMNAME, any::<String>(), 0..3),
|
||||
) -> Payment {
|
||||
|
||||
let is_sapling = match recipient_address {
|
||||
let is_shielded = match recipient_address {
|
||||
RecipientAddress::Transparent(_) => false,
|
||||
RecipientAddress::Shielded(_) => true,
|
||||
RecipientAddress::Shielded(_) | RecipientAddress::Unified(_) => true,
|
||||
};
|
||||
|
||||
Payment {
|
||||
recipient_address,
|
||||
amount,
|
||||
memo: memo.filter(|_| is_sapling),
|
||||
memo: memo.filter(|_| is_shielded),
|
||||
label,
|
||||
message,
|
||||
other_params: other_params.into_iter().collect(),
|
||||
|
|
|
@ -642,6 +642,17 @@ impl<'a, P: consensus::Parameters> WalletWrite for DataConnStmtCache<'a, P> {
|
|||
|
||||
for output in &sent_tx.outputs {
|
||||
match output.recipient_address {
|
||||
// TODO: Store the entire UA, not just the Sapling component.
|
||||
// This will require more info about the output index.
|
||||
RecipientAddress::Unified(ua) => wallet::insert_sent_note(
|
||||
up,
|
||||
tx_ref,
|
||||
output.output_index,
|
||||
sent_tx.account,
|
||||
ua.sapling().expect("TODO: Add Orchard support"),
|
||||
output.value,
|
||||
output.memo.as_ref(),
|
||||
)?,
|
||||
RecipientAddress::Shielded(addr) => wallet::insert_sent_note(
|
||||
up,
|
||||
tx_ref,
|
||||
|
|
Loading…
Reference in New Issue