1use crate::asset::AssetId;
7use crate::Error;
8use bitcoin::TxOut;
9
10const ASSET_PACKET_TYPE: u8 = 0x00;
12
13const PRESENCE_ASSET_ID: u8 = 0x01;
15const PRESENCE_CONTROL_ASSET: u8 = 0x02;
16const PRESENCE_METADATA: u8 = 0x04;
17
18#[derive(Clone, Debug)]
22pub struct Packet {
23 pub groups: Vec<AssetGroup>,
24}
25
26impl Packet {
27 pub fn encode(&self) -> Vec<u8> {
29 let mut buf = Vec::new();
30 encode_uvarint(&mut buf, self.groups.len() as u64);
31 for group in &self.groups {
32 group.encode(&mut buf);
33 }
34 buf
35 }
36
37 pub fn to_txout(&self) -> TxOut {
39 crate::extension::packet_txout(ASSET_PACKET_TYPE, &self.encode())
40 }
41}
42
43#[derive(Clone, Debug)]
45pub enum AssetRef {
46 ById(AssetId),
48 ByGroup(u16),
50}
51
52impl AssetRef {
53 fn encode(&self, buf: &mut Vec<u8>) {
54 match self {
55 AssetRef::ById(asset_id) => {
56 buf.push(0x01); asset_id.encode(buf);
58 }
59 AssetRef::ByGroup(gidx) => {
60 buf.push(0x02); buf.extend_from_slice(&gidx.to_le_bytes());
62 }
63 }
64 }
65}
66
67#[derive(Clone, Debug)]
69pub struct AssetGroup {
70 pub asset_id: Option<AssetId>,
73 pub control_asset: Option<AssetRef>,
75 pub metadata: Option<Metadata>,
77 pub inputs: Vec<AssetInput>,
79 pub outputs: Vec<AssetOutput>,
81}
82
83impl AssetGroup {
84 fn encode(&self, buf: &mut Vec<u8>) {
85 let mut presence: u8 = 0;
87 if self.asset_id.is_some() {
88 presence |= PRESENCE_ASSET_ID;
89 }
90 if self.control_asset.is_some() {
91 presence |= PRESENCE_CONTROL_ASSET;
92 }
93 if self.metadata.is_some() {
94 presence |= PRESENCE_METADATA;
95 }
96 buf.push(presence);
97
98 if let Some(asset_id) = &self.asset_id {
100 asset_id.encode(buf);
101 }
102 if let Some(control_asset) = &self.control_asset {
103 control_asset.encode(buf);
104 }
105 if let Some(metadata) = &self.metadata {
106 encode_metadata(buf, metadata);
107 }
108
109 encode_uvarint(buf, self.inputs.len() as u64);
111 for input in &self.inputs {
112 input.encode(buf);
113 }
114
115 encode_uvarint(buf, self.outputs.len() as u64);
117 for output in &self.outputs {
118 output.encode(buf);
119 }
120 }
121}
122
123#[derive(Clone, Debug)]
125pub struct AssetInput {
126 pub input_index: u16,
128 pub amount: u64,
130}
131
132impl AssetInput {
133 fn encode(&self, buf: &mut Vec<u8>) {
134 buf.push(0x01); buf.extend_from_slice(&self.input_index.to_le_bytes());
136 encode_uvarint(buf, self.amount);
137 }
138}
139
140#[derive(Clone, Debug)]
142pub struct AssetOutput {
143 pub output_index: u16,
145 pub amount: u64,
147}
148
149impl AssetOutput {
150 fn encode(&self, buf: &mut Vec<u8>) {
151 buf.push(0x01); buf.extend_from_slice(&self.output_index.to_le_bytes());
153 encode_uvarint(buf, self.amount);
154 }
155}
156
157pub type Metadata = Vec<(String, String)>;
159
160fn encode_metadata(buf: &mut Vec<u8>, metadata: &[(String, String)]) {
162 encode_uvarint(buf, metadata.len() as u64);
163 for (key, value) in metadata {
164 encode_uvarint(buf, key.len() as u64);
165 buf.extend_from_slice(key.as_bytes());
166 encode_uvarint(buf, value.len() as u64);
167 buf.extend_from_slice(value.as_bytes());
168 }
169}
170
171pub fn add_asset_packet_to_psbt(psbt: &mut bitcoin::Psbt, packet: &Packet) -> Result<(), Error> {
176 if packet.groups.is_empty() {
177 return Err(Error::ad_hoc(
178 "asset packet must contain at least one group",
179 ));
180 }
181
182 crate::extension::add_packet_to_psbt(psbt, ASSET_PACKET_TYPE, &packet.encode())
183 .map_err(Error::ad_hoc)?;
184
185 Ok(())
186}
187
188fn encode_uvarint(buf: &mut Vec<u8>, mut value: u64) {
192 loop {
193 let mut byte = (value & 0x7f) as u8;
194 value >>= 7;
195 if value != 0 {
196 byte |= 0x80;
197 }
198 buf.push(byte);
199 if value == 0 {
200 break;
201 }
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208 use bitcoin::hex::DisplayHex;
209
210 #[test]
211 fn test_encode_uvarint() {
212 let mut buf = Vec::new();
213 encode_uvarint(&mut buf, 0);
214 assert_eq!(buf, vec![0x00]);
215
216 buf.clear();
217 encode_uvarint(&mut buf, 127);
218 assert_eq!(buf, vec![0x7f]);
219
220 buf.clear();
221 encode_uvarint(&mut buf, 128);
222 assert_eq!(buf, vec![0x80, 0x01]);
223
224 buf.clear();
225 encode_uvarint(&mut buf, 300);
226 assert_eq!(buf, vec![0xac, 0x02]);
227 }
228
229 #[test]
230 fn test_fresh_issuance_no_control() {
231 let packet = Packet {
233 groups: vec![AssetGroup {
234 asset_id: None,
235 control_asset: None,
236 metadata: None,
237 inputs: vec![],
238 outputs: vec![AssetOutput {
239 output_index: 0,
240 amount: 1000,
241 }],
242 }],
243 };
244
245 let encoded = packet.encode();
246 assert_eq!(encoded[0], 0x01);
248 assert_eq!(encoded[1], 0x00);
250 assert_eq!(encoded[2], 0x00);
252 assert_eq!(encoded[3], 0x01);
254 }
255
256 #[test]
257 fn test_fresh_issuance_with_control_by_group() {
258 let packet = Packet {
260 groups: vec![
261 AssetGroup {
263 asset_id: None,
264 control_asset: None,
265 metadata: None,
266 inputs: vec![],
267 outputs: vec![AssetOutput {
268 output_index: 0,
269 amount: 1,
270 }],
271 },
272 AssetGroup {
274 asset_id: None,
275 control_asset: Some(AssetRef::ByGroup(0)),
276 metadata: None,
277 inputs: vec![],
278 outputs: vec![AssetOutput {
279 output_index: 0,
280 amount: 1000,
281 }],
282 },
283 ],
284 };
285
286 let encoded = packet.encode();
287 assert_eq!(encoded[0], 0x02);
289 }
290
291 #[test]
292 fn test_to_txout() {
293 let packet = Packet {
294 groups: vec![AssetGroup {
295 asset_id: None,
296 control_asset: None,
297 metadata: None,
298 inputs: vec![],
299 outputs: vec![AssetOutput {
300 output_index: 0,
301 amount: 100,
302 }],
303 }],
304 };
305
306 let txout = packet.to_txout();
307 assert_eq!(txout.value, bitcoin::Amount::ZERO);
308
309 let script_bytes = txout.script_pubkey.as_bytes();
311 assert_eq!(script_bytes[0], 0x6a);
312
313 let data_start = 2; assert_eq!(
317 &script_bytes[data_start..data_start + 3],
318 &crate::extension::MAGIC_BYTES
319 );
320 }
321
322 #[test]
323 fn test_asset_id_display_matches_from_str_format() {
324 let asset_id = AssetId {
325 txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
326 .parse()
327 .unwrap(),
328 group_index: 0,
329 };
330
331 let encoded = asset_id.to_string();
332 assert_eq!(
333 encoded,
334 "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0000"
335 );
336 assert_eq!(encoded.parse::<AssetId>().unwrap(), asset_id);
337 }
338
339 #[test]
340 fn test_asset_id_display_matches_from_str_format_for_non_zero_group() {
341 let asset_id = AssetId {
342 txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
343 .parse()
344 .unwrap(),
345 group_index: 1,
346 };
347
348 let encoded = asset_id.to_string();
349 assert_eq!(
350 encoded,
351 "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0100"
352 );
353 assert_eq!(encoded.parse::<AssetId>().unwrap(), asset_id);
354 }
355
356 #[test]
357 fn test_asset_id_binary_encoding_uses_txid_display_byte_order() {
358 let asset_id = AssetId {
359 txid: "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c"
360 .parse()
361 .unwrap(),
362 group_index: 1,
363 };
364
365 let mut buf = Vec::new();
366 asset_id.encode(&mut buf);
367
368 assert_eq!(
369 buf.to_lower_hex_string(),
370 "58534acb681218c0fda8f6b6ae3b4cb5d8897e7c5fcba5792621c368b3db479c0100"
371 );
372 }
373}