use bitcoin::hashes::sha256;
use bitcoin::secp256k1::{self, Secp256k1, SecretKey};
use bitcoin::taproot::TaprootBuilder;
use bitcoin::ScriptBuf;
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::bitcoin::error::BitcoinError;
#[derive(Debug)]
pub enum TaprootError {
KeyError(String),
ScriptError(String),
TaprootError(String),
BuilderError(String),
BitcoinError(String),
Secp256k1Error(secp256k1::Error),
HexError(hex::FromHexError),
ValidationError(String),
}
impl fmt::Display for TaprootError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::KeyError(e) => write!(f, "Key error: {e}"),
Self::ScriptError(e) => write!(f, "Script error: {e}"),
Self::TaprootError(e) => write!(f, "Taproot error: {e}"),
Self::BuilderError(e) => write!(f, "Builder error: {e}"),
Self::BitcoinError(e) => write!(f, "Bitcoin error: {e}"),
Self::Secp256k1Error(e) => write!(f, "Secp256k1 error: {e}"),
Self::HexError(e) => write!(f, "Hex error: {e}"),
Self::ValidationError(e) => write!(f, "Validation error: {e}"),
}
}
}
impl std::error::Error for TaprootError {}
impl From<bitcoin::secp256k1::Error> for TaprootError {
fn from(e: bitcoin::secp256k1::Error) -> Self {
Self::Secp256k1Error(e)
}
}
impl From<hex::FromHexError> for TaprootError {
fn from(e: hex::FromHexError) -> Self {
Self::HexError(e)
}
}
impl From<BitcoinError> for TaprootError {
fn from(e: BitcoinError) -> Self {
Self::BitcoinError(e.to_string())
}
}
impl From<bitcoin::taproot::TaprootError> for TaprootError {
fn from(e: bitcoin::taproot::TaprootError) -> Self {
Self::TaprootError(e.to_string())
}
}
impl From<bitcoin::taproot::IncompleteBuilderError> for TaprootError {
fn from(e: bitcoin::taproot::IncompleteBuilderError) -> Self {
Self::BuilderError(e.to_string())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TaprootAsset {
pub asset_id: [u8; 32],
pub name: String,
pub supply: u64,
pub precision: u8,
pub metadata: String,
pub issued: bool,
pub leaves: Vec<ScriptBuf>,
pub num_leaves: u32,
pub issuer_pubkey: [u8; 32],
}
impl TaprootAsset {
pub fn new(
name: &str,
supply: u64,
precision: u8,
metadata: &str,
issuer_secret_key: &[u8],
) -> Result<Self, TaprootError> {
if name.is_empty() || name.len() > 32 {
return Err(crate::bitcoin::taproot::TaprootError::ValidationError(
"Asset name must be 1-32 characters".to_string(),
));
}
if supply == 0 {
return Err(crate::bitcoin::taproot::TaprootError::ValidationError(
"Supply must be greater than 0".to_string(),
));
}
if precision > 8 {
return Err(crate::bitcoin::taproot::TaprootError::ValidationError(
"Precision must be 0-8".to_string(),
));
}
if metadata.len() > 1024 {
return Err(crate::bitcoin::taproot::TaprootError::ValidationError(
"Metadata too large (max 1024 bytes)".to_string(),
));
}
let asset_id = generate_asset_id(name, supply, precision, metadata)?;
let mut builder = bitcoin::blockdata::script::Builder::new();
builder = builder.push_opcode(bitcoin::opcodes::all::OP_RETURN);
builder = push_bytes_to_script(builder, &asset_id);
let leaves = vec![builder.into_script()];
let secp = Secp256k1::new();
let secret_key = SecretKey::from_slice(issuer_secret_key)
.map_err(|e| crate::bitcoin::taproot::TaprootError::KeyError(e.to_string()))?;
let (x_only_pubkey, _) = secret_key.public_key(&secp).x_only_public_key();
Ok(Self {
asset_id,
name: name.to_string(),
supply,
precision,
metadata: metadata.to_string(),
issued: true,
leaves,
num_leaves: 1,
issuer_pubkey: x_only_pubkey.serialize(),
})
}
pub fn create_asset_script(&self) -> Result<ScriptBuf, TaprootError> {
use bitcoin::blockdata::opcodes;
use bitcoin::blockdata::script::Builder;
let mut builder = Builder::new();
builder = builder.push_opcode(opcodes::all::OP_RETURN);
builder = push_bytes_to_script(builder, &self.asset_id);
let name_bytes = self.name.as_bytes();
let name_slice = if name_bytes.len() > 32 {
&name_bytes[..32]
} else {
name_bytes
};
builder = push_bytes_to_script(builder, name_slice);
let supply_bytes = self.supply.to_le_bytes();
builder = push_bytes_to_script(builder, &supply_bytes);
builder = builder.push_int(self.precision as i64);
Ok(builder.into_script())
}
}
pub fn generate_keypair() -> Result<(SecretKey, bitcoin::key::XOnlyPublicKey), TaprootError> {
let secp = Secp256k1::new();
let mut rng = rand::thread_rng();
let (secret_key, _) = secp.generate_keypair(&mut rng);
let x_only = secret_key.x_only_public_key(&secp);
Ok((secret_key, x_only.0))
}
fn generate_asset_id(
name: &str,
supply: u64,
precision: u8,
metadata: &str,
) -> Result<[u8; 32], TaprootError> {
use bitcoin::hashes::{Hash, HashEngine};
let mut engine = sha256::Hash::engine();
engine.input(name.as_bytes());
engine.input(&supply.to_le_bytes());
engine.input(&[precision]);
engine.input(metadata.as_bytes());
let hash = sha256::Hash::from_engine(engine);
Ok(hash.to_byte_array())
}
fn push_bytes_to_script(
mut builder: bitcoin::blockdata::script::Builder,
data: &[u8],
) -> bitcoin::blockdata::script::Builder {
for &byte in data {
builder = builder.push_int(byte as i64);
}
builder
}
pub fn create_asset(
name: &str,
supply: u64,
precision: u8,
metadata: &str,
issuer_secret_key: &[u8],
) -> Result<TaprootAsset, BitcoinError> {
if name.is_empty() || name.len() > 32 {
return Err(BitcoinError::InvalidScript(
"Asset name must be 1-32 characters".to_string(),
));
}
if supply == 0 {
return Err(BitcoinError::InvalidScript(
"Asset supply must be greater than 0".to_string(),
));
}
if precision > 8 {
return Err(BitcoinError::InvalidScript(
"Precision must be between 0 and 8".to_string(),
));
}
if metadata.len() > 1024 {
return Err(BitcoinError::InvalidScript(
"Metadata exceeds maximum length of 1024 bytes".to_string(),
));
}
if issuer_secret_key.len() != 32 {
return Err(BitcoinError::InvalidPrivateKey);
}
TaprootAsset::new(name, supply, precision, metadata, issuer_secret_key)
.map_err(|e| BitcoinError::TaprootError(e.to_string()))
}
pub fn issue_asset(asset: &TaprootAsset, issuer_secret_key: &[u8]) -> Result<String, TaprootError> {
let secp = Secp256k1::new();
let secret_key = secp256k1::SecretKey::from_slice(issuer_secret_key)
.map_err(|e| TaprootError::KeyError(e.to_string()))?;
let (x_only_pubkey, _) = secret_key.public_key(&secp).x_only_public_key();
let asset_script = asset.create_asset_script()?;
let mut builder = TaprootBuilder::new();
builder = builder
.add_leaf(1, asset_script.clone())
.map_err(|e| TaprootError::BuilderError(format!("Failed to add leaf: {e}")))?;
let taproot_spend_info = builder
.finalize(&secp, x_only_pubkey)
.map_err(|e| TaprootError::TaprootError(format!("Failed to finalize builder: {e:?}")))?;
let output_key = taproot_spend_info.output_key();
let output_script = bitcoin::ScriptBuf::new_p2tr(&secp, output_key.into(), None);
let script_bytes = output_script.as_bytes();
if script_bytes.len() != 34 || !script_bytes.starts_with(&[0x51, 0x20]) {
return Err(TaprootError::BuilderError(
"Output script does not match tr(KEY,{SILENT_LEAF}) pattern".to_string(),
));
}
Ok(hex::encode(output_script.as_bytes()))
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::secp256k1::rand::rngs::OsRng;
use bitcoin::secp256k1::Secp256k1;
#[test]
fn test_generate_keypair() {
let result = generate_keypair();
assert!(result.is_ok());
let (secret_key, public_key) = result.unwrap();
let secp = Secp256k1::new();
let derived_pubkey = secret_key.x_only_public_key(&secp).0;
assert_eq!(public_key, derived_pubkey);
}
#[test]
fn test_generate_asset_id() {
let asset_id1 = generate_asset_id("TEST", 1000, 8, "metadata").unwrap();
let asset_id2 = generate_asset_id("TEST", 1000, 8, "metadata").unwrap();
let asset_id3 = generate_asset_id("TEST", 1001, 8, "metadata").unwrap();
assert_eq!(asset_id1, asset_id2);
assert_ne!(asset_id1, asset_id3);
assert_eq!(asset_id1.len(), 32);
}
#[test]
fn test_taproot_asset_creation() {
let secp = Secp256k1::new();
let mut rng = OsRng;
let (secret_key, _) = secp.generate_keypair(&mut rng);
let asset = TaprootAsset::new(
"TESTCOIN",
1000000,
8,
"Test asset metadata",
&secret_key[..],
);
assert!(asset.is_ok());
let asset = asset.unwrap();
assert_eq!(asset.name, "TESTCOIN");
assert_eq!(asset.supply, 1000000);
assert_eq!(asset.precision, 8);
assert_eq!(asset.metadata, "Test asset metadata");
assert!(asset.issued);
assert_eq!(asset.num_leaves, 1);
assert_eq!(asset.leaves.len(), 1);
}
#[test]
fn test_asset_validation() {
let secp = Secp256k1::new();
let mut rng = OsRng;
let (secret_key, _) = secp.generate_keypair(&mut rng);
let result = TaprootAsset::new("", 1000, 8, "metadata", &secret_key[..]);
assert!(result.is_err());
let long_name = "a".repeat(33);
let result = TaprootAsset::new(&long_name, 1000, 8, "metadata", &secret_key[..]);
assert!(result.is_err());
let result = TaprootAsset::new("TEST", 0, 8, "metadata", &secret_key[..]);
assert!(result.is_err());
let result = TaprootAsset::new("TEST", 1000, 9, "metadata", &secret_key[..]);
assert!(result.is_err());
let large_metadata = "a".repeat(1025);
let result = TaprootAsset::new("TEST", 1000, 8, &large_metadata, &secret_key[..]);
assert!(result.is_err());
}
#[test]
fn test_create_asset_script() {
let secp = Secp256k1::new();
let mut rng = OsRng;
let (secret_key, _) = secp.generate_keypair(&mut rng);
let asset = TaprootAsset::new("TEST", 1000, 8, "metadata", &secret_key[..]).unwrap();
let script = asset.create_asset_script();
assert!(script.is_ok());
let script = script.unwrap();
assert!(!script.is_empty());
let script_bytes = script.as_bytes();
assert_eq!(script_bytes[0], 0x6a); }
#[test]
fn test_create_asset_function() {
let secp = Secp256k1::new();
let mut rng = OsRng;
let (secret_key, _) = secp.generate_keypair(&mut rng);
let result = create_asset("TESTCOIN", 1000000, 8, "Test metadata", &secret_key[..]);
assert!(result.is_ok());
let asset = result.unwrap();
assert_eq!(asset.name, "TESTCOIN");
assert_eq!(asset.supply, 1000000);
assert_eq!(asset.precision, 8);
assert_eq!(asset.metadata, "Test metadata");
assert!(asset.issued);
assert_eq!(asset.num_leaves, 1);
assert_eq!(asset.leaves.len(), 1);
}
}