1use 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(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 NoPrefix,
154
155 NoChecksum,
157
158 InvalidChecksum,
160
161 #[from]
163 Base64(DecodeError),
164
165 #[from(confinement::Error)]
167 TooLong,
168
169 #[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}