use crate::asset::AssetId;
use crate::Error;
use bitcoin::TxOut;
const ASSET_PACKET_TYPE: u8 = 0x00;
const PRESENCE_ASSET_ID: u8 = 0x01;
const PRESENCE_CONTROL_ASSET: u8 = 0x02;
const PRESENCE_METADATA: u8 = 0x04;
#[derive(Clone, Debug)]
pub struct Packet {
pub groups: Vec<AssetGroup>,
}
impl Packet {
pub fn encode(&self) -> Vec<u8> {
let mut buf = Vec::new();
encode_uvarint(&mut buf, self.groups.len() as u64);
for group in &self.groups {
group.encode(&mut buf);
}
buf
}
pub fn to_txout(&self) -> TxOut {
crate::extension::packet_txout(ASSET_PACKET_TYPE, &self.encode())
}
}
#[derive(Clone, Debug)]
pub enum AssetRef {
ById(AssetId),
ByGroup(u16),
}
impl AssetRef {
fn encode(&self, buf: &mut Vec<u8>) {
match self {
AssetRef::ById(asset_id) => {
buf.push(0x01); asset_id.encode(buf);
}
AssetRef::ByGroup(gidx) => {
buf.push(0x02); buf.extend_from_slice(&gidx.to_le_bytes());
}
}
}
}
#[derive(Clone, Debug)]
pub struct AssetGroup {
pub asset_id: Option<AssetId>,
pub control_asset: Option<AssetRef>,
pub metadata: Option<Metadata>,
pub inputs: Vec<AssetInput>,
pub outputs: Vec<AssetOutput>,
}
impl AssetGroup {
fn encode(&self, buf: &mut Vec<u8>) {
let mut presence: u8 = 0;
if self.asset_id.is_some() {
presence |= PRESENCE_ASSET_ID;
}
if self.control_asset.is_some() {
presence |= PRESENCE_CONTROL_ASSET;
}
if self.metadata.is_some() {
presence |= PRESENCE_METADATA;
}
buf.push(presence);
if let Some(asset_id) = &self.asset_id {
asset_id.encode(buf);
}
if let Some(control_asset) = &self.control_asset {
control_asset.encode(buf);
}
if let Some(metadata) = &self.metadata {
encode_metadata(buf, metadata);
}
encode_uvarint(buf, self.inputs.len() as u64);
for input in &self.inputs {
input.encode(buf);
}
encode_uvarint(buf, self.outputs.len() as u64);
for output in &self.outputs {
output.encode(buf);
}
}
}
#[derive(Clone, Debug)]
pub struct AssetInput {
pub input_index: u16,
pub amount: u64,
}
impl AssetInput {
fn encode(&self, buf: &mut Vec<u8>) {
buf.push(0x01); buf.extend_from_slice(&self.input_index.to_le_bytes());
encode_uvarint(buf, self.amount);
}
}
#[derive(Clone, Debug)]
pub struct AssetOutput {
pub output_index: u16,
pub amount: u64,
}
impl AssetOutput {
fn encode(&self, buf: &mut Vec<u8>) {
buf.push(0x01); buf.extend_from_slice(&self.output_index.to_le_bytes());
encode_uvarint(buf, self.amount);
}
}
pub type Metadata = Vec<(String, String)>;
fn encode_metadata(buf: &mut Vec<u8>, metadata: &[(String, String)]) {
encode_uvarint(buf, metadata.len() as u64);
for (key, value) in metadata {
encode_uvarint(buf, key.len() as u64);
buf.extend_from_slice(key.as_bytes());
encode_uvarint(buf, value.len() as u64);
buf.extend_from_slice(value.as_bytes());
}
}
pub fn add_asset_packet_to_psbt(psbt: &mut bitcoin::Psbt, packet: &Packet) -> Result<(), Error> {
if packet.groups.is_empty() {
return Err(Error::ad_hoc(
"asset packet must contain at least one group",
));
}
crate::extension::add_packet_to_psbt(psbt, ASSET_PACKET_TYPE, &packet.encode())
.map_err(Error::ad_hoc)?;
Ok(())
}
fn encode_uvarint(buf: &mut Vec<u8>, mut value: u64) {
loop {
let mut byte = (value & 0x7f) as u8;
value >>= 7;
if value != 0 {
byte |= 0x80;
}
buf.push(byte);
if value == 0 {
break;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use bitcoin::hex::DisplayHex;
#[test]
fn test_encode_uvarint() {
let mut buf = Vec::new();
encode_uvarint(&mut buf, 0);
assert_eq!(buf, vec![0x00]);
buf.clear();
encode_uvarint(&mut buf, 127);
assert_eq!(buf, vec![0x7f]);
buf.clear();
encode_uvarint(&mut buf, 128);
assert_eq!(buf, vec![0x80, 0x01]);
buf.clear();
encode_uvarint(&mut buf, 300);
assert_eq!(buf, vec![0xac, 0x02]);
}
#[test]
fn test_fresh_issuance_no_control() {
let packet = Packet {
groups: vec![AssetGroup {
asset_id: None,
control_asset: None,
metadata: None,
inputs: vec![],
outputs: vec![AssetOutput {
output_index: 0,
amount: 1000,
}],
}],
};
let encoded = packet.encode();
assert_eq!(encoded[0], 0x01);
assert_eq!(encoded[1], 0x00);
assert_eq!(encoded[2], 0x00);
assert_eq!(encoded[3], 0x01);
}
#[test]
fn test_fresh_issuance_with_control_by_group() {
let packet = Packet {
groups: vec![
AssetGroup {
asset_id: None,
control_asset: None,
metadata: None,
inputs: vec![],
outputs: vec![AssetOutput {
output_index: 0,
amount: 1,
}],
},
AssetGroup {
asset_id: None,
control_asset: Some(AssetRef::ByGroup(0)),
metadata: None,
inputs: vec![],
outputs: vec![AssetOutput {
output_index: 0,
amount: 1000,
}],
},
],
};
let encoded = packet.encode();
assert_eq!(encoded[0], 0x02);
}
#[test]
fn test_to_txout() {
let packet = Packet {
groups: vec![AssetGroup {
asset_id: None,
control_asset: None,
metadata: None,
inputs: vec![],
outputs: vec![AssetOutput {
output_index: 0,
amount: 100,
}],
}],
};
let txout = packet.to_txout();
assert_eq!(txout.value, bitcoin::Amount::ZERO);
let script_bytes = txout.script_pubkey.as_bytes();
assert_eq!(script_bytes[0], 0x6a);
let data_start = 2; assert_eq!(
&script_bytes[data_start..data_start + 3],
&crate::extension::MAGIC_BYTES
);
}
#[test]
fn test_asset_id_display_matches_from_str_format() {
let asset_id = AssetId {
txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
.parse()
.unwrap(),
group_index: 0,
};
let encoded = asset_id.to_string();
assert_eq!(
encoded,
"58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0000"
);
assert_eq!(encoded.parse::<AssetId>().unwrap(), asset_id);
}
#[test]
fn test_asset_id_display_matches_from_str_format_for_non_zero_group() {
let asset_id = AssetId {
txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
.parse()
.unwrap(),
group_index: 1,
};
let encoded = asset_id.to_string();
assert_eq!(
encoded,
"58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0100"
);
assert_eq!(encoded.parse::<AssetId>().unwrap(), asset_id);
}
#[test]
fn test_asset_id_binary_encoding_uses_txid_display_byte_order() {
let asset_id = AssetId {
txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
.parse()
.unwrap(),
group_index: 1,
};
let mut buf = Vec::new();
asset_id.encode(&mut buf);
assert_eq!(
buf.to_lower_hex_string(),
"58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0100"
);
}
}