-
Notifications
You must be signed in to change notification settings - Fork 254
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add 20-byte account id to subxt_core #1638
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright 2019-2024 Parity Technologies (UK) Ltd. | ||
// This file is dual-licensed as Apache-2.0 or GPL-3.0. | ||
// see LICENSE for license details. | ||
|
||
//! `AccountId20` is a repressentation of Ethereum address derived from hashing the public key. | ||
|
||
use core::fmt::Display; | ||
|
||
use alloc::format; | ||
use alloc::string::String; | ||
use codec::{Decode, Encode}; | ||
use keccak_hash::keccak; | ||
use serde::{Deserialize, Serialize}; | ||
|
||
#[derive( | ||
Copy, | ||
Clone, | ||
Eq, | ||
PartialEq, | ||
Ord, | ||
PartialOrd, | ||
Encode, | ||
Decode, | ||
Debug, | ||
scale_encode::EncodeAsType, | ||
scale_decode::DecodeAsType, | ||
scale_info::TypeInfo, | ||
)] | ||
/// Ethereum-compatible `AccountId`. | ||
pub struct AccountId20(pub [u8; 20]); | ||
|
||
impl AsRef<[u8]> for AccountId20 { | ||
fn as_ref(&self) -> &[u8] { | ||
&self.0[..] | ||
} | ||
} | ||
|
||
impl AsRef<[u8; 20]> for AccountId20 { | ||
fn as_ref(&self) -> &[u8; 20] { | ||
&self.0 | ||
} | ||
} | ||
|
||
impl From<[u8; 20]> for AccountId20 { | ||
fn from(x: [u8; 20]) -> Self { | ||
AccountId20(x) | ||
} | ||
} | ||
|
||
impl AccountId20 { | ||
/// Convert to a public key hash | ||
pub fn checksum(&self) -> String { | ||
let hex_address = hex::encode(self.0); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this code is just moved but we think we could get rid of this This probably doesn't matter in practice but just a thought :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. is it really that important? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. no, I don't think it matters just an unnecessary allocation :) |
||
let hash = keccak(hex_address.as_bytes()); | ||
|
||
let mut checksum_address = String::with_capacity(42); | ||
checksum_address.push_str("0x"); | ||
|
||
for (i, ch) in hex_address.chars().enumerate() { | ||
// Get the corresponding nibble from the hash | ||
let nibble = hash[i / 2] >> (if i % 2 == 0 { 4 } else { 0 }) & 0xf; | ||
|
||
if nibble >= 8 { | ||
checksum_address.push(ch.to_ascii_uppercase()); | ||
} else { | ||
checksum_address.push(ch); | ||
} | ||
} | ||
|
||
checksum_address | ||
} | ||
} | ||
|
||
/// An error obtained from trying to interpret a hex encoded string into an AccountId20 | ||
#[derive(Clone, Copy, Eq, PartialEq, Debug)] | ||
#[allow(missing_docs)] | ||
pub enum FromChecksumError { | ||
BadLength, | ||
InvalidChecksum, | ||
InvalidPrefix, | ||
} | ||
|
||
impl Display for FromChecksumError { | ||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { | ||
match self { | ||
FromChecksumError::BadLength => write!(f, "Length is bad"), | ||
FromChecksumError::InvalidChecksum => write!(f, "Invalid checksum"), | ||
FromChecksumError::InvalidPrefix => write!(f, "Invalid checksum prefix byte."), | ||
} | ||
} | ||
} | ||
|
||
#[cfg(feature = "std")] | ||
impl std::error::Error for FromChecksumError {} | ||
|
||
impl Serialize for AccountId20 { | ||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> | ||
where | ||
S: serde::Serializer, | ||
{ | ||
serializer.serialize_str(&self.checksum()) | ||
} | ||
} | ||
|
||
impl<'de> Deserialize<'de> for AccountId20 { | ||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> | ||
where | ||
D: serde::Deserializer<'de>, | ||
{ | ||
String::deserialize(deserializer)? | ||
.parse::<AccountId20>() | ||
.map_err(|e| serde::de::Error::custom(format!("{e:?}"))) | ||
} | ||
} | ||
|
||
impl core::fmt::Display for AccountId20 { | ||
fn fmt(&self, f: &mut core::fmt::Formatter) -> core::fmt::Result { | ||
write!(f, "{}", self.checksum()) | ||
} | ||
} | ||
|
||
impl core::str::FromStr for AccountId20 { | ||
type Err = FromChecksumError; | ||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||
niklasad1 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
if s.len() != 42 { | ||
return Err(FromChecksumError::BadLength); | ||
} | ||
if !s.starts_with("0x") { | ||
return Err(FromChecksumError::InvalidPrefix); | ||
} | ||
hex::decode(&s.as_bytes()[2..]) | ||
.map_err(|_| FromChecksumError::InvalidChecksum)? | ||
.try_into() | ||
.map(AccountId20) | ||
.map_err(|_| FromChecksumError::BadLength) | ||
} | ||
} | ||
|
||
#[cfg(test)] | ||
mod test { | ||
use super::*; | ||
|
||
#[test] | ||
fn deserialisation() { | ||
let key_hashes = vec![ | ||
"0xf24FF3a9CF04c71Dbc94D0b566f7A27B94566cac", | ||
"0x3Cd0A705a2DC65e5b1E1205896BaA2be8A07c6e0", | ||
"0x798d4Ba9baf0064Ec19eB4F0a1a45785ae9D6DFc", | ||
"0x773539d4Ac0e786233D90A233654ccEE26a613D9", | ||
"0xFf64d3F6efE2317EE2807d223a0Bdc4c0c49dfDB", | ||
"0xC0F0f4ab324C46e55D02D0033343B4Be8A55532d", | ||
]; | ||
|
||
for key_hash in key_hashes { | ||
let parsed: AccountId20 = key_hash.parse().expect("Failed to parse"); | ||
|
||
let encoded = parsed.checksum(); | ||
|
||
// `encoded` should be equal to the initial key_hash | ||
assert_eq!(encoded, key_hash); | ||
} | ||
} | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe add a comment that the type comes from frontier which probably works for frontier/moonbeam but probably not for all ethereum-based chains.
I have no strong opinions whether to support this override or not, doesn't hurt I guess...
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mmm it might be that we can add a few overrides if needed to cater for the common chains, but otherwise the user can do this themselves.
I was hesitent but don't see a reason not to add this since its super unlikely to ever break anything that doesn't care :)