use std::convert::TryInto;
use std::ops::Index;
use bitcoin_hashes::hex::ToHex;
use bitcoincash::blockdata::script::{read_scriptint, read_uint};
use bitcoincash::Network;
use bitcoincash::{blockdata::opcodes, Script};
use std::fmt::Display;
use crate::utilscript::read_push_from_script;
use anyhow::Result;
use super::cashaddr;
pub const PARENT_GROUP_ID_LENGTH: usize = 32;
const OPRETURN_GROUP_PREFIXES: [usize; 4] = [88888888, 88888890, 88888891, 88888892];
pub const MIN_TICKER_LENGTH: usize = 2;
pub const MAX_TICKER_LENGTH: usize = 8;
pub const MIN_NAME_LENGTH: usize = 2;
pub const MAX_NAME_LENGTH: usize = 25;
pub const MAX_DECIMAL_PLACES: i64 = 18;
#[derive(PartialEq, Eq, Clone, Hash)]
pub struct TokenID {
parent_id: [u8; 32],
subgroup: Vec<u8>,
}
impl TokenID {
pub fn from_vec(data: Vec<u8>) -> Result<Self> {
if data.len() < PARENT_GROUP_ID_LENGTH {
bail!(
"Token ID is too short ({} < {}",
data.len(),
PARENT_GROUP_ID_LENGTH
)
}
let (parent, subgroup) = data.split_at(PARENT_GROUP_ID_LENGTH);
Ok(Self {
parent_id: parent.try_into().unwrap(),
subgroup: subgroup.to_vec(),
})
}
pub fn from_slice(slice: &[u8]) -> Result<Self> {
Self::from_vec(slice.to_vec())
}
pub fn from_hex(hex: &str) -> Result<Self> {
let decoded = hex::decode(hex)?;
Self::from_vec(decoded)
}
pub fn from_inner(parent_id: [u8; 32]) -> Self {
Self {
parent_id,
subgroup: vec![],
}
}
pub fn into_vec(self) -> Vec<u8> {
self.parent_id
.iter()
.copied()
.chain(self.subgroup)
.collect::<Vec<u8>>()
}
pub fn as_vec(&self) -> Vec<u8> {
self.parent_id
.iter()
.copied()
.chain(self.subgroup.clone())
.collect::<Vec<u8>>()
}
pub fn to_hex(&self) -> String {
hex::encode(self.as_vec())
}
pub fn into_parent_and_subgroup(self) -> ([u8; 32], Vec<u8>) {
(self.parent_id, self.subgroup)
}
pub fn from_parent_and_subgroup(
parent_id: [u8; PARENT_GROUP_ID_LENGTH],
subgroup: Vec<u8>,
) -> TokenID {
Self {
parent_id,
subgroup,
}
}
pub fn is_subtoken(&self) -> bool {
!self.subgroup.is_empty()
}
pub fn parent_id(&self) -> [u8; 32] {
self.parent_id
}
pub fn all_zeros() -> TokenID {
TokenID {
parent_id: [0; 32],
subgroup: vec![],
}
}
pub fn as_cashaddr(&self, network: Network) -> String {
cashaddr::encode(
&self.as_vec(),
cashaddr::version_byte_flags::TYPE_GROUP,
network,
)
.expect("failed to encode token id")
}
}
impl Display for TokenID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.to_hex())
}
}
impl std::fmt::Debug for TokenID {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenID")
.field("parent_id", &self.parent_id.to_hex())
.field("subgroup", &self.subgroup.to_hex())
.finish()
}
}
impl serde::Serialize for TokenID {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&self.to_hex())
}
}
#[derive(Default)]
pub struct Token {
pub hash: Option<TokenID>,
pub ticker: Option<String>,
pub name: Option<String>,
pub document_url: Option<String>,
pub document_hash: Option<[u8; 32]>,
pub decimal_places: Option<u8>,
pub op_return_id: Option<u32>,
}
pub fn serialize_amount(amount: i64) -> Vec<u8> {
if amount < 0 {
return bitcoincash::consensus::serialize::<u64>(&(amount as u64));
}
if amount <= u16::MAX as i64 {
return bitcoincash::consensus::serialize::<u16>(&(amount as u16));
}
if amount <= u32::MAX as i64 {
return bitcoincash::consensus::serialize::<u32>(&(amount as u32));
}
bitcoincash::consensus::serialize::<u64>(&(amount as u64))
}
pub fn deserialize_amount(data: &[u8]) -> Result<i64> {
let decoded = match data.len() {
2 => bitcoincash::consensus::deserialize::<u16>(data).map(|x| x as u64),
4 => bitcoincash::consensus::deserialize::<u32>(data).map(|x| x as u64),
8 => bitcoincash::consensus::deserialize::<u64>(data),
_ => bail!("Invalid length {}", data.len()),
};
match decoded {
Err(e) => Err(anyhow::Error::from(e)),
Ok(n) => Ok(n as i64),
}
}
pub fn parse_token_from_scriptpubkey(s: &Script) -> Option<(TokenID, i64)> {
if s.len() < 1 + 20 + 1 + 2 {
return None;
}
if s.index(0) == &opcodes::all::OP_PUSHBYTES_0.to_u8() {
return None;
}
let iter = s.as_bytes().iter();
let (iter, tokenhash) = read_push_from_script(iter).ok()?;
let tokenhash = tokenhash?;
if tokenhash.len() < PARENT_GROUP_ID_LENGTH {
return None;
}
let (_, amount_serialized) = read_push_from_script(iter).ok()?;
let amount_serialized = amount_serialized?;
let amount = deserialize_amount(&amount_serialized).ok()?;
match TokenID::from_vec(tokenhash) {
Ok(hash) => Some((hash, amount)),
_ => None,
}
}
pub fn parse_token_description(script: &Script) -> Result<Token> {
let mut iter = script.as_ref().iter();
if let Some(o) = iter.next() {
if *o != 0x6a
{
bail!("Not a OP_RETURN script")
}
} else {
bail!("Empty script")
}
let (mut iter, prefix) = read_push_from_script(iter)?;
if let Some(ref p) = prefix {
let prefix = match read_uint(p, 4).ok() {
Some(p) => p,
None => return Ok(Token::default()),
};
if !OPRETURN_GROUP_PREFIXES
.iter()
.any(|valid_prefix| valid_prefix == &prefix)
{
return Ok(Token::default());
}
} else {
return Ok(Token::default());
};
let prefix: [u8; 4] = match prefix.unwrap().try_into() {
Ok(p) => p,
Err(_) => {
return Ok(Token::default());
}
};
let prefix: u32 = u32::from_ne_bytes(prefix);
let op_return_id = Some(prefix);
let mut ticker = None;
if prefix != 88888892 {
let tmp;
(iter, tmp) = read_push_from_script(iter)?;
ticker = if let Some(t) = tmp {
if prefix == 88888890 || prefix == 88888891 {
if t.len() < MIN_TICKER_LENGTH || t.len() > MAX_TICKER_LENGTH {
None
} else {
String::from_utf8(t).ok()
}
} else if t.len() <= MAX_TICKER_LENGTH {
String::from_utf8(t).ok()
} else {
None
}
} else {
None
};
}
let mut name = None;
if prefix != 88888892 {
let tmp;
(iter, tmp) = read_push_from_script(iter)?;
name = if let Some(n) = tmp {
if prefix == 88888890 || prefix == 88888891 {
if n.len() < MIN_NAME_LENGTH || n.len() > MAX_NAME_LENGTH {
None
} else {
String::from_utf8(n).ok()
}
} else if prefix == 88888888 {
String::from_utf8(n).ok()
} else {
None
}
} else {
None
};
}
let (iter, document_url) = read_push_from_script(iter)?;
let document_url = if let Some(url) = document_url {
String::from_utf8(url).ok()
} else {
None
};
let (iter, document_hash) = read_push_from_script(iter)?;
let document_hash = document_hash.unwrap_or_default();
let document_hash = if document_hash.len() == 32 {
let mut d: [u8; 32] = document_hash.try_into().unwrap();
d.reverse();
Some(d)
} else {
None
};
let (_iter, decimal_places) = read_push_from_script(iter)?;
let decimal_places = if let Some(d) = decimal_places {
if prefix == 88888892 {
None
}
else if prefix == 88888891 {
Some(0_u8)
} else {
match read_scriptint(&d) {
Ok(d) => {
if (0..=MAX_DECIMAL_PLACES).contains(&d) {
Some(d as u8)
} else {
None
}
}
Err(_) => {
None
}
}
}
} else {
if prefix == 88888892 {
None
} else {
Some(0)
}
};
Ok(Token {
hash: None,
name,
ticker,
document_url,
document_hash,
decimal_places,
op_return_id,
})
}
#[cfg(test)]
mod test {
use std::collections::HashMap;
use super::*;
#[test]
fn test_serialize_amount() {
assert_eq!(-42, deserialize_amount(&serialize_amount(-42)).unwrap());
assert_eq!(
u16::MAX as i64,
deserialize_amount(&serialize_amount(u16::MAX as i64)).unwrap()
);
assert_eq!(
u32::MAX as i64,
deserialize_amount(&serialize_amount(u32::MAX as i64)).unwrap()
);
assert_eq!(
i64::MAX as i64,
deserialize_amount(&serialize_amount(i64::MAX as i64)).unwrap()
);
}
#[test]
fn test_parse_scriptpubkey() {
let hex = "202ab2bcb6b2fb547edcdde5011e947c84b051f9767dc0758157b304ec452000000872790000000000fc511490793b86d7bdf8bd36ec795d0082ee41fbeff7e3";
let script = Script::from(hex::decode(hex).unwrap());
let (hash, amount) = parse_token_from_scriptpubkey(&script).unwrap();
assert_eq!(
hash.to_string(),
"2ab2bcb6b2fb547edcdde5011e947c84b051f9767dc0758157b304ec45200000"
);
assert_eq!(amount, -288230376151680654);
}
#[test]
fn test_parse_opreturn_legacy_token() {
let hex = "6a0438564c0504444f4745084d75636820576f771368747470733a2f2f6578616d706c652e6f726720efbead0b00000000000000000000000000000000000000000000000000000000";
let script = Script::from(hex::decode(hex).unwrap());
let token = parse_token_description(&script).unwrap();
assert_eq!("DOGE", token.ticker.unwrap());
assert_eq!("Much Wow", token.name.unwrap());
assert_eq!("https://example.org", token.document_url.unwrap());
assert_eq!(
hex::decode("000000000000000000000000000000000000000000000000000000000badbeef")
.unwrap(),
token.document_hash.unwrap()
);
assert_eq!(88888888, token.op_return_id.unwrap());
}
#[test]
fn test_parse_opreturn_nrc1_token() {
let hex = "6a043a564c05044e5553440a4e6174697665205553443268747470733a2f2f6e6174697665732e636173682f6170692f737461626c65732f6e7573642f676574546f6b656e496e666f20940106cb0d5a4ef94577ef444f765674008210e2e8f7679bdb4df8fe42cbe61456";
let script = Script::from(hex::decode(hex).unwrap());
let token = parse_token_description(&script).unwrap();
assert_eq!("NUSD", token.ticker.unwrap());
assert_eq!("Native USD", token.name.unwrap());
assert_eq!(
"https://natives.cash/api/stables/nusd/getTokenInfo",
token.document_url.unwrap()
);
assert_eq!(
hex::decode("14e6cb42fef84ddb9b67f7e8e21082007456764f44ef7745f94e5a0dcb060194")
.unwrap(),
token.document_hash.unwrap()
);
assert_eq!(6, token.decimal_places.unwrap());
assert_eq!(88888890, token.op_return_id.unwrap());
}
#[test]
fn test_parse_opreturn_nrc2_nft_collection() {
let hex = "6a043b564c05034c4443174c6567656e64617279204475656c6973742043617264734c5068747470733a2f2f697066732e696f2f697066732f6261666b766d69626c6d326464346d6535797236763566327977787665786d78667667726c63367971357365646d6764746e7535786e6e666663652011a5b4763b6d73183688ec107bb1a2a9e5b24beab558975e7dc49d303e86662b00";
let script = Script::from(hex::decode(hex).unwrap());
let token = parse_token_description(&script).unwrap();
assert_eq!("LDC", token.ticker.unwrap());
assert_eq!("Legendary Duelist Cards", token.name.unwrap());
assert_eq!(
"https://ipfs.io/ipfs/bafkvmiblm2dd4me5yr6v5f2ywxvexmxfvgrlc6yq5sedmgdtnu5xnnffce",
token.document_url.unwrap()
);
assert_eq!(
hex::decode("2b66863e309dc47d5e9758b5ea4bb2e5a9a2b17b10ec883618736d3b76b4a511")
.unwrap(),
token.document_hash.unwrap()
);
assert_eq!(0, token.decimal_places.unwrap());
assert_eq!(88888891, token.op_return_id.unwrap());
}
#[test]
fn test_parse_opreturn_nrc3_nft() {
let hex = "6a043c564c054c5068747470733a2f2f697066732e696f2f697066732f62616679666d69656f696b6e6336776e66636e36767664616e643633366264706f6d666f72657970713561667671626272676a6c3368726c63747120677705dab1ebbd58295991141085bc48eac52601bc43cb9cdf6710e7a4b9ac3c";
let script = Script::from(hex::decode(hex).unwrap());
let token = parse_token_description(&script).unwrap();
assert_eq!(None, token.ticker);
assert_eq!(None, token.name);
assert_eq!(
"https://ipfs.io/ipfs/bafyfmieoiknc6wnfcn6vvdand636bdpomforeypq5afvqbbrgjl3hrlctq",
token.document_url.unwrap()
);
assert_eq!(
hex::decode("3cacb9a4e71067df9ccb43bc0126c5ea48bc85101491592958bdebb1da057767")
.unwrap(),
token.document_hash.unwrap()
);
assert_eq!(None, token.decimal_places);
assert_eq!(88888892, token.op_return_id.unwrap());
}
#[test]
fn test_parse_subgroup_sciptpubkey() {
let hex = "28442dbb752c2fca49d5f6901a14d953e12dd4a84862f556005de47f5d03110000c800000000000000022a0051144d8f8df0ed540551c38cdf8443dcb549a2f93c2b";
let script = Script::from(hex::decode(hex).unwrap());
let (token, amount) = parse_token_from_scriptpubkey(&script).unwrap();
assert_eq!(
"442dbb752c2fca49d5f6901a14d953e12dd4a84862f556005de47f5d03110000c800000000000000",
token.to_hex()
);
assert_eq!(42, amount);
}
#[test]
fn test_parse_opreturn_longname() {
let hex = "6a0438564c050568656c6c6f2261206e616d65206f66206120746f6b656e20776974682061206c6f6e67206e616d650000";
let script = Script::from(hex::decode(hex).unwrap());
let token = parse_token_description(&script).unwrap();
assert_eq!("a name of a token with a long name", token.name.unwrap());
}
#[test]
fn test_tokenid_serialize() {
let dummy_id = TokenID::from_inner([0xAA; PARENT_GROUP_ID_LENGTH]);
assert_eq!(
format!("\"{}\"", dummy_id.to_hex()),
json!(dummy_id).to_string()
)
}
#[test]
fn test_tokenid_serialize_in_map() {
let mut map: HashMap<TokenID, i64> = HashMap::new();
let dummy_id = TokenID::from_inner([0xAA; PARENT_GROUP_ID_LENGTH]);
map.insert(dummy_id.clone(), 42);
assert_eq!(
format!("{{\"{}\":42}}", dummy_id.to_hex()),
json!(map).to_string()
);
}
#[test]
fn test_into_parent_and_subgroup() {
let id = TokenID::from_inner([0xaa; PARENT_GROUP_ID_LENGTH]);
let (parent, sub) = id.into_parent_and_subgroup();
assert_eq!([0xaa; PARENT_GROUP_ID_LENGTH], parent);
assert_eq!(0, sub.len());
let id = TokenID::from_vec(vec![0xaa; 512]).unwrap();
let (parent, sub) = id.into_parent_and_subgroup();
assert_eq!([0xaa; PARENT_GROUP_ID_LENGTH], parent);
assert_eq!(vec![0xaa; 512 - PARENT_GROUP_ID_LENGTH], sub);
}
#[test]
fn test_from_parent_and_subgroup() {
let parent = [0xaa; PARENT_GROUP_ID_LENGTH];
let sub = vec![0xbb; 5];
let id = TokenID::from_parent_and_subgroup(parent, sub.clone());
let expected = parent
.iter()
.chain(sub.iter())
.cloned()
.collect::<Vec<u8>>();
assert_eq!(expected, id.into_vec())
}
#[test]
fn subtoken_tohex() {
let parent = [0xaa; PARENT_GROUP_ID_LENGTH];
let sub = vec![0xbb; 5];
let id = TokenID::from_parent_and_subgroup(parent, sub.clone());
let expected = parent
.iter()
.chain(sub.iter())
.cloned()
.collect::<Vec<u8>>();
assert_eq!(PARENT_GROUP_ID_LENGTH + 5, expected.len());
assert_eq!(hex::encode(expected), id.to_hex())
}
}