zcash_client_backend: Track external addresses in inter-account transactions.

Previously, if the funding account for a received transaction output was
determined to be an account known to the wallet, the output was recorded
as though it were sent to an internal (change) address of the wallet.
This commit is contained in:
Kris Nuttycombe 2024-03-25 07:45:42 -06:00
parent 404132bce5
commit 151e6e526e
5 changed files with 85 additions and 37 deletions

View File

@ -103,6 +103,8 @@ and this library adheres to Rust's notion of
feature flag.
- `zcash_client_backend::proto`:
- `ProposalDecodingError` has a new variant `TransparentMemo`.
- `zcash_client_backend::wallet::Recipient::InternalAccount` is now a structured
variant with an additional `external_address` field.
- `zcash_client_backend::zip321::render::amount_str` now takes a
`NonNegativeAmount` rather than a signed `Amount` as its argument.
- `zcash_client_backend::zip321::parse::parse_amount` now parses a

View File

@ -1049,10 +1049,11 @@ where
memo.clone(),
)?;
sapling_output_meta.push((
Recipient::InternalAccount(
account,
PoolType::Shielded(ShieldedProtocol::Sapling),
),
Recipient::InternalAccount {
receiving_account: account,
external_address: None,
note: PoolType::Shielded(ShieldedProtocol::Sapling),
},
change_value.value(),
Some(memo),
))
@ -1072,10 +1073,11 @@ where
memo.clone(),
)?;
orchard_output_meta.push((
Recipient::InternalAccount(
account,
PoolType::Shielded(ShieldedProtocol::Orchard),
),
Recipient::InternalAccount {
receiving_account: account,
external_address: None,
note: PoolType::Shielded(ShieldedProtocol::Orchard),
},
change_value.value(),
Some(memo),
))

View File

@ -2,6 +2,7 @@
//! light client.
use incrementalmerkletree::Position;
use zcash_keys::address::Address;
use zcash_note_encryption::EphemeralKeyBytes;
use zcash_primitives::{
consensus::BlockHeight,
@ -70,7 +71,11 @@ pub enum Recipient<AccountId, N> {
Transparent(TransparentAddress),
Sapling(sapling::PaymentAddress),
Unified(UnifiedAddress, PoolType),
InternalAccount(AccountId, N),
InternalAccount {
receiving_account: AccountId,
external_address: Option<Address>,
note: N,
},
}
impl<AccountId, N> Recipient<AccountId, N> {
@ -79,7 +84,15 @@ impl<AccountId, N> Recipient<AccountId, N> {
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)),
Recipient::InternalAccount {
receiving_account,
external_address,
note,
} => Recipient::InternalAccount {
receiving_account,
external_address,
note: f(note),
},
}
}
}
@ -90,7 +103,15 @@ impl<AccountId, N> Recipient<AccountId, Option<N>> {
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)),
Recipient::InternalAccount {
receiving_account,
external_address,
note,
} => note.map(|n0| Recipient::InternalAccount {
receiving_account,
external_address,
note: n0,
}),
}
}
}

View File

@ -65,6 +65,7 @@ use zcash_client_backend::{
wallet::{Note, NoteId, ReceivedNote, Recipient, WalletTransparentOutput},
DecryptedOutput, PoolType, ShieldedProtocol, TransferType,
};
use zcash_keys::address::Address;
use zcash_primitives::{
block::BlockHash,
consensus::{self, BlockHeight},
@ -1066,10 +1067,11 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
//TODO: Recover the UA, if possible.
Recipient::Sapling(output.note().recipient())
} else {
Recipient::InternalAccount(
*output.account(),
Note::Sapling(output.note().clone()),
)
Recipient::InternalAccount {
receiving_account: *output.account(),
external_address: None,
note: Note::Sapling(output.note().clone()),
}
};
wallet::put_sent_output(
@ -1083,7 +1085,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
Some(output.memo()),
)?;
if matches!(recipient, Recipient::InternalAccount(_, _)) {
if matches!(recipient, Recipient::InternalAccount { .. }) {
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
@ -1091,11 +1093,12 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
wallet::sapling::put_received_note(wdb.conn.0, output, tx_ref, None)?;
if let Some(account_id) = funding_account {
// Even if the recipient address is external, record the send as internal.
let recipient = Recipient::InternalAccount(
*output.account(),
Note::Sapling(output.note().clone()),
);
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Sapling(output.note().recipient())),
note: Note::Sapling(output.note().clone()),
};
wallet::put_sent_output(
wdb.conn.0,
@ -1128,10 +1131,11 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
PoolType::Shielded(ShieldedProtocol::Orchard),
)
} else {
Recipient::InternalAccount(
*output.account(),
Note::Orchard(*output.note()),
)
Recipient::InternalAccount {
receiving_account: *output.account(),
external_address: None,
note: Note::Orchard(*output.note()),
}
};
wallet::put_sent_output(
@ -1145,7 +1149,7 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
Some(output.memo()),
)?;
if matches!(recipient, Recipient::InternalAccount(_, _)) {
if matches!(recipient, Recipient::InternalAccount { .. }) {
wallet::orchard::put_received_note(wdb.conn.0, output, tx_ref, None)?;
}
}
@ -1154,10 +1158,17 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
if let Some(account_id) = funding_account {
// Even if the recipient address is external, record the send as internal.
let recipient = Recipient::InternalAccount(
*output.account(),
Note::Orchard(*output.note()),
);
let recipient = Recipient::InternalAccount {
receiving_account: *output.account(),
// TODO: recover the actual UA, if possible
external_address: Some(Address::Unified(
UnifiedAddress::from_receivers(
Some(output.note().recipient()),
None,
None,
).expect("UA has an Orchard receiver by construction."))),
note: Note::Orchard(*output.note()),
};
wallet::put_sent_output(
wdb.conn.0,
@ -1288,13 +1299,17 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)?;
match output.recipient() {
Recipient::InternalAccount(account, Note::Sapling(note)) => {
Recipient::InternalAccount {
receiving_account,
note: Note::Sapling(note),
..
} => {
wallet::sapling::put_received_note(
wdb.conn.0,
&DecryptedOutput::new(
output.output_index(),
note.clone(),
*account,
*receiving_account,
output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),
@ -1305,13 +1320,17 @@ impl<P: consensus::Parameters> WalletWrite for WalletDb<rusqlite::Connection, P>
)?;
}
#[cfg(feature = "orchard")]
Recipient::InternalAccount(account, Note::Orchard(note)) => {
Recipient::InternalAccount {
receiving_account,
note: Note::Orchard(note),
..
} => {
wallet::orchard::put_received_note(
wdb.conn.0,
&DecryptedOutput::new(
output.output_index(),
*note,
*account,
*receiving_account,
output
.memo()
.map_or_else(MemoBytes::empty, |memo| memo.clone()),

View File

@ -2527,9 +2527,13 @@ fn recipient_params<P: consensus::Parameters>(
PoolType::Shielded(ShieldedProtocol::Sapling),
),
Recipient::Unified(addr, pool) => (Some(addr.encode(params)), None, *pool),
Recipient::InternalAccount(id, note) => (
None,
Some(id.to_owned()),
Recipient::InternalAccount {
receiving_account,
external_address,
note,
} => (
external_address.as_ref().map(|a| a.encode(params)),
Some(*receiving_account),
PoolType::Shielded(note.protocol()),
),
}