Skip to main content

tempo_primitives/
address.rs

1use alloy_primitives::{Address, FixedBytes, hex};
2
3/// TIP20 token address prefix (12 bytes)
4/// The full address is: TIP20_TOKEN_PREFIX (12 bytes) || derived_bytes (8 bytes)
5pub const TIP20_TOKEN_PREFIX: [u8; 12] = hex!("20C000000000000000000000");
6
7/// Returns `true` if `addr` has the TIP-20 token prefix.
8///
9/// NOTE: This only checks the prefix, not whether the token was actually created.
10/// Use `TIP20Factory::is_tip20()` for full validation.
11pub fn is_tip20_prefix(addr: Address) -> bool {
12    addr.as_slice().starts_with(&TIP20_TOKEN_PREFIX)
13}
14
15/// 4-byte master identifier derived from the registration hash.
16pub type MasterId = FixedBytes<4>;
17
18/// 6-byte user tag occupying the trailing bytes of a virtual address.
19pub type UserTag = FixedBytes<6>;
20
21/// Extension trait with helper functions for Tempo addresses.
22pub trait TempoAddressExt {
23    /// 12-byte prefix shared by all TIP-20 token addresses.
24    ///
25    /// NOTE: prefix alone does not prove a token exists — use `TIP20Factory::is_tip20()` for that.
26    const TIP20_PREFIX: [u8; 12];
27
28    /// 10-byte magic value occupying bytes `[4:14]` of every [TIP-1022] virtual address.
29    ///
30    /// [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
31    const VIRTUAL_MAGIC: [u8; 10];
32
33    /// Returns `true` if the address has the [TIP-20] token prefix.
34    ///
35    /// NOTE: This only checks the prefix, not whether the token was actually created.
36    /// Use `TIP20Factory::is_tip20()` for full validation.
37    ///
38    /// [TIP-20]: <https://docs.tempo.xyz/protocol/tip20>
39    fn is_tip20(&self) -> bool;
40
41    /// Returns `true` if the address matches the [TIP-1022] virtual-address format
42    /// (bytes `[4:14]` == [`Self::VIRTUAL_MAGIC`]).
43    ///
44    /// [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
45    fn is_virtual(&self) -> bool;
46
47    /// Returns `true` if the address is eligible to be a virtual-address master per TIP-1022.
48    fn is_valid_master(&self) -> bool;
49
50    /// Decodes a virtual address into its `(masterId, userTag)` components.
51    ///
52    /// Returns `None` if the address does not match the virtual-address format.
53    fn decode_virtual(&self) -> Option<(MasterId, UserTag)>;
54
55    /// Builds a [TIP-1022] virtual address from a `masterId` and `userTag`.
56    ///
57    /// [TIP-1022]: <https://docs.tempo.xyz/protocol/tip1022>
58    fn new_virtual(master_id: MasterId, user_tag: UserTag) -> Self;
59}
60
61impl TempoAddressExt for Address {
62    const TIP20_PREFIX: [u8; 12] = TIP20_TOKEN_PREFIX;
63    const VIRTUAL_MAGIC: [u8; 10] = [0xFD; 10];
64
65    fn is_tip20(&self) -> bool {
66        is_tip20_prefix(*self)
67    }
68
69    fn is_virtual(&self) -> bool {
70        self.as_slice()[4..14] == Self::VIRTUAL_MAGIC
71    }
72
73    fn is_valid_master(&self) -> bool {
74        !self.is_zero() && !self.is_virtual() && !self.is_tip20()
75    }
76
77    fn decode_virtual(&self) -> Option<(MasterId, UserTag)> {
78        if !self.is_virtual() {
79            return None;
80        }
81        let bytes = self.as_slice();
82        Some((
83            MasterId::from_slice(&bytes[0..4]),
84            UserTag::from_slice(&bytes[14..20]),
85        ))
86    }
87
88    fn new_virtual(master_id: MasterId, user_tag: UserTag) -> Self {
89        let mut bytes = [0u8; 20];
90        bytes[0..4].copy_from_slice(master_id.as_slice());
91        bytes[4..14].copy_from_slice(&Self::VIRTUAL_MAGIC);
92        bytes[14..20].copy_from_slice(user_tag.as_slice());
93        Self::from(bytes)
94    }
95}
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use alloy_primitives::address;
101
102    #[test]
103    fn is_tip20_prefix_variations() {
104        // address with exact TIP20 prefix
105        let mut bytes = [0u8; 20];
106        bytes[..12].copy_from_slice(&TIP20_TOKEN_PREFIX);
107        let tip20_addr = Address::from(bytes);
108        assert!(is_tip20_prefix(tip20_addr));
109        assert!(tip20_addr.is_tip20());
110
111        // zero address is not TIP20
112        assert!(!Address::ZERO.is_tip20());
113
114        // random address is not TIP20
115        assert!(!address!("0x1111111111111111111111111111111111111111").is_tip20());
116
117        // differs at byte index 1 (0xC0 → 0x00) — not TIP20
118        let mut wrong = [0u8; 20];
119        wrong[0] = TIP20_TOKEN_PREFIX[0];
120        // skip byte 1 (leave as 0 instead of 0xC0)
121        assert!(!is_tip20_prefix(Address::from(wrong)));
122    }
123
124    #[test]
125    fn virtual_address_variations() {
126        let master_id = MasterId::from([0xAA, 0xBB, 0xCC, 0xDD]);
127        let user_tag = UserTag::from([1, 2, 3, 4, 5, 6]);
128
129        // construct → decode roundtrip
130        let vaddr = Address::new_virtual(master_id, user_tag);
131        assert!(vaddr.is_virtual());
132        let (decoded_master, decoded_tag) = vaddr.decode_virtual().unwrap();
133        assert_eq!(decoded_master, master_id);
134        assert_eq!(decoded_tag, user_tag);
135
136        // non-virtual address returns None
137        assert!(Address::ZERO.decode_virtual().is_none());
138        assert!(!Address::ZERO.is_virtual());
139
140        // zero master_id + zero user_tag
141        let zero_vaddr = Address::new_virtual(MasterId::ZERO, UserTag::ZERO);
142        assert!(zero_vaddr.is_virtual());
143        let (m, t) = zero_vaddr.decode_virtual().unwrap();
144        assert_eq!(m, MasterId::ZERO);
145        assert_eq!(t, UserTag::ZERO);
146    }
147
148    #[test]
149    fn is_valid_master_variations() {
150        // regular address is valid master
151        let regular = address!("0x1111111111111111111111111111111111111111");
152        assert!(regular.is_valid_master());
153
154        // zero address is not valid master
155        assert!(!Address::ZERO.is_valid_master());
156
157        // virtual address is not valid master
158        let vaddr = Address::new_virtual(
159            MasterId::from([1, 2, 3, 4]),
160            UserTag::from([5, 6, 7, 8, 9, 10]),
161        );
162        assert!(!vaddr.is_valid_master());
163
164        // TIP20 address is not valid master
165        let mut tip20_bytes = [0u8; 20];
166        tip20_bytes[..12].copy_from_slice(&TIP20_TOKEN_PREFIX);
167        assert!(!Address::from(tip20_bytes).is_valid_master());
168    }
169}