use core::ops::Index;
use crate::{TokenID, Script, consensus::{serialize, Encodable, Decodable, deserialize_partial}, VarInt};
use super::{opcodes};
pub const PREFIX_BYTE: u8 = opcodes::all::OP_SPECIAL_TOKEN_PREFIX.to_u8();
pub const MAX_CONSENSUS_COMMITMENT_LENGTH: u8 = 40;
#[repr(u8)]
pub enum Structure {
HasAmount = 0x10,
HasNFT = 0x20,
HasCommitmentLength = 0x40,
Reserved = 0x80,
}
#[repr(u8)]
pub enum Capability {
None = 0x00,
Mutable = 0x01,
Minting = 0x02,
}
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(crate = "actual_serde"))]
pub struct OutputData {
pub id: TokenID,
pub bitfield: u8,
pub amount: i64,
pub commitment: Vec<u8>
}
impl OutputData {
pub fn has_commitment_length(&self) -> bool {
(self.bitfield & Structure::HasCommitmentLength as u8) != 0
}
pub fn has_amount(&self) -> bool {
(self.bitfield & Structure::HasAmount as u8) != 0
}
pub fn capability(&self) -> u8 {
self.bitfield & 0x0f
}
pub fn has_nft(&self) -> bool {
(self.bitfield & Structure::HasNFT as u8) != 0
}
pub fn is_minting_nft(&self) -> bool {
return self.has_nft() && ((self.capability() & Capability::Minting as u8) != 0)
}
}
impl Encodable for OutputData {
fn consensus_encode<W: std::io::Write + ?Sized>(&self, writer: &mut W) -> Result<usize, std::io::Error> {
let mut len = 0;
len += self.id.consensus_encode(writer)?;
len += self.bitfield.consensus_encode(writer)?;
if self.has_commitment_length() {
len += self.commitment.consensus_encode(writer)?;
}
if self.has_amount() {
len += VarInt(self.amount as u64).consensus_encode(writer)?;
}
Ok(len)
}
}
impl Decodable for OutputData {
fn consensus_decode<R: std::io::Read + ?Sized>(reader: &mut R) -> Result<Self, crate::consensus::encode::Error> {
let id = TokenID::consensus_decode(reader)?;
let bitfield = u8::consensus_decode(reader)?;
let commitment = if (bitfield & Structure::HasCommitmentLength as u8) != 0 {
Vec::<u8>::consensus_decode(reader)?
} else {
vec![]
};
let amount = if (bitfield & Structure::HasAmount as u8) != 0 {
VarInt::consensus_decode(reader)?
}
else {
VarInt(0)
};
Ok(OutputData {
id,
bitfield,
amount: amount.0 as i64,
commitment,
})
}
}
pub fn wrap_scriptpubkey(scriptpubkey: Script, token_data: &Option<OutputData>) -> Script {
match token_data {
Some(data) => {
let bytes: Vec<u8> = std::iter::once(opcodes::all::OP_SPECIAL_TOKEN_PREFIX.to_u8())
.chain(serialize(data))
.chain(scriptpubkey.into_bytes()).collect();
Script::from(bytes)
}
None => scriptpubkey
}
}
pub fn unwrap_scriptpubkey(scriptpubkey: Script) -> Result<(Script, Option<OutputData>), crate::blockdata::script::Error> {
if scriptpubkey.is_empty() || scriptpubkey.index(0) != &opcodes::all::OP_SPECIAL_TOKEN_PREFIX.to_u8() {
return Ok((scriptpubkey, None))
}
let scriptpubkey = scriptpubkey.into_bytes();
let (output_data, consumed) = match deserialize_partial::<OutputData>(&scriptpubkey[1..]) {
Ok((o, size)) => (o, size),
Err(e) => {
println!("{:?}", e);
return Err(crate::blockdata::script::Error::Other("Failed to parse token output from script."))
}
};
let remaining: Vec<u8> = scriptpubkey[1 + consumed ..].to_vec();
Ok((Script::from(remaining), Some(output_data)))
}
#[cfg(test)]
mod test {
use bitcoin_hashes::hex::{FromHex, ToHex};
use super::*;
#[test]
fn test_vectors() {
let prefix = "efaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa1001".to_string();
let other_payload = "f00d".to_string();
let script = Script::from_hex(&(prefix + &other_payload)).unwrap();
let (unwrapped_script, token_data) = unwrap_scriptpubkey(script).unwrap();
let token_data = token_data.unwrap();
assert_eq!(other_payload, unwrapped_script.to_hex());
assert_eq!(1, token_data.amount);
assert_eq!("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", token_data.id.to_hex());
let prefix = "efbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb7229ccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccffffffffffffffff7f".to_string();
let script = Script::from_hex(&(prefix + &other_payload)).unwrap();
let (unwrapped_script, token_data) = unwrap_scriptpubkey(script).unwrap();
let token_data = token_data.unwrap();
assert_eq!(other_payload, unwrapped_script.to_hex());
assert_eq!(9223372036854775807, token_data.amount);
assert_eq!("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", token_data.id.to_hex());
assert_eq!("cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", token_data.commitment.to_hex());
assert!(token_data.is_minting_nft());
}
}