use chrono::DateTime;
use json::JsonValue;
use zcash_protocol::{PoolType, TxId, consensus::BlockHeight};
use pepper_sync::keys::transparent::TransparentScope;
use zingo_status::confirmation_status::ConfirmationStatus;
use crate::wallet::output::SpendStatus;
#[derive(Clone, Debug, PartialEq)]
pub enum Scope {
External,
Internal,
}
impl std::fmt::Display for Scope {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
Scope::External => "external",
Scope::Internal => "internal",
}
)
}
}
impl From<zip32::Scope> for Scope {
fn from(value: zip32::Scope) -> Self {
match value {
zip32::Scope::External => Scope::External,
zip32::Scope::Internal => Scope::Internal,
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum TransactionKind {
Sent(SendType),
Received,
}
impl std::fmt::Display for TransactionKind {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
TransactionKind::Received => write!(f, "received"),
TransactionKind::Sent(SendType::Send) => write!(f, "sent"),
TransactionKind::Sent(SendType::Shield) => write!(f, "shield"),
TransactionKind::Sent(SendType::SendToSelf) => write!(f, "send-to-self"),
}
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum SendType {
Send,
Shield,
SendToSelf,
}
fn pool_names(pools: &[PoolType]) -> Vec<String> {
pools.iter().map(ToString::to_string).collect()
}
pub(crate) fn display_pools(pools: &[PoolType]) -> String {
pool_names(pools).join(", ")
}
pub(crate) fn pools_to_json(pools: &[PoolType]) -> JsonValue {
JsonValue::from(pool_names(pools))
}
pub(crate) fn pools_present(present: [bool; 4]) -> Vec<PoolType> {
let [transparent, sapling, orchard, ironwood] = present;
[
(transparent, PoolType::TRANSPARENT),
(sapling, PoolType::SAPLING),
(orchard, PoolType::ORCHARD),
(ironwood, PoolType::IRONWOOD),
]
.into_iter()
.filter_map(|(present, pool)| present.then_some(pool))
.collect()
}
#[derive(Clone, PartialEq, Debug)]
pub struct TransactionSummary {
pub txid: TxId,
pub datetime: u32,
pub status: ConfirmationStatus,
pub blockheight: BlockHeight,
pub kind: TransactionKind,
pub value: u64,
pub fee: Option<u64>,
pub zec_price: Option<f32>,
pub pools_sent_from: Vec<PoolType>,
pub ironwood_notes: Vec<BasicNoteSummary>,
pub orchard_notes: Vec<BasicNoteSummary>,
pub sapling_notes: Vec<BasicNoteSummary>,
pub transparent_coins: Vec<BasicCoinSummary>,
pub outgoing_ironwood_notes: Vec<OutgoingNoteSummary>,
pub outgoing_orchard_notes: Vec<OutgoingNoteSummary>,
pub outgoing_sapling_notes: Vec<OutgoingNoteSummary>,
pub outgoing_transparent_coins: Vec<OutgoingCoinSummary>,
}
impl TransactionSummary {
#[must_use]
pub fn pools_received(&self) -> Vec<PoolType> {
pools_present([
!self.transparent_coins.is_empty(),
!self.sapling_notes.is_empty(),
!self.orchard_notes.is_empty(),
!self.ironwood_notes.is_empty(),
])
}
#[must_use]
pub fn is_orchard_to_ironwood_migration(&self) -> bool {
self.pools_sent_from.contains(&PoolType::ORCHARD)
&& self.pools_received().contains(&PoolType::IRONWOOD)
}
#[must_use]
pub fn prepare_for_display(
&self,
) -> (
String,
String,
String,
BasicNoteSummaries,
BasicNoteSummaries,
BasicNoteSummaries,
BasicCoinSummaries,
OutgoingNoteSummaries,
OutgoingNoteSummaries,
OutgoingNoteSummaries,
OutgoingCoinSummaries,
) {
let datetime = if let Some(dt) = DateTime::from_timestamp(i64::from(self.datetime), 0) {
format!("{dt}")
} else {
"not available".to_string()
};
let fee = if let Some(f) = self.fee {
f.to_string()
} else {
"not available".to_string()
};
let zec_price = if let Some(price) = self.zec_price {
price.to_string()
} else {
"not available".to_string()
};
let ironwood_notes = BasicNoteSummaries(self.ironwood_notes.clone());
let orchard_notes = BasicNoteSummaries(self.orchard_notes.clone());
let sapling_notes = BasicNoteSummaries(self.sapling_notes.clone());
let transparent_coins = BasicCoinSummaries(self.transparent_coins.clone());
let outgoing_ironwood_notes = OutgoingNoteSummaries(self.outgoing_ironwood_notes.clone());
let outgoing_orchard_notes = OutgoingNoteSummaries(self.outgoing_orchard_notes.clone());
let outgoing_sapling_notes = OutgoingNoteSummaries(self.outgoing_sapling_notes.clone());
let outgoing_transparent_coins =
OutgoingCoinSummaries(self.outgoing_transparent_coins.clone());
(
datetime,
fee,
zec_price,
ironwood_notes,
orchard_notes,
sapling_notes,
transparent_coins,
outgoing_ironwood_notes,
outgoing_orchard_notes,
outgoing_sapling_notes,
outgoing_transparent_coins,
)
}
}
impl std::fmt::Display for TransactionSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (
datetime,
fee,
zec_price,
ironwood_notes,
orchard_notes,
sapling_notes,
transparent_coins,
outgoing_ironwood_notes,
outgoing_orchard_notes,
outgoing_sapling_notes,
outgoing_transparent_coins,
) = self.prepare_for_display();
write!(
f,
"{{
txid: {}
datetime: {}
status: {}
blockheight: {}
kind: {}
value: {}
fee: {}
zec price: {}
pools sent from: {}
ironwood notes: {}
orchard notes: {}
sapling notes: {}
transparent coins: {}
outgoing ironwood notes: {}
outgoing orchard notes: {}
outgoing sapling notes: {}
outgoing transparent coins: {}
}}",
self.txid,
datetime,
self.status,
u64::from(self.blockheight),
self.kind,
self.value,
fee,
zec_price,
display_pools(&self.pools_sent_from),
ironwood_notes,
orchard_notes,
sapling_notes,
transparent_coins,
outgoing_ironwood_notes,
outgoing_orchard_notes,
outgoing_sapling_notes,
outgoing_transparent_coins,
)
}
}
impl From<TransactionSummary> for JsonValue {
fn from(transaction: TransactionSummary) -> Self {
json::object! {
"txid" => transaction.txid.to_string(),
"datetime" => transaction.datetime,
"status" => transaction.status.to_string(),
"blockheight" => u64::from(transaction.blockheight),
"kind" => transaction.kind.to_string(),
"value" => transaction.value,
"fee" => transaction.fee,
"zec_price" => transaction.zec_price,
"pools_sent_from" => pools_to_json(&transaction.pools_sent_from),
"ironwood_notes" => JsonValue::from(transaction.ironwood_notes),
"orchard_notes" => JsonValue::from(transaction.orchard_notes),
"sapling_notes" => JsonValue::from(transaction.sapling_notes),
"transparent_coins" => JsonValue::from(transaction.transparent_coins),
"outgoing_ironwood_notes" => JsonValue::from(transaction.outgoing_ironwood_notes),
"outgoing_orchard_notes" => JsonValue::from(transaction.outgoing_orchard_notes),
"outgoing_sapling_notes" => JsonValue::from(transaction.outgoing_sapling_notes),
"outgoing_transparent_coins" => JsonValue::from(transaction.outgoing_transparent_coins),
}
}
}
#[derive(PartialEq, Debug)]
pub struct TransactionSummaries(pub Vec<TransactionSummary>);
impl TransactionSummaries {
#[must_use]
pub fn new(transaction_summaries: Vec<TransactionSummary>) -> Self {
TransactionSummaries(transaction_summaries)
}
pub fn iter(&self) -> std::slice::Iter<'_, TransactionSummary> {
self.0.iter()
}
}
impl std::fmt::Display for TransactionSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for transaction_summary in &self.0 {
write!(f, "\n{transaction_summary}")?;
}
Ok(())
}
}
impl From<TransactionSummaries> for JsonValue {
fn from(transaction_summaries: TransactionSummaries) -> Self {
let transaction_summaries: Vec<JsonValue> = transaction_summaries
.0
.into_iter()
.map(JsonValue::from)
.collect();
json::object! {
"transaction_summaries" => transaction_summaries
}
}
}
#[allow(missing_docs)]
#[derive(Debug)]
pub struct NoteSummary {
pub value: u64,
pub status: ConfirmationStatus,
pub block_height: BlockHeight,
pub spend_status: SpendStatus,
pub memo: Option<String>,
pub time: u32,
pub txid: TxId,
pub output_index: u32,
pub account_id: zip32::AccountId,
pub scope: Scope,
}
impl std::fmt::Display for NoteSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let memo = self.memo.clone().unwrap_or_default();
let time = if let Some(dt) = chrono::DateTime::from_timestamp(i64::from(self.time), 0) {
format!("{dt}")
} else {
"not available".to_string()
};
write!(
f,
"{{
value: {}
status: {} at block height {}
spend status: {}
memo: {}
time: {}
txid: {}
output index: {}
account id: {}
scope: {}
}}",
self.value,
self.status,
self.block_height,
self.spend_status,
memo,
time,
self.txid,
self.output_index,
u32::from(self.account_id),
self.scope
)
}
}
impl From<NoteSummary> for json::JsonValue {
fn from(note: NoteSummary) -> Self {
json::object! {
"value" => note.value,
"status" => format!("{} at block height {}", note.status, note.block_height),
"spend_status" => note.spend_status.to_string(),
"memo" => note.memo,
"time" => note.time,
"txid" => note.txid.to_string(),
"output_index" => note.output_index,
"account_id" => u32::from(note.account_id),
"scope" => note.scope.to_string(),
}
}
}
#[derive(Debug)]
pub struct NoteSummaries(Vec<NoteSummary>);
impl NoteSummaries {
#[must_use]
pub fn new(note_summaries: Vec<NoteSummary>) -> Self {
NoteSummaries(note_summaries)
}
}
impl<'a> std::iter::IntoIterator for &'a NoteSummaries {
type Item = &'a NoteSummary;
type IntoIter = std::slice::Iter<'a, NoteSummary>;
fn into_iter(self) -> Self::IntoIter {
self.iter()
}
}
impl std::ops::Deref for NoteSummaries {
type Target = Vec<NoteSummary>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for NoteSummaries {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::ops::Index<usize> for NoteSummaries {
type Output = NoteSummary;
fn index(&self, index: usize) -> &Self::Output {
&self.0[index] }
}
impl std::fmt::Display for NoteSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for value_transfer in &self.0 {
write!(f, "\n{value_transfer}")?;
}
Ok(())
}
}
impl From<NoteSummaries> for json::JsonValue {
fn from(note_summaries: NoteSummaries) -> Self {
let note_summaries: Vec<json::JsonValue> = note_summaries
.0
.into_iter()
.map(json::JsonValue::from)
.collect();
json::object! {
"note_summaries" => note_summaries
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct BasicNoteSummary {
pub value: u64,
pub spend_status: SpendStatus,
pub output_index: u32,
pub memo: Option<String>,
}
impl BasicNoteSummary {
#[must_use]
pub fn from_parts(
value: u64,
spend_status: SpendStatus,
output_index: u32,
memo: Option<String>,
) -> Self {
BasicNoteSummary {
value,
spend_status,
output_index,
memo,
}
}
}
impl std::fmt::Display for BasicNoteSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let memo = self.memo.clone().unwrap_or_default();
write!(
f,
"\t{{
value: {}
spend status: {}
output index: {}
memo: {}
}}",
self.value, self.spend_status, self.output_index, memo,
)
}
}
impl From<BasicNoteSummary> for JsonValue {
fn from(note: BasicNoteSummary) -> Self {
json::object! {
"value" => note.value,
"spend_status" => note.spend_status.to_string(),
"output_index" => note.output_index,
"memo" => note.memo,
}
}
}
pub struct BasicNoteSummaries(Vec<BasicNoteSummary>);
impl std::fmt::Display for BasicNoteSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for note in &self.0 {
write!(f, "\n{note}")?;
}
Ok(())
}
}
#[allow(missing_docs)]
pub struct CoinSummary {
pub value: u64,
pub status: ConfirmationStatus,
pub block_height: BlockHeight,
pub spend_status: SpendStatus,
pub time: u32,
pub txid: TxId,
pub output_index: u32,
pub account_id: zip32::AccountId,
pub scope: TransparentScope,
pub address_index: u32,
}
impl std::fmt::Display for CoinSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let time = if let Some(dt) = chrono::DateTime::from_timestamp(i64::from(self.time), 0) {
format!("{dt}")
} else {
"not available".to_string()
};
write!(
f,
"{{
value: {}
status: {} at block height {}
spend status: {}
time: {}
txid: {}
output index: {}
account id: {}
scope: {}
address_index: {}
}}",
self.value,
self.status,
self.block_height,
self.spend_status,
time,
self.txid,
self.output_index,
u32::from(self.account_id),
self.scope,
self.address_index,
)
}
}
impl From<CoinSummary> for json::JsonValue {
fn from(coin: CoinSummary) -> Self {
json::object! {
"value" => coin.value,
"status" => format!("{} at block height {}", coin.status, coin.block_height),
"spend_status" => coin.spend_status.to_string(),
"time" => coin.time,
"txid" => coin.txid.to_string(),
"output_index" => coin.output_index,
"account_id" => u32::from(coin.account_id),
"scope" => coin.scope.to_string(),
"address_index" => coin.address_index
}
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct BasicCoinSummary {
pub value: u64,
pub spend_summary: SpendStatus,
pub output_index: u32,
}
impl BasicCoinSummary {
#[must_use]
pub fn from_parts(value: u64, spend_status: SpendStatus, output_index: u32) -> Self {
BasicCoinSummary {
value,
spend_summary: spend_status,
output_index,
}
}
}
impl std::fmt::Display for BasicCoinSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\t{{
value: {}
spend status: {}
output index: {}
}}",
self.value, self.spend_summary, self.output_index,
)
}
}
impl From<BasicCoinSummary> for JsonValue {
fn from(note: BasicCoinSummary) -> Self {
json::object! {
"value" => note.value,
"spend_status" => note.spend_summary.to_string(),
"output_index" => note.output_index,
}
}
}
pub struct BasicCoinSummaries(Vec<BasicCoinSummary>);
impl std::fmt::Display for BasicCoinSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for coin in &self.0 {
write!(f, "\n{coin}")?;
}
Ok(())
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct OutgoingNoteSummary {
pub value: u64,
pub memo: Option<String>,
pub recipient: String,
pub recipient_unified_address: Option<String>,
pub output_index: u32,
pub account_id: zip32::AccountId,
pub scope: Scope,
}
impl std::fmt::Display for OutgoingNoteSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let memo = self.memo.clone().unwrap_or_default();
let recipient_unified_address = self
.recipient_unified_address
.clone()
.unwrap_or_else(|| "not available".to_string());
write!(
f,
"\t{{
value: {}
memo: {}
recipient: {}
recipient unified address: {}
output index: {}
account id: {}
scope: {}
}}",
self.value,
memo,
self.recipient,
recipient_unified_address,
self.output_index,
u32::from(self.account_id),
self.scope,
)
}
}
impl From<OutgoingNoteSummary> for JsonValue {
fn from(note: OutgoingNoteSummary) -> Self {
json::object! {
"value" => note.value,
"memo" => note.memo,
"recipient" => note.recipient,
"recipient_unified_address" => note.recipient_unified_address,
"output_index" => note.output_index,
"account_id" => u32::from(note.account_id),
"scope" => note.scope.to_string(),
}
}
}
pub struct OutgoingNoteSummaries(Vec<OutgoingNoteSummary>);
impl std::fmt::Display for OutgoingNoteSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for note in &self.0 {
write!(f, "\n{note}")?;
}
Ok(())
}
}
#[derive(Clone, PartialEq, Debug)]
pub struct OutgoingCoinSummary {
pub value: u64,
pub recipient: String,
pub output_index: u32,
}
impl std::fmt::Display for OutgoingCoinSummary {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"\t{{
value: {}
recipient: {}
output index: {}
}}",
self.value, self.recipient, self.output_index,
)
}
}
impl From<OutgoingCoinSummary> for JsonValue {
fn from(note: OutgoingCoinSummary) -> Self {
json::object! {
"value" => note.value,
"recipient" => note.recipient,
"output_index" => note.output_index,
}
}
}
pub struct OutgoingCoinSummaries(Vec<OutgoingCoinSummary>);
impl std::fmt::Display for OutgoingCoinSummaries {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
for coin in &self.0 {
write!(f, "\n{coin}")?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::{BasicNoteSummary, SendType, TransactionKind, TransactionSummary};
use crate::wallet::output::SpendStatus;
use zcash_protocol::{PoolType, TxId};
use zingo_status::confirmation_status::ConfirmationStatus;
fn note(value: u64) -> BasicNoteSummary {
BasicNoteSummary::from_parts(value, SpendStatus::Unspent, 0, None)
}
fn self_send_summary(
pools_sent_from: Vec<PoolType>,
ironwood_notes: Vec<BasicNoteSummary>,
orchard_notes: Vec<BasicNoteSummary>,
) -> TransactionSummary {
TransactionSummary {
txid: TxId::from_bytes([0; 32]),
datetime: 0,
status: ConfirmationStatus::Confirmed(10u32.into()),
blockheight: 10u32.into(),
kind: TransactionKind::Sent(SendType::SendToSelf),
value: 0,
fee: Some(0),
zec_price: None,
pools_sent_from,
ironwood_notes,
orchard_notes,
sapling_notes: vec![],
transparent_coins: vec![],
outgoing_ironwood_notes: vec![],
outgoing_orchard_notes: vec![],
outgoing_sapling_notes: vec![],
outgoing_transparent_coins: vec![],
}
}
#[test]
fn orchard_funded_ironwood_receive_is_a_migration() {
let summary = self_send_summary(vec![PoolType::ORCHARD], vec![note(100_000)], vec![]);
assert!(summary.is_orchard_to_ironwood_migration());
}
#[test]
fn orchard_to_orchard_note_split_is_not_a_migration() {
let summary = self_send_summary(vec![PoolType::ORCHARD], vec![], vec![note(100_000)]);
assert!(!summary.is_orchard_to_ironwood_migration());
}
#[test]
fn ironwood_receive_without_orchard_funding_is_not_a_migration() {
let unfunded = self_send_summary(vec![], vec![note(100_000)], vec![]);
assert!(!unfunded.is_orchard_to_ironwood_migration());
let ironwood_funded =
self_send_summary(vec![PoolType::IRONWOOD], vec![note(100_000)], vec![]);
assert!(!ironwood_funded.is_orchard_to_ironwood_migration());
}
}