bpwallet/
rows.rs

1// Modern, minimalistic & standard-compliant cold wallet library.
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Written in 2020-2024 by
6//     Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2020-2024 LNP/BP Standards Association. All rights reserved.
9// Copyright (C) 2020-2024 Dr Maxim Orlovsky. All rights reserved.
10//
11// Licensed under the Apache License, Version 2.0 (the "License");
12// you may not use this file except in compliance with the License.
13// You may obtain a copy of the License at
14//
15//     http://www.apache.org/licenses/LICENSE-2.0
16//
17// Unless required by applicable law or agreed to in writing, software
18// distributed under the License is distributed on an "AS IS" BASIS,
19// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20// See the License for the specific language governing permissions and
21// limitations under the License.
22
23use std::fmt::{self, Display, Formatter, LowerHex};
24use std::str::FromStr;
25
26use amplify::hex::FromHex;
27use bpstd::{Address, DerivedAddr, Outpoint, Sats, ScriptPubkey, Txid};
28
29use crate::{
30    BlockHeight, Layer2Cache, Layer2Coin, Layer2Empty, Layer2Tx, Party, TxStatus, WalletCache,
31};
32
33#[cfg_attr(
34    feature = "serde",
35    derive(serde::Serialize, serde::Deserialize),
36    serde(crate = "serde_crate", rename_all = "camelCase")
37)]
38#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Display)]
39pub enum OpType {
40    #[display("+")]
41    Credit,
42    #[display("-")]
43    Debit,
44}
45
46#[derive(Clone, Eq, PartialEq, Hash, Debug, From)]
47#[cfg_attr(
48    feature = "serde",
49    derive(serde::Serialize, serde::Deserialize),
50    serde(crate = "serde_crate", rename_all = "camelCase")
51)]
52pub enum Counterparty {
53    Miner,
54    #[from]
55    Address(Address),
56    #[from]
57    Unknown(ScriptPubkey),
58}
59
60impl From<Party> for Counterparty {
61    fn from(party: Party) -> Self {
62        match party {
63            Party::Subsidy => Counterparty::Miner,
64            Party::Counterparty(addr) => Counterparty::Address(addr),
65            Party::Unknown(script) => Counterparty::Unknown(script),
66            Party::Wallet(_) => {
67                panic!("counterparty must be constructed only for external parties")
68            }
69        }
70    }
71}
72
73impl Display for Counterparty {
74    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
75        match self {
76            Counterparty::Miner => f.write_str("miner"),
77            Counterparty::Address(addr) => Display::fmt(addr, f),
78            Counterparty::Unknown(script) => LowerHex::fmt(script, f),
79        }
80    }
81}
82
83impl FromStr for Counterparty {
84    type Err = String;
85
86    fn from_str(s: &str) -> Result<Self, Self::Err> {
87        if s == "miner" {
88            return Ok(Counterparty::Miner);
89        }
90        Address::from_str(s)
91            .map(Self::from)
92            .or_else(|_| ScriptPubkey::from_hex(s).map(Self::from))
93            .map_err(|_| s.to_owned())
94    }
95}
96
97#[cfg_attr(
98    feature = "serde",
99    derive(serde::Serialize, serde::Deserialize),
100    serde(
101        crate = "serde_crate",
102        rename_all = "camelCase",
103        bound(
104            serialize = "L2: serde::Serialize",
105            deserialize = "for<'d> L2: serde::Deserialize<'d>"
106        )
107    )
108)]
109#[derive(Clone, Eq, PartialEq, Hash, Debug)]
110pub struct TxRow<L2: Layer2Tx = Layer2Empty> {
111    pub height: TxStatus<BlockHeight>,
112    // TODO: Add date/time
113    pub operation: OpType,
114    pub our_inputs: Vec<u32>,
115    pub counterparties: Vec<(Counterparty, i64)>,
116    pub own: Vec<(DerivedAddr, i64)>,
117    pub txid: Txid,
118    pub fee: Sats,
119    pub weight: u32,
120    pub size: u32,
121    pub total: Sats,
122    pub amount: Sats,
123    pub balance: Sats,
124    pub layer2: L2,
125}
126
127#[cfg_attr(
128    feature = "serde",
129    derive(serde::Serialize, serde::Deserialize),
130    serde(
131        crate = "serde_crate",
132        rename_all = "camelCase",
133        bound(
134            serialize = "L2: serde::Serialize",
135            deserialize = "for<'d> L2: serde::Deserialize<'d>"
136        )
137    )
138)]
139#[derive(Clone, Eq, PartialEq, Hash, Debug)]
140pub struct CoinRow<L2: Layer2Coin> {
141    pub height: TxStatus<BlockHeight>,
142    // TODO: Add date/time
143    pub address: DerivedAddr,
144    pub outpoint: Outpoint,
145    pub amount: Sats,
146    pub layer2: Vec<L2>,
147}
148
149impl<L2: Layer2Cache> WalletCache<L2> {
150    pub fn coins(&self) -> impl Iterator<Item = CoinRow<L2::Coin>> + '_ {
151        self.utxo.iter().map(|outpoint| {
152            let tx = self.tx.get(&outpoint.txid).expect("cache data inconsistency");
153            let out = tx.outputs.get(outpoint.vout_usize()).expect("cache data inconsistency");
154            CoinRow {
155                height: tx.status.map(|info| info.height),
156                outpoint: *outpoint,
157                address: out.derived_addr().expect("cache data inconsistency"),
158                amount: out.value,
159                layer2: none!(), // TODO: Add support to WalletTx
160            }
161        })
162    }
163
164    pub fn history(&self) -> impl Iterator<Item = TxRow<L2::Tx>> + '_ {
165        self.tx.values().map(|tx| {
166            let (credit, debit) = tx.credited_debited();
167            let mut row = TxRow {
168                height: tx.status.map(|info| info.height),
169                operation: OpType::Credit,
170                our_inputs: tx
171                    .inputs
172                    .iter()
173                    .enumerate()
174                    .filter_map(|(idx, inp)| inp.derived_addr().map(|_| idx as u32))
175                    .collect(),
176                counterparties: none!(),
177                own: none!(),
178                txid: tx.txid,
179                fee: tx.fee,
180                weight: tx.weight,
181                size: tx.size,
182                total: tx.total_moved(),
183                amount: Sats::ZERO,
184                balance: Sats::ZERO,
185                layer2: none!(), // TODO: Add support to WalletTx
186            };
187            // TODO: Add balance calculation
188            row.own = tx
189                .inputs
190                .iter()
191                .filter_map(|i| i.derived_addr().map(|a| (a, -i.value.sats_i64())))
192                .chain(
193                    tx.outputs
194                        .iter()
195                        .filter_map(|o| o.derived_addr().map(|a| (a, o.value.sats_i64()))),
196                )
197                .collect();
198            if credit.is_non_zero() {
199                row.counterparties = tx.credits().fold(Vec::new(), |mut cp, inp| {
200                    let party = Counterparty::from(inp.payer.clone());
201                    cp.push((party, inp.value.sats_i64()));
202                    cp
203                });
204                row.counterparties.extend(tx.debits().fold(Vec::new(), |mut cp, out| {
205                    let party = Counterparty::from(out.beneficiary.clone());
206                    cp.push((party, -out.value.sats_i64()));
207                    cp
208                }));
209                row.operation = OpType::Credit;
210                row.amount = credit - debit - tx.fee;
211            } else if debit.is_non_zero() {
212                row.counterparties = tx.debits().fold(Vec::new(), |mut cp, out| {
213                    let party = Counterparty::from(out.beneficiary.clone());
214                    cp.push((party, -out.value.sats_i64()));
215                    cp
216                });
217                row.operation = OpType::Debit;
218                row.amount = debit;
219            }
220            row
221        })
222    }
223}
224
225#[cfg(test)]
226mod tests {
227    use super::*;
228
229    #[test]
230    fn test_counterparty_str_round_trip() {
231        fn assert_from_str_to_str(counterparty: Counterparty) {
232            let str = counterparty.to_string();
233            let from_str = Counterparty::from_str(&str).unwrap();
234
235            assert_eq!(counterparty, from_str);
236        }
237
238        assert_from_str_to_str(Counterparty::Miner);
239        assert_from_str_to_str(Counterparty::Address(
240            Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq").unwrap(),
241        ));
242        assert_from_str_to_str(Counterparty::Unknown(
243            ScriptPubkey::from_hex("0014a3f8e1f1e1c7e8b4b2f4f3a1b4f7f0a1b4f7f0a1").unwrap(),
244        ));
245    }
246}