pcli/command/view/
balance.rs

1use anyhow::Result;
2use comfy_table::{presets, Table};
3
4use penumbra_sdk_keys::AddressView;
5use penumbra_sdk_sct::CommitmentSource;
6use penumbra_sdk_view::ViewClient;
7
8#[derive(Debug, clap::Args)]
9pub struct BalanceCmd {
10    #[clap(long)]
11    /// If set, prints the value of each note individually.
12    pub by_note: bool,
13}
14
15impl BalanceCmd {
16    pub fn offline(&self) -> bool {
17        false
18    }
19
20    pub async fn exec<V: ViewClient>(&self, view: &mut V) -> Result<()> {
21        let asset_cache = view.assets().await?;
22
23        // Initialize the table
24        let mut table = Table::new();
25        table.load_preset(presets::NOTHING);
26
27        let notes = view.unspent_notes_by_account_and_asset().await?;
28
29        if self.by_note {
30            table.set_header(vec!["Account", "Value", "Source", "Sender"]);
31
32            let rows = notes
33                .iter()
34                .flat_map(|(index, notes_by_asset)| {
35                    // Include each note individually:
36                    notes_by_asset.iter().flat_map(|(asset, notes)| {
37                        notes.iter().map(|record| {
38                            (
39                                *index,
40                                asset.value(record.note.amount()),
41                                record.source.clone(),
42                                record.return_address.clone(),
43                            )
44                        })
45                    })
46                })
47                /* Don't exclude withdrawn LPNFTs in by_note, which is a more precise view.
48                // Exclude withdrawn LPNFTs.
49                .filter(|(_, value, _, _)| match asset_cache.get(&value.asset_id) {
50                    None => true,
51                    Some(denom) => !denom.is_withdrawn_position_nft(),
52                });
53                 */
54                ;
55
56            for (index, value, source, return_address) in rows {
57                table.add_row(vec![
58                    format!("# {}", index),
59                    value.format(&asset_cache),
60                    format_source(&source),
61                    format_return_address(&return_address),
62                ]);
63            }
64
65            println!("{table}");
66
67            return Ok(());
68        } else {
69            table.set_header(vec!["Account", "Amount"]);
70
71            let rows = notes
72                .iter()
73                .flat_map(|(index, notes_by_asset)| {
74                    // Sum the notes for each asset:
75                    notes_by_asset.iter().map(|(asset, notes)| {
76                        let sum: u128 = notes
77                            .iter()
78                            .map(|record| u128::from(record.note.amount()))
79                            .sum();
80                        (*index, asset.value(sum.into()))
81                    })
82                })
83                // Exclude withdrawn LPNFTs and withdrawn auction NFTs.
84                .filter(|(_, value)| match asset_cache.get(&value.asset_id) {
85                    None => true,
86                    Some(denom) => {
87                        !denom.is_withdrawn_position_nft() && !denom.is_withdrawn_auction_nft()
88                    }
89                });
90
91            for (index, value) in rows {
92                table.add_row(vec![format!("# {}", index), value.format(&asset_cache)]);
93            }
94
95            println!("{table}");
96
97            return Ok(());
98        }
99    }
100}
101
102fn format_source(source: &CommitmentSource) -> String {
103    match source {
104        CommitmentSource::Genesis => "Genesis".to_owned(),
105        CommitmentSource::Transaction { id: None } => "Tx (Unknown)".to_owned(),
106        CommitmentSource::Transaction { id: Some(id) } => format!("Tx {}", hex::encode(&id[..])),
107        CommitmentSource::FundingStreamReward { epoch_index } => {
108            format!("Funding Stream (Epoch {})", epoch_index)
109        }
110        CommitmentSource::CommunityPoolOutput => format!("CommunityPoolOutput"),
111        CommitmentSource::Ics20Transfer {
112            packet_seq,
113            channel_id,
114            sender,
115        } => format!(
116            "ICS20 packet {} via {} from {}",
117            packet_seq, channel_id, sender
118        ),
119    }
120}
121
122fn format_return_address(return_address: &Option<penumbra_sdk_keys::AddressView>) -> String {
123    match return_address {
124        None => "Unknown".to_owned(),
125        Some(AddressView::Opaque { address }) => address.display_short_form(),
126        Some(AddressView::Decoded { index, .. }) => {
127            if index.is_ephemeral() {
128                format!("[account {} (IBC deposit address)]", index.account)
129            } else {
130                format!("[account {}]", index.account)
131            }
132        }
133    }
134}