fuel_tx/
tx_pointer.rs

1use fuel_types::{
2    bytes::WORD_SIZE,
3    BlockHeight,
4};
5
6use fuel_types::canonical::{
7    Deserialize,
8    Serialize,
9};
10
11use core::{
12    fmt,
13    str,
14};
15
16#[cfg(feature = "random")]
17use rand::{
18    distributions::{
19        Distribution,
20        Standard,
21    },
22    Rng,
23};
24
25/// Identification of unspend transaction output.
26#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
27#[cfg_attr(feature = "typescript", wasm_bindgen::prelude::wasm_bindgen)]
28#[derive(serde::Serialize, serde::Deserialize)]
29#[cfg_attr(
30    feature = "da-compression",
31    derive(fuel_compression::Compress, fuel_compression::Decompress)
32)]
33#[derive(Deserialize, Serialize)]
34pub struct TxPointer {
35    /// Block height
36    block_height: BlockHeight,
37    /// Transaction index
38    tx_index: u16,
39}
40
41impl TxPointer {
42    pub const LEN: usize = 2 * WORD_SIZE;
43
44    pub const fn new(block_height: BlockHeight, tx_index: u16) -> Self {
45        Self {
46            block_height,
47            tx_index,
48        }
49    }
50
51    pub const fn block_height(&self) -> BlockHeight {
52        self.block_height
53    }
54
55    pub const fn tx_index(&self) -> u16 {
56        self.tx_index
57    }
58}
59
60#[cfg(feature = "random")]
61impl Distribution<TxPointer> for Standard {
62    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TxPointer {
63        TxPointer::new(rng.gen(), rng.gen())
64    }
65}
66
67impl fmt::Display for TxPointer {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        fmt::LowerHex::fmt(self, f)
70    }
71}
72
73impl fmt::LowerHex for TxPointer {
74    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
75        write!(f, "{:08x}{:04x}", self.block_height, self.tx_index)
76    }
77}
78
79impl fmt::UpperHex for TxPointer {
80    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
81        write!(f, "{:08X}{:04X}", self.block_height, self.tx_index)
82    }
83}
84
85impl str::FromStr for TxPointer {
86    type Err = &'static str;
87
88    /// TxPointer is encoded as 12 hex characters:
89    /// - 8 characters for block height
90    /// - 4 characters for tx index
91    fn from_str(s: &str) -> Result<Self, Self::Err> {
92        const ERR: &str = "Invalid encoded byte in TxPointer";
93
94        if s.len() != 12 || !s.is_char_boundary(8) {
95            return Err(ERR)
96        }
97
98        let (block_height, tx_index) = s.split_at(8);
99
100        let block_height = u32::from_str_radix(block_height, 16).map_err(|_| ERR)?;
101        let tx_index = u16::from_str_radix(tx_index, 16).map_err(|_| ERR)?;
102
103        Ok(Self::new(block_height.into(), tx_index))
104    }
105}
106
107#[cfg(feature = "typescript")]
108pub mod typescript {
109    use super::*;
110
111    use wasm_bindgen::prelude::*;
112
113    use alloc::{
114        format,
115        string::String,
116        vec::Vec,
117    };
118
119    #[wasm_bindgen]
120    impl TxPointer {
121        #[wasm_bindgen(constructor)]
122        pub fn typescript_new(value: &str) -> Result<TxPointer, js_sys::Error> {
123            use core::str::FromStr;
124            TxPointer::from_str(value).map_err(js_sys::Error::new)
125        }
126
127        #[wasm_bindgen(js_name = toString)]
128        pub fn typescript_to_string(&self) -> String {
129            format!("{:#x}", self)
130        }
131
132        #[wasm_bindgen(js_name = to_bytes)]
133        pub fn typescript_to_bytes(&self) -> Vec<u8> {
134            use fuel_types::canonical::Serialize;
135            <Self as Serialize>::to_bytes(self)
136        }
137
138        #[wasm_bindgen(js_name = from_bytes)]
139        pub fn typescript_from_bytes(value: &[u8]) -> Result<TxPointer, js_sys::Error> {
140            use fuel_types::canonical::Deserialize;
141            <Self as Deserialize>::from_bytes(value)
142                .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))
143        }
144    }
145}
146
147#[test]
148fn fmt_encode_decode() {
149    use core::str::FromStr;
150
151    let cases = vec![(83473, 3829)];
152
153    for (block_height, tx_index) in cases {
154        let tx_pointer = TxPointer::new(block_height.into(), tx_index);
155
156        let lower = format!("{tx_pointer:x}");
157        let upper = format!("{tx_pointer:X}");
158
159        assert_eq!(lower, format!("{block_height:08x}{tx_index:04x}"));
160        assert_eq!(upper, format!("{block_height:08X}{tx_index:04X}"));
161
162        let x = TxPointer::from_str(&lower).expect("failed to decode from str");
163        assert_eq!(tx_pointer, x);
164
165        let x = TxPointer::from_str(&upper).expect("failed to decode from str");
166        assert_eq!(tx_pointer, x);
167
168        let bytes = tx_pointer.clone().to_bytes();
169        let tx_pointer_p = TxPointer::from_bytes(&bytes).expect("failed to deserialize");
170
171        assert_eq!(tx_pointer, tx_pointer_p);
172    }
173}
174
175/// See https://github.com/FuelLabs/fuel-vm/issues/521
176#[test]
177fn decode_bug() {
178    use core::str::FromStr;
179    TxPointer::from_str("00000😎000").expect_err("Should fail on incorrect input");
180}