use rusqlite::named_params;
use zcash_client_backend::data_api::transparent_ledger::{
AccountActivity, ActivityOutputs, FeeState, OutputRef,
};
use zcash_protocol::value::Zatoshis;
use crate::{AccountUuid, error::SqliteClientError, wallet::encoding::parse_pool_code};
const TRANSPARENT_POOL: i64 = 0;
const SAPLING_POOL: i64 = 2;
const ORCHARD_POOL: i64 = 3;
const IRONWOOD_POOL: i64 = 4;
const IRONWOOD_NOTE_VERSION: i64 = 3;
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(super) struct ActivityOutput {
pub(super) pool: i64,
pub(super) index: u32,
pub(super) value: u64,
pub(super) from_own: bool,
pub(super) from_unknown: bool,
pub(super) to_own: bool,
pub(super) to_key_scope: Option<i64>,
pub(super) sent_to_address: bool,
pub(super) ironwood_note: bool,
}
impl ActivityOutput {
fn key(&self) -> Result<OutputRef, SqliteClientError> {
Ok(OutputRef {
pool: parse_pool_code(self.pool)?,
index: self.index,
})
}
fn is_ironwood(&self) -> bool {
self.pool == IRONWOOD_POOL || self.ironwood_note
}
pub(super) fn has_unknown_transparent_scope(&self) -> bool {
self.pool == TRANSPARENT_POOL && self.to_own && !matches!(self.to_key_scope, Some(-1..=2))
}
fn is_user_visible_self_output(&self) -> bool {
let external = matches!(self.to_key_scope, Some(0) | Some(-1));
match self.pool {
TRANSPARENT_POOL => external,
SAPLING_POOL | ORCHARD_POOL | IRONWOOD_POOL => external || self.sent_to_address,
_ => false,
}
}
}
pub(super) fn read_outputs(
conn: &rusqlite::Connection,
account: AccountUuid,
account_id: i64,
transaction_id: i64,
) -> Result<Vec<ActivityOutput>, SqliteClientError> {
let mut stmt = conn.prepare_cached(
"SELECT txo.output_pool, txo.output_index, txo.value,
txo.from_account_uuid = :uuid, txo.from_account_uuid IS NULL,
txo.to_account_uuid IS NOT NULL AND txo.to_account_uuid = :uuid,
(SELECT a.key_scope FROM accounts acc
JOIN addresses a ON a.account_id = acc.id
WHERE acc.uuid = txo.to_account_uuid
AND (a.address = txo.to_address
OR a.cached_transparent_receiver_address = txo.to_address)
LIMIT 1),
EXISTS (SELECT 1 FROM sent_notes sn
WHERE sn.transaction_id = txo.transaction_id
AND sn.from_account_id = :account_id
AND sn.output_pool = txo.output_pool
AND sn.output_index = txo.output_index
AND sn.to_address IS NOT NULL),
EXISTS (SELECT 1 FROM orchard_received_notes orn
WHERE txo.output_pool IN (3, 4)
AND orn.transaction_id = txo.transaction_id
AND orn.action_index = txo.output_index
AND orn.note_version = :ironwood_version)
FROM v_tx_outputs txo
WHERE txo.transaction_id = :transaction_id
AND (txo.from_account_uuid = :uuid OR txo.to_account_uuid = :uuid)
ORDER BY txo.output_pool, txo.output_index",
)?;
let rows = stmt.query_map(
named_params![
":uuid": account.0,
":account_id": account_id,
":transaction_id": transaction_id,
":ironwood_version": IRONWOOD_NOTE_VERSION,
],
|row| {
Ok(ActivityOutput {
pool: row.get(0)?,
index: row.get(1)?,
value: row.get::<_, i64>(2)?.unsigned_abs(),
from_own: row.get::<_, Option<bool>>(3)?.unwrap_or(false),
from_unknown: row.get(4)?,
to_own: row.get(5)?,
to_key_scope: row.get(6)?,
sent_to_address: row.get(7)?,
ironwood_note: row.get(8)?,
})
},
)?;
Ok(rows.collect::<Result<_, _>>()?)
}
pub(super) fn has_orchard_spend(
conn: &rusqlite::Connection,
transaction_id: i64,
) -> Result<bool, SqliteClientError> {
Ok(conn.query_row(
"SELECT EXISTS (
SELECT 1 FROM orchard_received_note_spends s
JOIN orchard_received_notes n ON n.id = s.orchard_received_note_id
WHERE s.transaction_id = :transaction_id AND n.note_version = 2
)",
named_params![":transaction_id": transaction_id],
|row| row.get(0),
)?)
}
#[derive(Clone, Debug)]
pub(super) struct ActivityEvidence {
pub(super) orchard_spend: bool,
pub(super) mined: bool,
pub(super) spent: u64,
pub(super) net: i128,
pub(super) provisional: bool,
pub(super) effects_settled: bool,
pub(super) fee: FeeState,
pub(super) whole_fee: Option<u64>,
pub(super) inferred_payment: Option<u64>,
pub(super) inferred_outgoing: Option<u64>,
}
impl ActivityEvidence {
fn known_fee_or_zero(&self) -> u64 {
match self.fee {
FeeState::Known(fee) => fee.into_u64(),
_ => 0,
}
}
fn reconstruction_fee(&self) -> Option<u64> {
match self.fee {
FeeState::Known(fee) | FeeState::Unattributed { whole: fee } => Some(fee.into_u64()),
FeeState::Unknown | FeeState::NotApplicable => None,
}
}
fn debit(&self) -> u64 {
u64::try_from(self.net.unsigned_abs()).unwrap_or(u64::MAX)
}
fn recovered_self_transfer(&self) -> bool {
let debit = self.debit();
self.inferred_payment == Some(0)
&& self.net < 0
&& self.whole_fee == Some(debit)
&& self.reconstruction_fee() == Some(debit)
}
}
#[derive(Clone, Debug, Default)]
struct Group {
amount: u64,
count: usize,
outputs: Vec<OutputRef>,
}
impl Group {
fn add(&mut self, output: &ActivityOutput) -> Result<(), SqliteClientError> {
self.amount = self.amount.saturating_add(output.value);
self.count += 1;
self.outputs.push(output.key()?);
Ok(())
}
fn rows(&self) -> Result<ActivityOutputs, SqliteClientError> {
Ok(ActivityOutputs {
amount: zatoshis(self.amount)?,
outputs: self.outputs.clone(),
})
}
}
#[derive(Debug, Default)]
pub(super) struct Summary {
sent: Group,
received: Group,
received_transparent: Group,
internal_transparent: Group,
pub(super) unknown_transparent_scope: bool,
}
pub(super) fn summarize(
ev: &ActivityEvidence,
outputs: &[ActivityOutput],
) -> Result<Summary, SqliteClientError> {
let mut summary = Summary::default();
let pays_others = outputs.iter().any(|o| o.from_own && !o.to_own);
let recovered_self_transfer = ev.recovered_self_transfer();
for output in outputs {
let (from_own, to_own) = (output.from_own, output.to_own);
let recovered_from_own =
output.from_unknown && (ev.inferred_outgoing.is_some() || recovered_self_transfer);
if output.has_unknown_transparent_scope() {
summary.unknown_transparent_scope = true;
}
if from_own && to_own && ev.orchard_spend && output.is_ironwood() {
continue;
}
if output.pool == TRANSPARENT_POOL
&& to_own
&& (from_own || recovered_from_own)
&& matches!(output.to_key_scope, Some(1) | Some(2))
{
summary.internal_transparent.add(output)?;
continue;
}
let visible_self_output = (from_own || (recovered_self_transfer && recovered_from_own))
&& to_own
&& output.is_user_visible_self_output();
let visible_sent = (from_own && (!to_own || (visible_self_output && !pays_others)))
|| (recovered_self_transfer && visible_self_output && !pays_others);
let visible_received = to_own && (!from_own || visible_self_output);
if visible_sent {
summary.sent.add(output)?;
}
if visible_received {
summary.received.add(output)?;
if output.pool == TRANSPARENT_POOL {
summary.received_transparent.add(output)?;
}
}
}
Ok(summary)
}
pub(super) fn activity(
ev: &ActivityEvidence,
summary: &Summary,
) -> Result<AccountActivity, SqliteClientError> {
if let Some(payment) = ev.inferred_payment {
if payment > 0 && ev.net < 0 && summary.sent.count == 0 {
return Ok(AccountActivity::InferredPayment {
amount: zatoshis(payment)?,
});
}
let debit = ev.debit();
if payment == 0
&& ev.net < 0
&& summary.sent.count == 0
&& ev.whole_fee == Some(debit)
&& ev.reconstruction_fee() == Some(debit)
{
return Ok(AccountActivity::FeeOnly {
fee: zatoshis(debit)?,
});
}
}
if let Some(outgoing) = ev.inferred_outgoing
&& outgoing > 0
&& summary.sent.count == 0
{
let visible = outgoing.checked_sub(summary.internal_transparent.amount);
if visible == Some(0) && summary.received_transparent.count == 0 {
return Ok(AccountActivity::HiddenFundingStep);
}
let shown = visible.filter(|amount| *amount > 0).unwrap_or(outgoing);
let settled = ev.effects_settled
&& ev.mined
&& !summary.unknown_transparent_scope
&& visible
.is_some_and(|value| value > 0 && value >= summary.received_transparent.amount);
return Ok(AccountActivity::InferredOutgoing {
outgoing: zatoshis(shown)?,
received: if summary.received_transparent.count > 0 {
Some(summary.received_transparent.rows()?)
} else {
None
},
settled,
});
}
if ev.provisional && ev.net < 0 && summary.sent.count == 0 {
return Ok(AccountActivity::MovementDebit {
amount: zatoshis(ev.debit().saturating_sub(ev.known_fee_or_zero()))?,
});
}
let sent = (summary.sent.count > 0)
.then(|| summary.sent.rows())
.transpose()?;
let zero_value_receipt_allowed = ev.spent == 0 || summary.sent.count > 0;
let received = (summary.received.amount > 0
|| (summary.received.count > 0 && zero_value_receipt_allowed))
.then(|| summary.received.rows())
.transpose()?;
if sent.is_some() || received.is_some() {
return Ok(AccountActivity::Outputs { sent, received });
}
Ok(AccountActivity::Unknown)
}
fn zatoshis(value: u64) -> Result<Zatoshis, SqliteClientError> {
Zatoshis::from_u64(value)
.map_err(|_| SqliteClientError::CorruptedData(format!("invalid activity amount {value}")))
}
#[cfg(test)]
mod tests {
use zcash_client_backend::data_api::transparent_ledger::{
AccountActivity, ActivityOutputs, FeeState, OutputRef,
};
use zcash_protocol::{PoolType, value::Zatoshis};
use super::{
ActivityEvidence, ActivityOutput, IRONWOOD_POOL, ORCHARD_POOL, TRANSPARENT_POOL, activity,
summarize,
};
fn zat(value: u64) -> Zatoshis {
Zatoshis::const_from_u64(value)
}
fn known(fee: u64) -> FeeState {
FeeState::Known(zat(fee))
}
fn base() -> ActivityEvidence {
ActivityEvidence {
orchard_spend: true,
mined: true,
spent: 625_000_000,
net: -625_000_000,
provisional: false,
effects_settled: true,
fee: known(20_000),
whole_fee: None,
inferred_payment: None,
inferred_outgoing: None,
}
}
fn provisional_debit() -> ActivityEvidence {
ActivityEvidence {
orchard_spend: false,
spent: 100_000_000,
net: -70_000_000,
provisional: true,
effects_settled: false,
fee: FeeState::Unknown,
..base()
}
}
fn received(pool: i64, index: u32, value: u64, scope: Option<i64>) -> ActivityOutput {
ActivityOutput {
pool,
index,
value,
from_unknown: true,
to_own: true,
to_key_scope: scope,
..ActivityOutput::default()
}
}
fn own(pool: i64, index: u32, value: u64, scope: Option<i64>) -> ActivityOutput {
ActivityOutput {
from_own: true,
from_unknown: false,
..received(pool, index, value, scope)
}
}
fn paid(pool: i64, index: u32, value: u64) -> ActivityOutput {
ActivityOutput {
pool,
index,
value,
from_own: true,
..ActivityOutput::default()
}
}
fn row(amount: u64, outputs: &[(i64, u32)]) -> Option<ActivityOutputs> {
Some(ActivityOutputs {
amount: zat(amount),
outputs: outputs
.iter()
.map(|&(pool, index)| OutputRef {
pool: match pool {
TRANSPARENT_POOL => PoolType::Transparent,
ORCHARD_POOL => PoolType::ORCHARD,
IRONWOOD_POOL => PoolType::IRONWOOD,
_ => PoolType::SAPLING,
},
index,
})
.collect(),
})
}
fn classify(ev: &ActivityEvidence, outputs: &[ActivityOutput]) -> AccountActivity {
activity(ev, &summarize(ev, outputs).unwrap()).unwrap()
}
#[test]
fn an_internal_ironwood_move_has_no_activity_rows() {
let outputs = [own(IRONWOOD_POOL, 0, 624_980_000, None)];
for outgoing in [None, Some(0)] {
let ev = ActivityEvidence {
inferred_outgoing: outgoing,
..base()
};
assert_eq!(classify(&ev, &outputs), AccountActivity::Unknown);
}
let legacy = ActivityOutput {
ironwood_note: true,
..own(ORCHARD_POOL, 0, 624_980_000, None)
};
let ev = ActivityEvidence {
mined: false,
..base()
};
assert_eq!(classify(&ev, &[legacy]), AccountActivity::Unknown);
assert!(matches!(
classify(&base(), &[received(IRONWOOD_POOL, 0, 624_980_000, None)]),
AccountActivity::Outputs { sent: None, .. }
));
}
#[test]
fn private_orchard_unshielding_with_ironwood_change_matches_public_activity() {
for change_pool in [ORCHARD_POOL, IRONWOOD_POOL] {
for owned_receipt in [false, true] {
let ev = ActivityEvidence {
spent: 1_000_000,
net: if owned_receipt { -15_000 } else { -265_000 },
provisional: true,
effects_settled: false,
fee: FeeState::Unattributed { whole: zat(15_000) },
whole_fee: Some(15_000),
inferred_outgoing: Some(250_000),
..base()
};
let change = ActivityOutput {
ironwood_note: true,
..own(change_pool, 0, 735_000, Some(1))
};
let mut private_outputs = vec![change.clone()];
if owned_receipt {
private_outputs.push(received(TRANSPARENT_POOL, 0, 250_000, Some(0)));
}
let private = classify(&ev, &private_outputs);
assert_eq!(
private,
AccountActivity::InferredOutgoing {
outgoing: zat(250_000),
received: if owned_receipt {
row(250_000, &[(TRANSPARENT_POOL, 0)])
} else {
None
},
settled: false,
}
);
let public = ActivityEvidence {
inferred_outgoing: None,
provisional: false,
fee: known(15_000),
..ev
};
let recipient = if owned_receipt {
own(TRANSPARENT_POOL, 0, 250_000, Some(0))
} else {
paid(TRANSPARENT_POOL, 0, 250_000)
};
let rows = row(250_000, &[(TRANSPARENT_POOL, 0)]);
assert_eq!(
classify(&public, &[change, recipient]),
AccountActivity::Outputs {
sent: rows.clone(),
received: if owned_receipt { rows } else { None },
}
);
}
}
}
#[test]
fn compact_enhanced_outgoing_matches_public_activity_with_or_without_owned_receipt() {
for owned_receipt in [false, true] {
let ev = ActivityEvidence {
spent: 107_485_000,
net: if owned_receipt { -15_000 } else { -265_000 },
whole_fee: Some(15_000),
inferred_outgoing: Some(250_000),
..provisional_debit()
};
let mut outputs = vec![received(IRONWOOD_POOL, 0, 107_220_000, None)];
if owned_receipt {
outputs.push(received(TRANSPARENT_POOL, 0, 250_000, Some(0)));
}
assert_eq!(
classify(&ev, &outputs),
AccountActivity::InferredOutgoing {
outgoing: zat(250_000),
received: if owned_receipt {
row(250_000, &[(TRANSPARENT_POOL, 0)])
} else {
None
},
settled: false,
}
);
}
}
fn compact_funding(amount: u64) -> ActivityEvidence {
ActivityEvidence {
spent: 1_000_000 + amount + 15_000,
net: -15_000,
whole_fee: Some(15_000),
inferred_outgoing: Some(amount),
..provisional_debit()
}
}
#[test]
fn recovered_activity_hides_funding_steps_and_keeps_external_self_payments() {
for amount in [20_000, 110_000] {
for scope in [1, 2] {
assert_eq!(
classify(
&compact_funding(amount),
&[received(TRANSPARENT_POOL, 0, amount, Some(scope))]
),
AccountActivity::HiddenFundingStep
);
}
}
for scope in [0, -1] {
assert_eq!(
classify(
&compact_funding(250_000),
&[received(TRANSPARENT_POOL, 0, 250_000, Some(scope))]
),
AccountActivity::InferredOutgoing {
outgoing: zat(250_000),
received: row(250_000, &[(TRANSPARENT_POOL, 0)]),
settled: false,
}
);
}
}
#[test]
fn recovered_activity_subtracts_only_known_internal_funding() {
assert_eq!(
classify(
&compact_funding(270_000),
&[
received(TRANSPARENT_POOL, 0, 20_000, Some(2)),
received(TRANSPARENT_POOL, 1, 250_000, Some(0)),
]
),
AccountActivity::InferredOutgoing {
outgoing: zat(250_000),
received: row(250_000, &[(TRANSPARENT_POOL, 1)]),
settled: false,
}
);
assert_eq!(
classify(
&compact_funding(220_000),
&[received(TRANSPARENT_POOL, 0, 20_000, Some(1))]
),
AccountActivity::InferredOutgoing {
outgoing: zat(200_000),
received: None,
settled: false,
}
);
}
#[test]
fn recovered_activity_does_not_hide_unknown_scope_or_erase_conflicting_debits() {
let ev = ActivityEvidence {
effects_settled: true,
..compact_funding(20_000)
};
let shown = |output| classify(&ev, &[output]);
assert_eq!(
shown(received(TRANSPARENT_POOL, 0, 20_000, None)),
AccountActivity::InferredOutgoing {
outgoing: zat(20_000),
received: row(20_000, &[(TRANSPARENT_POOL, 0)]),
settled: false,
}
);
assert_eq!(
shown(received(TRANSPARENT_POOL, 0, 30_000, Some(2))),
AccountActivity::InferredOutgoing {
outgoing: zat(20_000),
received: None,
settled: false,
}
);
assert_eq!(
shown(received(TRANSPARENT_POOL, 0, 30_000, Some(0))),
AccountActivity::InferredOutgoing {
outgoing: zat(20_000),
received: row(30_000, &[(TRANSPARENT_POOL, 0)]),
settled: false,
}
);
assert_eq!(
shown(received(TRANSPARENT_POOL, 0, 20_000, Some(0))),
AccountActivity::InferredOutgoing {
outgoing: zat(20_000),
received: row(20_000, &[(TRANSPARENT_POOL, 0)]),
settled: true,
}
);
}
#[test]
fn recovered_activity_keeps_unrelated_incoming_on_internal_receivers() {
let ev = ActivityEvidence {
spent: 0,
net: 20_000,
..provisional_debit()
};
assert_eq!(
classify(&ev, &[received(TRANSPARENT_POOL, 0, 20_000, Some(2))]),
AccountActivity::Outputs {
sent: None,
received: row(20_000, &[(TRANSPARENT_POOL, 0)]),
}
);
}
fn recovered_self_transfer() -> ActivityEvidence {
ActivityEvidence {
orchard_spend: false,
spent: 110_000,
net: -10_000,
fee: known(10_000),
whole_fee: Some(10_000),
inferred_payment: Some(0),
..base()
}
}
#[test]
fn recovered_activity_restores_visible_transparent_self_transfer_instead_of_fee_only() {
let rows = row(100_000, &[(TRANSPARENT_POOL, 0)]);
for scope in [0, -1] {
assert_eq!(
classify(
&recovered_self_transfer(),
&[received(TRANSPARENT_POOL, 0, 100_000, Some(scope))]
),
AccountActivity::Outputs {
sent: rows.clone(),
received: rows.clone(),
}
);
}
assert_eq!(
classify(
&recovered_self_transfer(),
&[received(TRANSPARENT_POOL, 0, 100_000, Some(1))]
),
AccountActivity::FeeOnly { fee: zat(10_000) }
);
}
#[test]
fn a_recovered_self_transfer_is_its_network_fee() {
let outputs = [received(ORCHARD_POOL, 0, 100_000, None)];
assert_eq!(
classify(&recovered_self_transfer(), &outputs),
AccountActivity::FeeOnly { fee: zat(10_000) }
);
let sent = [paid(ORCHARD_POOL, 1, 0)];
assert_eq!(
classify(&recovered_self_transfer(), &sent),
AccountActivity::Outputs {
sent: row(0, &[(ORCHARD_POOL, 1)]),
received: None,
}
);
}
#[test]
fn an_inexact_self_transfer_is_not_shown_as_its_fee() {
let outputs = [received(ORCHARD_POOL, 0, 100_000, None)];
let receipt = AccountActivity::Outputs {
sent: None,
received: row(100_000, &[(ORCHARD_POOL, 0)]),
};
for ev in [
ActivityEvidence {
inferred_payment: None,
whole_fee: None,
..recovered_self_transfer()
},
ActivityEvidence {
whole_fee: None,
..recovered_self_transfer()
},
ActivityEvidence {
whole_fee: Some(20_000),
..recovered_self_transfer()
},
] {
assert_eq!(classify(&ev, &outputs), receipt);
}
}
#[test]
fn a_recovered_send_with_change_is_its_exact_payment_not_a_receive() {
let ev = ActivityEvidence {
provisional: false,
effects_settled: true,
fee: known(10_000),
inferred_payment: Some(69_990_000),
..provisional_debit()
};
let change = [received(TRANSPARENT_POOL, 1, 30_000_000, Some(0))];
let payment = AccountActivity::InferredPayment {
amount: zat(69_990_000),
};
assert_eq!(classify(&ev, &change), payment);
assert_eq!(classify(&ev, &[]), payment);
}
#[test]
fn provisional_debits_are_their_movement() {
let change = [received(TRANSPARENT_POOL, 1, 30_000_000, Some(0))];
assert_eq!(
classify(&provisional_debit(), &change),
AccountActivity::MovementDebit {
amount: zat(70_000_000)
}
);
let ev = ActivityEvidence {
fee: known(10_000),
..provisional_debit()
};
assert_eq!(
classify(&ev, &change),
AccountActivity::MovementDebit {
amount: zat(69_990_000)
}
);
let ev = ActivityEvidence {
fee: FeeState::Unattributed { whole: zat(10_000) },
..provisional_debit()
};
assert_eq!(
classify(&ev, &change),
AccountActivity::MovementDebit {
amount: zat(70_000_000)
}
);
let ev = ActivityEvidence {
net: -10_000,
fee: known(10_000),
..provisional_debit()
};
assert_eq!(
classify(&ev, &[]),
AccountActivity::MovementDebit {
amount: Zatoshis::ZERO
}
);
let ev = ActivityEvidence {
net: -10_000,
fee: known(10_000),
..provisional_debit()
};
assert_eq!(
classify(&ev, &[paid(ORCHARD_POOL, 0, 0)]),
AccountActivity::Outputs {
sent: row(0, &[(ORCHARD_POOL, 0)]),
received: None,
}
);
}
#[test]
fn a_complete_debit_with_change_keeps_its_classification() {
let ev = ActivityEvidence {
provisional: false,
effects_settled: true,
fee: known(10_000),
..provisional_debit()
};
assert_eq!(
classify(&ev, &[received(TRANSPARENT_POOL, 1, 30_000_000, Some(0))]),
AccountActivity::Outputs {
sent: None,
received: row(30_000_000, &[(TRANSPARENT_POOL, 1)]),
}
);
}
#[test]
fn transactions_no_rule_covers_are_the_wallets() {
for (mined, spent, net) in [
(false, 100_000, -60_000),
(true, 100_000, -60_000),
(true, 0, 5_000),
(true, 0, 0),
] {
let ev = ActivityEvidence {
mined,
spent,
net,
fee: known(10_000),
..base()
};
assert_eq!(classify(&ev, &[]), AccountActivity::Unknown);
}
}
#[test]
fn zero_value_receipts_need_a_send_or_no_spend() {
let zero = [received(ORCHARD_POOL, 0, 0, None)];
let ev = ActivityEvidence {
spent: 10_000,
net: -10_000,
mined: true,
..base()
};
assert_eq!(classify(&ev, &zero), AccountActivity::Unknown);
let ev = ActivityEvidence {
spent: 0,
net: 0,
..base()
};
assert_eq!(
classify(&ev, &zero),
AccountActivity::Outputs {
sent: None,
received: row(0, &[(ORCHARD_POOL, 0)]),
}
);
}
#[test]
fn a_self_payment_is_only_received_when_the_account_also_paid_others() {
let ev = ActivityEvidence {
orchard_spend: false,
..base()
};
let self_payment = own(TRANSPARENT_POOL, 1, 50_000, Some(0));
assert_eq!(
classify(&ev, &[paid(ORCHARD_POOL, 0, 70_000), self_payment.clone()]),
AccountActivity::Outputs {
sent: row(70_000, &[(ORCHARD_POOL, 0)]),
received: row(50_000, &[(TRANSPARENT_POOL, 1)]),
}
);
assert_eq!(
classify(&ev, &[self_payment]),
AccountActivity::Outputs {
sent: row(50_000, &[(TRANSPARENT_POOL, 1)]),
received: row(50_000, &[(TRANSPARENT_POOL, 1)]),
}
);
}
#[test]
fn cross_account_shapes_keep_each_accounts_view() {
let payer = ActivityEvidence {
orchard_spend: false,
spent: 1_500_000,
net: -1_020_000,
..base()
};
for outputs in [
vec![paid(IRONWOOD_POOL, 0, 1_000_000)],
vec![
paid(IRONWOOD_POOL, 0, 1_000_000),
own(TRANSPARENT_POOL, 1, 480_000, Some(1)),
],
] {
assert_eq!(
classify(&payer, &outputs),
AccountActivity::Outputs {
sent: row(1_000_000, &[(IRONWOOD_POOL, 0)]),
received: None,
},
"payer: {outputs:?}"
);
}
let receiver = ActivityEvidence {
orchard_spend: false,
spent: 0,
net: 1_000_000,
fee: FeeState::NotApplicable,
..base()
};
assert_eq!(
classify(&receiver, &[received(IRONWOOD_POOL, 0, 1_000_000, Some(0))]),
AccountActivity::Outputs {
sent: None,
received: row(1_000_000, &[(IRONWOOD_POOL, 0)]),
},
"recipient"
);
}
}