Skip to content

Commit

Permalink
[WIP] cosmos-tx: MsgSend support w\ integration test
Browse files Browse the repository at this point in the history
Basic support for `MsgSend` with end-to-end integration test.

Adds the following:

- Traits for simplifying Protobuf serialization:
  - `prost_ext::MessageExt` for Protobuf encoding
  - `msg::MsgType` and `msg::MsgProto` for decoding/encoding `Msg`
    types as Protobuf `Any` messages.
- Domain types which model the following:
  - `Coin`: amount to send and a denom
  - `Denom`: name of a particular denomination
  - `Fee`: transaction fees
  - `MsgSend`: transaction message for performing a simple send

Additionally includes an end-to-end test which uses Docker to spawn a
single-node `gaia` and send a transaction.
  • Loading branch information
tony-iqlusion committed Apr 6, 2021
1 parent 4d3db43 commit cc5a5c5
Show file tree
Hide file tree
Showing 14 changed files with 590 additions and 49 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cosmos-tx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ jobs:
profile: minimal
toolchain: ${{ matrix.rust }}
override: true
- run: cargo build --release --all-features # TODO(tarcieri): just test with --all-features
- run: cargo test --release
15 changes: 13 additions & 2 deletions cosmos-tx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ eyre = "0.6"
k256 = { version = "0.7", features = ["ecdsa", "sha256"] }
prost = "0.7"
prost-types = "0.7"
rand_core = "0.5"
rand_core = { version = "0.5", features = ["std"] }
rust_decimal = "1.9"
tendermint = { version = "0.18", features = ["secp256k1"] }
tendermint = { version = "0.18", default-features = false, features = ["secp256k1"] }
tendermint-rpc = { version = "0.18", optional = true, features = ["http-client"] }
thiserror = "1"

[dev-dependencies]
tokio = "1"

[features]
rpc = ["tendermint-rpc"]

[package.metadata.docs.rs]
all-features = true
rustdoc-args = ["--cfg", "docsrs"]
70 changes: 70 additions & 0 deletions cosmos-tx/src/bank.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
//! Bank module support
//!
//! <https://docs.cosmos.network/master/modules/bank/>

use crate::{AccountId, Coin, Error, Msg, MsgType, Result};
use cosmos_sdk_proto::cosmos;
use std::convert::{TryFrom, TryInto};

/// MsgSend represents a message to send coins from one account to another.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct MsgSend {
/// Sender's address.
pub from_address: AccountId,

/// Recipient's address.
pub to_address: AccountId,

/// Amount to send
pub amount: Vec<Coin>,
}

impl MsgType for MsgSend {
fn from_msg(msg: &Msg) -> Result<Self> {
cosmos::bank::v1beta1::MsgSend::from_msg(msg).and_then(TryInto::try_into)
}

fn to_msg(&self) -> Result<Msg> {
cosmos::bank::v1beta1::MsgSend::from(self).to_msg()
}
}

impl TryFrom<cosmos::bank::v1beta1::MsgSend> for MsgSend {
type Error = eyre::Report;

fn try_from(proto: cosmos::bank::v1beta1::MsgSend) -> Result<MsgSend> {
MsgSend::try_from(&proto)
}
}

impl TryFrom<&cosmos::bank::v1beta1::MsgSend> for MsgSend {
type Error = eyre::Report;

fn try_from(proto: &cosmos::bank::v1beta1::MsgSend) -> Result<MsgSend> {
Ok(MsgSend {
from_address: proto.from_address.parse().map_err(|_| Error::Account)?,
to_address: proto.to_address.parse().map_err(|_| Error::Account)?,
amount: proto
.amount
.iter()
.map(TryFrom::try_from)
.collect::<Result<_, _>>()?,
})
}
}

impl From<MsgSend> for cosmos::bank::v1beta1::MsgSend {
fn from(coin: MsgSend) -> cosmos::bank::v1beta1::MsgSend {
cosmos::bank::v1beta1::MsgSend::from(&coin)
}
}

impl From<&MsgSend> for cosmos::bank::v1beta1::MsgSend {
fn from(msg: &MsgSend) -> cosmos::bank::v1beta1::MsgSend {
cosmos::bank::v1beta1::MsgSend {
from_address: msg.from_address.to_string(),
to_address: msg.to_address.to_string(),
amount: msg.amount.iter().map(Into::into).collect(),
}
}
}
82 changes: 82 additions & 0 deletions cosmos-tx/src/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
//! Base functionality.

