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