rgb_invoice/
bp.rs

1// Invoice Library for RGB smart contracts
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Designed in 2019-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
6// Written in 2024-2025 by Dr Maxim Orlovsky <orlovsky@lnp-bp.org>
7//
8// Copyright (C) 2019-2024 LNP/BP Standards Association, Switzerland.
9// Copyright (C) 2024-2025 LNP/BP Laboratories,
10//                         Institute for Distributed and Cognitive Systems (InDCS), Switzerland.
11// Copyright (C) 2025 RGB Consortium, Switzerland.
12// Copyright (C) 2019-2025 Dr Maxim Orlovsky.
13// All rights under the above copyrights are reserved.
14//
15// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
16// in compliance with the License. You may obtain a copy of the License at
17//
18//        http://www.apache.org/licenses/LICENSE-2.0
19//
20// Unless required by applicable law or agreed to in writing, software distributed under the License
21// is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
22// or implied. See the License for the specific language governing permissions and limitations under
23// the License.
24
25use core::fmt::{self, Display, Formatter};
26use core::str::FromStr;
27
28use amplify::confinement::{self, TinyBlob};
29use amplify::Bytes;
30use baid64::base64::alphabet::Alphabet;
31use baid64::base64::engine::{DecodePaddingMode, GeneralPurpose, GeneralPurposeConfig};
32use baid64::base64::{DecodeError, Engine};
33use baid64::BAID64_ALPHABET;
34use bp::seals::Noise;
35use bp::ScriptPubkey;
36use commit_verify::{Digest, DigestExt, ReservedBytes, Sha256};
37pub use invoice::*;
38use strict_encoding::{DeserializeError, StrictDeserialize, StrictSerialize};
39
40pub const WITNESS_OUT_HRI: &str = "wout";
41
42#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
43#[derive(StrictType, StrictDumb, StrictEncode, StrictDecode)]
44// Strict type here is used only for Display serialization, so we do not include the type into any
45// library
46#[strict_type(lib = "_")]
47#[cfg_attr(feature = "serde", derive(Serialize, Deserialize), serde(rename_all = "camelCase"))]
48pub struct WitnessOut {
49    #[strict_type(skip)]
50    #[cfg_attr(feature = "serde", serde(skip))]
51    reserved: ReservedBytes<1>,
52    salt: u64,
53    address: AddressPayload,
54}
55impl StrictSerialize for WitnessOut {}
56impl StrictDeserialize for WitnessOut {}
57
58impl From<WitnessOut> for ScriptPubkey {
59    fn from(val: WitnessOut) -> Self { val.script_pubkey() }
60}
61
62impl WitnessOut {
63    pub fn new(address: impl Into<AddressPayload>, salt: u64) -> Self {
64        WitnessOut { reserved: default!(), salt, address: address.into() }
65    }
66
67    pub fn noise(&self) -> Noise {
68        let mut noise_engine = Sha256::new();
69        noise_engine.input_raw(&self.salt.to_le_bytes());
70        noise_engine.input_raw(self.script_pubkey().as_ref());
71        let mut noise = [0xFFu8; 40];
72        noise[..32].copy_from_slice(&noise_engine.finish());
73        Bytes::from(noise).into()
74    }
75
76    pub fn script_pubkey(&self) -> ScriptPubkey { self.address.script_pubkey() }
77
78    pub fn checksum(&self) -> [u8; 4] {
79        let key = Sha256::digest(WITNESS_OUT_HRI.as_bytes());
80        let mut sha = Sha256::new_with_prefix(key);
81        sha.update([0]);
82        sha.update(self.salt.to_le_bytes());
83        sha.update(self.script_pubkey().as_slice());
84        let sha = sha.finalize();
85        [sha[0], sha[1], sha[1], sha[2]]
86    }
87}
88
89impl Display for WitnessOut {
90    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
91        f.write_str(WITNESS_OUT_HRI)?;
92        f.write_str(":")?;
93
94        let mut data = self
95            .to_strict_serialized::<{ u8::MAX as usize }>()
96            .expect("script pubkey length in WitnessOut should be controlled during creation")
97            .release();
98        data.extend(self.checksum());
99
100        let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
101        let engine =
102            GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new().with_encode_padding(false));
103        let encoded = engine.encode(data).chars().collect::<Vec<_>>();
104
105        let mut iter = encoded.chunks(8).peekable();
106        while let Some(chunk) = iter.next() {
107            f.write_str(&chunk.iter().collect::<String>())?;
108            if iter.by_ref().peek().is_some() {
109                f.write_str("-")?;
110            }
111        }
112
113        Ok(())
114    }
115}
116
117impl FromStr for WitnessOut {
118    type Err = ParseWitnessOutError;
119
120    fn from_str(s: &str) -> Result<Self, Self::Err> {
121        let s = s
122            .strip_prefix(WITNESS_OUT_HRI)
123            .and_then(|s| s.strip_prefix(':'))
124            .ok_or(ParseWitnessOutError::NoPrefix)?
125            .replace('-', "");
126
127        let alphabet = Alphabet::new(BAID64_ALPHABET).expect("invalid Baid64 alphabet");
128        let engine = GeneralPurpose::new(
129            &alphabet,
130            GeneralPurposeConfig::new().with_decode_padding_mode(DecodePaddingMode::RequireNone),
131        );
132        let decoded = engine.decode(s.as_bytes())?;
133
134        let (data, checksum) = decoded
135            .split_last_chunk::<4>()
136            .ok_or(ParseWitnessOutError::NoChecksum)?;
137
138        let data = TinyBlob::try_from_slice(data)?;
139        let wout = WitnessOut::from_strict_serialized::<{ u8::MAX as usize }>(data)?;
140
141        if *checksum != wout.checksum() {
142            return Err(ParseWitnessOutError::InvalidChecksum);
143        }
144
145        Ok(wout)
146    }
147}
148
149#[derive(Clone, PartialEq, Eq, Debug, Display, Error, From)]
150#[display(doc_comments)]
151pub enum ParseWitnessOutError {
152    /// witness output seal definition doesn't start with a necessary prefix `wout:`.
153    NoPrefix,
154
155    /// the provided witness output seal definition doesn't contain checksum.
156    NoChecksum,
157
158    /// checksum of the provided witness output seal definition is invalid.
159    InvalidChecksum,
160
161    /// invalid Base64 encoding in witness output seal definition - {0}.
162    #[from]
163    Base64(DecodeError),
164
165    /// the length of encoded witness output seal definition string exceeds 255 chars.
166    #[from(confinement::Error)]
167    TooLong,
168
169    /// invalid witness output seal definition binary data - {0}.
170    #[from]
171    Encoding(DeserializeError),
172}
173
174#[cfg(test)]
175mod tests {
176    #![cfg_attr(coverage_nightly, coverage(off))]
177
178    use bp::OutputPk;
179    use strict_encoding::StrictDumb;
180
181    use super::*;
182
183    #[test]
184    fn display_from_str() {
185        let wout = WitnessOut::new(AddressPayload::Tr(OutputPk::strict_dumb()), 0xdeadbeaf1badcafe);
186        let s = wout.to_string();
187        assert_eq!(s, "wout:~sqtG6__-rd4gAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQEB-AQEBAQFM-DAx2");
188        let wout2 = WitnessOut::from_str(&s).unwrap();
189        assert_eq!(wout, wout2);
190    }
191}