1use crate::intent;
7use crate::script::arknote_script;
8use crate::script::tr_script_pubkey;
9use crate::Error;
10use crate::UNSPENDABLE_KEY;
11use bitcoin::hashes::sha256;
12use bitcoin::hashes::Hash;
13use bitcoin::key::Secp256k1;
14use bitcoin::taproot::LeafVersion;
15use bitcoin::taproot::TaprootBuilder;
16use bitcoin::Amount;
17use bitcoin::OutPoint;
18use bitcoin::PublicKey;
19use bitcoin::ScriptBuf;
20use bitcoin::Sequence;
21use bitcoin::TxOut;
22use bitcoin::Txid;
23use std::fmt;
24
25pub const DEFAULT_HRP: &str = "arknote";
27
28pub const PREIMAGE_LENGTH: usize = 32;
30
31const VALUE_LENGTH: usize = 4;
33
34const ARKNOTE_LENGTH: usize = PREIMAGE_LENGTH + VALUE_LENGTH;
36
37pub const FAKE_VOUT: u32 = 0;
39
40#[derive(Debug, Clone, PartialEq, Eq)]
49pub struct ArkNote {
50 preimage: [u8; PREIMAGE_LENGTH],
51 value: Amount,
52 hrp: String,
53}
54
55impl ArkNote {
56 pub fn new(preimage: [u8; PREIMAGE_LENGTH], value: Amount) -> Result<Self, Error> {
58 Self::new_with_hrp(preimage, value, DEFAULT_HRP.to_string())
59 }
60
61 pub fn new_with_hrp(
63 preimage: [u8; PREIMAGE_LENGTH],
64 value: Amount,
65 hrp: String,
66 ) -> Result<Self, Error> {
67 if value.to_sat() > u32::MAX as u64 {
69 return Err(Error::ad_hoc(format!(
70 "value {} exceeds maximum of {} sats",
71 value.to_sat(),
72 u32::MAX
73 )));
74 }
75
76 Ok(Self {
77 preimage,
78 value,
79 hrp,
80 })
81 }
82
83 pub fn from_string(s: &str) -> Result<Self, Error> {
85 Self::from_string_with_hrp(s, DEFAULT_HRP)
86 }
87
88 pub fn from_string_with_hrp(s: &str, hrp: &str) -> Result<Self, Error> {
90 let s = s.trim();
91
92 if !s.starts_with(hrp) {
93 return Err(Error::ad_hoc(format!(
94 "invalid prefix: expected '{}', got '{}'",
95 hrp,
96 &s[..hrp.len().min(s.len())]
97 )));
98 }
99
100 let encoded = &s[hrp.len()..];
101 let decoded = bs58::decode(encoded)
102 .into_vec()
103 .map_err(|e| Error::ad_hoc(format!("invalid base58: {e}")))?;
104
105 if decoded.len() != ARKNOTE_LENGTH {
106 return Err(Error::ad_hoc(format!(
107 "invalid payload length: expected {}, got {}",
108 ARKNOTE_LENGTH,
109 decoded.len()
110 )));
111 }
112
113 let mut preimage = [0u8; PREIMAGE_LENGTH];
114 preimage.copy_from_slice(&decoded[..PREIMAGE_LENGTH]);
115
116 let value_bytes: [u8; 4] = decoded[PREIMAGE_LENGTH..]
117 .try_into()
118 .map_err(|_| Error::ad_hoc("invalid value bytes"))?;
119 let value = Amount::from_sat(u32::from_be_bytes(value_bytes) as u64);
120
121 Self::new_with_hrp(preimage, value, hrp.to_string())
122 }
123
124 pub fn to_encoded_string(&self) -> String {
126 self.to_string()
127 }
128
129 pub fn preimage(&self) -> &[u8; PREIMAGE_LENGTH] {
131 &self.preimage
132 }
133
134 pub fn preimage_hash(&self) -> sha256::Hash {
136 sha256::Hash::hash(&self.preimage)
137 }
138
139 pub fn value(&self) -> Amount {
141 self.value
142 }
143
144 pub fn hrp(&self) -> &str {
146 &self.hrp
147 }
148
149 pub fn script(&self) -> ScriptBuf {
151 arknote_script(&self.preimage_hash())
152 }
153
154 pub fn txid(&self) -> Txid {
158 Txid::from_byte_array(*self.preimage_hash().as_byte_array())
159 }
160
161 pub fn outpoint(&self) -> OutPoint {
163 OutPoint::new(self.txid(), FAKE_VOUT)
164 }
165
166 pub fn to_intent_input(&self) -> Result<intent::Input, Error> {
171 let secp = Secp256k1::new();
172
173 let unspendable_key: PublicKey = UNSPENDABLE_KEY
174 .parse()
175 .map_err(|e| Error::ad_hoc(format!("invalid unspendable key: {e}")))?;
176 let (unspendable_xonly, _) = unspendable_key.inner.x_only_public_key();
177
178 let note_script = self.script();
179
180 let spend_info = TaprootBuilder::new()
182 .add_leaf(0, note_script.clone())
183 .map_err(|e| Error::ad_hoc(format!("failed to add leaf: {e:?}")))?
184 .finalize(&secp, unspendable_xonly)
185 .map_err(|e| Error::ad_hoc(format!("failed to finalize taproot: {e:?}")))?;
186
187 let control_block = spend_info
188 .control_block(&(note_script.clone(), LeafVersion::TapScript))
189 .ok_or_else(|| Error::ad_hoc("failed to get control block for note script"))?;
190
191 let script_pubkey = tr_script_pubkey(&spend_info);
192
193 Ok(intent::Input::new_with_extra_witness(
194 self.outpoint(),
195 Sequence::MAX,
196 None,
197 TxOut {
198 value: self.value,
199 script_pubkey,
200 },
201 vec![note_script.clone()],
202 (note_script, control_block),
203 false, false, vec![self.preimage.to_vec()],
206 ))
207 }
208}
209
210impl fmt::Display for ArkNote {
211 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
212 let mut payload = Vec::with_capacity(ARKNOTE_LENGTH);
213 payload.extend_from_slice(&self.preimage);
214 payload.extend_from_slice(&(self.value.to_sat() as u32).to_be_bytes());
215
216 write!(f, "{}{}", self.hrp, bs58::encode(payload).into_string())
217 }
218}
219
220#[cfg(test)]
221mod tests {
222 use super::*;
223
224 fn hex_to_array32(hex: &str) -> [u8; 32] {
225 let bytes = hex::decode(hex).expect("valid hex");
226 bytes.try_into().expect("32 bytes")
227 }
228
229 #[test]
230 fn roundtrip_encoding() {
231 let preimage =
232 hex_to_array32("11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3");
233 let value = Amount::from_sat(900_000);
234
235 let note = ArkNote::new(preimage, value).unwrap();
236 let encoded = note.to_string();
237 let decoded = ArkNote::from_string(&encoded).unwrap();
238
239 assert_eq!(decoded.preimage(), &preimage);
240 assert_eq!(decoded.value(), value);
241 }
242
243 #[test]
244 fn test_vectors() {
245 let cases = [
247 (
248 "arknote",
249 "arknote8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT",
250 "11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3",
251 900_000u64,
252 ),
253 (
254 "arknote",
255 "arknoteSkB92YpWm4Q2ijQHH34cqbKkCZWszsiQgHVjtNeFF2Cwp59D",
256 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
257 1_828_932u64,
258 ),
259 (
260 "noteark",
261 "noteark8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT",
262 "11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3",
263 900_000u64,
264 ),
265 ];
266
267 for (hrp, note_str, preimage_hex, expected_sats) in cases {
268 let note = ArkNote::from_string_with_hrp(note_str, hrp).unwrap();
269
270 assert_eq!(note.preimage(), &hex_to_array32(preimage_hex));
271 assert_eq!(note.value(), Amount::from_sat(expected_sats));
272 assert_eq!(note.hrp(), hrp);
273
274 let reconstructed = ArkNote::new_with_hrp(
276 hex_to_array32(preimage_hex),
277 Amount::from_sat(expected_sats),
278 hrp.to_string(),
279 )
280 .unwrap();
281 assert_eq!(reconstructed.to_string(), note_str);
282 }
283 }
284
285 #[test]
286 fn invalid_prefix() {
287 let result = ArkNote::from_string("wrongprefix123456789");
288 assert!(result.is_err());
289 assert!(result.unwrap_err().to_string().contains("invalid prefix"));
290 }
291
292 #[test]
293 fn invalid_base58() {
294 let result = ArkNote::from_string("arknote!!!invalid!!!");
295 assert!(result.is_err());
296 assert!(result.unwrap_err().to_string().contains("base58"));
297 }
298
299 #[test]
300 fn value_overflow() {
301 let preimage = [0u8; 32];
302 let result = ArkNote::new(preimage, Amount::from_sat(u64::MAX));
303 assert!(result.is_err());
304 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
305 }
306
307 #[test]
308 fn script_is_hash_lock() {
309 let preimage = [0x42u8; 32];
310 let note = ArkNote::new(preimage, Amount::from_sat(1000)).unwrap();
311 let script = note.script();
312
313 let bytes = script.as_bytes();
315 assert_eq!(bytes[0], bitcoin::opcodes::all::OP_SHA256.to_u8());
316 assert_eq!(bytes[1], 0x20); assert_eq!(bytes[34], bitcoin::opcodes::all::OP_EQUAL.to_u8());
318 }
319
320 #[test]
321 fn whitespace_handling() {
322 let note_str = " arknote8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT ";
323 let note = ArkNote::from_string(note_str).unwrap();
324 assert_eq!(note.value(), Amount::from_sat(900_000));
325 }
326}