bpwallet/
data.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::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    /// malformed string representation of transaction input '{0}' lacking txid and vin
164    /// separator '.'
165    MalformedSeparator(String),
166
167    /// malformed transaction input number. Details: {0}
168    #[from]
169    InvalidVout(ParseIntError),
170
171    /// malformed transaction input txid value. Details: {0}
172    #[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    // TODO: Add multiple spends (for RBFs) and mining info
332    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    // TODO: Add layer 2
348}
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}