use std::fmt::{
self,
Debug,
Display,
Formatter,
};
use std::str::FromStr;
use hedera_proto::services;
use hedera_proto::services::account_id::Account;
use crate::entity_id::{
Checksum,
PartialEntityId,
ValidateChecksums,
};
use crate::evm_address::{
EvmAddress,
IdEvmAddress,
};
use crate::{
Client,
EntityId,
Error,
FromProtobuf,
LedgerId,
PublicKey,
ToProtobuf,
};
#[derive(Copy, Hash, PartialEq, Eq, Clone)]
#[cfg_attr(feature = "ffi", derive(serde_with::SerializeDisplay, serde_with::DeserializeFromStr))]
pub struct AccountId {
pub shard: u64,
pub realm: u64,
pub num: u64,
pub alias: Option<PublicKey>,
pub evm_address: Option<EvmAddress>,
pub checksum: Option<Checksum>,
}
impl AccountId {
pub fn from_bytes(bytes: &[u8]) -> crate::Result<Self> {
FromProtobuf::from_bytes(bytes)
}
pub fn from_solidity_address(address: &str) -> crate::Result<Self> {
let EntityId { shard, realm, num, checksum } = EntityId::from_solidity_address(address)?;
Ok(Self { shard, realm, num, alias: None, evm_address: None, checksum })
}
#[must_use]
pub fn from_evm_address(address: &EvmAddress) -> Self {
Self {
shard: 0,
realm: 0,
num: 0,
alias: None,
evm_address: Some(*address),
checksum: None,
}
}
#[must_use]
pub fn to_bytes(&self) -> Vec<u8> {
ToProtobuf::to_bytes(self)
}
pub fn to_solidity_address(&self) -> crate::Result<String> {
EntityId { shard: self.shard, realm: self.realm, num: self.num, checksum: None }
.to_solidity_address()
}
pub(crate) fn _to_evm_address(&self) -> crate::Result<String> {
if let Some(evm_address) = &self.evm_address {
Ok(evm_address.to_string())
} else {
Err(Error::NoEvmAddressPresent { task: "convert account ID to evm address string" })
}
}
pub async fn to_string_with_checksum(&self, client: &Client) -> Result<String, Error> {
if self.alias.is_some() || self.evm_address.is_some() {
Err(Error::CannotToStringWithChecksum)
} else {
EntityId::to_string_with_checksum(self.to_string(), client).await
}
}
pub async fn validate_checksum(&self, client: &Client) -> Result<(), Error> {
if self.alias.is_some() || self.evm_address.is_some() {
Ok(())
} else {
EntityId::validate_checksum(self.shard, self.realm, self.num, &self.checksum, client)
.await
}
}
}
impl ValidateChecksums for AccountId {
fn validate_checksums(&self, ledger_id: &LedgerId) -> Result<(), Error> {
if self.alias.is_some() || self.evm_address.is_some() {
Ok(())
} else {
EntityId::validate_checksum_for_ledger_id(
self.shard,
self.realm,
self.num,
&self.checksum,
ledger_id,
)
}
}
}
impl Debug for AccountId {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "\"{self}\"")
}
}
impl Display for AccountId {
#[allow(clippy::uninlined_format_args)]
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if let Some(alias) = &self.alias {
write!(f, "{}.{}.{}", self.shard, self.realm, alias)
} else if let Some(evm_address) = &self.evm_address {
write!(f, "{evm_address}")
} else {
write!(f, "{}.{}.{}", self.shard, self.realm, self.num)
}
}
}
impl ToProtobuf for AccountId {
type Protobuf = services::AccountId;
fn to_protobuf(&self) -> Self::Protobuf {
services::AccountId {
realm_num: self.realm as i64,
shard_num: self.shard as i64,
account: Some(match &self.alias {
None => services::account_id::Account::AccountNum(self.num as i64),
Some(alias) => services::account_id::Account::Alias(ToProtobuf::to_bytes(alias)),
}),
}
}
}
impl FromProtobuf<services::AccountId> for AccountId {
fn from_protobuf(pb: services::AccountId) -> crate::Result<Self> {
let account = pb_getf!(pb, account)?;
let (num, alias, evm_address) = match account {
services::account_id::Account::AccountNum(num) => (num, None, None),
services::account_id::Account::Alias(alias) => {
(0, Some(FromProtobuf::from_bytes(&alias)?), None)
}
Account::EvmAddress(evm_address) => {
(0, None, Some(IdEvmAddress::try_from(evm_address)?.0))
}
};
Ok(Self {
num: num as u64,
alias,
evm_address,
shard: pb.shard_num as u64,
realm: pb.realm_num as u64,
checksum: None,
})
}
}
impl From<u64> for AccountId {
fn from(num: u64) -> Self {
Self { num, alias: None, evm_address: None, checksum: None, shard: 0, realm: 0 }
}
}
impl From<PublicKey> for AccountId {
fn from(alias: PublicKey) -> Self {
Self { num: 0, shard: 0, realm: 0, evm_address: None, alias: Some(alias), checksum: None }
}
}
impl FromStr for AccountId {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let partial = PartialEntityId::from_str(s)
.map_err(|_| Error::basic_parse("expecting <shard>.<realm>.<num> (ex. `0.0.1001`) or <shard>.<realm>.<alias> (ex. `0.0.0a410c8fe4912e3652b61dd222b1b4d7773261537d7ebad59df6cd33622a693e"))?;
match partial {
PartialEntityId::ShortNum(it) => Ok(it.into()),
PartialEntityId::LongNum(it) => Ok(it.into()),
PartialEntityId::ShortOther(evm_address) => {
Ok(Self::from_evm_address(&evm_address.parse()?))
}
PartialEntityId::LongOther { shard, realm, last } => Ok(Self {
shard,
realm,
num: 0,
alias: Some(last.parse()?),
evm_address: None,
checksum: None,
}),
}
}
}
impl From<EntityId> for AccountId {
fn from(value: EntityId) -> Self {
let EntityId { shard, realm, num, checksum } = value;
Self { shard, realm, num, checksum, alias: None, evm_address: None }
}
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use hex_literal::hex;
use crate::evm_address::EvmAddress;
use crate::{
AccountId,
Client,
};
#[test]
fn parse() {
let account_id: AccountId = "0.0.1001".parse().unwrap();
assert_eq!(
account_id,
AccountId {
shard: 0,
realm: 0,
num: 1001,
alias: None,
evm_address: None,
checksum: None
}
);
}
#[test]
fn to_from_bytes_roundtrip() {
let account_id = AccountId {
shard: 0,
realm: 0,
num: 1001,
alias: None,
evm_address: None,
checksum: None,
};
assert_eq!(account_id, AccountId::from_bytes(&account_id.to_bytes()).unwrap());
}
#[test]
fn from_evm_address_string() {
let evm_address = hex!("302a300506032b6570032100114e6abc371b82da");
assert_eq!(
AccountId::from_str("0x302a300506032b6570032100114e6abc371b82da").unwrap(),
AccountId {
shard: 0,
realm: 0,
num: 0,
alias: None,
evm_address: Some(EvmAddress(evm_address)),
checksum: None
}
)
}
#[test]
fn to_evm_address_string() {
assert_eq!(
&AccountId {
shard: 0,
realm: 0,
num: 0,
alias: None,
evm_address: Some(EvmAddress(hex!("302a300506032b6570032100114e6abc371b82da"))),
checksum: None
}
.to_string(),
"0x302a300506032b6570032100114e6abc371b82da"
)
}
#[tokio::test]
async fn good_checksum_on_mainnet() {
AccountId::from_str("0.0.123-vfmkw")
.unwrap()
.validate_checksum(&Client::for_mainnet())
.await
.unwrap();
}
#[tokio::test]
async fn good_checksum_on_testnet() {
AccountId::from_str("0.0.123-esxsf")
.unwrap()
.validate_checksum(&Client::for_testnet())
.await
.unwrap();
}
#[tokio::test]
async fn good_checksum_on_previewnet() {
AccountId::from_str("0.0.123-ogizo")
.unwrap()
.validate_checksum(&Client::for_previewnet())
.await
.unwrap();
}
#[tokio::test]
async fn to_string_with_checksum() {
assert_eq!(
AccountId::from_str("0.0.123")
.unwrap()
.to_string_with_checksum(&Client::for_testnet())
.await
.unwrap(),
"0.0.123-esxsf"
);
}
}