1use anyhow::{Result, anyhow, bail};
12use pallas_crypto::hash::Hasher;
13use pallas_primitives::conway::{self, AuxiliaryData};
14use pallas_primitives::{Fragment, KeyValuePairs, Metadatum, Nullable};
15use pallas_txbuilder::BuiltTransaction;
16
17use crate::build::tx_id;
18
19pub const NOTE_LABEL: u64 = 674;
21
22pub const MAX_NOTE_CHARS: usize = 64;
24
25const MAX_LINE_BYTES: usize = 64;
28
29#[derive(Debug, Clone, PartialEq, Eq)]
31pub struct Note {
32 text: String,
33 lines: Vec<String>,
34}
35
36impl Note {
37 pub fn new(text: &str) -> Result<Option<Note>> {
41 let text = text.trim();
42 if text.is_empty() {
43 return Ok(None);
44 }
45 if text.chars().any(char::is_control) {
46 bail!("A note is one line of text, with no tabs or line breaks");
47 }
48 let chars = text.chars().count();
49 if chars > MAX_NOTE_CHARS {
50 bail!("A note is at most {MAX_NOTE_CHARS} characters, not {chars}");
51 }
52 Ok(Some(Note {
53 text: text.to_string(),
54 lines: split(text),
55 }))
56 }
57
58 pub fn lines(&self) -> &[String] {
60 &self.lines
61 }
62
63 pub fn text(&self) -> &str {
67 &self.text
68 }
69
70 fn auxiliary_data(&self) -> AuxiliaryData {
72 let msg = Metadatum::Array(self.lines.iter().cloned().map(Metadatum::Text).collect());
73 let message = Metadatum::Map(KeyValuePairs::Def(vec![(
74 Metadatum::Text("msg".to_string()),
75 msg,
76 )]));
77 AuxiliaryData::Shelley(KeyValuePairs::Def(vec![(NOTE_LABEL, message)]))
78 }
79
80 pub fn patch(&self, mut built: BuiltTransaction) -> Result<BuiltTransaction> {
83 if built.signatures.is_some() {
84 bail!("A transaction is patched before it's signed, not after");
85 }
86 let mut tx = conway::Tx::decode_fragment(&built.tx_bytes.0)
87 .map_err(|e| anyhow!("The built transaction isn't a Conway transaction: {e}"))?;
88 if matches!(tx.auxiliary_data, Nullable::Some(_))
89 || tx.transaction_body.auxiliary_data_hash.is_some()
90 {
91 bail!("The transaction already has metadata");
92 }
93 let data = self.auxiliary_data();
94 tx.transaction_body.auxiliary_data_hash =
95 Some(Hasher::<256>::hash_cbor(&data).to_vec().into());
96 tx.auxiliary_data = Nullable::Some(data);
97 let bytes = tx
98 .encode_fragment()
99 .map_err(|e| anyhow!("Failed to encode the transaction: {e}"))?;
100 built.tx_hash.0 = *tx_id(&bytes)?;
101 built.tx_bytes.0 = bytes;
102 Ok(built)
103 }
104}
105
106fn split(text: &str) -> Vec<String> {
109 let mut lines = Vec::new();
110 let mut rest = text;
111 while rest.len() > MAX_LINE_BYTES {
112 let mut cut = MAX_LINE_BYTES;
113 while !rest.is_char_boundary(cut) {
114 cut -= 1;
115 }
116 let (line, next) = match rest[..cut].rfind(' ') {
117 Some(space) if space > 0 => (&rest[..space], &rest[space + 1..]),
118 _ => (&rest[..cut], &rest[cut..]),
119 };
120 lines.push(line.to_string());
121 rest = next;
122 }
123 lines.push(rest.to_string());
124 lines
125}