pub use v1::*;
mod v1 {
use crate::errors::chain::{Error, NamespaceError, ReferenceError};
use base58::{FromBase58, ToBase58};
use hex_literal::hex;
use core::str;
use parity_scale_codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
use frame_support::{sp_runtime::RuntimeDebug, traits::ConstU32, BoundedVec};
use sp_std::{fmt::Display, vec, vec::Vec};
pub const MINIMUM_CHAIN_ID_LENGTH: usize = MINIMUM_CHAIN_NAMESPACE_LENGTH + 1 + MINIMUM_CHAIN_REFERENCE_LENGTH;
pub const MAXIMUM_CHAIN_ID_LENGTH: usize = MAXIMUM_CHAIN_NAMESPACE_LENGTH + 1 + MAXIMUM_CHAIN_REFERENCE_LENGTH;
pub const MINIMUM_CHAIN_NAMESPACE_LENGTH: usize = 3;
pub const MAXIMUM_CHAIN_NAMESPACE_LENGTH: usize = 8;
const MAXIMUM_CHAIN_NAMESPACE_LENGTH_U32: u32 = MAXIMUM_CHAIN_NAMESPACE_LENGTH as u32;
pub const MINIMUM_CHAIN_REFERENCE_LENGTH: usize = 1;
pub const MAXIMUM_CHAIN_REFERENCE_LENGTH: usize = 32;
const MAXIMUM_CHAIN_REFERENCE_LENGTH_U32: u32 = MAXIMUM_CHAIN_REFERENCE_LENGTH as u32;
const CHAIN_NAMESPACE_REFERENCE_SEPARATOR: u8 = b':';
pub const EIP155_NAMESPACE: &[u8] = b"eip155";
pub const BIP122_NAMESPACE: &[u8] = b"bip122";
pub const DOTSAMA_NAMESPACE: &[u8] = b"polkadot";
pub const SOLANA_NAMESPACE: &[u8] = b"solana";
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub enum ChainId {
Eip155(Eip155Reference),
Bip122(GenesisHexHash32Reference),
Dotsama(GenesisHexHash32Reference),
Solana(GenesisBase58Hash32Reference),
Generic(GenericChainId),
}
impl From<Eip155Reference> for ChainId {
fn from(reference: Eip155Reference) -> Self {
Self::Eip155(reference)
}
}
impl From<GenesisBase58Hash32Reference> for ChainId {
fn from(reference: GenesisBase58Hash32Reference) -> Self {
Self::Solana(reference)
}
}
impl From<GenericChainId> for ChainId {
fn from(chain_id: GenericChainId) -> Self {
Self::Generic(chain_id)
}
}
impl ChainId {
pub fn ethereum_mainnet() -> Self {
Eip155Reference::ethereum_mainnet().into()
}
pub fn moonriver_eth() -> Self {
Eip155Reference::moonriver_eth().into()
}
pub fn moonbeam_eth() -> Self {
Eip155Reference::moonbeam_eth().into()
}
pub fn bitcoin_mainnet() -> Self {
Self::Bip122(GenesisHexHash32Reference::bitcoin_mainnet())
}
pub fn litecoin_mainnet() -> Self {
Self::Bip122(GenesisHexHash32Reference::litecoin_mainnet())
}
pub fn polkadot() -> Self {
Self::Dotsama(GenesisHexHash32Reference::polkadot())
}
pub fn kusama() -> Self {
Self::Dotsama(GenesisHexHash32Reference::kusama())
}
pub fn kilt_spiritnet() -> Self {
Self::Dotsama(GenesisHexHash32Reference::kilt_spiritnet())
}
pub fn solana_mainnet() -> Self {
GenesisBase58Hash32Reference::solana_mainnet().into()
}
}
impl ChainId {
pub fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
let input_length = input.len();
if !(MINIMUM_CHAIN_ID_LENGTH..=MAXIMUM_CHAIN_ID_LENGTH).contains(&input_length) {
log::trace!(
"Length of provided input {} is not included in the inclusive range [{},{}]",
input_length,
MINIMUM_CHAIN_ID_LENGTH,
MAXIMUM_CHAIN_ID_LENGTH
);
return Err(Error::InvalidFormat);
}
let ChainComponents { namespace, reference } = split_components(input);
match (namespace, reference) {
(Some(EIP155_NAMESPACE), Some(eip155_reference)) => {
Eip155Reference::from_utf8_encoded(eip155_reference).map(Self::Eip155)
}
(Some(BIP122_NAMESPACE), Some(bip122_reference)) => {
GenesisHexHash32Reference::from_utf8_encoded(bip122_reference).map(Self::Bip122)
}
(Some(DOTSAMA_NAMESPACE), Some(dotsama_reference)) => {
GenesisHexHash32Reference::from_utf8_encoded(dotsama_reference).map(Self::Dotsama)
}
(Some(SOLANA_NAMESPACE), Some(solana_reference)) => {
GenesisBase58Hash32Reference::from_utf8_encoded(solana_reference).map(Self::Solana)
}
_ => GenericChainId::from_utf8_encoded(input).map(Self::Generic),
}
}
}
impl Display for ChainId {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::Bip122(reference) => {
write!(
f,
"{}",
str::from_utf8(BIP122_NAMESPACE)
.expect("Conversion of Bip122 namespace to string should never fail.")
)?;
write!(f, "{}", char::from(CHAIN_NAMESPACE_REFERENCE_SEPARATOR))?;
reference.fmt(f)?;
}
Self::Eip155(reference) => {
write!(
f,
"{}",
str::from_utf8(EIP155_NAMESPACE)
.expect("Conversion of Eip155 namespace to string should never fail.")
)?;
write!(f, "{}", char::from(CHAIN_NAMESPACE_REFERENCE_SEPARATOR))?;
reference.fmt(f)?;
}
Self::Dotsama(reference) => {
write!(
f,
"{}",
str::from_utf8(DOTSAMA_NAMESPACE)
.expect("Conversion of Dotsama namespace to string should never fail.")
)?;
write!(f, "{}", char::from(CHAIN_NAMESPACE_REFERENCE_SEPARATOR))?;
reference.fmt(f)?;
}
Self::Solana(reference) => {
write!(
f,
"{}",
str::from_utf8(SOLANA_NAMESPACE)
.expect("Conversion of Solana namespace to string should never fail.")
)?;
write!(f, "{}", char::from(CHAIN_NAMESPACE_REFERENCE_SEPARATOR))?;
reference.fmt(f)?;
}
Self::Generic(GenericChainId { namespace, reference }) => {
namespace.fmt(f)?;
write!(f, "{}", char::from(CHAIN_NAMESPACE_REFERENCE_SEPARATOR))?;
reference.fmt(f)?;
}
}
Ok(())
}
}
const fn check_namespace_length_bounds(namespace: &[u8]) -> Result<(), NamespaceError> {
let namespace_length = namespace.len();
if namespace_length < MINIMUM_CHAIN_NAMESPACE_LENGTH {
Err(NamespaceError::TooShort)
} else if namespace_length > MAXIMUM_CHAIN_NAMESPACE_LENGTH {
Err(NamespaceError::TooLong)
} else {
Ok(())
}
}
const fn check_reference_length_bounds(reference: &[u8]) -> Result<(), ReferenceError> {
let reference_length = reference.len();
if reference_length < MINIMUM_CHAIN_REFERENCE_LENGTH {
Err(ReferenceError::TooShort)
} else if reference_length > MAXIMUM_CHAIN_REFERENCE_LENGTH {
Err(ReferenceError::TooLong)
} else {
Ok(())
}
}
fn split_components(input: &[u8]) -> ChainComponents {
let mut split = input.as_ref().splitn(2, |c| *c == CHAIN_NAMESPACE_REFERENCE_SEPARATOR);
ChainComponents {
namespace: split.next(),
reference: split.next(),
}
}
struct ChainComponents<'a> {
namespace: Option<&'a [u8]>,
reference: Option<&'a [u8]>,
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct Eip155Reference(pub(crate) u128);
impl Eip155Reference {
pub const fn ethereum_mainnet() -> Self {
Self(1)
}
pub const fn moonriver_eth() -> Self {
Self(1285)
}
pub const fn moonbeam_eth() -> Self {
Self(1284)
}
}
impl Eip155Reference {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
check_reference_length_bounds(input)?;
let decoded = str::from_utf8(input).map_err(|_| {
log::trace!("Provided input is not a valid UTF8 string as expected by an Eip155 reference.");
ReferenceError::InvalidFormat
})?;
let parsed = decoded.parse::<u128>().map_err(|_| {
log::trace!("Provided input is not a valid u128 value as expected by an Eip155 reference.");
ReferenceError::InvalidFormat
})?;
Ok(Self(parsed))
}
}
impl Eip155Reference {
pub fn inner(&self) -> &u128 {
&self.0
}
}
impl TryFrom<u128> for Eip155Reference {
type Error = Error;
fn try_from(value: u128) -> Result<Self, Self::Error> {
if value <= 99999999999999999999999999999999 {
Ok(Self(value))
} else {
Err(ReferenceError::TooLong.into())
}
}
}
impl From<u64> for Eip155Reference {
fn from(value: u64) -> Self {
Self(value.into())
}
}
impl Display for Eip155Reference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct GenesisHexHash32Reference(pub(crate) [u8; 16]);
impl GenesisHexHash32Reference {
pub const fn bitcoin_mainnet() -> Self {
Self(hex!("000000000019d6689c085ae165831e93"))
}
pub const fn litecoin_mainnet() -> Self {
Self(hex!("12a765e31ffd4059bada1e25190f6e98"))
}
pub const fn polkadot() -> Self {
Self(hex!("91b171bb158e2d3848fa23a9f1c25182"))
}
pub const fn kusama() -> Self {
Self(hex!("b0a8d493285c2df73290dfb7e61f870f"))
}
pub const fn kilt_spiritnet() -> Self {
Self(hex!("411f057b9107718c9624d6aa4a3f23c1"))
}
}
impl GenesisHexHash32Reference {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
check_reference_length_bounds(input)?;
let decoded = hex::decode(input).map_err(|_| {
log::trace!("Provided input is not a valid hex value as expected by a genesis HEX reference.");
ReferenceError::InvalidFormat
})?;
let inner: [u8; 16] = decoded.try_into().map_err(|_| {
log::trace!("Provided input is not 16 bytes long as expected by a genesis HEX reference.");
ReferenceError::InvalidFormat
})?;
Ok(Self(inner))
}
}
impl GenesisHexHash32Reference {
pub fn inner(&self) -> &[u8] {
&self.0
}
}
impl Display for GenesisHexHash32Reference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", hex::encode(self.0))
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct GenesisBase58Hash32Reference(pub(crate) BoundedVec<u8, ConstU32<32>>);
impl GenesisBase58Hash32Reference {
pub fn solana_mainnet() -> Self {
Self(
vec![
187, 54, 81, 91, 131, 4, 217, 218, 81, 6, 169, 34, 88, 214, 125, 109, 223, 209, 236, 21, 49, 109,
82,
]
.try_into()
.expect("Well-known chain ID for solana mainnet should never fail."),
)
}
}
impl GenesisBase58Hash32Reference {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
check_reference_length_bounds(input)?;
let decoded_string = str::from_utf8(input).map_err(|_| {
log::trace!("Provided input is not a valid UTF8 string as expected by a genesis base58 reference.");
ReferenceError::InvalidFormat
})?;
let decoded = decoded_string.from_base58().map_err(|_| {
log::trace!("Provided input is not a valid base58 value as expected by a genesis base58 reference.");
ReferenceError::InvalidFormat
})?;
let inner: BoundedVec<u8, ConstU32<32>> = decoded.try_into().map_err(|_| ReferenceError::InvalidFormat)?;
Ok(Self(inner))
}
}
impl GenesisBase58Hash32Reference {
pub fn inner(&self) -> &[u8] {
&self.0
}
}
impl Display for GenesisBase58Hash32Reference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(f, "{}", &self.0.to_base58())
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct GenericChainId {
pub(crate) namespace: GenericChainNamespace,
pub(crate) reference: GenericChainReference,
}
impl GenericChainId {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let ChainComponents { namespace, reference } = split_components(input.as_ref());
match (namespace, reference) {
(Some(namespace), Some(reference)) => Ok(Self {
namespace: GenericChainNamespace::from_utf8_encoded(namespace)?,
reference: GenericChainReference::from_utf8_encoded(reference)?,
}),
_ => Err(Error::InvalidFormat),
}
}
}
impl GenericChainId {
pub fn namespace(&self) -> &GenericChainNamespace {
&self.namespace
}
pub fn reference(&self) -> &GenericChainReference {
&self.reference
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct GenericChainNamespace(pub(crate) BoundedVec<u8, ConstU32<MAXIMUM_CHAIN_NAMESPACE_LENGTH_U32>>);
impl GenericChainNamespace {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
check_namespace_length_bounds(input)?;
input.iter().try_for_each(|c| {
if !matches!(c, b'-' | b'a'..=b'z' | b'0'..=b'9') {
log::trace!("Provided input has some invalid values as expected by a generic chain namespace.");
Err(NamespaceError::InvalidFormat)
} else {
Ok(())
}
})?;
Ok(Self(
Vec::<u8>::from(input)
.try_into()
.map_err(|_| NamespaceError::InvalidFormat)?,
))
}
}
impl GenericChainNamespace {
pub fn inner(&self) -> &[u8] {
&self.0
}
}
impl Display for GenericChainNamespace {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
str::from_utf8(&self.0).expect("Conversion of GenericChainNamespace to string should never fail.")
)
}
}
#[derive(Clone, Eq, PartialEq, Ord, PartialOrd, RuntimeDebug, Encode, Decode, MaxEncodedLen, TypeInfo)]
pub struct GenericChainReference(pub(crate) BoundedVec<u8, ConstU32<MAXIMUM_CHAIN_REFERENCE_LENGTH_U32>>);
impl GenericChainReference {
pub(crate) fn from_utf8_encoded<I>(input: I) -> Result<Self, Error>
where
I: AsRef<[u8]> + Into<Vec<u8>>,
{
let input = input.as_ref();
check_reference_length_bounds(input)?;
input.iter().try_for_each(|c| {
if !matches!(c, b'-' | b'_' | b'a'..=b'z' | b'A'..=b'Z' | b'0'..=b'9') {
log::trace!("Provided input has some invalid values as expected by a generic chain reference.");
Err(ReferenceError::InvalidFormat)
} else {
Ok(())
}
})?;
Ok(Self(
Vec::<u8>::from(input)
.try_into()
.map_err(|_| ReferenceError::InvalidFormat)?,
))
}
}
impl GenericChainReference {
pub fn inner(&self) -> &[u8] {
&self.0
}
}
impl Display for GenericChainReference {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{}",
str::from_utf8(&self.0).expect("Conversion of GenericChainReference to string should never fail.")
)
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_eip155_chains() {
let valid_chains = [
"eip155:1",
"eip155:5",
"eip155:99999999999999999999999999999999",
"eip155:0",
];
for chain in valid_chains {
let chain_id = ChainId::from_utf8_encoded(chain.as_bytes())
.unwrap_or_else(|_| panic!("Chain ID {:?} should not fail for eip155 chains", chain));
assert_eq!(chain_id.to_string(), chain);
}
let invalid_chains = [
"",
"e",
"ei",
"eip",
"eip1",
"eip15",
"eip155",
"eip155:",
"eip155:a",
"eip155::",
"eip155:›",
"eip155:😁",
"eip155:999999999999999999999999999999999",
];
for chain in invalid_chains {
assert!(
ChainId::from_utf8_encoded(chain.as_bytes()).is_err(),
"Chain ID {:?} should fail to parse for eip155 chains",
chain
);
}
}
#[test]
fn test_bip122_chains() {
let valid_chains = [
"bip122:000000000019d6689c085ae165831e93",
"bip122:12a765e31ffd4059bada1e25190f6e98",
"bip122:fdbe99b90c90bae7505796461471d89a",
"bip122:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for chain in valid_chains {
let chain_id = ChainId::from_utf8_encoded(chain.as_bytes())
.unwrap_or_else(|_| panic!("Chain ID {:?} should not fail for bip122 chains", chain));
assert_eq!(chain_id.to_string(), chain);
}
let invalid_chains = [
"",
"b",
"bi",
"bip",
"bip1",
"bip12",
"bip122",
"bip122:",
"bip122:gg",
"bip122::",
"bip122:›",
"bip122:😁",
"bip122:a",
"bip122:aa",
"bip122:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for chain in invalid_chains {
assert!(
ChainId::from_utf8_encoded(chain.as_bytes()).is_err(),
"Chain ID {:?} should fail to parse for bip122 chains",
chain
);
}
}
#[test]
fn test_dotsama_chains() {
let valid_chains = [
"polkadot:b0a8d493285c2df73290dfb7e61f870f",
"polkadot:742a2ca70c2fda6cee4f8df98d64c4c6",
"polkadot:37e1f8125397a98630013a4dff89b54c",
"polkadot:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for chain in valid_chains {
let chain_id = ChainId::from_utf8_encoded(chain.as_bytes())
.unwrap_or_else(|_| panic!("Chain ID {:?} should not fail for dotsama chains", chain));
assert_eq!(chain_id.to_string(), chain);
}
let invalid_chains = [
"",
"p",
"po",
"pol",
"polk",
"polka",
"polkad",
"polkado",
"polkadot",
"polkadot:",
"polkadot:gg",
"polkadot::",
"polkadot:›",
"polkadot:😁",
"polkadot:a",
"polkadot:aa",
"polkadot:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
];
for chain in invalid_chains {
assert!(
ChainId::from_utf8_encoded(chain.as_bytes()).is_err(),
"Chain ID {:?} should fail to parse for polkadot chains",
chain
);
}
}
#[test]
fn test_solana_chains() {
let valid_chains = [
"solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ",
"solana:8E9rvCKLFQia2Y35HXjjpWzj8weVo44K",
];
for chain in valid_chains {
let chain_id = ChainId::from_utf8_encoded(chain.as_bytes())
.unwrap_or_else(|_| panic!("Chain ID {:?} should not fail for solana chains", chain));
assert_eq!(chain_id.to_string(), chain);
}
let invalid_chains = [
"",
"s",
"so",
"sol",
"sola",
"solan",
"solana",
"solana:",
"solana::",
"solana:›",
"solana:😁",
"solana:random-string",
"solana:TJ24pxm996UCBScuQRwjYo4wvPjUa8pzKo",
];
for chain in invalid_chains {
assert!(
ChainId::from_utf8_encoded(chain.as_bytes()).is_err(),
"Chain ID {:?} should fail to parse for generic chains",
chain
);
}
}
#[test]
fn test_generic_chains() {
let valid_chains = [
"abc:-",
"abc:_",
"-as01-aa:A",
"12345678:abcdefghjklmnopqrstuvwxyzABCD012",
"fil:t",
"fil:f",
"tezos:NetXdQprcVkpaWU",
"tezos:NetXm8tYqnMWky1",
"cosmos:cosmoshub-2",
"cosmos:cosmoshub-3",
"cosmos:Binance-Chain-Tigris",
"cosmos:iov-mainnet",
"cosmos:x",
"cosmos:hash-",
"cosmos:hashed",
"lip9:9ee11e9df416b18b",
"lip9:e48feb88db5b5cf5",
"eosio:aca376f206b8fc25a6ed44dbdc66547c",
"eosio:e70aaab8997e1dfce58fbfac80cbbb8f",
"eosio:4667b205c6838ef70ff7988f6e8257e8",
"eosio:1eaa0824707c8c16bd25145493bf062a",
"stellar:testnet",
"stellar:pubnet",
];
for chain in valid_chains {
let chain_id = ChainId::from_utf8_encoded(chain.as_bytes())
.unwrap_or_else(|_| panic!("Chain ID {:?} should not fail for generic chains", chain));
assert_eq!(chain_id.to_string(), chain);
}
let invalid_chains = [
"",
"a",
"ab",
"01:",
"ab-:",
"123456789:1",
"12345678:123456789123456789123456789123456",
"123456789:123456789123456789123456789123456",
"::",
"c?1:›",
"de:😁",
];
for chain in invalid_chains {
assert!(
ChainId::from_utf8_encoded(chain.as_bytes()).is_err(),
"Chain ID {:?} should fail to parse for solana chains",
chain
);
}
}
#[test]
fn test_helpers() {
assert_eq!(ChainId::ethereum_mainnet().to_string(), "eip155:1");
assert_eq!(ChainId::moonbeam_eth().to_string(), "eip155:1284");
assert_eq!(
ChainId::bitcoin_mainnet().to_string(),
"bip122:000000000019d6689c085ae165831e93"
);
assert_eq!(
ChainId::litecoin_mainnet().to_string(),
"bip122:12a765e31ffd4059bada1e25190f6e98"
);
assert_eq!(
ChainId::polkadot().to_string(),
"polkadot:91b171bb158e2d3848fa23a9f1c25182"
);
assert_eq!(
ChainId::kusama().to_string(),
"polkadot:b0a8d493285c2df73290dfb7e61f870f"
);
assert_eq!(
ChainId::kilt_spiritnet().to_string(),
"polkadot:411f057b9107718c9624d6aa4a3f23c1"
);
assert_eq!(
ChainId::solana_mainnet().to_string(),
"solana:4sGjMW1sUnHzSxGspuhpqLDx6wiyjNtZ"
);
}
}
}