fuel_tx/
tx_pointer.rs

1use fuel_types::{
2    BlockHeight,
3    bytes::WORD_SIZE,
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    Rng,
19    distributions::{
20        Distribution,
21        Standard,
22    },
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    #[cfg(feature = "u32-tx-pointer")]
39    tx_index: u32,
40    #[cfg(not(feature = "u32-tx-pointer"))]
41    tx_index: u16,
42}
43
44impl TxPointer {
45    pub const LEN: usize = 2 * WORD_SIZE;
46
47    pub const fn new(
48        block_height: BlockHeight,
49        #[cfg(feature = "u32-tx-pointer")] tx_index: u32,
50        #[cfg(not(feature = "u32-tx-pointer"))] tx_index: u16,
51    ) -> Self {
52        Self {
53            block_height,
54            tx_index,
55        }
56    }
57
58    pub const fn block_height(&self) -> BlockHeight {
59        self.block_height
60    }
61
62    #[cfg(feature = "u32-tx-pointer")]
63    pub const fn tx_index(&self) -> u32 {
64        self.tx_index
65    }
66
67    #[cfg(not(feature = "u32-tx-pointer"))]
68    pub const fn tx_index(&self) -> u16 {
69        self.tx_index
70    }
71}
72
73#[cfg(feature = "random")]
74impl Distribution<TxPointer> for Standard {
75    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> TxPointer {
76        TxPointer::new(rng.r#gen(), rng.r#gen())
77    }
78}
79
80impl fmt::Display for TxPointer {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        fmt::LowerHex::fmt(self, f)
83    }
84}
85
86#[cfg(feature = "u32-tx-pointer")]
87impl fmt::LowerHex for TxPointer {
88    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
89        if self.tx_index > u16::MAX.into() {
90            write!(f, "{:08x}{:08x}", self.block_height, self.tx_index)
91        } else {
92            write!(f, "{:08x}{:04x}", self.block_height, self.tx_index)
93        }
94    }
95}
96
97#[cfg(not(feature = "u32-tx-pointer"))]
98impl fmt::LowerHex for TxPointer {
99    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
100        write!(f, "{:08x}{:04x}", self.block_height, self.tx_index)
101    }
102}
103
104#[cfg(feature = "u32-tx-pointer")]
105impl fmt::UpperHex for TxPointer {
106    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
107        if self.tx_index > u16::MAX.into() {
108            write!(f, "{:08X}{:08X}", self.block_height, self.tx_index)
109        } else {
110            write!(f, "{:08X}{:04X}", self.block_height, self.tx_index)
111        }
112    }
113}
114
115#[cfg(not(feature = "u32-tx-pointer"))]
116impl fmt::UpperHex for TxPointer {
117    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
118        write!(f, "{:08X}{:04X}", self.block_height, self.tx_index)
119    }
120}
121
122impl str::FromStr for TxPointer {
123    type Err = &'static str;
124
125    #[cfg(feature = "u32-tx-pointer")]
126    /// TxPointer is encoded as 16 hex characters:
127    /// - 8 characters for block height
128    /// - 4 or 8 characters for tx index (old version used 4 characters (u16::MAX))
129    fn from_str(s: &str) -> Result<Self, Self::Err> {
130        const ERR: &str = "Invalid encoded byte in TxPointer";
131
132        if (s.len() != 16 && s.len() != 12) || !s.is_char_boundary(8) {
133            return Err(ERR)
134        }
135
136        let (block_height, tx_index) = s.split_at(8);
137
138        let block_height = u32::from_str_radix(block_height, 16).map_err(|_| ERR)?;
139        let tx_index = match tx_index.len() {
140            4 => u16::from_str_radix(tx_index, 16).map_err(|_| ERR)?.into(),
141            8 => u32::from_str_radix(tx_index, 16).map_err(|_| ERR)?,
142            _ => return Err(ERR),
143        };
144
145        Ok(Self::new(block_height.into(), tx_index))
146    }
147
148    #[cfg(not(feature = "u32-tx-pointer"))]
149    /// TxPointer is encoded as 12 hex characters:
150    /// - 8 characters for block height
151    /// - 4 characters for tx index
152    fn from_str(s: &str) -> Result<Self, Self::Err> {
153        const ERR: &str = "Invalid encoded byte in TxPointer";
154
155        if s.len() != 12 || !s.is_char_boundary(8) {
156            return Err(ERR)
157        }
158
159        let (block_height, tx_index) = s.split_at(8);
160
161        let block_height = u32::from_str_radix(block_height, 16).map_err(|_| ERR)?;
162        let tx_index = u16::from_str_radix(tx_index, 16).map_err(|_| ERR)?;
163
164        Ok(Self::new(block_height.into(), tx_index))
165    }
166}
167
168#[cfg(feature = "typescript")]
169pub mod typescript {
170    use super::*;
171
172    use wasm_bindgen::prelude::*;
173
174    use alloc::{
175        format,
176        string::String,
177        vec::Vec,
178    };
179
180    #[wasm_bindgen]
181    impl TxPointer {
182        #[wasm_bindgen(constructor)]
183        pub fn typescript_new(value: &str) -> Result<TxPointer, js_sys::Error> {
184            use core::str::FromStr;
185            TxPointer::from_str(value).map_err(js_sys::Error::new)
186        }
187
188        #[wasm_bindgen(js_name = toString)]
189        pub fn typescript_to_string(&self) -> String {
190            format!("{:#x}", self)
191        }
192
193        #[wasm_bindgen(js_name = to_bytes)]
194        pub fn typescript_to_bytes(&self) -> Vec<u8> {
195            use fuel_types::canonical::Serialize;
196            <Self as Serialize>::to_bytes(self)
197        }
198
199        #[wasm_bindgen(js_name = from_bytes)]
200        pub fn typescript_from_bytes(value: &[u8]) -> Result<TxPointer, js_sys::Error> {
201            use fuel_types::canonical::Deserialize;
202            <Self as Deserialize>::from_bytes(value)
203                .map_err(|e| js_sys::Error::new(&format!("{:?}", e)))
204        }
205    }
206}
207
208#[cfg(not(feature = "u32-tx-pointer"))]
209#[test]
210fn fmt_encode_decode() {
211    use core::str::FromStr;
212
213    let cases = vec![(83473, 3829)];
214
215    for (block_height, tx_index) in cases {
216        let tx_pointer = TxPointer::new(block_height.into(), tx_index);
217
218        let lower = format!("{tx_pointer:x}");
219        let upper = format!("{tx_pointer:X}");
220
221        assert_eq!(lower, format!("{block_height:08x}{tx_index:04x}"));
222        assert_eq!(upper, format!("{block_height:08X}{tx_index:04X}"));
223
224        let x = TxPointer::from_str(&lower).expect("failed to decode from str");
225        assert_eq!(tx_pointer, x);
226
227        let x = TxPointer::from_str(&upper).expect("failed to decode from str");
228        assert_eq!(tx_pointer, x);
229
230        let bytes = tx_pointer.clone().to_bytes();
231        let tx_pointer_p = TxPointer::from_bytes(&bytes).expect("failed to deserialize");
232
233        assert_eq!(tx_pointer, tx_pointer_p);
234    }
235}
236
237#[cfg(feature = "u32-tx-pointer")]
238#[test]
239fn fmt_encode_decode_u32() {
240    use core::str::FromStr;
241
242    let cases = vec![(83473, u32::from(u16::MAX) + 478930)];
243
244    for (block_height, tx_index) in cases {
245        let tx_pointer = TxPointer::new(block_height.into(), tx_index);
246
247        let lower = format!("{tx_pointer:x}");
248        let upper = format!("{tx_pointer:X}");
249
250        assert_eq!(lower, format!("{block_height:08x}{tx_index:08x}"));
251        assert_eq!(upper, format!("{block_height:08X}{tx_index:08X}"));
252
253        let x = TxPointer::from_str(&lower).expect("failed to decode from str");
254        assert_eq!(tx_pointer, x);
255
256        let x = TxPointer::from_str(&upper).expect("failed to decode from str");
257        assert_eq!(tx_pointer, x);
258
259        let bytes = tx_pointer.clone().to_bytes();
260        let tx_pointer_p = TxPointer::from_bytes(&bytes).expect("failed to deserialize");
261
262        assert_eq!(tx_pointer, tx_pointer_p);
263    }
264}
265
266#[cfg(feature = "u32-tx-pointer")]
267#[test]
268fn fmt_backward_compatibility_u32() {
269    use core::str::FromStr;
270
271    let cases = vec![(83473, 647)];
272
273    for (block_height, tx_index) in cases {
274        let tx_pointer = TxPointer::new(block_height.into(), tx_index);
275
276        let lower = format!("{tx_pointer:x}");
277        let upper = format!("{tx_pointer:X}");
278
279        assert_eq!(lower, format!("{block_height:08x}{tx_index:04x}"));
280        assert_eq!(upper, format!("{block_height:08X}{tx_index:04X}"));
281
282        let x = TxPointer::from_str(&lower).expect("failed to decode from str");
283        assert_eq!(tx_pointer, x);
284
285        let x = TxPointer::from_str(&upper).expect("failed to decode from str");
286        assert_eq!(tx_pointer, x);
287
288        let bytes = tx_pointer.clone().to_bytes();
289        let tx_pointer_p = TxPointer::from_bytes(&bytes).expect("failed to deserialize");
290
291        assert_eq!(tx_pointer, tx_pointer_p);
292    }
293}
294
295/// See https://github.com/FuelLabs/fuel-vm/issues/521
296#[test]
297fn decode_bug() {
298    use core::str::FromStr;
299    TxPointer::from_str("00000😎000").expect_err("Should fail on incorrect input");
300}