diff --git a/cosmos-sdk-proto/src/lib.rs b/cosmos-sdk-proto/src/lib.rs index 1fe95d9b..23ea6961 100644 --- a/cosmos-sdk-proto/src/lib.rs +++ b/cosmos-sdk-proto/src/lib.rs @@ -15,6 +15,8 @@ #![forbid(unsafe_code)] #![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)] +pub use tendermint_proto as tendermint; + /// The version (commit hash) of the Cosmos SDK used when generating this library. pub const COSMOS_SDK_VERSION: &str = include_str!("prost/COSMOS_SDK_COMMIT"); diff --git a/cosmrs/Cargo.toml b/cosmrs/Cargo.toml index 38644c22..e39aa52f 100644 --- a/cosmrs/Cargo.toml +++ b/cosmrs/Cargo.toml @@ -18,6 +18,8 @@ k256 = { version = "0.9", features = ["ecdsa", "sha256"] } prost = "0.7" prost-types = "0.7" rand_core = { version = "0.6", features = ["std"] } +serde = { version = "1", features = ["serde_derive"] } +serde_json = "1" subtle-encoding = { version = "0.5", features = ["bech32-preview"] } tendermint = { version = "0.22", default-features = false, features = ["secp256k1"] } thiserror = "1" diff --git a/cosmrs/src/base.rs b/cosmrs/src/base.rs index 01ecd6a3..e2c40b89 100644 --- a/cosmrs/src/base.rs +++ b/cosmrs/src/base.rs @@ -2,6 +2,7 @@ use crate::{proto, Decimal, Error, Result}; use eyre::WrapErr; +use serde::{de, de::Error as _, ser, Deserialize, Serialize}; use std::{ convert::{TryFrom, TryInto}, fmt, @@ -104,6 +105,20 @@ impl From<&AccountId> for tendermint::account::Id { } } +impl<'de> Deserialize<'de> for AccountId { + fn deserialize>(deserializer: D) -> Result { + String::deserialize(deserializer)? + .parse() + .map_err(D::Error::custom) + } +} + +impl Serialize for AccountId { + fn serialize(&self, serializer: S) -> Result { + self.bech32.serialize(serializer) + } +} + /// Coin defines a token with a denomination and an amount. #[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)] pub struct Coin { diff --git a/cosmrs/src/crypto/public_key.rs b/cosmrs/src/crypto/public_key.rs index 3bad818c..cd9c8b41 100644 --- a/cosmrs/src/crypto/public_key.rs +++ b/cosmrs/src/crypto/public_key.rs @@ -4,7 +4,12 @@ use crate::{prost_ext::MessageExt, proto, AccountId, Error, Result}; use eyre::WrapErr; use prost::Message; use prost_types::Any; -use std::convert::{TryFrom, TryInto}; +use serde::{de, de::Error as _, ser, Deserialize, Serialize}; +use std::{ + convert::{TryFrom, TryInto}, + str::FromStr, +}; +use subtle_encoding::base64; /// Protobuf [`Any`] type URL for Ed25519 public keys const ED25519_TYPE_URL: &str = "/cosmos.crypto.ed25519.PubKey"; @@ -17,6 +22,16 @@ const SECP256K1_TYPE_URL: &str = "/cosmos.crypto.secp256k1.PubKey"; pub struct PublicKey(tendermint::PublicKey); impl PublicKey { + /// Parse public key from Cosmos JSON format. + pub fn from_json(s: &str) -> Result { + Ok(serde_json::from_str::(s)?) + } + + /// Serialize public key as Cosmos JSON. + pub fn to_json(&self) -> String { + serde_json::to_string(&self).expect("JSON serialization error") + } + /// Get the [`AccountId`] for this [`PublicKey`] (if applicable). pub fn account_id(&self, prefix: &str) -> Result { match &self.0 { @@ -28,31 +43,34 @@ impl PublicKey { } } + /// Get the type URL for this [`PublicKey`]. + pub fn type_url(&self) -> &'static str { + match &self.0 { + tendermint::PublicKey::Ed25519(_) => ED25519_TYPE_URL, + tendermint::PublicKey::Secp256k1(_) => SECP256K1_TYPE_URL, + // `tendermint::PublicKey` is `non_exhaustive` + _ => unreachable!("unknown pubic key type"), + } + } + /// Convert this [`PublicKey`] to a Protobuf [`Any`] type. pub fn to_any(&self) -> Result { - match self.0 { - tendermint::PublicKey::Ed25519(_) => { - let proto = proto::cosmos::crypto::secp256k1::PubKey { - key: self.to_bytes(), - }; - - Ok(Any { - type_url: ED25519_TYPE_URL.to_owned(), - value: proto.to_bytes()?, - }) + let value = match self.0 { + tendermint::PublicKey::Ed25519(_) => proto::cosmos::crypto::secp256k1::PubKey { + key: self.to_bytes(), } - tendermint::PublicKey::Secp256k1(_) => { - let proto = proto::cosmos::crypto::secp256k1::PubKey { - key: self.to_bytes(), - }; - - Ok(Any { - type_url: SECP256K1_TYPE_URL.to_owned(), - value: proto.to_bytes()?, - }) + .to_bytes(), + tendermint::PublicKey::Secp256k1(_) => proto::cosmos::crypto::secp256k1::PubKey { + key: self.to_bytes(), } + .to_bytes(), _ => Err(Error::Crypto.into()), - } + }?; + + Ok(Any { + type_url: self.type_url().to_owned(), + value, + }) } /// Serialize this [`PublicKey`] as a byte vector. @@ -85,16 +103,22 @@ impl TryFrom<&Any> for PublicKey { type Error = eyre::Report; fn try_from(any: &Any) -> Result { - match any.type_url.as_str() { + let tm_key = match any.type_url.as_str() { + ED25519_TYPE_URL => { + let proto = proto::cosmos::crypto::ed25519::PubKey::decode(&*any.value)?; + tendermint::PublicKey::from_raw_ed25519(&proto.key) + } SECP256K1_TYPE_URL => { let proto = proto::cosmos::crypto::secp256k1::PubKey::decode(&*any.value)?; tendermint::PublicKey::from_raw_secp256k1(&proto.key) - .map(Into::into) - .ok_or_else(|| Error::Crypto.into()) } - other => Err(Error::Crypto) - .wrap_err_with(|| format!("invalid type URL for public key: {}", other)), - } + other => { + return Err(Error::Crypto) + .wrap_err_with(|| format!("invalid type URL for public key: {}", other)) + } + }; + + tm_key.map(Into::into).ok_or_else(|| Error::Crypto.into()) } } @@ -117,3 +141,106 @@ impl From for tendermint::PublicKey { pk.0 } } + +impl FromStr for PublicKey { + type Err = eyre::Report; + + fn from_str(s: &str) -> Result { + Self::from_json(s) + } +} + +impl ToString for PublicKey { + fn to_string(&self) -> String { + self.to_json() + } +} + +impl<'de> Deserialize<'de> for PublicKey { + fn deserialize>(deserializer: D) -> Result { + PublicKeyJson::deserialize(deserializer)? + .try_into() + .map_err(D::Error::custom) + } +} + +impl Serialize for PublicKey { + fn serialize(&self, serializer: S) -> Result { + PublicKeyJson::from(self).serialize(serializer) + } +} + +/// Serde encoding type for JSON public keys. +/// +/// Uses Protobuf JSON encoding conventions. +#[derive(Deserialize, Serialize)] +struct PublicKeyJson { + /// `@type` field e.g. `/cosmos.crypto.ed25519.PubKey`. + #[serde(rename = "@type")] + type_url: String, + + /// Key data: standard Base64 encoded with padding. + key: String, +} + +impl From for PublicKeyJson { + fn from(public_key: PublicKey) -> PublicKeyJson { + PublicKeyJson::from(&public_key) + } +} + +impl From<&PublicKey> for PublicKeyJson { + fn from(public_key: &PublicKey) -> PublicKeyJson { + let type_url = public_key.type_url().to_owned(); + let key = String::from_utf8(base64::encode(&public_key.to_bytes())).expect("UTF-8 error"); + PublicKeyJson { type_url, key } + } +} + +impl TryFrom for PublicKey { + type Error = eyre::Report; + + fn try_from(json: PublicKeyJson) -> Result { + PublicKey::try_from(&json) + } +} + +impl TryFrom<&PublicKeyJson> for PublicKey { + type Error = eyre::Report; + + fn try_from(json: &PublicKeyJson) -> Result { + let pk_bytes = base64::decode(&json.key)?; + + let tm_key = match json.type_url.as_str() { + ED25519_TYPE_URL => tendermint::PublicKey::from_raw_ed25519(&pk_bytes), + SECP256K1_TYPE_URL => tendermint::PublicKey::from_raw_secp256k1(&pk_bytes), + other => { + return Err(Error::Crypto) + .wrap_err_with(|| format!("invalid public key @type: {}", other)) + } + }; + + tm_key.map(Into::into).ok_or_else(|| Error::Crypto.into()) + } +} + +#[cfg(test)] +mod tests { + use super::PublicKey; + + const EXAMPLE_JSON: &str = "{\"@type\":\"/cosmos.crypto.ed25519.PubKey\",\"key\":\"sEEsVGkXvyewKLWMJbHVDRkBoerW0IIwmj1rHkabtHU=\"}"; + + #[test] + fn json_round_trip() { + let example_key = EXAMPLE_JSON.parse::().unwrap(); + assert_eq!(example_key.type_url(), "/cosmos.crypto.ed25519.PubKey"); + assert_eq!( + example_key.to_bytes().as_slice(), + &[ + 176, 65, 44, 84, 105, 23, 191, 39, 176, 40, 181, 140, 37, 177, 213, 13, 25, 1, 161, + 234, 214, 208, 130, 48, 154, 61, 107, 30, 70, 155, 180, 117 + ] + ); + assert_eq!(EXAMPLE_JSON, example_key.to_string()); + } +}