use anyhow::{Result, anyhow, bail};
use pallas_crypto::hash::Hasher;
use pallas_primitives::conway::{self, AuxiliaryData};
use pallas_primitives::{Fragment, KeyValuePairs, Metadatum, Nullable};
use pallas_txbuilder::BuiltTransaction;
use crate::build::tx_id;
pub const NOTE_LABEL: u64 = 674;
pub const MAX_NOTE_CHARS: usize = 64;
const MAX_LINE_BYTES: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Note {
text: String,
lines: Vec<String>,
}
impl Note {
pub fn new(text: &str) -> Result<Option<Note>> {
let text = text.trim();
if text.is_empty() {
return Ok(None);
}
if text.chars().any(char::is_control) {
bail!("A note is one line of text, with no tabs or line breaks");
}
let chars = text.chars().count();
if chars > MAX_NOTE_CHARS {
bail!("A note is at most {MAX_NOTE_CHARS} characters, not {chars}");
}
Ok(Some(Note {
text: text.to_string(),
lines: split(text),
}))
}
pub fn lines(&self) -> &[String] {
&self.lines
}
pub fn text(&self) -> &str {
&self.text
}
fn auxiliary_data(&self) -> AuxiliaryData {
let msg = Metadatum::Array(self.lines.iter().cloned().map(Metadatum::Text).collect());
let message = Metadatum::Map(KeyValuePairs::Def(vec![(
Metadatum::Text("msg".to_string()),
msg,
)]));
AuxiliaryData::Shelley(KeyValuePairs::Def(vec![(NOTE_LABEL, message)]))
}
pub fn patch(&self, mut built: BuiltTransaction) -> Result<BuiltTransaction> {
if built.signatures.is_some() {
bail!("A transaction is patched before it's signed, not after");
}
let mut tx = conway::Tx::decode_fragment(&built.tx_bytes.0)
.map_err(|e| anyhow!("The built transaction isn't a Conway transaction: {e}"))?;
if matches!(tx.auxiliary_data, Nullable::Some(_))
|| tx.transaction_body.auxiliary_data_hash.is_some()
{
bail!("The transaction already has metadata");
}
let data = self.auxiliary_data();
tx.transaction_body.auxiliary_data_hash =
Some(Hasher::<256>::hash_cbor(&data).to_vec().into());
tx.auxiliary_data = Nullable::Some(data);
let bytes = tx
.encode_fragment()
.map_err(|e| anyhow!("Failed to encode the transaction: {e}"))?;
built.tx_hash.0 = *tx_id(&bytes)?;
built.tx_bytes.0 = bytes;
Ok(built)
}
}
fn split(text: &str) -> Vec<String> {
let mut lines = Vec::new();
let mut rest = text;
while rest.len() > MAX_LINE_BYTES {
let mut cut = MAX_LINE_BYTES;
while !rest.is_char_boundary(cut) {
cut -= 1;
}
let (line, next) = match rest[..cut].rfind(' ') {
Some(space) if space > 0 => (&rest[..space], &rest[space + 1..]),
_ => (&rest[..cut], &rest[cut..]),
};
lines.push(line.to_string());
rest = next;
}
lines.push(rest.to_string());
lines
}