use {
bitcoin::{
opcodes,
script::{self, Instruction},
Transaction,
},
serde::Serialize,
std::fmt::{self, Display, Formatter},
};
#[derive(Default, Serialize, Debug, PartialEq)]
pub struct Runestone {
edicts: Vec<Edict>,
}
#[derive(Default, Serialize, Debug, PartialEq)]
struct Edict {
id: u128,
amount: u128,
output: u128,
}
#[derive(Debug, PartialEq)]
pub enum Error {
OpReturn,
Script(script::Error),
Opcode(opcodes::All),
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::OpReturn => write!(f, "no OP_RETURN output found"),
Self::Script(err) => write!(f, "failed to parse script: {err}"),
Self::Opcode(op) => write!(f, "non-push opcode in OP_RETURN: {op}"),
}
}
}
impl std::error::Error for Error {}
impl Runestone {
pub fn decipher(transaction: &Transaction) -> Result<Self, Error> {
let payload = Runestone::payload(&transaction)?;
let integers = payload
.chunks_exact(16)
.map(|chunk| u128::from_le_bytes(chunk.try_into().unwrap()))
.collect::<Vec<u128>>();
let transfers = integers
.chunks_exact(4)
.map(|chunk| Edict {
id: chunk[0],
amount: chunk[1],
output: chunk[2],
})
.collect::<Vec<Edict>>();
Ok(Self { edicts: transfers })
}
fn payload(transaction: &Transaction) -> Result<Vec<u8>, Error> {
let output = transaction
.output
.iter()
.find(|output| output.script_pubkey.is_op_return())
.ok_or(Error::OpReturn)?;
let mut payload = Vec::new();
for result in output.script_pubkey.instructions().skip(1) {
match result.map_err(Error::Script)? {
Instruction::PushBytes(push) => payload.extend_from_slice(push.as_bytes()),
Instruction::Op(op) => return Err(Error::Opcode(op)),
}
}
Ok(payload)
}
}
#[cfg(test)]
mod tests {
use {
super::*,
bitcoin::{locktime, TxOut},
};
#[test]
fn deciphering_transaction_with_no_outputs_returns_no_op_return_error() {
assert_eq!(
Runestone::decipher(&Transaction {
input: Vec::new(),
output: Vec::new(),
lock_time: locktime::absolute::LockTime::ZERO,
version: 0,
}),
Err(Error::OpReturn)
);
}
#[test]
fn deciphering_transaction_with_non_op_return_outputs_returns_no_op_return_error() {
assert_eq!(
Runestone::decipher(&Transaction {
input: Vec::new(),
output: vec![TxOut {
script_pubkey: script::Builder::new().push_slice(&[]).into_script(),
value: 0
}],
lock_time: locktime::absolute::LockTime::ZERO,
version: 0,
}),
Err(Error::OpReturn)
);
}
}