pcli/command/view/
balance.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
use anyhow::Result;
use comfy_table::{presets, Table};

use penumbra_keys::AddressView;
use penumbra_sct::CommitmentSource;
use penumbra_view::ViewClient;

#[derive(Debug, clap::Args)]
pub struct BalanceCmd {
    #[clap(long)]
    /// If set, prints the value of each note individually.
    pub by_note: bool,
}

impl BalanceCmd {
    pub fn offline(&self) -> bool {
        false
    }

    pub async fn exec<V: ViewClient>(&self, view: &mut V) -> Result<()> {
        let asset_cache = view.assets().await?;

        // Initialize the table
        let mut table = Table::new();
        table.load_preset(presets::NOTHING);

        let notes = view.unspent_notes_by_account_and_asset().await?;

        if self.by_note {
            table.set_header(vec!["Account", "Value", "Source", "Sender"]);

            let rows = notes
                .iter()
                .flat_map(|(index, notes_by_asset)| {
                    // Include each note individually:
                    notes_by_asset.iter().flat_map(|(asset, notes)| {
                        notes.iter().map(|record| {
                            (
                                *index,
                                asset.value(record.note.amount()),
                                record.source.clone(),
                                record.return_address.clone(),
                            )
                        })
                    })
                })
                /* Don't exclude withdrawn LPNFTs in by_note, which is a more precise view.
                // Exclude withdrawn LPNFTs.
                .filter(|(_, value, _, _)| match asset_cache.get(&value.asset_id) {
                    None => true,
                    Some(denom) => !denom.is_withdrawn_position_nft(),
                });
                 */
                ;

            for (index, value, source, return_address) in rows {
                table.add_row(vec![
                    format!("# {}", index),
                    value.format(&asset_cache),
                    format_source(&source),
                    format_return_address(&return_address),
                ]);
            }

            println!("{table}");

            return Ok(());
        } else {
            table.set_header(vec!["Account", "Amount"]);

            let rows = notes
                .iter()
                .flat_map(|(index, notes_by_asset)| {
                    // Sum the notes for each asset:
                    notes_by_asset.iter().map(|(asset, notes)| {
                        let sum: u128 = notes
                            .iter()
                            .map(|record| u128::from(record.note.amount()))
                            .sum();
                        (*index, asset.value(sum.into()))
                    })
                })
                // Exclude withdrawn LPNFTs and withdrawn auction NFTs.
                .filter(|(_, value)| match asset_cache.get(&value.asset_id) {
                    None => true,
                    Some(denom) => {
                        !denom.is_withdrawn_position_nft() && !denom.is_withdrawn_auction_nft()
                    }
                });

            for (index, value) in rows {
                table.add_row(vec![format!("# {}", index), value.format(&asset_cache)]);
            }

            println!("{table}");

            return Ok(());
        }
    }
}

fn format_source(source: &CommitmentSource) -> String {
    match source {
        CommitmentSource::Genesis => "Genesis".to_owned(),
        CommitmentSource::Transaction { id: None } => "Tx (Unknown)".to_owned(),
        CommitmentSource::Transaction { id: Some(id) } => format!("Tx {}", hex::encode(&id[..])),
        CommitmentSource::FundingStreamReward { epoch_index } => {
            format!("Funding Stream (Epoch {})", epoch_index)
        }
        CommitmentSource::CommunityPoolOutput => format!("CommunityPoolOutput"),
        CommitmentSource::Ics20Transfer {
            packet_seq,
            channel_id,
            sender,
        } => format!(
            "ICS20 packet {} via {} from {}",
            packet_seq, channel_id, sender
        ),
    }
}

fn format_return_address(return_address: &Option<penumbra_keys::AddressView>) -> String {
    match return_address {
        None => "Unknown".to_owned(),
        Some(AddressView::Opaque { address }) => address.display_short_form(),
        Some(AddressView::Decoded { index, .. }) => {
            if index.is_ephemeral() {
                format!("[account {} (IBC deposit address)]", index.account)
            } else {
                format!("[account {}]", index.account)
            }
        }
    }
}