1use std::cmp::Ordering;
24use std::fmt::{self, Display, Formatter, LowerHex};
25use std::num::{NonZeroU32, ParseIntError};
26use std::str::FromStr;
27
28use amplify::hex;
29use amplify::hex::FromHex;
30use bpstd::{
31 Address, BlockHash, BlockHeader, DerivedAddr, Keychain, LockTime, NormalIndex, Outpoint, Sats,
32 ScriptPubkey, SeqNo, SigScript, Terminal, TxVer, Txid, Witness,
33};
34use psbt::{Prevout, Utxo};
35
36pub type BlockHeight = NonZeroU32;
37
38#[cfg_attr(
39 feature = "serde",
40 derive(serde::Serialize, serde::Deserialize),
41 serde(crate = "serde_crate", rename_all = "camelCase")
42)]
43#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
44pub struct BlockInfo {
45 pub mined: MiningInfo,
46 pub header: BlockHeader,
47 pub difficulty: u8,
48 pub tx_count: u32,
49 pub size: u32,
50 pub weight: u32,
51 pub mediantime: u32,
52}
53
54impl Ord for BlockInfo {
55 fn cmp(&self, other: &Self) -> Ordering { self.mined.cmp(&other.mined) }
56}
57
58impl PartialOrd for BlockInfo {
59 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
60}
61
62#[cfg_attr(
63 feature = "serde",
64 derive(serde::Serialize, serde::Deserialize),
65 serde(crate = "serde_crate", rename_all = "camelCase")
66)]
67#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
68pub struct MiningInfo {
69 pub height: BlockHeight,
70 pub time: u64,
71 pub block_hash: BlockHash,
72}
73
74impl Ord for MiningInfo {
75 fn cmp(&self, other: &Self) -> Ordering { self.height.cmp(&other.height) }
76}
77
78impl PartialOrd for MiningInfo {
79 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
80}
81
82impl MiningInfo {
83 pub fn genesis() -> Self {
84 MiningInfo {
85 height: BlockHeight::MIN,
86 time: 1231006505,
87 block_hash: BlockHash::from_hex(
88 "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f",
89 )
90 .unwrap(),
91 }
92 }
93}
94
95#[cfg_attr(
96 feature = "serde",
97 derive(serde::Serialize, serde::Deserialize),
98 serde(crate = "serde_crate", rename_all = "camelCase")
99)]
100#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
101pub enum TxStatus<T = MiningInfo> {
102 Unknown,
103 Mempool,
104 Channel,
105 Mined(T),
106}
107
108impl<T> TxStatus<T> {
109 pub fn map<U>(&self, f: impl FnOnce(&T) -> U) -> TxStatus<U> {
110 match self {
111 TxStatus::Mined(info) => TxStatus::Mined(f(info)),
112 TxStatus::Mempool => TxStatus::Mempool,
113 TxStatus::Channel => TxStatus::Channel,
114 TxStatus::Unknown => TxStatus::Unknown,
115 }
116 }
117
118 pub fn is_mined(&self) -> bool { matches!(self, Self::Mined(_)) }
119}
120
121impl<T> Display for TxStatus<T>
122where T: Display
123{
124 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
125 match self {
126 TxStatus::Mined(info) => Display::fmt(info, f),
127 TxStatus::Mempool => f.write_str("mempool"),
128 TxStatus::Channel => f.write_str("channel"),
129 TxStatus::Unknown => f.write_str("unknown"),
130 }
131 }
132}
133
134#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Display)]
135#[cfg_attr(
136 feature = "serde",
137 derive(serde::Serialize, serde::Deserialize),
138 serde(crate = "serde_crate", rename_all = "camelCase")
139)]
140#[display("{txid}.{vin}")]
141pub struct Inpoint {
142 pub txid: Txid,
143 pub vin: u32,
144}
145
146impl Inpoint {
147 #[inline]
148 pub fn new(txid: Txid, vin: u32) -> Self { Inpoint { txid, vin } }
149}
150
151impl From<Outpoint> for Inpoint {
152 fn from(outpoint: Outpoint) -> Self {
153 Inpoint {
154 txid: outpoint.txid,
155 vin: outpoint.vout.into_u32(),
156 }
157 }
158}
159
160#[derive(Clone, Eq, PartialEq, Debug, Display, From, Error)]
161#[display(doc_comments)]
162pub enum InpointParseError {
163 MalformedSeparator(String),
166
167 #[from]
169 InvalidVout(ParseIntError),
170
171 #[from]
173 InvalidTxid(hex::Error),
174}
175
176impl FromStr for Inpoint {
177 type Err = InpointParseError;
178
179 fn from_str(s: &str) -> Result<Self, Self::Err> {
180 let (txid, vin) =
181 s.split_once('.').ok_or_else(|| InpointParseError::MalformedSeparator(s.to_owned()))?;
182 Ok(Inpoint::new(txid.parse()?, u32::from_str(vin)?))
183 }
184}
185
186#[cfg_attr(
187 feature = "serde",
188 derive(serde::Serialize, serde::Deserialize),
189 serde(crate = "serde_crate", rename_all = "camelCase")
190)]
191#[derive(Clone, Eq, PartialEq, Hash, Debug)]
192pub struct WalletTx {
193 pub txid: Txid,
194 pub status: TxStatus,
195 pub inputs: Vec<TxCredit>,
196 pub outputs: Vec<TxDebit>,
197 pub fee: Sats,
198 pub size: u32,
199 pub weight: u32,
200 pub version: TxVer,
201 pub locktime: LockTime,
202}
203
204impl WalletTx {
205 pub fn credits(&self) -> impl Iterator<Item = &TxCredit> {
206 self.inputs.iter().filter(|c| c.is_external())
207 }
208
209 pub fn debits(&self) -> impl Iterator<Item = &TxDebit> {
210 self.outputs.iter().filter(|d| d.is_external())
211 }
212
213 pub fn total_moved(&self) -> Sats { self.inputs.iter().map(|vin| vin.value).sum::<Sats>() }
214
215 pub fn credit_sum(&self) -> Sats { self.credits().map(|vin| vin.value).sum::<Sats>() }
216
217 pub fn debit_sum(&self) -> Sats { self.debits().map(|vout| vout.value).sum::<Sats>() }
218
219 pub fn credited_debited(&self) -> (Sats, Sats) { (self.credit_sum(), self.debit_sum()) }
220
221 pub fn balance_change(&self) -> i64 {
222 let credit = self.credit_sum().sats_i64();
223 let debit = self.debit_sum().sats_i64();
224 debit - credit
225 }
226}
227
228#[derive(Clone, Eq, PartialEq, Hash, Debug, From)]
229#[cfg_attr(
230 feature = "serde",
231 derive(serde::Serialize, serde::Deserialize),
232 serde(crate = "serde_crate", rename_all = "camelCase")
233)]
234pub enum Party {
235 Subsidy,
236
237 #[from]
238 Counterparty(Address),
239
240 #[from]
241 Unknown(ScriptPubkey),
242
243 #[from]
244 Wallet(DerivedAddr),
245}
246
247impl Party {
248 pub fn is_ourself(&self) -> bool { matches!(self, Party::Wallet(_)) }
249 pub fn is_external(&self) -> bool { !self.is_ourself() }
250 pub fn is_unknown(&self) -> bool { matches!(self, Party::Unknown(_)) }
251 pub fn derived_addr(&self) -> Option<DerivedAddr> {
252 match self {
253 Party::Wallet(addr) => Some(*addr),
254 _ => None,
255 }
256 }
257 pub fn from_wallet_addr<T>(wallet_addr: &WalletAddr<T>) -> Self {
258 Party::Wallet(DerivedAddr {
259 addr: wallet_addr.addr,
260 terminal: wallet_addr.terminal,
261 })
262 }
263 pub fn script_pubkey(&self) -> Option<ScriptPubkey> {
264 match self {
265 Party::Subsidy => None,
266 Party::Counterparty(addr) => Some(addr.script_pubkey()),
267 Party::Unknown(script) => Some(script.clone()),
268 Party::Wallet(derive) => Some(derive.addr.script_pubkey()),
269 }
270 }
271}
272
273impl Display for Party {
274 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
275 match self {
276 Party::Subsidy => f.write_str("coinbase"),
277 Party::Counterparty(addr) => Display::fmt(addr, f),
278 Party::Unknown(script) => LowerHex::fmt(script, f),
279 Party::Wallet(term) => Display::fmt(term, f),
280 }
281 }
282}
283
284impl FromStr for Party {
285 type Err = String;
286
287 fn from_str(s: &str) -> Result<Self, Self::Err> {
288 if s == "coinbase" {
289 return Ok(Party::Subsidy);
290 }
291 Address::from_str(s)
292 .map(Self::from)
293 .or_else(|_| DerivedAddr::from_str(s).map(Self::from))
294 .or_else(|_| ScriptPubkey::from_hex(s).map(Self::from))
295 .map_err(|_| s.to_owned())
296 }
297}
298
299#[cfg_attr(
300 feature = "serde",
301 derive(serde::Serialize, serde::Deserialize),
302 serde(crate = "serde_crate", rename_all = "camelCase")
303)]
304#[derive(Clone, Eq, PartialEq, Hash, Debug)]
305pub struct TxCredit {
306 pub outpoint: Outpoint,
307 pub payer: Party,
308 pub sequence: SeqNo,
309 pub coinbase: bool,
310 pub script_sig: SigScript,
311 pub witness: Witness,
312 pub value: Sats,
313}
314
315impl TxCredit {
316 pub fn is_ourself(&self) -> bool { self.payer.is_ourself() }
317 pub fn is_external(&self) -> bool { !self.is_ourself() }
318 pub fn derived_addr(&self) -> Option<DerivedAddr> { self.payer.derived_addr() }
319}
320
321#[cfg_attr(
322 feature = "serde",
323 derive(serde::Serialize, serde::Deserialize),
324 serde(crate = "serde_crate", rename_all = "camelCase")
325)]
326#[derive(Clone, Eq, PartialEq, Hash, Debug)]
327pub struct TxDebit {
328 pub outpoint: Outpoint,
329 pub beneficiary: Party,
330 pub value: Sats,
331 pub spent: Option<Inpoint>,
333}
334
335impl TxDebit {
336 pub fn is_ourself(&self) -> bool { self.beneficiary.is_ourself() }
337 pub fn is_external(&self) -> bool { !self.is_ourself() }
338 pub fn derived_addr(&self) -> Option<DerivedAddr> { self.beneficiary.derived_addr() }
339}
340
341#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
342pub struct WalletUtxo {
343 pub outpoint: Outpoint,
344 pub value: Sats,
345 pub terminal: Terminal,
346 pub status: TxStatus,
347 }
349
350impl WalletUtxo {
351 #[inline]
352 pub fn to_prevout(&self) -> Prevout { Prevout::new(self.outpoint, self.value) }
353 pub fn into_outpoint(self) -> Outpoint { self.outpoint }
354 pub fn into_utxo(self) -> Utxo {
355 Utxo {
356 outpoint: self.outpoint,
357 value: self.value,
358 terminal: self.terminal,
359 }
360 }
361}
362
363#[cfg_attr(
364 feature = "serde",
365 derive(serde::Serialize, serde::Deserialize),
366 serde(crate = "serde_crate", rename_all = "camelCase")
367)]
368#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug)]
369pub struct WalletAddr<T = Sats> {
370 pub terminal: Terminal,
371 pub addr: Address,
372 pub used: u32,
373 pub volume: Sats,
374 pub balance: T,
375}
376
377impl<T> Ord for WalletAddr<T>
378where T: Eq
379{
380 fn cmp(&self, other: &Self) -> Ordering { self.terminal.cmp(&other.terminal) }
381}
382
383impl<T> PartialOrd for WalletAddr<T>
384where T: Eq
385{
386 fn partial_cmp(&self, other: &Self) -> Option<Ordering> { Some(self.cmp(other)) }
387}
388
389impl<T> From<DerivedAddr> for WalletAddr<T>
390where T: Default
391{
392 fn from(derived: DerivedAddr) -> Self {
393 WalletAddr {
394 addr: derived.addr,
395 terminal: derived.terminal,
396 used: 0,
397 volume: Sats::ZERO,
398 balance: zero!(),
399 }
400 }
401}
402
403impl<T> WalletAddr<T>
404where T: Default
405{
406 pub fn new(addr: Address, keychain: Keychain, index: NormalIndex) -> Self {
407 WalletAddr::<T>::from(DerivedAddr::new(addr, keychain, index))
408 }
409}
410
411impl WalletAddr<i64> {
412 pub fn expect_transmute(self) -> WalletAddr<Sats> {
413 WalletAddr {
414 terminal: self.terminal,
415 addr: self.addr,
416 used: self.used,
417 volume: self.volume,
418 balance: Sats(u64::try_from(self.balance).expect("negative balance")),
419 }
420 }
421}
422
423#[cfg(test)]
424mod tests {
425 use super::*;
426
427 #[test]
428 fn test_inpoint_str_round_trip() {
429 let s = "cca7507897abc89628f450e8b1e0c6fca4ec3f7b34cccf55f3f531c659ff4d79.1";
430 assert_eq!(Inpoint::from_str(s).unwrap().to_string(), s);
431 }
432
433 #[test]
434 fn test_party_str_round_trip() {
435 fn assert_from_str_to_str(party: Party) {
436 let str = party.to_string();
437 let from_str = Party::from_str(&str).unwrap();
438
439 assert_eq!(party, from_str);
440 }
441
442 assert_from_str_to_str(Party::Subsidy);
443 assert_from_str_to_str(Party::Counterparty(
444 Address::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq").unwrap(),
445 ));
446 assert_from_str_to_str(Party::Unknown(
447 ScriptPubkey::from_hex("76a91455ae51684c43435da751ac8d2173b2652eb6410588ac").unwrap(),
448 ));
449 assert_from_str_to_str(Party::Wallet(
450 DerivedAddr::from_str("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq&1/1").unwrap(),
451 ));
452 }
453}