use std::fmt::{self, Debug, Display, Formatter};
use std::str::FromStr;
use r402_core::chain::{ChainId, DeployedTokenAmount};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use crate::hex;
use crate::motes::{CSPR_DECIMALS, Motes, MotesParseError};
pub const CASPER_NAMESPACE: &str = "casper";
const HASH_HEX_LEN: usize = 64;
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub enum CasperChainReference {
Mainnet,
Testnet,
}
impl CasperChainReference {
pub const CASPER: Self = Self::Mainnet;
pub const CASPER_TEST: Self = Self::Testnet;
pub const ALL: &'static [Self] = &[Self::Mainnet, Self::Testnet];
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::Mainnet => "casper",
Self::Testnet => "casper-test",
}
}
#[must_use]
pub const fn chain_name(self) -> &'static str {
self.as_str()
}
#[must_use]
pub const fn default_rpc_url(self) -> &'static str {
match self {
Self::Mainnet => "https://node.mainnet.casper.network/rpc",
Self::Testnet => "https://node.testnet.casper.network/rpc",
}
}
#[must_use]
pub const fn is_testnet(self) -> bool {
matches!(self, Self::Testnet)
}
}
impl Debug for CasperChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "CasperChainReference({})", self.as_str())
}
}
impl Display for CasperChainReference {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
impl FromStr for CasperChainReference {
type Err = CasperChainReferenceFormatError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::ALL
.iter()
.copied()
.find(|candidate| candidate.as_str() == s)
.ok_or_else(|| CasperChainReferenceFormatError::InvalidReference(s.to_owned()))
}
}
impl Serialize for CasperChainReference {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.as_str())
}
}
impl<'de> Deserialize<'de> for CasperChainReference {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
impl From<CasperChainReference> for ChainId {
fn from(value: CasperChainReference) -> Self {
Self::new(CASPER_NAMESPACE, value.as_str())
}
}
impl TryFrom<ChainId> for CasperChainReference {
type Error = CasperChainReferenceFormatError;
fn try_from(value: ChainId) -> Result<Self, Self::Error> {
let (namespace, reference) = value.into_parts();
if namespace != CASPER_NAMESPACE {
return Err(CasperChainReferenceFormatError::InvalidNamespace(namespace));
}
Self::from_str(&reference)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum CasperChainReferenceFormatError {
#[error("Invalid namespace {0}, expected casper")]
InvalidNamespace(String),
#[error("Invalid casper chain reference {0}")]
InvalidReference(String),
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum AddressParseError {
#[error("casper address must be 66 hex characters, got {0}")]
InvalidLength(usize),
#[error("unsupported casper key tag {0:?}, expected 00 (account) or 01 (hash)")]
InvalidTag(String),
#[error("casper address is not valid hex: {0}")]
NotHex(#[from] hex::HexDecodeError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum AddressKind {
Account,
Hash,
}
impl AddressKind {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::Account => "00",
Self::Hash => "01",
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Address {
kind: AddressKind,
bytes: [u8; 32],
}
impl Address {
#[must_use]
pub const fn new(kind: AddressKind, bytes: [u8; 32]) -> Self {
Self { kind, bytes }
}
#[must_use]
pub const fn kind(&self) -> AddressKind {
self.kind
}
#[must_use]
pub const fn hash_bytes(&self) -> &[u8; 32] {
&self.bytes
}
#[must_use]
pub fn to_tagged_bytes(self) -> [u8; 33] {
let mut out = [0u8; 33];
out[0] = match self.kind {
AddressKind::Account => 0x00,
AddressKind::Hash => 0x01,
};
out[1..].copy_from_slice(&self.bytes);
out
}
#[must_use]
pub const fn is_account(&self) -> bool {
matches!(self.kind, AddressKind::Account)
}
#[must_use]
pub fn hash_hex(&self) -> String {
hex::encode(&self.bytes)
}
#[must_use]
pub fn is_valid(value: &str) -> bool {
Self::from_str(value).is_ok()
}
}
impl Debug for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "Address({self})")
}
}
impl Display for Address {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.kind.tag())?;
f.write_str(&hex::encode(&self.bytes))
}
}
impl FromStr for Address {
type Err = AddressParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != HASH_HEX_LEN + 2 {
return Err(AddressParseError::InvalidLength(s.len()));
}
let (tag, rest) = s.split_at(2);
let kind = match tag {
"00" => AddressKind::Account,
"01" => AddressKind::Hash,
other => return Err(AddressParseError::InvalidTag(other.to_owned())),
};
let bytes = hex::decode_exact::<32>(rest)?;
Ok(Self { kind, bytes })
}
}
impl Serialize for Address {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for Address {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum PublicKeyParseError {
#[error("unsupported casper public key tag {0:?}, expected 01 (ed25519) or 02 (secp256k1)")]
InvalidTag(String),
#[error("casper {algorithm} public key must be {expected} hex characters, got {actual}")]
InvalidLength {
algorithm: &'static str,
expected: usize,
actual: usize,
},
#[error("casper public key is not valid hex: {0}")]
NotHex(#[from] hex::HexDecodeError),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum KeyAlgorithm {
Ed25519,
Secp256k1,
}
impl KeyAlgorithm {
#[must_use]
pub const fn tag(self) -> &'static str {
match self {
Self::Ed25519 => "01",
Self::Secp256k1 => "02",
}
}
#[must_use]
pub const fn tag_byte(self) -> u8 {
match self {
Self::Ed25519 => 0x01,
Self::Secp256k1 => 0x02,
}
}
#[must_use]
pub const fn name(self) -> &'static str {
match self {
Self::Ed25519 => "ed25519",
Self::Secp256k1 => "secp256k1",
}
}
#[must_use]
pub const fn body_len(self) -> usize {
match self {
Self::Ed25519 => 32,
Self::Secp256k1 => 33,
}
}
}
#[derive(Clone, PartialEq, Eq, Hash)]
pub struct PublicKey {
algorithm: KeyAlgorithm,
body: Vec<u8>,
}
impl PublicKey {
#[must_use]
pub const fn algorithm(&self) -> KeyAlgorithm {
self.algorithm
}
#[must_use]
pub fn body(&self) -> &[u8] {
&self.body
}
#[must_use]
pub fn account_hash(&self) -> Address {
use blake2::Digest as _;
use blake2::digest::consts::U32;
type Blake2b256 = blake2::Blake2b<U32>;
let mut hasher = Blake2b256::new();
hasher.update(self.algorithm.name().as_bytes());
hasher.update([0u8]);
hasher.update(&self.body);
let digest = hasher.finalize();
let mut bytes = [0u8; 32];
bytes.copy_from_slice(&digest);
Address::new(AddressKind::Account, bytes)
}
#[must_use]
pub fn is_valid(value: &str) -> bool {
Self::from_str(value).is_ok()
}
}
impl Debug for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "PublicKey({self})")
}
}
impl Display for PublicKey {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(self.algorithm.tag())?;
f.write_str(&hex::encode(&self.body))
}
}
impl FromStr for PublicKey {
type Err = PublicKeyParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() < 2 {
return Err(PublicKeyParseError::InvalidTag(s.to_owned()));
}
let (tag, body) = s.split_at(2);
let algorithm = match tag {
"01" => KeyAlgorithm::Ed25519,
"02" => KeyAlgorithm::Secp256k1,
other => return Err(PublicKeyParseError::InvalidTag(other.to_owned())),
};
let expected = algorithm.body_len() * 2 + 2;
if s.len() != expected {
return Err(PublicKeyParseError::InvalidLength {
algorithm: algorithm.name(),
expected,
actual: s.len(),
});
}
let body = hex::decode(body)?;
Ok(Self { algorithm, body })
}
}
impl Serialize for PublicKey {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for PublicKey {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ContractPackageHashParseError {
#[error("contract package hash must be 64 hex characters, got {0}")]
InvalidLength(usize),
#[error("contract package hash is not valid hex: {0}")]
NotHex(#[from] hex::HexDecodeError),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct ContractPackageHash([u8; 32]);
impl ContractPackageHash {
#[must_use]
pub const fn new(bytes: [u8; 32]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; 32] {
&self.0
}
#[must_use]
pub fn is_valid(value: &str) -> bool {
Self::from_str(value).is_ok()
}
}
impl Debug for ContractPackageHash {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "ContractPackageHash({self})")
}
}
impl Display for ContractPackageHash {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str(&hex::encode(&self.0))
}
}
impl FromStr for ContractPackageHash {
type Err = ContractPackageHashParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.len() != HASH_HEX_LEN {
return Err(ContractPackageHashParseError::InvalidLength(s.len()));
}
Ok(Self(hex::decode_exact::<32>(s)?))
}
}
impl Serialize for ContractPackageHash {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for ContractPackageHash {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct CasperTokenDeployment {
pub chain_reference: CasperChainReference,
pub address: ContractPackageHash,
pub decimals: u8,
pub name: &'static str,
pub version: &'static str,
}
impl CasperTokenDeployment {
#[must_use]
pub const fn new(
chain_reference: CasperChainReference,
address: ContractPackageHash,
decimals: u8,
name: &'static str,
version: &'static str,
) -> Self {
Self {
chain_reference,
address,
decimals,
name,
version,
}
}
#[must_use]
pub const fn amount(&self, motes: u128) -> DeployedTokenAmount<Motes, Self> {
DeployedTokenAmount {
amount: Motes::new(motes),
token: *self,
}
}
pub fn parse(&self, value: &str) -> Result<DeployedTokenAmount<Motes, Self>, MotesParseError> {
if self.decimals != CSPR_DECIMALS {
return Err(MotesParseError::SubMotePrecision {
digits: usize::from(self.decimals),
});
}
Ok(DeployedTokenAmount {
amount: Motes::from_cspr_str(value)?,
token: *self,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
const ACCOUNT_HEX: &str = "001234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd";
const PACKAGE_HEX: &str = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef";
fn account() -> String {
format!("00{PACKAGE_HEX}")
}
#[test]
fn chain_reference_maps_to_caip2() {
let chain_id: ChainId = CasperChainReference::CASPER.into();
assert_eq!(chain_id.to_string(), "casper:casper");
let testnet: ChainId = CasperChainReference::CASPER_TEST.into();
assert_eq!(testnet.to_string(), "casper:casper-test");
}
#[test]
fn chain_reference_round_trips_through_chain_id() {
for reference in CasperChainReference::ALL {
let chain_id: ChainId = (*reference).into();
assert_eq!(
CasperChainReference::try_from(chain_id).unwrap(),
*reference
);
}
}
#[test]
fn chain_reference_rejects_foreign_namespace() {
let err = CasperChainReference::try_from(ChainId::new("eip155", "casper")).unwrap_err();
assert!(matches!(
err,
CasperChainReferenceFormatError::InvalidNamespace(ref ns) if ns == "eip155"
));
}
#[test]
fn chain_reference_rejects_unknown_network() {
assert!("casper-dev".parse::<CasperChainReference>().is_err());
}
#[test]
fn chain_reference_exposes_chain_name_and_rpc() {
assert_eq!(CasperChainReference::CASPER.chain_name(), "casper");
assert_eq!(
CasperChainReference::CASPER_TEST.chain_name(),
"casper-test"
);
assert!(CasperChainReference::CASPER_TEST.is_testnet());
assert!(!CasperChainReference::CASPER.is_testnet());
assert!(
CasperChainReference::CASPER_TEST
.default_rpc_url()
.contains("testnet")
);
}
#[test]
fn address_parses_account_and_hash_tags() {
let acc: Address = account().parse().unwrap();
assert_eq!(acc.kind(), AddressKind::Account);
assert!(acc.is_account());
assert_eq!(acc.hash_hex(), PACKAGE_HEX);
let hash: Address = format!("01{PACKAGE_HEX}").parse().unwrap();
assert_eq!(hash.kind(), AddressKind::Hash);
assert!(!hash.is_account());
}
#[test]
fn address_rejects_bad_tag_length_and_hex() {
assert!(matches!(
format!("02{PACKAGE_HEX}").parse::<Address>().unwrap_err(),
AddressParseError::InvalidTag(_)
));
assert!(matches!(
ACCOUNT_HEX.parse::<Address>().unwrap_err(),
AddressParseError::InvalidLength(64)
));
assert!(matches!(
format!("00{}", "z".repeat(64))
.parse::<Address>()
.unwrap_err(),
AddressParseError::NotHex(_)
));
assert!(!Address::is_valid(""));
assert!(Address::is_valid(&account()));
}
#[test]
fn address_round_trips_through_serde() {
let address: Address = account().parse().unwrap();
let json = serde_json::to_string(&address).unwrap();
assert_eq!(json, format!("\"{}\"", account()));
assert_eq!(serde_json::from_str::<Address>(&json).unwrap(), address);
}
#[test]
fn public_key_accepts_ed25519_and_secp256k1() {
let ed25519: PublicKey = format!("01{PACKAGE_HEX}").parse().unwrap();
assert_eq!(ed25519.algorithm(), KeyAlgorithm::Ed25519);
assert_eq!(ed25519.body().len(), 32);
let secp: PublicKey = format!("02{PACKAGE_HEX}ab").parse().unwrap();
assert_eq!(secp.algorithm(), KeyAlgorithm::Secp256k1);
assert_eq!(secp.body().len(), 33);
assert_eq!(secp.to_string(), format!("02{PACKAGE_HEX}ab"));
}
#[test]
fn public_key_rejects_wrong_body_length_for_tag() {
let err = format!("02{PACKAGE_HEX}").parse::<PublicKey>().unwrap_err();
assert!(matches!(
err,
PublicKeyParseError::InvalidLength {
algorithm: "secp256k1",
expected: 68,
actual: 66
}
));
assert!(matches!(
format!("01{PACKAGE_HEX}ab")
.parse::<PublicKey>()
.unwrap_err(),
PublicKeyParseError::InvalidLength {
algorithm: "ed25519",
..
}
));
}
#[test]
fn public_key_rejects_account_hash_tag() {
assert!(matches!(
format!("00{PACKAGE_HEX}").parse::<PublicKey>().unwrap_err(),
PublicKeyParseError::InvalidTag(_)
));
assert!(!PublicKey::is_valid("01"));
}
#[test]
fn account_hash_matches_scheme_exact_casper_fixture() {
let public_key: PublicKey =
"020376e4f8766e4f33bcc6e20b331b5163f363dc0106063b052ad38afe08637bd867"
.parse()
.unwrap();
assert_eq!(
public_key.account_hash().to_string(),
"0076d080b4e769f0b29c77fc6472d6e425710840c2f46a4506e5544d2ce34f43a3"
);
}
#[test]
fn contract_package_hash_is_untagged() {
let hash: ContractPackageHash = PACKAGE_HEX.parse().unwrap();
assert_eq!(hash.to_string(), PACKAGE_HEX);
assert_eq!(hash.as_bytes().len(), 32);
assert!(ContractPackageHash::is_valid(PACKAGE_HEX));
assert!(!ContractPackageHash::is_valid(&account()));
}
#[test]
fn deployment_parses_exact_amounts_only() {
let deployment = CasperTokenDeployment::new(
CasperChainReference::CASPER,
PACKAGE_HEX.parse().unwrap(),
CSPR_DECIMALS,
"Wrapped CSPR",
"1",
);
assert_eq!(
deployment.parse("2.5").unwrap().amount.inner(),
2_500_000_000
);
assert_eq!(deployment.amount(7).amount.inner(), 7);
assert!(matches!(
deployment.parse("0.0000000001").unwrap_err(),
MotesParseError::SubMotePrecision { digits: 10 }
));
}
}