Skip to main content

sns_sdk/record/
mod.rs

1use crate::{
2    derivation::{derive, get_prefix, trim_tld, Domain, ROOT_DOMAIN_ACCOUNT},
3    error::SnsError,
4};
5use sns_records::state::validation::Validation;
6use solana_program::pubkey;
7use {bech32::u5, solana_program::pubkey::Pubkey};
8pub mod record_v1;
9pub mod record_v2;
10
11pub const CENTRAL_STATE_RECORD_V2: Pubkey = pubkey!("2pMnqHvei2N5oDcVGCRdZx48gqti199wr5CsyTTafsbo");
12
13#[derive(Copy, Clone, Debug)]
14pub enum Record {
15    Ipfs,
16    Arwv,
17    Sol,
18    Eth,
19    Btc,
20    Ltc,
21    Doge,
22    Email,
23    Url,
24    Discord,
25    Github,
26    Reddit,
27    Twitter,
28    Telegram,
29    Pic,
30    Shdw,
31    Point,
32    Bsc,
33    Injective,
34    Backpack,
35    A,
36    AAAA,
37    CNAME,
38    TXT,
39    BASE,
40}
41
42#[derive(Copy, Clone, Debug)]
43pub enum RecordVersion {
44    V1 = 1,
45    V2 = 2,
46}
47
48impl Record {
49    pub fn as_str(&self) -> &'static str {
50        match self {
51            Record::Ipfs => "IPFS",
52            Record::Arwv => "ARWV",
53            Record::Sol => "SOL",
54            Record::Eth => "ETH",
55            Record::Btc => "BTC",
56            Record::Ltc => "LTC",
57            Record::Doge => "DOGE",
58            Record::Email => "email",
59            Record::Url => "url",
60            Record::Discord => "discord",
61            Record::Github => "github",
62            Record::Reddit => "reddit",
63            Record::Twitter => "twitter",
64            Record::Telegram => "telegram",
65            Record::Pic => "pic",
66            Record::Shdw => "SHDW",
67            Record::Point => "POINT",
68            Record::Bsc => "BSC",
69            Record::Injective => "INJ",
70            Record::Backpack => "backpack",
71            Record::A => "A",
72            Record::AAAA => "AAAA",
73            Record::CNAME => "CNAME",
74            Record::TXT => "TXT",
75            Record::BASE => "BASE",
76        }
77    }
78
79    pub fn try_from_str(input: &str) -> Result<Record, SnsError> {
80        match input {
81            "IPFS" => Ok(Record::Ipfs),
82            "ARWV" => Ok(Record::Arwv),
83            "SOL" => Ok(Record::Sol),
84            "ETH" => Ok(Record::Eth),
85            "BTC" => Ok(Record::Btc),
86            "LTC" => Ok(Record::Ltc),
87            "DOGE" => Ok(Record::Doge),
88            "email" => Ok(Record::Email),
89            "url" => Ok(Record::Url),
90            "discord" => Ok(Record::Discord),
91            "github" => Ok(Record::Github),
92            "reddit" => Ok(Record::Reddit),
93            "twitter" => Ok(Record::Twitter),
94            "telegram" => Ok(Record::Telegram),
95            "pic" => Ok(Record::Pic),
96            "SHDW" => Ok(Record::Shdw),
97            "POINT" => Ok(Record::Point),
98            "BSC" => Ok(Record::Bsc),
99            "INJ" => Ok(Record::Injective),
100            "backpack" => Ok(Record::Backpack),
101            "A" => Ok(Record::A),
102            "AAAA" => Ok(Record::AAAA),
103            "CNAME" => Ok(Record::CNAME),
104            "TXT" => Ok(Record::TXT),
105            "BASE" => Ok(Record::BASE),
106            _ => Err(SnsError::UnrecognizedRecord),
107        }
108    }
109
110    pub fn utf8_encoded(&self) -> bool {
111        matches!(
112            self,
113            Record::Ipfs
114                | Record::Arwv
115                | Record::Ltc
116                | Record::Doge
117                | Record::Email
118                | Record::Url
119                | Record::Discord
120                | Record::Github
121                | Record::Reddit
122                | Record::Twitter
123                | Record::Telegram
124                | Record::Pic
125                | Record::Shdw
126                | Record::Point
127                | Record::Backpack
128                | Record::TXT
129                | Record::CNAME
130        )
131    }
132
133    pub fn roa_validation(&self) -> Validation {
134        match self {
135            Record::Sol | Record::CNAME | Record::Url => Validation::Solana,
136            Record::Injective | Record::Eth | Record::Bsc | Record::BASE => Validation::Ethereum,
137            _ => Validation::None,
138        }
139    }
140}
141
142pub fn get_record_class(record_version: RecordVersion) -> Option<Pubkey> {
143    match record_version {
144        RecordVersion::V2 => Some(CENTRAL_STATE_RECORD_V2),
145        _ => None,
146    }
147}
148
149pub fn get_record_key(
150    domain: &str,
151    record: Record,
152    record_version: RecordVersion,
153) -> Result<Pubkey, SnsError> {
154    let domain = trim_tld(domain);
155    let splitted = domain.split('.').collect::<Vec<_>>();
156    match splitted.len() {
157        1 => {
158            let parent = derive(domain, &ROOT_DOMAIN_ACCOUNT, None);
159            let prefix = get_prefix(Domain::Record(record_version));
160            let key = derive(
161                &format!("{}{}", prefix, record.as_str()),
162                &parent,
163                get_record_class(record_version),
164            );
165            Ok(key)
166        }
167        2 => {
168            let parent = derive(splitted[1], &ROOT_DOMAIN_ACCOUNT, None);
169            let sub_domain = get_prefix(Domain::Sub) + splitted[1];
170            let sub_key = derive(&sub_domain, &parent, None);
171
172            let record_prefix = get_prefix(Domain::Record(record_version));
173            let key = derive(
174                &format!("{record_prefix}{}", record.as_str()),
175                &sub_key,
176                get_record_class(record_version),
177            );
178            Ok(key)
179        }
180        _ => Err(SnsError::InvalidDomain),
181    }
182}
183
184pub fn get_record_v2_key(domain: &str, record: Record) -> Result<Pubkey, SnsError> {
185    get_record_key(domain, record, RecordVersion::V2)
186}
187
188pub fn get_record_v1_key(domain: &str, record: Record) -> Result<Pubkey, SnsError> {
189    get_record_key(domain, record, RecordVersion::V1)
190}
191
192pub fn convert_u5_array(u5_data: &[u5]) -> Vec<u8> {
193    let mut u8_data: Vec<u8> = Vec::new();
194    let mut buffer: u16 = 0;
195    let mut buffer_length: u8 = 0;
196    for u5 in u5_data {
197        buffer = (buffer << 5) | (u5.to_u8() as u16);
198        buffer_length += 5;
199        while buffer_length >= 8 {
200            u8_data.push((buffer >> (buffer_length - 8)) as u8);
201            buffer_length -= 8;
202        }
203    }
204    // Make sure there's no remaining data in the buffer
205    if buffer_length > 0 {
206        u8_data.push((buffer << (8 - buffer_length)) as u8);
207    }
208    u8_data
209}
210
211#[cfg(test)]
212mod test {
213    use super::*;
214    use solana_sdk::pubkey;
215
216    #[test]
217    fn test_get_record_key() {
218        let v1 = pubkey!("3RfzNCvEqEKZeohqVN16Z1oi6rw5TrANwqAo4hMx6njv");
219        let v2 = pubkey!("6xdnfxf7URWom6oP7MMS39bFVEMMfufmFvJXFyd2xwoP");
220        let domain = "something.sol";
221        assert_eq!(
222            get_record_key(domain, Record::CNAME, RecordVersion::V1).unwrap(),
223            v1
224        );
225        assert_eq!(
226            get_record_key(domain, Record::CNAME, RecordVersion::V2).unwrap(),
227            v2
228        );
229    }
230}