use crate::{Decimal, Error, Result};
use cosmos_sdk_proto::cosmos;
use std::{
convert::TryFrom,
fmt::{self, Display},
str::FromStr,
};

/// Coin defines a token with a denomination and an amount.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Coin {
/// Denomination
pub denom: Denom,

/// Amount
pub amount: Decimal,
}

impl TryFrom<cosmos::base::v1beta1::Coin> for Coin {
type Error = eyre::Report;

fn try_from(proto: cosmos::base::v1beta1::Coin) -> Result<Coin> {
Coin::try_from(&proto)
}
}

impl TryFrom<&cosmos::base::v1beta1::Coin> for Coin {
type Error = eyre::Report;

fn try_from(proto: &cosmos::base::v1beta1::Coin) -> Result<Coin> {
Ok(Coin {
denom: proto.denom.parse()?,
amount: proto.amount.parse()?,
})
}
}

impl From<Coin> for cosmos::base::v1beta1::Coin {
fn from(coin: Coin) -> cosmos::base::v1beta1::Coin {
cosmos::base::v1beta1::Coin::from(&coin)
}
}

impl From<&Coin> for cosmos::base::v1beta1::Coin {
fn from(coin: &Coin) -> cosmos::base::v1beta1::Coin {
cosmos::base::v1beta1::Coin {
denom: coin.denom.to_string(),
amount: coin.amount.to_string(),
}
}
}

/// Denomination.
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct Denom(String);

impl AsRef<str> for Denom {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}

impl Display for Denom {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_ref())
}
}

impl FromStr for Denom {
type Err = eyre::Report;

fn from_str(s: &str) -> Result<Self> {
// TODO(tarcieri): ensure this is the proper validation for a denom name
if s.chars().all(|c| matches!(c, 'a'..='z')) {
Ok(Denom(s.to_owned()))
} else {
Err(Error::Denom.into())
}
}
}
45 changes: 15 additions & 30 deletions cosmos-tx/src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
// Licensed under the Apache 2.0 license

use super::msg::Msg;
use crate::SigningKey;
use crate::{prost_ext::MessageExt, tx::TxRaw, Fee, SigningKey};
use cosmos_sdk_proto::cosmos::tx::v1beta1::{
mode_info, AuthInfo, Fee, ModeInfo, SignDoc, SignerInfo, TxBody, TxRaw,
mode_info, AuthInfo, ModeInfo, SignDoc, SignerInfo, TxBody,
};
use eyre::Result;
use tendermint::{block, chain};
Expand Down Expand Up @@ -50,7 +50,7 @@ impl Builder {
fee: Fee,
memo: impl Into<String>,
timeout_height: block::Height,
) -> Result<Vec<u8>> {
) -> Result<TxRaw> {
// Create TxBody
let body = TxBody {
messages: messages.iter().map(|msg| msg.0.clone()).collect(),
Expand All @@ -60,13 +60,7 @@ impl Builder {
non_critical_extension_options: Default::default(),
};

// A protobuf serialization of a TxBody
let mut body_buf = Vec::new();
prost::Message::encode(&body, &mut body_buf).unwrap();

let pk = signing_key.public_key();
let mut pk_buf = Vec::new();
prost::Message::encode(&pk.as_bytes().to_vec(), &mut pk_buf).unwrap();

// TODO(tarcieri): extract proper key type
let pk_any = prost_types::Any {
Expand All @@ -88,36 +82,27 @@ impl Builder {

let auth_info = AuthInfo {
signer_infos: vec![signer_info],
fee: Some(fee),
fee: Some(fee.into()),
};

// Protobuf serialization of `AuthInfo`
let mut auth_buf = Vec::new();
prost::Message::encode(&auth_info, &mut auth_buf)?;
let body_bytes = body.to_bytes()?;
let auth_info_bytes = auth_info.to_bytes()?;

let sign_doc = SignDoc {
body_bytes: body_buf.clone(),
auth_info_bytes: auth_buf.clone(),
body_bytes: body_bytes.clone(),
auth_info_bytes: auth_info_bytes.clone(),
chain_id: self.chain_id.to_string(),
account_number: self.account_number,
};

// Protobuf serialization of `SignDoc`
let mut signdoc_buf = Vec::new();
prost::Message::encode(&sign_doc, &mut signdoc_buf)?;

// Sign the signdoc
let signed = signing_key.sign(&signdoc_buf)?;
let sign_doc_bytes = sign_doc.to_bytes()?;
let signed = signing_key.sign(&sign_doc_bytes)?;

let tx_raw = TxRaw {
body_bytes: body_buf,
auth_info_bytes: auth_buf,
Ok(cosmos_sdk_proto::cosmos::tx::v1beta1::TxRaw {
body_bytes,
auth_info_bytes,
signatures: vec![signed.as_ref().to_vec()],
};

let mut txraw_buf = Vec::new();
prost::Message::encode(&tx_raw, &mut txraw_buf)?;

Ok(txraw_buf)
}
.into())
}
}
24 changes: 21 additions & 3 deletions cosmos-tx/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,28 @@ pub use eyre::{Report, Result};

use thiserror::Error;

/// Kinds of errors
#[derive(Copy, Clone, Debug, Error, Eq, PartialEq)]
/// Kinds of errors.
#[derive(Clone, Debug, Error, Eq, PartialEq)]
pub enum Error {
/// Invalid decimal value
/// Invalid account.
#[error("invalid account")]
Account,

/// Invalid decimal value.
#[error("invalid decimal value")]
Decimal,

/// Invalid denomination.
#[error("invalid denomination")]
Denom,

/// Unexpected message type.
#[error("unexpected Msg type: {found:?}, expected {expected:?}")]
MsgType {
/// Expected type URL.
expected: &'static str,

/// Actual type URL found in the [`prost_types::Any`] message.
found: String,
},
}
19 changes: 17 additions & 2 deletions cosmos-tx/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,20 +1,35 @@
//! Transaction builder and signer for Cosmos-based blockchains

#![cfg_attr(docsrs, feature(doc_cfg))]
#![doc(
html_logo_url = "https://raw.githubusercontent.com/cosmos/cosmos-rust/main/.images/cosmos.png",
html_root_url = "https://docs.rs/cosmos-sdk-proto/0.2.0-pre"
)]
#![forbid(unsafe_code)]
#![warn(trivial_casts, trivial_numeric_casts, unused_import_braces)]

