1use 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 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 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!(), }
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!(), };
187 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}