diff --git a/chronik/bitcoinsuite-slp/src/alp/consts.rs b/chronik/bitcoinsuite-slp/src/alp/consts.rs index 0c69a47e3..1f92dacd1 100644 --- a/chronik/bitcoinsuite-slp/src/alp/consts.rs +++ b/chronik/bitcoinsuite-slp/src/alp/consts.rs @@ -1,22 +1,22 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for constants used in ALP. -use crate::structs::LokadId; +use crate::lokad_id::LokadId; /// LOKAD ID of the ALP protocol. /// It was originally intended to be called "SLPv2", but later renamed to ALP to /// avoid confusion of SLP token type 2. The LOKAD ID of course could not be /// updated. pub const ALP_LOKAD_ID: LokadId = *b"SLP2"; /// Token type of standard ALP txs. pub const STANDARD_TOKEN_TYPE: u8 = 0; /// Max. number of inputs we can handle per tx (2**15 - 1). /// This is set such that an overflow can't occur when summing this many 48-bit /// numbers. With current consensus rules, no valid tx can have this many /// inputs, but we don't want to depend on this. pub const MAX_TX_INPUTS: usize = 32767; diff --git a/chronik/bitcoinsuite-slp/src/alp/parse.rs b/chronik/bitcoinsuite-slp/src/alp/parse.rs index ebbb36f64..ec9a6a138 100644 --- a/chronik/bitcoinsuite-slp/src/alp/parse.rs +++ b/chronik/bitcoinsuite-slp/src/alp/parse.rs @@ -1,304 +1,305 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. use bitcoinsuite_core::{ bytes::{read_array, read_bytes}, error::DataError, tx::TxId, }; use bytes::Bytes; use thiserror::Error; use crate::{ alp::{ consts::{ALP_LOKAD_ID, STANDARD_TOKEN_TYPE}, ParseError::*, }, consts::{BURN, GENESIS, MINT, SEND}, + lokad_id::LokadId, parsed::{ParsedData, ParsedGenesis, ParsedMintData, ParsedTxType}, slp::consts::SLP_LOKAD_ID, - structs::{Amount, GenesisInfo, LokadId, TokenMeta}, + structs::{Amount, GenesisInfo, TokenMeta}, token_id::TokenId, token_type::{AlpTokenType, TokenType}, }; /// Errors when parsing an ALP tx. #[derive(Clone, Debug, Error, Eq, PartialEq)] pub enum ParseError { /// DataError when trying to parse pushdata, e.g. when bytes run out #[error("Failed parsing pushdata: {0}")] DataError(#[from] DataError), /// Not enough bytes #[error( "Not enough bytes: expected {expected} more bytes but got {actual} \ for field {field_name}" )] NotEnoughBytes { /// Name of the field according to the spec field_name: &'static str, /// List of allowed sizes expected: usize, /// Actual invalid size actual: usize, }, /// Reading a size fell out of the 0-127 range. #[error("Size out of range: {size}, must be 0-127 for field {field_name}")] SizeOutOfRange { /// Name of the field according to the spec field_name: &'static str, /// Out-of-range size size: u8, }, /// Not enough bytes to encode a LOKAD ID #[error("Missing LOKAD ID: {0:?}")] MissingLokadId(Bytes), /// Expected "SLP2" LOKAD ID #[error("Invalid LOKAD ID: {0:?}")] InvalidLokadId(LokadId), /// Used the "SLP\0" prefix, this is almost certainly a mistake, so we /// handle it separately. #[error("Invalid LOKAD ID \"SLP\\0\", did you accidentally use eMPP?")] InvalidSlpLokadId, /// Unknown tx type. /// Note: For known token types, this does not color outputs as "unknown", /// as token types cannot update their coloring rules after they're /// established. #[error("Unknown tx type: {0:?}")] UnknownTxType(Bytes), /// Decimals must be between 0-9. #[error("Decimals out of range: {0}, must be 0-9")] DecimalsOutOfRange(u8), /// Trailing bytes are not allowed. #[error("Leftover bytes: {0:?}")] LeftoverBytes(Bytes), } trait ParsedNamed { fn named(self, field_name: &'static str) -> Result; } impl> ParsedNamed for Result { fn named(self, field_name: &'static str) -> Result { let result = self.map_err(Into::into); if let Err(DataError(DataError::InvalidLength { expected, actual })) = result { return Err(NotEnoughBytes { field_name, expected, actual, }); } if let Err(SizeOutOfRange { field_name: _, size, }) = result { return Err(SizeOutOfRange { field_name, size }); } result } } /// Parse an individual eMPP pushdata as ALP section. /// The txid is used to assign GENESIS token IDs. pub fn parse_section( txid: &TxId, pushdata: Bytes, ) -> Result, ParseError> { match parse_section_with_ignored_err(txid, pushdata) { Ok(parsed) => Ok(Some(parsed)), Err(MissingLokadId(_) | InvalidLokadId(_)) => Ok(None), Err(err) => Err(err), } } /// Parse an individual eMPP pushdata as ALP section, but return an error /// instead of [`None`]` if the script doesn't look like ALP at all. /// /// Exposed for testing. pub fn parse_section_with_ignored_err( txid: &TxId, mut pushdata: Bytes, ) -> Result { if pushdata.len() < LokadId::default().len() { return Err(MissingLokadId(pushdata)); } let lokad_id: LokadId = read_array(&mut pushdata).unwrap(); if lokad_id == SLP_LOKAD_ID { return Err(InvalidSlpLokadId); } if lokad_id != ALP_LOKAD_ID { return Err(InvalidLokadId(lokad_id)); } let token_type = parse_token_type(&mut pushdata)?; if let AlpTokenType::Unknown(_) = token_type { return Ok(ParsedData { meta: TokenMeta { token_id: TokenId::new(TxId::from([0; 32])), token_type: TokenType::Alp(token_type), }, tx_type: ParsedTxType::Unknown, }); } let tx_type = read_var_bytes(&mut pushdata).named("tx_type")?; let parsed = match tx_type.as_ref() { GENESIS => parse_genesis(txid, token_type, &mut pushdata)?, MINT => parse_mint(token_type, &mut pushdata)?, SEND => parse_send(token_type, &mut pushdata)?, BURN => parse_burn(token_type, &mut pushdata)?, _ => return Err(UnknownTxType(tx_type)), }; if !pushdata.is_empty() { return Err(LeftoverBytes(pushdata.split_off(0))); } Ok(parsed) } fn parse_genesis( txid: &TxId, token_type: AlpTokenType, pushdata: &mut Bytes, ) -> Result { let token_ticker = read_var_bytes(pushdata).named("token_ticker")?; let token_name = read_var_bytes(pushdata).named("token_name")?; let url = read_var_bytes(pushdata).named("url")?; let data = read_var_bytes(pushdata).named("data")?; let auth_pubkey = read_var_bytes(pushdata).named("auth_pubkey")?; let decimals = read_byte(pushdata).named("decimals")?; let mint_data = parse_mint_data(pushdata)?; if decimals > 9 { return Err(DecimalsOutOfRange(decimals)); } Ok(ParsedData { meta: TokenMeta { token_id: TokenId::from(*txid), token_type: TokenType::Alp(token_type), }, tx_type: ParsedTxType::Genesis(ParsedGenesis { info: GenesisInfo { token_ticker, token_name, mint_vault_scripthash: None, url, hash: None, data: Some(data), auth_pubkey: Some(auth_pubkey), decimals, }, mint_data, }), }) } fn parse_mint( token_type: AlpTokenType, pushdata: &mut Bytes, ) -> Result { let token_id = read_token_id(pushdata)?; let mint_data = parse_mint_data(pushdata)?; Ok(ParsedData { meta: TokenMeta { token_id, token_type: TokenType::Alp(token_type), }, tx_type: ParsedTxType::Mint(mint_data), }) } fn parse_send( token_type: AlpTokenType, pushdata: &mut Bytes, ) -> Result { let token_id = read_token_id(pushdata)?; let output_amounts = read_amounts(pushdata).named("send_amount")?; Ok(ParsedData { meta: TokenMeta { token_id, token_type: TokenType::Alp(token_type), }, tx_type: ParsedTxType::Send(output_amounts), }) } fn parse_burn( token_type: AlpTokenType, pushdata: &mut Bytes, ) -> Result { let token_id = read_token_id(pushdata)?; let amount = read_amount(pushdata).named("burn_amount")?; Ok(ParsedData { meta: TokenMeta { token_id, token_type: TokenType::Alp(token_type), }, tx_type: ParsedTxType::Burn(amount), }) } fn parse_token_type(pushdata: &mut Bytes) -> Result { let token_type = read_array::<1>(pushdata).named("token_type")?[0]; Ok(match token_type { STANDARD_TOKEN_TYPE => AlpTokenType::Standard, _ => AlpTokenType::Unknown(token_type), }) } fn parse_mint_data(pushdata: &mut Bytes) -> Result { let amounts = read_amounts(pushdata).named("mint_amount")?; let num_batons = read_size(pushdata).named("num_batons")?; Ok(ParsedMintData { amounts, num_batons, }) } fn read_token_id(pushdata: &mut Bytes) -> Result { let token_id: [u8; 32] = read_array(pushdata).named("token_id")?; Ok(TokenId::new(TxId::from(token_id))) } fn read_byte(pushdata: &mut Bytes) -> Result { Ok(read_array::<1>(pushdata)?[0]) } fn read_size(pushdata: &mut Bytes) -> Result { let size = read_byte(pushdata)?; if size > 127 { return Err(SizeOutOfRange { field_name: "", size, }); } Ok(size.into()) } fn read_amount(pushdata: &mut Bytes) -> Result { let amount6: [u8; 6] = read_array(pushdata)?; let mut amount = [0u8; 8]; amount[..6].copy_from_slice(&amount6); Ok(Amount::from_le_bytes(amount)) } fn read_amounts(pushdata: &mut Bytes) -> Result, ParseError> { let size = read_size(pushdata)?; let mut amounts = Vec::with_capacity(size); for _ in 0..size { amounts.push(read_amount(pushdata)?); } Ok(amounts) } fn read_var_bytes(pushdata: &mut Bytes) -> Result { let size = read_size(pushdata)?; Ok(read_bytes(pushdata, size)?) } diff --git a/chronik/bitcoinsuite-slp/src/lib.rs b/chronik/bitcoinsuite-slp/src/lib.rs index 3cdfb24fe..49212af3f 100644 --- a/chronik/bitcoinsuite-slp/src/lib.rs +++ b/chronik/bitcoinsuite-slp/src/lib.rs @@ -1,19 +1,20 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. abc_rust_lint::lint! { pub mod alp; mod burn_summary; pub mod color; pub mod consts; pub mod empp; + pub mod lokad_id; pub mod parsed; pub mod slp; pub mod structs; pub mod test_helpers; pub mod token_id; pub mod token_tx; pub mod token_type; pub mod verify; } diff --git a/chronik/bitcoinsuite-slp/src/lokad_id.rs b/chronik/bitcoinsuite-slp/src/lokad_id.rs new file mode 100644 index 000000000..bafb6ce18 --- /dev/null +++ b/chronik/bitcoinsuite-slp/src/lokad_id.rs @@ -0,0 +1,289 @@ +// Copyright (c) 2023 The Bitcoin developers +// Distributed under the MIT software license, see the accompanying +// file COPYING or http://www.opensource.org/licenses/mit-license.php. + +//! Module for [`LokadId`] and parsing helpers. + +use std::iter::Chain; + +use bitcoinsuite_core::{ + script::{opcode::OP_RETURN, Op, Script}, + tx::{TxInput, TxMut}, +}; +use bytes::Bytes; + +use crate::empp; + +/// A LOKAD ID is a 4 byte prefix identifying an on-chain protocol: +/// "SLP\0" for SLP +/// "SLP2" for ALP +/// ".xec" for alias +pub type LokadId = [u8; 4]; + +/// Parse the script as `OP_RETURN ...`, or [`None`] otherwise. +fn parse_opreturn_lokad_id(script: &Script) -> Option { + let mut ops = script.iter_ops(); + if ops.next()?.ok()? != Op::Code(OP_RETURN) { + return None; + } + parse_lokad_id_pushop_prefix(&ops.next()?.ok()?) +} + +fn parse_lokad_id_pushop_prefix(op: &Op) -> Option { + match op { + Op::Push(_, data) => LokadId::try_from(data.as_ref()).ok(), + _ => None, + } +} + +/// Parse script as `OP_RETURN OP_RESERVED "..." "..." ...` +#[derive(Debug, Default)] +pub struct EmppLokadIdIter { + pushdata_iter: std::vec::IntoIter, +} + +impl EmppLokadIdIter { + /// Create a new [`EmppLokadIdIter`] from a script. + pub fn new(pushdata: Vec) -> Self { + EmppLokadIdIter { + pushdata_iter: pushdata.into_iter(), + } + } +} + +impl Iterator for EmppLokadIdIter { + type Item = LokadId; + + fn next(&mut self) -> Option { + let pushdata = self.pushdata_iter.next()?; + LokadId::try_from(pushdata.get(..4)?).ok() + } +} + +fn parse_input_script_lokad_id(tx_input: &TxInput) -> Option { + let mut ops = tx_input.script.iter_ops(); + parse_lokad_id_pushop_prefix(&ops.next()?.ok()?) +} + +/// Parse the script as ` ...`, or [`None`] otherwise. +#[derive(Debug, Default)] +pub struct TxInputLokadIdIter<'a> { + inputs_iter: std::slice::Iter<'a, TxInput>, +} + +impl<'a> TxInputLokadIdIter<'a> { + /// Create a new [`TxInputLokadIdIter`]. + pub fn new(tx_inputs: &'a [TxInput]) -> Self { + TxInputLokadIdIter { + inputs_iter: tx_inputs.iter(), + } + } +} + +impl Iterator for TxInputLokadIdIter<'_> { + type Item = LokadId; + + fn next(&mut self) -> Option { + parse_input_script_lokad_id(self.inputs_iter.next()?) + } +} + +/// Return type of [`parse_tx_lokad_ids`]. +pub type LokadIdIter<'a> = Chain< + Chain, EmppLokadIdIter>, + TxInputLokadIdIter<'a>, +>; + +/// Parse all the LOKAD IDs of the tx as an iterator. +/// +/// We allow the following patterns for a LOKAD ID: +/// - `OP_RETURN ...` (first output), where the LOKAD ID is a 4-byte +/// pushdata op. This is the only allowed variant in the original spec, and +/// still kind-of the standard. +/// - `OP_RETURN OP_RESERVED "..." "..." ...` (first +/// output), where the OP_RETURN is encoded as eMPP and every pushop is +/// considered prefixed by a 4-byte LOKAD ID. This is new after the +/// introduction of eMPP. +/// - ` ...` (every input), where any input starting with a 4-byte +/// pushop is interpreted as a LOKAD ID. This allows covenants to easily +/// enforce a LOKAD ID by simply doing ` OP_EQUAL` at the end of all +/// the ops. +pub fn parse_tx_lokad_ids(tx: &TxMut) -> LokadIdIter<'_> { + let opreturn_lokad_id = tx + .outputs + .first() + .and_then(|output| parse_opreturn_lokad_id(&output.script)); + let empp_pushdata = tx + .outputs + .first() + .and_then(|output| empp::parse(&output.script).ok()) + .flatten() + .unwrap_or_default(); + opreturn_lokad_id + .into_iter() + .chain(EmppLokadIdIter::new(empp_pushdata)) + .chain(TxInputLokadIdIter::new(&tx.inputs)) +} + +#[cfg(test)] +mod tests { + use bitcoinsuite_core::{ + hash::ShaRmd160, + script::Script, + tx::{TxInput, TxMut, TxOutput}, + }; + use pretty_assertions::assert_eq; + + use crate::{ + empp, + lokad_id::{ + parse_input_script_lokad_id, parse_opreturn_lokad_id, + parse_tx_lokad_ids, EmppLokadIdIter, LokadId, + }, + }; + + #[test] + fn test_parse_lokad_id_opreturn() { + let parse = parse_opreturn_lokad_id; + let script = |script: &'static [u8]| Script::new(script.into()); + assert_eq!(parse(&Script::default()), None); + assert_eq!(parse(&Script::p2pkh(&ShaRmd160::default())), None); + assert_eq!(parse(&Script::p2sh(&ShaRmd160::default())), None); + assert_eq!(parse(&script(b"\0")), None); + assert_eq!(parse(&script(b"\x04")), None); + assert_eq!(parse(&script(b"\x04abcd")), None); + assert_eq!(parse(&script(b"\x06abcdef")), None); + assert_eq!(parse(&script(b"\x6a")), None); + assert_eq!(parse(&script(b"\x6a\0")), None); + assert_eq!(parse(&script(b"\x6a\x01\x01")), None); + assert_eq!(parse(&script(b"\x6a\x04")), None); + assert_eq!(parse(&script(b"\x6a\x04abc")), None); + assert_eq!(parse(&script(b"\x6a\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4c\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4d\x04\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4e\x04\0\0\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x04abcd\x041234")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x4c\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&script(b"\x6a\x05abcde")), None); + assert_eq!(parse(&script(b"\x6a\x06abcdef")), None); + } + + #[test] + fn test_parse_lokad_id_input() { + let parse = parse_input_script_lokad_id; + let input = |script: &'static [u8]| TxInput { + script: Script::new(script.into()), + ..Default::default() + }; + assert_eq!(parse(&input(b"")), None); + assert_eq!(parse(&input(b"\0")), None); + assert_eq!(parse(&input(b"\x04")), None); + assert_eq!(parse(&input(b"\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4c\x04abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4d\x04\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x4e\x04\0\0\0abcd")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x04abcdef")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x04abcd\x041234")), Some(*b"abcd")); + assert_eq!(parse(&input(b"\x06abcdef")), None); + assert_eq!(parse(&input(b"\x6a\x04abcd")), None); + } + + #[test] + fn test_parse_lokad_id_empp() { + let parse = |script| { + EmppLokadIdIter::new( + empp::parse(&script).ok().flatten().unwrap_or_default(), + ) + .collect::>() + }; + let script = |script: &'static [u8]| Script::new(script.into()); + let empty: Vec = vec![]; + assert_eq!(parse(Script::default()), empty); + assert_eq!(parse(Script::p2pkh(&ShaRmd160::default())), empty); + assert_eq!(parse(Script::p2sh(&ShaRmd160::default())), empty); + assert_eq!(parse(script(b"\0")), empty); + assert_eq!(parse(script(b"\x04")), empty); + assert_eq!(parse(script(b"\x04abcd")), empty); + assert_eq!(parse(script(b"\x06abcdef")), empty); + assert_eq!(parse(script(b"\x6a")), empty); + assert_eq!(parse(script(b"\x6a\0")), empty); + assert_eq!(parse(script(b"\x6a\x01\x01")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abc")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x4c\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x4d\x04\0abcd")), vec![*b"abcd"]); + assert_eq!( + parse(script(b"\x6a\x50\x4e\x04\0\0\0abcd")), + vec![*b"abcd"] + ); + assert_eq!(parse(script(b"\x6a\x50\x04abcdef")), empty); + assert_eq!( + parse(script(b"\x6a\x50\x04abcd\x041234")), + vec![*b"abcd", *b"1234"] + ); + assert_eq!(parse(script(b"\x6a\x50\x4c\x04abcdef")), empty); + assert_eq!(parse(script(b"\x6a\x50\x04abcd")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x05abcde")), vec![*b"abcd"]); + assert_eq!(parse(script(b"\x6a\x50\x06abcdef")), vec![*b"abcd"]); + } + + #[test] + fn test_parse_tx_lokad_ids() { + let script = |script: &[u8]| Script::new(script.to_vec().into()); + assert_eq!( + parse_tx_lokad_ids(&TxMut { + version: 1, + inputs: vec![ + TxInput { + script: script(b"\x04abcd"), + ..Default::default() + }, + TxInput { + script: script(b"\x4c\x041234"), + ..Default::default() + }, + TxInput { + // ignored if first pushop is not 4 bytes + script: script( + &[b"\x41".as_ref(), &[0x41; 5], b"\x04xxxx"] + .concat(), + ), + ..Default::default() + }, + ], + outputs: vec![ + TxOutput { + script: script(b"\x6a\x046789\x04yyyy"), + value: 0, + }, + // Ignored: OP_RETURN must be first + TxOutput { + script: script(b"\x6a\x04zzzz"), + value: 0, + }, + ], + locktime: 0, + }) + .collect::>(), + vec![*b"6789", *b"abcd", *b"1234"], + ); + assert_eq!( + parse_tx_lokad_ids(&TxMut { + version: 1, + inputs: vec![TxInput { + script: script(b"\x4d\x04\0efgh"), + ..Default::default() + }], + outputs: vec![TxOutput { + script: script(b"\x6a\x50\x046789\x044321"), + value: 0, + }], + locktime: 0, + }) + .collect::>(), + vec![*b"6789", *b"4321", *b"efgh"], + ); + } +} diff --git a/chronik/bitcoinsuite-slp/src/slp/consts.rs b/chronik/bitcoinsuite-slp/src/slp/consts.rs index be8addca7..702d914ec 100644 --- a/chronik/bitcoinsuite-slp/src/slp/consts.rs +++ b/chronik/bitcoinsuite-slp/src/slp/consts.rs @@ -1,30 +1,30 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for constants used in SLP. -use crate::structs::LokadId; +use crate::lokad_id::LokadId; /// LOKAD ID of SLP is "SLP\0" pub const SLP_LOKAD_ID: LokadId = *b"SLP\0"; /// Token type 1 is identified by [1] pub const TOKEN_TYPE_V1: u8 = 1; /// Token type 2 (Mint Vault) is indentified by [2] pub const TOKEN_TYPE_V2: u8 = 2; /// Token type 1 NFT GROUP is identified by [0x81] pub const TOKEN_TYPE_V1_NFT1_GROUP: u8 = 0x81; /// Token type 1 NFT CHILD is identified by [0x41] pub const TOKEN_TYPE_V1_NFT1_CHILD: u8 = 0x41; /// All token types (as individual bytes) pub static ALL_TOKEN_TYPES: &[u8] = &[ TOKEN_TYPE_V1, TOKEN_TYPE_V2, TOKEN_TYPE_V1_NFT1_GROUP, TOKEN_TYPE_V1_NFT1_CHILD, ]; diff --git a/chronik/bitcoinsuite-slp/src/structs.rs b/chronik/bitcoinsuite-slp/src/structs.rs index 315dec190..be1b64dd3 100644 --- a/chronik/bitcoinsuite-slp/src/structs.rs +++ b/chronik/bitcoinsuite-slp/src/structs.rs @@ -1,181 +1,175 @@ // Copyright (c) 2023 The Bitcoin developers // Distributed under the MIT software license, see the accompanying // file COPYING or http://www.opensource.org/licenses/mit-license.php. //! Module for common structs for SLP and ALP transactions. use bitcoinsuite_core::hash::ShaRmd160; use bytes::Bytes; use serde::{Deserialize, Serialize}; use crate::{token_id::TokenId, token_type::TokenType}; -/// A LOKAD ID is a 4 byte prefix identifying the protocol: -/// "SLP\0" for SLP -/// "SLP2" for ALP -/// ".xec" for alias -pub type LokadId = [u8; 4]; - /// SLP or ALP amount pub type Amount = u64; /// Common token info identifying tokens, which are essential for verification. /// A token ID uniquely determines the protocol and token type, and bundling /// them like this makes mixing protocols or token types more difficult. #[derive( Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, )] pub struct TokenMeta { /// Unique token ID, which is the txid of the GENESIS tx for this token. pub token_id: TokenId, /// Token type within the protocol, defining token rules etc. pub token_type: TokenType, } /// SLP or ALP tx type, indicating what token action to perform #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum TxType { /// Create a new token with its own token ID GENESIS, /// Issue new tokens into existence MINT, /// Transfer tokens SEND, /// Remove tokens from supply BURN, /// Unknown tx type UNKNOWN, } /// "Taint" of a UTXO, e.g a token amount or mint baton #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)] pub enum TokenVariant { /// UTXO has a token amount that can be transferred Amount(Amount), /// UTXO can be used to mint new tokens MintBaton, /// UTXO has a new unknown token type. /// This exists to gracefully introduce new token types, so wallets don't /// accidentally burn them. Unknown(u8), } /// A [`TokenVariant`] which also stores at which index the token metadata is /// stored. Token transactions can involve multiple tokens, and this allows us /// to distinguish them cleanly by referencing a token in a list of tokens. #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] pub struct TokenOutput { /// Index of the token metadata in the tx. pub token_idx: usize, /// Amount of the token, or whether it's a mint baton, or an unknown token. pub variant: TokenVariant, } /// A [`TokenVariant`] which also stores the [`TokenMeta`] of the token. /// This is similar to [`TokenOutput`] but stores the token metadata within, so /// it doesn't have to reference a list of tokens. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Token { /// Which token ID etc. this token has. pub meta: TokenMeta, /// Amount of the token, or whether it's a mint baton, or an unknown token. pub variant: TokenVariant, } /// GENESIS transactions can contain some extra info, some of which is important /// for verification (e.g. `mint_vault_scripthash`), and other which is /// indicating wallets and explorers how to display tokens. #[derive( Clone, Debug, Default, Deserialize, PartialEq, Eq, Hash, Serialize, )] pub struct GenesisInfo { /// Short ticker of the token, like used on exchanges pub token_ticker: Bytes, /// Long name of the token pub token_name: Bytes, /// For SLP Token Type 2 txs; define which script hash input is required /// for MINT txs to be valid. pub mint_vault_scripthash: Option, /// URL for this token, can be used to reference a common document etc. /// On SLP, this is also called "token_document_url". pub url: Bytes, /// For SLP: "token_document_hash", these days mostly unused pub hash: Option<[u8; 32]>, /// For ALP; arbitrary data attached with the token pub data: Option, /// For ALP; public key for signing messages by the original creator pub auth_pubkey: Option, /// How many decimal places to use when displaying the token. /// Token amounts are stored in their "base" form, but should be displayed /// as `base_amount * 10^-decimals`. E.g. a base amount of 12345 and /// decimals of 4 should be displayed as "1.2345". pub decimals: u8, } impl TokenVariant { /// Amount associated with the token variant. pub fn amount(&self) -> Amount { match self { &TokenVariant::Amount(amount) => amount, TokenVariant::MintBaton => 0, TokenVariant::Unknown(_) => 0, } } /// Whether the token variant is a mint baton. pub fn is_mint_baton(&self) -> bool { *self == TokenVariant::MintBaton } } impl std::fmt::Display for Token { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self.variant { TokenVariant::Amount(amount) => write!(f, "{amount}")?, TokenVariant::MintBaton => write!(f, "Mint baton")?, TokenVariant::Unknown(_) => { return write!(f, "{}", self.meta.token_type) } }; write!(f, " of {} ({})", self.meta.token_id, self.meta.token_type) } } impl GenesisInfo { /// Make an empty SLP [`GenesisInfo`]. pub const fn empty_slp() -> GenesisInfo { GenesisInfo { token_ticker: Bytes::new(), token_name: Bytes::new(), mint_vault_scripthash: None, url: Bytes::new(), hash: None, data: None, auth_pubkey: None, decimals: 0, } } /// Make an empty ALP [`GenesisInfo`]. pub const fn empty_alp() -> GenesisInfo { GenesisInfo { token_ticker: Bytes::new(), token_name: Bytes::new(), mint_vault_scripthash: None, url: Bytes::new(), hash: None, data: Some(Bytes::new()), auth_pubkey: Some(Bytes::new()), decimals: 0, } } }