Documentation
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)
    );
  }
}