kaspa_consensus_core/
network.rs

1//!
2//! # Network Types
3//!
4//! This module implements [`NetworkType`] (such as `mainnet`, `testnet`, `devnet`, and `simnet`)
5//! and [`NetworkId`] that combines a network type with an optional numerical suffix.
6//!
7//! The suffix is used to differentiate between multiple networks of the same type and is used
8//! explicitly with `testnet` networks, allowing declaration of testnet versions such as
9//! `testnet-10`, `testnet-11`, etc.
10//!
11
12#![allow(non_snake_case)]
13
14use borsh::{BorshDeserialize, BorshSerialize};
15use kaspa_addresses::Prefix;
16use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
17use std::fmt::{Debug, Display, Formatter};
18use std::ops::Deref;
19use std::str::FromStr;
20use wasm_bindgen::convert::TryFromJsValue;
21use wasm_bindgen::prelude::*;
22use workflow_wasm::prelude::*;
23
24#[derive(thiserror::Error, PartialEq, Eq, Debug, Clone)]
25pub enum NetworkTypeError {
26    #[error("Invalid network type: {0}")]
27    InvalidNetworkType(String),
28}
29
30/// @category Consensus
31#[derive(Clone, Copy, Debug, Serialize, Deserialize, BorshSerialize, BorshDeserialize, PartialEq, Eq, Hash, Ord, PartialOrd)]
32#[serde(rename_all = "lowercase")]
33#[wasm_bindgen]
34pub enum NetworkType {
35    Mainnet,
36    Testnet,
37    Devnet,
38    Simnet,
39}
40
41impl NetworkType {
42    pub fn default_rpc_port(&self) -> u16 {
43        match self {
44            NetworkType::Mainnet => 16110,
45            NetworkType::Testnet => 16210,
46            NetworkType::Simnet => 16510,
47            NetworkType::Devnet => 16610,
48        }
49    }
50
51    pub fn default_borsh_rpc_port(&self) -> u16 {
52        match self {
53            NetworkType::Mainnet => 17110,
54            NetworkType::Testnet => 17210,
55            NetworkType::Simnet => 17510,
56            NetworkType::Devnet => 17610,
57        }
58    }
59
60    pub fn default_json_rpc_port(&self) -> u16 {
61        match self {
62            NetworkType::Mainnet => 18110,
63            NetworkType::Testnet => 18210,
64            NetworkType::Simnet => 18510,
65            NetworkType::Devnet => 18610,
66        }
67    }
68
69    pub fn iter() -> impl Iterator<Item = Self> {
70        static NETWORK_TYPES: [NetworkType; 4] =
71            [NetworkType::Mainnet, NetworkType::Testnet, NetworkType::Devnet, NetworkType::Simnet];
72        NETWORK_TYPES.iter().copied()
73    }
74}
75
76impl TryFrom<Prefix> for NetworkType {
77    type Error = NetworkTypeError;
78    fn try_from(prefix: Prefix) -> Result<Self, Self::Error> {
79        match prefix {
80            Prefix::Mainnet => Ok(NetworkType::Mainnet),
81            Prefix::Testnet => Ok(NetworkType::Testnet),
82            Prefix::Simnet => Ok(NetworkType::Simnet),
83            Prefix::Devnet => Ok(NetworkType::Devnet),
84            #[allow(unreachable_patterns)]
85            #[cfg(test)]
86            _ => Err(NetworkTypeError::InvalidNetworkType(prefix.to_string())),
87        }
88    }
89}
90
91impl From<NetworkType> for Prefix {
92    fn from(network_type: NetworkType) -> Self {
93        match network_type {
94            NetworkType::Mainnet => Prefix::Mainnet,
95            NetworkType::Testnet => Prefix::Testnet,
96            NetworkType::Devnet => Prefix::Devnet,
97            NetworkType::Simnet => Prefix::Simnet,
98        }
99    }
100}
101
102impl FromStr for NetworkType {
103    type Err = NetworkTypeError;
104    fn from_str(network_type: &str) -> Result<Self, Self::Err> {
105        match network_type.to_lowercase().as_str() {
106            "mainnet" => Ok(NetworkType::Mainnet),
107            "testnet" => Ok(NetworkType::Testnet),
108            "simnet" => Ok(NetworkType::Simnet),
109            "devnet" => Ok(NetworkType::Devnet),
110            _ => Err(NetworkTypeError::InvalidNetworkType(network_type.to_string())),
111        }
112    }
113}
114
115impl Display for NetworkType {
116    #[inline]
117    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
118        let s = match self {
119            NetworkType::Mainnet => "mainnet",
120            NetworkType::Testnet => "testnet",
121            NetworkType::Simnet => "simnet",
122            NetworkType::Devnet => "devnet",
123        };
124        f.write_str(s)
125    }
126}
127
128impl TryFrom<&NetworkTypeT> for NetworkType {
129    type Error = NetworkTypeError;
130    fn try_from(value: &NetworkTypeT) -> Result<Self, Self::Error> {
131        if let Ok(network_id) = NetworkId::try_cast_from(value) {
132            Ok(network_id.network_type())
133        } else if let Some(network_type) = value.as_string() {
134            Self::from_str(&network_type)
135        } else if let Ok(network_type) = NetworkType::try_from_js_value(JsValue::from(value)) {
136            Ok(network_type)
137        } else {
138            Err(NetworkTypeError::InvalidNetworkType(format!("{value:?}")))
139        }
140    }
141}
142
143#[wasm_bindgen]
144extern "C" {
145    #[wasm_bindgen(js_name = "Network", typescript_type = "NetworkType | NetworkId | string")]
146    #[derive(Debug)]
147    pub type NetworkTypeT;
148}
149
150impl TryFrom<&NetworkTypeT> for Prefix {
151    type Error = NetworkIdError;
152    fn try_from(value: &NetworkTypeT) -> Result<Self, Self::Error> {
153        Ok(NetworkType::try_from(value)?.into())
154    }
155}
156
157#[derive(thiserror::Error, Debug, Clone)]
158pub enum NetworkIdError {
159    #[error("Invalid network name prefix: {0}. The expected prefix is 'kaspa'.")]
160    InvalidPrefix(String),
161
162    #[error(transparent)]
163    InvalidNetworkType(#[from] NetworkTypeError),
164
165    #[error("Invalid network suffix: {0}. Only 32 bits unsigned integer (u32) are supported.")]
166    InvalidSuffix(String),
167
168    #[error("Unexpected extra token: {0}.")]
169    UnexpectedExtraToken(String),
170
171    #[error("Missing network suffix: '{0}'")]
172    MissingNetworkSuffix(String),
173
174    #[error("Network suffix required for network type: '{0}'")]
175    NetworkSuffixRequired(String),
176
177    #[error("Invalid network id: '{0}'")]
178    InvalidNetworkId(String),
179
180    #[error(transparent)]
181    Wasm(#[from] workflow_wasm::error::Error),
182}
183
184impl From<NetworkIdError> for JsValue {
185    fn from(err: NetworkIdError) -> Self {
186        JsValue::from_str(&err.to_string())
187    }
188}
189
190///
191/// NetworkId is a unique identifier for a kaspa network instance.
192/// It is composed of a network type and an optional suffix.
193///
194/// @category Consensus
195///
196#[derive(Clone, Copy, Debug, BorshSerialize, BorshDeserialize, PartialEq, Eq, Hash, Ord, PartialOrd, CastFromJs)]
197#[wasm_bindgen(inspectable)]
198pub struct NetworkId {
199    #[wasm_bindgen(js_name = "type")]
200    pub network_type: NetworkType,
201    #[wasm_bindgen(js_name = "suffix")]
202    pub suffix: Option<u32>,
203}
204
205impl NetworkId {
206    pub const fn new(network_type: NetworkType) -> Self {
207        if !matches!(network_type, NetworkType::Mainnet | NetworkType::Devnet | NetworkType::Simnet) {
208            panic!("network suffix required for this network type");
209        }
210
211        Self { network_type, suffix: None }
212    }
213
214    pub fn try_new(network_type: NetworkType) -> Result<Self, NetworkIdError> {
215        if !matches!(network_type, NetworkType::Mainnet | NetworkType::Devnet | NetworkType::Simnet) {
216            return Err(NetworkIdError::NetworkSuffixRequired(network_type.to_string()));
217        }
218
219        Ok(Self { network_type, suffix: None })
220    }
221
222    pub const fn with_suffix(network_type: NetworkType, suffix: u32) -> Self {
223        Self { network_type, suffix: Some(suffix) }
224    }
225
226    pub fn network_type(&self) -> NetworkType {
227        self.network_type
228    }
229
230    pub fn is_mainnet(&self) -> bool {
231        self.network_type == NetworkType::Mainnet
232    }
233
234    pub fn suffix(&self) -> Option<u32> {
235        self.suffix
236    }
237
238    pub fn default_p2p_port(&self) -> u16 {
239        // We define the P2P port on the [`networkId`] type in order to adapt testnet ports according to testnet suffix,
240        // hence avoiding repeatedly failing P2P handshakes between nodes on different networks. RPC does not have
241        // this reasoning so we keep it on the same port in order to simplify RPC client management (hence [`default_rpc_port`]
242        // is defined on the [`NetworkType`] struct
243        match self.network_type {
244            NetworkType::Mainnet => 16111,
245            NetworkType::Testnet => match self.suffix {
246                Some(10) => 16211,
247                Some(11) => 16311,
248                None | Some(_) => 16411,
249            },
250            NetworkType::Simnet => 16511,
251            NetworkType::Devnet => 16611,
252        }
253    }
254
255    pub fn iter() -> impl Iterator<Item = Self> {
256        static NETWORK_IDS: [NetworkId; 5] = [
257            NetworkId::new(NetworkType::Mainnet),
258            NetworkId::with_suffix(NetworkType::Testnet, 10),
259            NetworkId::with_suffix(NetworkType::Testnet, 11),
260            NetworkId::new(NetworkType::Devnet),
261            NetworkId::new(NetworkType::Simnet),
262        ];
263        NETWORK_IDS.iter().copied()
264    }
265
266    /// Returns a textual description of the network prefixed with `kaspa-`
267    pub fn to_prefixed(&self) -> String {
268        format!("kaspa-{}", self)
269    }
270
271    pub fn from_prefixed(prefixed: &str) -> Result<Self, NetworkIdError> {
272        if let Some(stripped) = prefixed.strip_prefix("kaspa-") {
273            Self::from_str(stripped)
274        } else {
275            Err(NetworkIdError::InvalidPrefix(prefixed.to_string()))
276        }
277    }
278}
279
280impl Deref for NetworkId {
281    type Target = NetworkType;
282
283    fn deref(&self) -> &Self::Target {
284        &self.network_type
285    }
286}
287
288impl TryFrom<NetworkType> for NetworkId {
289    type Error = NetworkIdError;
290    fn try_from(value: NetworkType) -> Result<Self, Self::Error> {
291        Self::try_new(value)
292    }
293}
294
295impl From<NetworkId> for Prefix {
296    fn from(net: NetworkId) -> Self {
297        (*net).into()
298    }
299}
300
301impl From<NetworkId> for NetworkType {
302    fn from(net: NetworkId) -> Self {
303        *net
304    }
305}
306
307impl FromStr for NetworkId {
308    type Err = NetworkIdError;
309    fn from_str(network_name: &str) -> Result<Self, Self::Err> {
310        let mut parts = network_name.split('-').fuse();
311        let network_type = NetworkType::from_str(parts.next().unwrap_or_default())?;
312        let suffix = parts.next().map(|x| u32::from_str(x).map_err(|_| NetworkIdError::InvalidSuffix(x.to_string()))).transpose()?;
313        // Disallow testnet network without suffix.
314        // Lack of suffix makes it impossible to distinguish between
315        // multiple testnet networks
316        if !matches!(network_type, NetworkType::Mainnet | NetworkType::Devnet | NetworkType::Simnet) && suffix.is_none() {
317            return Err(NetworkIdError::MissingNetworkSuffix(network_name.to_string()));
318        }
319        match parts.next() {
320            Some(extra_token) => Err(NetworkIdError::UnexpectedExtraToken(extra_token.to_string())),
321            None => Ok(Self { network_type, suffix }),
322        }
323    }
324}
325
326impl Display for NetworkId {
327    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
328        if let Some(suffix) = self.suffix {
329            write!(f, "{}-{}", self.network_type, suffix)
330        } else {
331            write!(f, "{}", self.network_type)
332        }
333    }
334}
335
336impl Serialize for NetworkId {
337    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
338    where
339        S: Serializer,
340    {
341        serializer.serialize_str(&self.to_string())
342    }
343}
344
345struct NetworkIdVisitor;
346
347impl<'de> de::Visitor<'de> for NetworkIdVisitor {
348    type Value = NetworkId;
349
350    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
351        formatter.write_str("a string containing network_type and optional suffix separated by a '-'")
352    }
353
354    fn visit_str<E>(self, value: &str) -> std::result::Result<Self::Value, E>
355    where
356        E: de::Error,
357    {
358        NetworkId::from_str(value).map_err(|err| de::Error::custom(err.to_string()))
359    }
360}
361
362impl<'de> Deserialize<'de> for NetworkId {
363    fn deserialize<D>(deserializer: D) -> Result<NetworkId, D::Error>
364    where
365        D: Deserializer<'de>,
366    {
367        deserializer.deserialize_str(NetworkIdVisitor)
368    }
369}
370
371#[wasm_bindgen]
372impl NetworkId {
373    #[wasm_bindgen(constructor)]
374    pub fn ctor(value: &JsValue) -> Result<NetworkId, NetworkIdError> {
375        Ok(NetworkId::try_cast_from(value)?.into_owned())
376    }
377
378    #[wasm_bindgen(getter, js_name = "id")]
379    pub fn js_id(&self) -> String {
380        self.to_string()
381    }
382
383    #[wasm_bindgen(js_name = "toString")]
384    pub fn js_to_string(&self) -> String {
385        self.to_string()
386    }
387
388    #[wasm_bindgen(js_name = "addressPrefix")]
389    pub fn js_address_prefix(&self) -> String {
390        Prefix::from(self.network_type).to_string()
391    }
392}
393
394#[wasm_bindgen]
395extern "C" {
396    #[wasm_bindgen(typescript_type = "NetworkId | string")]
397    pub type NetworkIdT;
398}
399
400impl TryFrom<&JsValue> for NetworkId {
401    type Error = NetworkIdError;
402    fn try_from(value: &JsValue) -> Result<Self, Self::Error> {
403        Self::try_owned_from(value)
404    }
405}
406
407impl TryFrom<JsValue> for NetworkId {
408    type Error = NetworkIdError;
409    fn try_from(value: JsValue) -> Result<Self, Self::Error> {
410        Self::try_owned_from(value)
411    }
412}
413
414impl TryCastFromJs for NetworkId {
415    type Error = NetworkIdError;
416    fn try_cast_from<'a, R>(value: &'a R) -> Result<Cast<Self>, Self::Error>
417    where
418        R: AsRef<JsValue> + 'a,
419    {
420        Self::resolve(value, || {
421            if let Some(network_id) = value.as_ref().as_string() {
422                Ok(NetworkId::from_str(&network_id)?)
423            } else {
424                Err(NetworkIdError::InvalidNetworkId(format!("{:?}", value.as_ref())))
425            }
426        })
427    }
428}
429
430#[cfg(test)]
431mod tests {
432    use super::*;
433
434    #[test]
435    fn test_network_id_parse_roundtrip() {
436        for nt in NetworkType::iter() {
437            if matches!(nt, NetworkType::Mainnet | NetworkType::Devnet | NetworkType::Simnet) {
438                let ni = NetworkId::try_from(nt).expect("failed to create network id");
439                assert_eq!(nt, *NetworkId::from_str(ni.to_string().as_str()).unwrap());
440                assert_eq!(ni, NetworkId::from_str(ni.to_string().as_str()).unwrap());
441            }
442            let nis = NetworkId::with_suffix(nt, 1);
443            assert_eq!(nt, *NetworkId::from_str(nis.to_string().as_str()).unwrap());
444            assert_eq!(nis, NetworkId::from_str(nis.to_string().as_str()).unwrap());
445
446            assert_eq!(nis, NetworkId::from_str(nis.to_string().as_str()).unwrap());
447        }
448    }
449
450    #[test]
451    fn test_network_id_parse() {
452        struct Test {
453            name: &'static str,
454            expr: &'static str,
455            expected: Result<NetworkId, NetworkIdError>,
456        }
457
458        let tests = vec![
459            Test { name: "Valid mainnet", expr: "mainnet", expected: Ok(NetworkId::new(NetworkType::Mainnet)) },
460            Test { name: "Valid testnet", expr: "testnet-88", expected: Ok(NetworkId::with_suffix(NetworkType::Testnet, 88)) },
461            Test { name: "Missing network", expr: "", expected: Err(NetworkTypeError::InvalidNetworkType("".to_string()).into()) },
462            Test {
463                name: "Invalid network",
464                expr: "gamenet",
465                expected: Err(NetworkTypeError::InvalidNetworkType("gamenet".to_string()).into()),
466            },
467            Test { name: "Invalid suffix", expr: "testnet-x", expected: Err(NetworkIdError::InvalidSuffix("x".to_string())) },
468            Test {
469                name: "Unexpected extra token",
470                expr: "testnet-10-x",
471                expected: Err(NetworkIdError::UnexpectedExtraToken("x".to_string())),
472            },
473        ];
474
475        for test in tests {
476            let Test { name, expr, expected } = test;
477            match NetworkId::from_str(expr) {
478                Ok(nid) => assert_eq!(nid, expected.unwrap(), "{}: unexpected result", name),
479                Err(err) => assert_eq!(err.to_string(), expected.unwrap_err().to_string(), "{}: unexpected error", name),
480            }
481        }
482    }
483}