pub mod bank;
pub mod tx;

mod base;
mod builder;
mod decimal;
mod error;
mod msg;
mod prost_ext;
mod signing_key;

pub use crate::{
builder::Builder, decimal::Decimal, error::Error, msg::Msg, signing_key::SigningKey,
base::{Coin, Denom},
builder::Builder,
decimal::Decimal,
error::{Error, Result},
msg::{Msg, MsgType},
signing_key::SigningKey,
tx::Fee,
};
pub use k256::ecdsa::{Signature, VerifyingKey};
pub use tendermint::PublicKey;
pub use tendermint::{self, account::Id as AccountId, PublicKey};

#[cfg(feature = "rpc")]
pub use tendermint_rpc as rpc;
47 changes: 47 additions & 0 deletions cosmos-tx/src/msg.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
//! Transaction messages

use crate::{prost_ext::MessageExt, Error, Result};
use cosmos_sdk_proto::cosmos;
use prost_types::Any;

/// Transaction messages
Expand All @@ -26,3 +28,48 @@ impl From<Msg> for Any {
msg.0
}
}

/// Message types that can be converted to/from a [`Msg`].
pub trait MsgType {
/// Attempt to parse this value from a [`Msg`].
fn from_msg(msg: &Msg) -> Result<Self>
where
Self: Sized;

/// Serialize this value as a [`Msg`].
fn to_msg(&self) -> Result<Msg>;
}

/// Proto types which can be used as a [`Msg`].
pub trait MsgProto: Default + MessageExt {
/// Type URL value
const TYPE_URL: &'static str;
}

impl<T> MsgType for T
where
T: MsgProto,
{
fn from_msg(msg: &Msg) -> Result<Self>
where
Self: Sized,
{
if msg.0.type_url == Self::TYPE_URL {
Ok(Self::decode(&*msg.0.value)?)
} else {
Err(Error::MsgType {
expected: Self::TYPE_URL,
found: msg.0.type_url.clone(),
}
.into())
}
}

fn to_msg(&self) -> Result<Msg> {
self.to_bytes().map(|bytes| Msg::new(Self::TYPE_URL, bytes))
}
}

impl MsgProto for cosmos::bank::v1beta1::MsgSend {
const TYPE_URL: &'static str = "/cosmos.bank.v1beta1.MsgSend";
}
Loading

0 comments on commit cc5a5c5

Please sign in to comment.