Skip to main content

brc20_prog/api/
types.rs

1use std::collections::HashMap;
2use std::error::Error;
3
4use alloy::primitives::hex::FromHex;
5use alloy::primitives::{Bytes, B256};
6use base64::prelude::BASE64_STANDARD_NO_PAD;
7use base64::Engine;
8use serde::{Deserialize, Deserializer, Serialize, Serializer};
9use serde_either::SingleOrVec;
10
11use crate::global::CALLDATA_LIMIT;
12use crate::types::{AddressED, B256ED};
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
15/// Represents a call to a contract with optional parameters for from, to, data, and input.
16pub struct EthCall {
17    /// The address of the sender
18    pub from: Option<AddressED>,
19    /// The address of the contract to call
20    pub to: Option<AddressED>,
21    /// The data to send with the call
22    #[serde(alias = "input", alias = "data")]
23    pub data: Option<RawBytes>,
24}
25
26impl EthCall {
27    /// Creates a new EthCall with the given parameters
28    pub fn new(from: Option<AddressED>, to: Option<AddressED>, data: RawBytes) -> Self {
29        Self {
30            from,
31            to,
32            data: Some(data),
33        }
34    }
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize)]
38/// Represents the data returned from a precompile execution.
39pub struct PrecompileData {
40    /// The transaction ID of the OP_RETURN transaction, a list of B256, one for each transaction.
41    #[serde(rename = "opReturnTxIds")]
42    pub op_return_tx_ids: Vec<B256ED>,
43    /// A mapping of Bitcoin transaction IDs (B256ED) to their raw hex bytes (RawBytes).
44    #[serde(rename = "bitcoinTxHexes")]
45    pub bitcoin_tx_hexes: HashMap<B256ED, RawBytes>,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
49/// Represents a filter for retrieving logs from the blockchain.
50pub struct GetLogsFilter {
51    #[serde(rename = "fromBlock")]
52    /// The block number to start searching from
53    pub from_block: Option<String>,
54    #[serde(rename = "toBlock")]
55    /// The block number to stop searching at
56    pub to_block: Option<String>,
57    /// The address of the contract to filter logs from
58    pub address: Option<AddressED>,
59    /// The topics to filter logs by
60    pub topics: Option<Vec<SingleOrVec<Option<B256ED>>>>,
61}
62
63impl GetLogsFilter {
64    // This is used by the server, so doesn't need to be public
65    pub(crate) fn topics_as_b256(&self) -> Option<Vec<SingleOrVec<Option<B256>>>> {
66        self.topics.as_ref().map(|topics| {
67            topics
68                .iter()
69                .map(|topic| match topic {
70                    SingleOrVec::Single(t) => SingleOrVec::Single(t.clone().map(|t| t.bytes)),
71                    SingleOrVec::Vec(ts) => SingleOrVec::Vec(
72                        ts.iter()
73                            .map(|t| t.clone().map(|t| t.bytes))
74                            .collect::<Vec<Option<B256>>>(),
75                    ),
76                })
77                .collect()
78        })
79    }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83/// A wrapper for base64 encoded bytes that can be serialized and deserialized.
84/// This struct is used to handle the encoding and decoding of bytes in the BRC20 protocol.
85///
86/// This struct supports compression and base64 encoding of the bytes.
87pub struct Base64Bytes(Option<String>);
88
89impl Base64Bytes {
90    /// Creates a new InscriptionBytes instance with the given inner string.
91    pub fn new(inner: String) -> Self {
92        Self(Some(inner))
93    }
94
95    /// Returns the inner string if it exists, otherwise returns empty string.
96    pub fn to_string(&self) -> String {
97        self.0.clone().unwrap_or_default()
98    }
99
100    /// Creates a new InscriptionBytes from the given bytes and the block height.
101    ///
102    /// The bytes are encoded using either nada or zstd compression.
103    /// The resulting string is base64 encoded and can be used for the data field in brc20 indexer methods.
104    pub fn from_bytes(bytes: Bytes) -> Result<Self, Box<dyn Error>> {
105        let mut data = vec![];
106        let nada_encoded = nada::encode(bytes.clone());
107        let mut zstd_compressed = vec![0; CALLDATA_LIMIT];
108        let zstd_length =
109            zstd_safe::compress(zstd_compressed.as_mut_slice(), bytes.iter().as_slice(), 22)
110                .map_err(|e| format!("Failed to compress with zstd: {}", e))?;
111        // Pick compression method with the shortest length
112        // 0x00 = uncompressed
113        // 0x01 = nada
114        // 0x02 = zstd
115        if bytes.len() < nada_encoded.len() && bytes.len() < zstd_length {
116            data.insert(0, 0x00);
117            data.extend_from_slice(&bytes);
118        } else if nada_encoded.len() < zstd_length {
119            data.insert(0, 0x01);
120            data.extend_from_slice(&nada_encoded);
121        } else {
122            data.insert(0, 0x02);
123            data.extend_from_slice(&zstd_compressed[..zstd_length]);
124        }
125        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(data);
126        Ok(Self(Some(base64_encoded)))
127    }
128
129    /// Creates a new InscriptionBytes instance with no inner string (None).
130    pub fn empty() -> Self {
131        Self(None)
132    }
133
134    // This is used by the server, so doesn't need to be public
135    #[cfg(feature = "server")]
136    pub(crate) fn value(&self) -> Option<Bytes> {
137        self.0
138            .as_ref()
139            .and_then(|s| decode_bytes_from_inscription_data(s))
140    }
141}
142
143impl Serialize for Base64Bytes {
144    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
145    where
146        S: Serializer,
147    {
148        if let Some(inner) = &self.0 {
149            serializer.serialize_str(inner)
150        } else {
151            serializer.serialize_none()
152        }
153    }
154}
155
156impl<'de> Deserialize<'de> for Base64Bytes {
157    fn deserialize<D>(deserializer: D) -> Result<Base64Bytes, D::Error>
158    where
159        D: Deserializer<'de>,
160    {
161        let Ok(s) = String::deserialize(deserializer) else {
162            return Ok(Base64Bytes::empty());
163        };
164        Ok(Base64Bytes::new(s))
165    }
166}
167
168#[cfg(feature = "server")]
169pub fn select_bytes(
170    raw_bytes: &Option<RawBytes>,
171    base64_bytes: &Option<Base64Bytes>,
172) -> Result<Option<Bytes>, Box<dyn Error>> {
173    match (raw_bytes, base64_bytes) {
174        (Some(raw), None) => Ok(raw.value()),
175        (None, Some(encoded)) => Ok(encoded.value()),
176        (None, None) => Err("Both raw_bytes and base64_bytes are None".into()),
177        (Some(_), Some(_)) => {
178            Err("Both raw_bytes and base64_bytes are provided, only one should be used".into())
179        }
180    }
181}
182
183#[derive(Debug, Clone, PartialEq, Eq)]
184/// A wrapper for raw bytes in their 0x prefixed hex string representation that can be
185/// serialized and deserialized.
186/// This struct is used to handle the encoding and decoding of bytes in the BRC20 protocol.
187///
188/// It can be used to represent both the data and input fields in the EthCall struct.
189/// It can also handle the case where the data is not present (None).
190pub struct RawBytes(Option<String>);
191
192impl RawBytes {
193    /// Creates a new RawBytes instance with the given inner string.
194    pub fn new(inner: String) -> Self {
195        Self(Some(inner))
196    }
197
198    /// Returns the inner string if it exists, otherwise returns empty string.
199    pub fn to_string(&self) -> String {
200        self.0.clone().unwrap_or_default()
201    }
202
203    /// Creates a new EthBytes instance from the given bytes.
204    /// The bytes are converted to a hex string with a 0x prefix.
205    /// This is used for encoding the data and input fields in the EthCall struct.
206    pub fn from_bytes(bytes: Bytes) -> Self {
207        Self(Some(format!("0x{}", hex::encode(bytes))))
208    }
209
210    /// Creates a new EthBytes instance with no inner string (None).
211    pub fn empty() -> Self {
212        Self(None)
213    }
214
215    pub(crate) fn value(&self) -> Option<Bytes> {
216        self.0.as_ref().and_then(|s| Bytes::from_hex(s).ok())
217    }
218}
219
220impl Serialize for RawBytes {
221    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
222    where
223        S: Serializer,
224    {
225        if let Some(inner) = &self.0 {
226            serializer.serialize_str(inner)
227        } else {
228            serializer.serialize_none()
229        }
230    }
231}
232
233impl<'de> Deserialize<'de> for RawBytes {
234    fn deserialize<D>(deserializer: D) -> Result<RawBytes, D::Error>
235    where
236        D: Deserializer<'de>,
237    {
238        let Ok(s) = String::deserialize(deserializer) else {
239            return Ok(RawBytes::empty());
240        };
241        Ok(RawBytes::new(s))
242    }
243}
244
245#[cfg(feature = "server")]
246pub fn decode_bytes_from_inscription_data(mut inscription_data: &str) -> Option<Bytes> {
247    if let Some((base64, _)) = inscription_data.split_once('=') {
248        // Remove any padding '=' characters from the end of the base64 string
249        inscription_data = base64;
250    }
251    let base64_decoded = BASE64_STANDARD_NO_PAD.decode(inscription_data).ok()?;
252    // Use first byte to determine compression method
253    // 0x00 = uncompressed
254    // 0x01 = nada
255    // 0x02 = zstd
256    match base64_decoded[0] {
257        0x00 => {
258            // Uncompressed
259            if base64_decoded.len() > CALLDATA_LIMIT {
260                None
261            } else {
262                Some(Bytes::from(base64_decoded[1..].to_vec()))
263            }
264        }
265        0x01 => {
266            // Nada
267            nada::decode_with_limit(base64_decoded[1..].iter().cloned(), CALLDATA_LIMIT)
268                .ok()
269                .map(Bytes::from)
270        }
271        0x02 => {
272            // Zstd
273            match zstd_safe::get_frame_content_size(&base64_decoded[1..]) {
274                Ok(Some(size)) => {
275                    // Early exit if size is too large
276                    if size > CALLDATA_LIMIT as u64 {
277                        return None;
278                    }
279                }
280                Ok(None) => {
281                    // No size information available, proceed with decompression anyway, it will fail eventually
282                    // This is a valid case, as zstd can be used without size information
283                }
284                Err(_) => {
285                    return None;
286                }
287            }
288            // In a separate method to avoid unnecessary stack allocation for zstd
289            decode_zstd_into_bytes(&base64_decoded[1..])
290        }
291        _ => {
292            // Unknown compression method
293            None
294        }
295    }
296}
297
298#[cfg(feature = "server")]
299fn decode_zstd_into_bytes(data: &[u8]) -> Option<Bytes> {
300    let mut decompressed = vec![0u8; CALLDATA_LIMIT]; // 1MB buffer
301    if let Ok(length) = zstd_safe::decompress(decompressed.as_mut_slice(), &data) {
302        if length > CALLDATA_LIMIT {
303            None
304        } else {
305            let mut decompressed = decompressed.to_vec();
306            decompressed.truncate(length);
307            Some(Bytes::from(decompressed))
308        }
309    } else {
310        None
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use alloy::primitives::U256;
317    use alloy::sol_types::{sol, SolCall};
318    use base64::prelude::BASE64_STANDARD_NO_PAD;
319
320    use super::*;
321
322    sol! {
323        function transfer(address receiver, bytes ticker, uint256 amount);
324    }
325
326    #[test]
327    fn test_calldata_base64_roundtrip() {
328        let address: &str = "0xdead09C7d1621C9D49EdD5c070933b500ac5beef";
329        let ticker = vec![0x6f, 0x72, 0x64, 0x69];
330        let amount = 42;
331        let data = Bytes::from(
332            transferCall::new((
333                address.parse().unwrap(),
334                Bytes::from(ticker),
335                U256::from(amount),
336            ))
337            .abi_encode(),
338        );
339
340        let encoded = Base64Bytes::from_bytes(data.clone()).unwrap();
341
342        let decoded = encoded.value().unwrap();
343
344        assert_eq!(decoded, data);
345    }
346
347    #[test]
348    fn test_decode_bytes_from_base64_data_uncompressed() {
349        // 0x00 to indicate uncompressed
350        let data = vec![0x00, 0xde, 0xad, 0xbe, 0xef, 0xff];
351        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(data);
352        let result = decode_bytes_from_inscription_data(&base64_encoded);
353        assert_eq!(
354            result,
355            Some(Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0xff]))
356        );
357    }
358
359    #[test]
360    fn test_decode_bytes_from_base64_data_extra_equals() {
361        // 0x00 to indicate uncompressed
362        let data = vec![0x00, 0xde, 0xad, 0xbe, 0xef, 0xff];
363        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(data);
364
365        // Add extra '=' to the end to test padding handling
366        let base64_encoded = format!("{}====", base64_encoded);
367        let result = decode_bytes_from_inscription_data(&base64_encoded);
368        assert_eq!(
369            result,
370            Some(Bytes::from(vec![0xde, 0xad, 0xbe, 0xef, 0xff]))
371        );
372    }
373
374    #[test]
375    fn test_decode_bytes_from_base64_data_nada() {
376        let data = vec![
377            0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0xff, 0x00, 0x00, 0x00, 0x00, 0xff, 0xff,
378        ];
379        let mut nada_encoded = nada::encode(data.clone());
380        // Prepend 0x01 to indicate nada compression
381        nada_encoded.insert(0, 0x01);
382        assert_eq!(
383            nada_encoded,
384            vec![0x01, 0xde, 0xad, 0xbe, 0xef, 0x00, 0x00, 0xff, 0x01, 0xff, 0x04, 0xff, 0x02]
385        );
386
387        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(nada_encoded);
388        let result = decode_bytes_from_inscription_data(&base64_encoded);
389        assert_eq!(result, Some(Bytes::from(data)));
390    }
391
392    #[test]
393    fn test_decode_bytes_from_base64_data_repetition_zstd() {
394        // Repeated data is better for zstd compression, this is a test for that
395        let data = vec![0xde, 0xad, 0xbe, 0xef].repeat(4096);
396        let mut compressed = [0u8; 1024 * 1024];
397        let length = zstd_safe::compress(&mut compressed, data.as_slice(), 22).unwrap();
398        // Prepend 0x02 to indicate zstd compression
399        let mut compressed_vec = compressed.to_vec();
400        compressed_vec.truncate(length);
401        compressed_vec.insert(0, 0x02);
402        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(compressed_vec);
403        let result = decode_bytes_from_inscription_data(&base64_encoded);
404        assert_eq!(result, Some(Bytes::from(data)));
405    }
406
407    #[test]
408    fn test_decode_bytes_from_base64_data_random_zstd() {
409        // Generate random data of size 32k, this performs very poorly with zstd, for testing purposes
410        // Real life data will be much smaller and more compressible
411        let mut data = vec![0; 32 * 1024];
412        for i in 0..data.len() {
413            data[i] = rand::random();
414        }
415        let mut compressed = [0u8; 1024 * 1024];
416        let length = zstd_safe::compress(&mut compressed, data.as_slice(), 22).unwrap();
417        // Prepend 0x02 to indicate zstd compression
418        let mut compressed_vec = compressed.to_vec();
419        compressed_vec.truncate(length);
420        compressed_vec.insert(0, 0x02);
421        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(compressed_vec);
422        let result = decode_bytes_from_inscription_data(&base64_encoded);
423        assert_eq!(result, Some(Bytes::from(data)));
424    }
425
426    #[test]
427    fn test_decode_bytes_from_base64_data_huge_zstd() {
428        // Generate repeated data of size 2MB, this should be rejected by the decoder, but also not crash and burn
429        let data = vec![0xde, 0xad, 0xbe, 0xef].repeat(512 * 1024);
430        let mut compressed = [0u8; 1024 * 1024];
431        let length = zstd_safe::compress(&mut compressed, data.as_slice(), 22).unwrap();
432        // Prepend 0x02 to indicate zstd compression
433        let mut compressed_vec = compressed.to_vec();
434        compressed_vec.truncate(length);
435        compressed_vec.insert(0, 0x02);
436        // This is compressed to around 200 bytes
437        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(compressed_vec);
438        let result = decode_bytes_from_inscription_data(&base64_encoded);
439        assert_eq!(result, None);
440    }
441
442    #[test]
443    fn test_invalid_zstd() {
444        let data = vec![0x02, 0xde, 0xad, 0xbe, 0xef];
445        let base64_encoded = BASE64_STANDARD_NO_PAD.encode(data);
446        let result = decode_bytes_from_inscription_data(&base64_encoded);
447        assert_eq!(result, None);
448    }
449
450    #[test]
451    fn test_invalid_base64() {
452        let invalid_base64 = "invalid_base64";
453        let result = decode_bytes_from_inscription_data(&invalid_base64.to_string());
454        assert_eq!(result, None);
455    }
456}