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::new(),
206 vec![self.preimage.to_vec()],
207 ))
208 }
209}
210
211impl fmt::Display for ArkNote {
212 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213 let mut payload = Vec::with_capacity(ARKNOTE_LENGTH);
214 payload.extend_from_slice(&self.preimage);
215 payload.extend_from_slice(&(self.value.to_sat() as u32).to_be_bytes());
216
217 write!(f, "{}{}", self.hrp, bs58::encode(payload).into_string())
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 fn hex_to_array32(hex: &str) -> [u8; 32] {
226 let bytes = hex::decode(hex).expect("valid hex");
227 bytes.try_into().expect("32 bytes")
228 }
229
230 #[test]
231 fn roundtrip_encoding() {
232 let preimage =
233 hex_to_array32("11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3");
234 let value = Amount::from_sat(900_000);
235
236 let note = ArkNote::new(preimage, value).unwrap();
237 let encoded = note.to_string();
238 let decoded = ArkNote::from_string(&encoded).unwrap();
239
240 assert_eq!(decoded.preimage(), &preimage);
241 assert_eq!(decoded.value(), value);
242 }
243
244 #[test]
245 fn test_vectors() {
246 let cases = [
248 (
249 "arknote",
250 "arknote8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT",
251 "11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3",
252 900_000u64,
253 ),
254 (
255 "arknote",
256 "arknoteSkB92YpWm4Q2ijQHH34cqbKkCZWszsiQgHVjtNeFF2Cwp59D",
257 "0102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f20",
258 1_828_932u64,
259 ),
260 (
261 "noteark",
262 "noteark8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT",
263 "11d2a03264d0efd311d2a03264d0efd311d2a03264d0efd311d2a03264d0efd3",
264 900_000u64,
265 ),
266 ];
267
268 for (hrp, note_str, preimage_hex, expected_sats) in cases {
269 let note = ArkNote::from_string_with_hrp(note_str, hrp).unwrap();
270
271 assert_eq!(note.preimage(), &hex_to_array32(preimage_hex));
272 assert_eq!(note.value(), Amount::from_sat(expected_sats));
273 assert_eq!(note.hrp(), hrp);
274
275 let reconstructed = ArkNote::new_with_hrp(
277 hex_to_array32(preimage_hex),
278 Amount::from_sat(expected_sats),
279 hrp.to_string(),
280 )
281 .unwrap();
282 assert_eq!(reconstructed.to_string(), note_str);
283 }
284 }
285
286 #[test]
287 fn invalid_prefix() {
288 let result = ArkNote::from_string("wrongprefix123456789");
289 assert!(result.is_err());
290 assert!(result.unwrap_err().to_string().contains("invalid prefix"));
291 }
292
293 #[test]
294 fn invalid_base58() {
295 let result = ArkNote::from_string("arknote!!!invalid!!!");
296 assert!(result.is_err());
297 assert!(result.unwrap_err().to_string().contains("base58"));
298 }
299
300 #[test]
301 fn value_overflow() {
302 let preimage = [0u8; 32];
303 let result = ArkNote::new(preimage, Amount::from_sat(u64::MAX));
304 assert!(result.is_err());
305 assert!(result.unwrap_err().to_string().contains("exceeds maximum"));
306 }
307
308 #[test]
309 fn script_is_hash_lock() {
310 let preimage = [0x42u8; 32];
311 let note = ArkNote::new(preimage, Amount::from_sat(1000)).unwrap();
312 let script = note.script();
313
314 let bytes = script.as_bytes();
316 assert_eq!(bytes[0], bitcoin::opcodes::all::OP_SHA256.to_u8());
317 assert_eq!(bytes[1], 0x20); assert_eq!(bytes[34], bitcoin::opcodes::all::OP_EQUAL.to_u8());
319 }
320
321 #[test]
322 fn whitespace_handling() {
323 let note_str = " arknote8rFzGqZsG9RCLripA6ez8d2hQEzFKsqCeiSnXhQj56Ysw7ZQT ";
324 let note = ArkNote::from_string(note_str).unwrap();
325 assert_eq!(note.value(), Amount::from_sat(900_000));
326 }
327}