Skip to content

Commit e6cb41b

Browse files
committed
fix decimal vs hex equivalence
1 parent bdcb442 commit e6cb41b

File tree

8 files changed

+280
-39
lines changed

8 files changed

+280
-39
lines changed

SPECIFICATION.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ Requirements:
115115
- All keys of `RpcConfig` and `Endpoint` are required. No additional keys must be present, except within `global_metadata`, `profile_metadata`, and `endpoint_metadata`.
116116
- Every endpoint name specified in `RpcConfig.default_endpoint` and in `RpcConfig.network_defaults` must exist in `RpcConfig.endpoints`.
117117
- These key-value structures can be easily represented in JSON and in most common programming languages.
118-
- EVM `chain_id`'s must be represented using either a decimal string or a hex string. Strings are used because chain id's can be 256 bits and most programming languages do not have native 256 bit integer types. For readability, decimal should be used for small chain id values and hex should be used for values that use the entire 256 bits.
118+
- EVM `chain_id`'s must be represented using either a decimal string or a `0x`-prefixed hex string. Strings are used because chain id's can be 256 bits and most programming languages do not have native 256 bit integer types. For readability, decimal should be used for small chain id values and hex should be used for values that use the entire 256 bits.
119119
- Names of endpoints, networks, and profiles should be composed of characters that are either alphanumeric, dashes, underscores, or periods. Names should be at least 1 character long.
120120

121121
##### Metadata

python/mesc/interface.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -71,15 +71,18 @@ def get_endpoint_by_network(
7171
raise ValueError('chain_id must be a str or int')
7272
chain_id = str(chain_id)
7373
network_defaults = config['network_defaults']
74-
default_name = network_defaults.get(chain_id)
74+
default_name = network_utils.get_by_chain_id(network_defaults, chain_id)
7575

7676
# get profile default for network
77-
if profile and profile in config['profiles']:
77+
if profile is not None and profile in config['profiles']:
7878
if not config['profiles'][profile]['use_mesc']:
7979
return None
80-
name = config['profiles'][profile]['network_defaults'].get(
81-
chain_id, default_name
80+
name = network_utils.get_by_chain_id(
81+
config['profiles'][profile]['network_defaults'],
82+
chain_id,
8283
)
84+
if name is None:
85+
name = default_name
8386
else:
8487
name = default_name
8588

@@ -143,8 +146,12 @@ def find_endpoints(
143146
if chain_id is not None:
144147
if isinstance(chain_id, int):
145148
chain_id = str(chain_id)
149+
chain_id = network_utils.chain_id_to_standard_hex(chain_id)
146150
endpoints = [
147-
endpoint for endpoint in endpoints if endpoint['chain_id'] == chain_id
151+
endpoint
152+
for endpoint in endpoints
153+
if endpoint['chain_id'] is not None
154+
and network_utils.chain_id_to_standard_hex(endpoint['chain_id']) == chain_id
148155
]
149156

150157
# check name_contains

python/mesc/network_utils.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import typing
34
from .types import RpcConfig
45
from . import network_names
56

@@ -31,3 +32,31 @@ def network_name_to_chain_id(
3132
return chain_id
3233
else:
3334
return None
35+
36+
37+
def chain_id_to_standard_hex(chain_id: str) -> str | None:
38+
if chain_id.startswith('0x'):
39+
if len(chain_id) > 2:
40+
as_hex = chain_id
41+
else:
42+
try:
43+
as_hex = hex(int(chain_id))
44+
except ValueError:
45+
return None
46+
47+
return '0x' + as_hex[2:].lstrip('0')
48+
49+
50+
T = typing.TypeVar('T')
51+
52+
53+
def get_by_chain_id(mapping: typing.Mapping[str, T], chain_id: str) -> T | None:
54+
if chain_id in mapping:
55+
return mapping[chain_id]
56+
57+
standard_mapping = {chain_id_to_standard_hex(k): v for k, v in mapping.items()}
58+
return standard_mapping.get(chain_id_to_standard_hex(chain_id))
59+
60+
61+
def chain_ids_equal(lhs: str, rhs: str) -> bool:
62+
return chain_id_to_standard_hex(lhs) == chain_id_to_standard_hex(rhs)

python/mesc/validation.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
from typing_extensions import Any
4+
from typing import Sequence
45

56
from .exceptions import InvalidConfig
67
from .types import rpc_config_types, endpoint_types, profile_types
@@ -100,7 +101,9 @@ def validate(config: Any) -> None:
100101

101102
# default endpoints of each network actually use that specified network
102103
for chain_id, endpoint_name in config['network_defaults'].items():
103-
if chain_id != config['endpoints'][endpoint_name]['chain_id']:
104+
if not network_utils.chain_ids_equal(
105+
chain_id, config['endpoints'][endpoint_name]['chain_id']
106+
):
104107
raise InvalidConfig(
105108
'Endpoint is set as the default endpoint of network '
106109
+ chain_id
@@ -109,7 +112,9 @@ def validate(config: Any) -> None:
109112
)
110113
for profile_name, profile in config['profiles'].items():
111114
for chain_id, endpoint_name in profile['network_defaults'].items():
112-
if chain_id != config['endpoints'][endpoint_name]['chain_id']:
115+
if not network_utils.chain_ids_equal(
116+
chain_id, config['endpoints'][endpoint_name]['chain_id']
117+
):
113118
raise InvalidConfig(
114119
'Endpoint is set as the default endpoint of network '
115120
+ chain_id
@@ -165,7 +170,28 @@ def validate(config: Any) -> None:
165170
)
166171

167172
# no duplicate default network entries using decimal vs hex
168-
pass
173+
ensure_no_chain_id_collisions(
174+
list(config['network_defaults'].keys()), 'network defaults'
175+
)
176+
for profile_name, profile in config['profiles'].items():
177+
ensure_no_chain_id_collisions(
178+
list(config['network_defaults'].keys()), 'profile ' + profile_name
179+
)
180+
181+
182+
def ensure_no_chain_id_collisions(chain_ids: Sequence[str], name: str) -> None:
183+
hex_numbers = set()
184+
for chain_id in chain_ids:
185+
as_hex = network_utils.chain_id_to_standard_hex(chain_id)
186+
if as_hex in hex_numbers:
187+
raise Exception(
188+
'chain_id collision, '
189+
+ str(name)
190+
+ ' has multiple decimal/hex values for chain_id: '
191+
+ str(chain_id)
192+
)
193+
else:
194+
hex_numbers.add(as_hex)
169195

170196

171197
def _check_type(

rust/crates/mesc/src/interface.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ pub fn get_default_endpoint(profile: Option<&str>) -> Result<Option<Endpoint>, M
1818
}
1919

2020
/// get endpoint by network
21-
pub fn get_endpoint_by_network<T: TryIntoChainId>(
21+
pub fn get_endpoint_by_network<T: TryIntoChainId + std::fmt::Debug + std::clone::Clone>(
2222
chain_id: T,
2323
profile: Option<&str>,
2424
) -> Result<Option<Endpoint>, MescError> {

rust/crates/mesc/src/query.rs

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use crate::{
22
directory,
33
types::{Endpoint, MescError, RpcConfig},
4-
MultiEndpointQuery, TryIntoChainId,
4+
ChainId, MultiEndpointQuery, TryIntoChainId,
55
};
66
use std::collections::HashMap;
77

@@ -10,14 +10,14 @@ pub fn get_default_endpoint(
1010
config: &RpcConfig,
1111
profile: Option<&str>,
1212
) -> Result<Option<Endpoint>, MescError> {
13-
// if using a profile, check if that profile has a default endpoint for chain_id
13+
// if using a profile, check if that profile has a default endpoint
1414
if let Some(profile) = profile {
1515
if let Some(profile_data) = config.profiles.get(profile) {
1616
if !profile_data.use_mesc {
17-
return Ok(None)
17+
return Ok(None);
1818
}
1919
if let Some(endpoint_name) = profile_data.default_endpoint.as_deref() {
20-
return get_endpoint_by_name(config, endpoint_name)
20+
return get_endpoint_by_name(config, endpoint_name);
2121
}
2222
}
2323
};
@@ -29,7 +29,7 @@ pub fn get_default_endpoint(
2929
}
3030

3131
/// get endpoint by network
32-
pub fn get_endpoint_by_network<T: TryIntoChainId>(
32+
pub fn get_endpoint_by_network<T: TryIntoChainId + std::fmt::Debug + std::clone::Clone>(
3333
config: &RpcConfig,
3434
chain_id: T,
3535
profile: Option<&str>,
@@ -40,21 +40,40 @@ pub fn get_endpoint_by_network<T: TryIntoChainId>(
4040
if let Some(profile) = profile {
4141
if let Some(profile_data) = config.profiles.get(profile) {
4242
if !profile_data.use_mesc {
43-
return Ok(None)
43+
return Ok(None);
4444
}
4545
if let Some(endpoint_name) = profile_data.network_defaults.get(&chain_id) {
46-
return get_endpoint_by_name(config, endpoint_name)
46+
return get_endpoint_by_name(config, endpoint_name);
4747
}
4848
}
4949
};
5050

5151
// check if base configuration has a default endpoint for that chain_id
52-
match config.network_defaults.get(&chain_id) {
53-
Some(name) => get_endpoint_by_name(config, name),
52+
match get_by_chain_id(&config.network_defaults, chain_id)? {
53+
Some(name) => get_endpoint_by_name(config, name.as_str()),
5454
None => Ok(None),
5555
}
5656
}
5757

58+
fn get_by_chain_id<T: TryIntoChainId, S: std::fmt::Debug + Clone>(
59+
mapping: &HashMap<ChainId, S>,
60+
chain_id: T,
61+
) -> Result<Option<S>, MescError> {
62+
let chain_id = chain_id.try_into_chain_id()?;
63+
if let Some(value) = mapping.get(&chain_id) {
64+
Ok(Some(value.clone()))
65+
} else {
66+
let standard_chain_id = chain_id.to_hex_256()?;
67+
let results: Result<HashMap<String, S>, _> = mapping
68+
.iter()
69+
.map(|(k, v)| k.to_hex_256().map(|hex| (hex, v.clone())))
70+
.collect::<Result<Vec<_>, _>>() // Collect into a Result<Vec<(String, S)>, Error>
71+
.map(|pairs| pairs.into_iter().collect::<HashMap<_, _>>());
72+
let standard_mapping = results?;
73+
Ok(standard_mapping.get(&standard_chain_id).cloned())
74+
}
75+
}
76+
5877
/// get endpoint by name
5978
pub fn get_endpoint_by_name(config: &RpcConfig, name: &str) -> Result<Option<Endpoint>, MescError> {
6079
if let Some(endpoint) = config.endpoints.get(name) {
@@ -73,7 +92,7 @@ pub fn get_endpoint_by_query(
7392
if let Some(profile) = profile {
7493
if let Some(profile_data) = config.profiles.get(profile) {
7594
if !profile_data.use_mesc {
76-
return Ok(None)
95+
return Ok(None);
7796
}
7897
}
7998
}
@@ -108,7 +127,7 @@ pub fn find_endpoints(
108127
let mut candidates: Vec<Endpoint> = config.endpoints.clone().into_values().collect();
109128

110129
if let Some(chain_id) = query.chain_id {
111-
candidates.retain(|endpoint| endpoint.chain_id.as_ref() == Some(&chain_id))
130+
candidates.retain(|endpoint| endpoint.chain_id.as_ref() == Some(&chain_id));
112131
}
113132

114133
if let Some(name) = query.name_contains {
@@ -133,7 +152,7 @@ pub fn get_global_metadata(
133152
if let Some(profile) = profile {
134153
if let Some(profile_data) = config.profiles.get(profile) {
135154
if !profile_data.use_mesc {
136-
return Ok(HashMap::new())
155+
return Ok(HashMap::new());
137156
}
138157
metadata.extend(profile_data.profile_metadata.clone())
139158
}

rust/crates/mesc/src/types/chain_ids.rs

Lines changed: 46 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use serde::{Deserialize, Serialize};
33

44
/// ChainId is a string representation of an integer chain id
55
/// - TryFrom conversions allow specifying as String, &str, uint, or binary data
6-
#[derive(Serialize, Deserialize, Debug, Clone, Eq, PartialEq, Hash)]
6+
#[derive(Serialize, Deserialize, Debug, Clone, Eq)]
77
pub struct ChainId(String);
88

99
impl ChainId {
@@ -23,12 +23,14 @@ impl ChainId {
2323
/// convert to hex representation, zero-padded to 256 bits
2424
pub fn to_hex_256(&self) -> Result<String, MescError> {
2525
let ChainId(chain_id) = self;
26-
if chain_id.starts_with("0x") {
27-
Ok(chain_id.clone())
26+
if let Some(stripped) = chain_id.strip_prefix("0x") {
27+
Ok(format!("0x{:0>64}", stripped))
2828
} else {
29-
match chain_id.parse::<u64>() {
30-
Ok(number) => Ok(format!("0x{:016x}", number)),
31-
Err(_) => Err(MescError::IntegrityError("bad chain_id".to_string())),
29+
match chain_id.parse::<u128>() {
30+
Ok(number) => Ok(format!("0x{:064x}", number)),
31+
Err(_) => {
32+
Err(MescError::InvalidChainId("cannot convert chain_id to hex".to_string()))
33+
}
3234
}
3335
}
3436
}
@@ -40,12 +42,42 @@ impl ChainId {
4042
}
4143
}
4244

45+
impl std::hash::Hash for ChainId {
46+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
47+
match self.to_hex_256() {
48+
Ok(as_hex) => {
49+
as_hex.hash(state);
50+
}
51+
_ => {
52+
let ChainId(contents) = self;
53+
contents.hash(state);
54+
}
55+
}
56+
}
57+
}
58+
4359
impl PartialOrd for ChainId {
4460
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
4561
Some(self.cmp(other))
4662
}
4763
}
4864

65+
impl PartialEq for ChainId {
66+
fn eq(&self, other: &Self) -> bool {
67+
let self_string: String = match self.to_hex() {
68+
Ok(s) => s[2..].to_string(),
69+
Err(_) => return self == other,
70+
};
71+
let other_string = match other.to_hex() {
72+
Ok(s) => s[2..].to_string(),
73+
Err(_) => return self == other,
74+
};
75+
let self_str = format!("{:0>79}", self_string);
76+
let other_str = format!("{:0>79}", other_string);
77+
self_str.eq(&other_str)
78+
}
79+
}
80+
4981
impl Ord for ChainId {
5082
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
5183
let self_string: String = match self.to_hex() {
@@ -96,7 +128,10 @@ impl TryIntoChainId for ChainId {
96128

97129
impl TryIntoChainId for String {
98130
fn try_into_chain_id(self) -> Result<ChainId, MescError> {
99-
if !self.is_empty() && self.chars().all(|c| c.is_ascii_digit()) {
131+
if !self.is_empty() &&
132+
(self.chars().all(|c| c.is_ascii_digit()) ||
133+
(self.starts_with("0x") && self[2..].chars().all(|c| c.is_ascii_hexdigit())))
134+
{
100135
Ok(ChainId(self))
101136
} else {
102137
Err(MescError::InvalidChainId(self))
@@ -106,7 +141,10 @@ impl TryIntoChainId for String {
106141

107142
impl TryIntoChainId for &str {
108143
fn try_into_chain_id(self) -> Result<ChainId, MescError> {
109-
if self.chars().all(|c| c.is_ascii_digit()) {
144+
if !self.is_empty() &&
145+
(self.chars().all(|c| c.is_ascii_digit()) ||
146+
(self.starts_with("0x") && self[2..].chars().all(|c| c.is_ascii_hexdigit())))
147+
{
110148
Ok(ChainId(self.to_string()))
111149
} else {
112150
Err(MescError::InvalidChainId(self.to_string()))

0 commit comments

Comments
 (0)