Skip to content

Commit

Permalink
cosmos-tx: MsgSend support w\ integration test (#71)
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:
  - `Body`: body of a transaction
  - `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 authored Apr 9, 2021
1 parent 3c6ead6 commit 386d7e0
Show file tree
Hide file tree
Showing 18 changed files with 1,041 additions and 190 deletions.
1 change: 1 addition & 0 deletions .github/workflows/cosmos-tx.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,3 +53,4 @@ jobs:
toolchain: ${{ matrix.rust }}
override: true
- run: cargo test --release
- run: cargo test --release --all-features
17 changes: 14 additions & 3 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"
rust_decimal = "1.9"
tendermint = { version = "0.19", features = ["secp256k1"] }
rand_core = { version = "0.5", features = ["std"] }
subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
tendermint = { version = "0.19", default-features = false, features = ["secp256k1"] }
tendermint-rpc = { version = "0.19", 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"]
73 changes: 73 additions & 0 deletions cosmos-tx/src/bank.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
//! Bank module support
//!
//! <https://docs.cosmos.network/master/modules/bank/>
use crate::{
tx::{Msg, MsgType},
AccountId, Coin, 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()?,
to_address: proto.to_address.parse()?,
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(),
}
}
}
171 changes: 171 additions & 0 deletions cosmos-tx/src/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
//! Base functionality.
use crate::{Decimal, Error, Result};
use cosmos_sdk_proto::cosmos;
use std::{
convert::{TryFrom, TryInto},
fmt,
str::FromStr,
};
use subtle_encoding::bech32;

/// Account identifiers
#[derive(Clone, Eq, PartialEq, PartialOrd, Ord)]
pub struct AccountId {
/// Account ID encoded as Bech32
bech32: String,

/// Length of the human-readable prefix of the address
hrp_length: usize,
}

impl AccountId {
/// Create an [`AccountId`] with the given human-readable prefix and
/// public key hash.
pub fn new(prefix: &str, bytes: [u8; tendermint::account::LENGTH]) -> Result<Self> {
let id = bech32::encode(prefix, &bytes);

// TODO(tarcieri): ensure this is the proper validation for an account prefix
if prefix.chars().all(|c| matches!(c, 'a'..='z')) {
Ok(Self {
bech32: id,
hrp_length: prefix.len(),
})
} else {
Err(Error::AccountId { id }.into())
}
}

/// Get the human-readable prefix of this account.
pub fn prefix(&self) -> &str {
&self.bech32[..self.hrp_length]
}

/// Decode an account ID from Bech32 to an inner byte value.
pub fn to_bytes(&self) -> [u8; tendermint::account::LENGTH] {
bech32::decode(&self.bech32)
.ok()
.and_then(|result| result.1.try_into().ok())
.expect("malformed Bech32 AccountId")
}
}

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

impl fmt::Debug for AccountId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("AccountId").field(&self.as_ref()).finish()
}
}

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

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

fn from_str(s: &str) -> Result<Self> {
let (hrp, bytes) = bech32::decode(s)?;

if bytes.len() == tendermint::account::LENGTH {
Ok(Self {
bech32: s.to_owned(),
hrp_length: hrp.len(),
})
} else {
Err(Error::AccountId { id: s.to_owned() }.into())
}
}
}

impl From<AccountId> for tendermint::account::Id {
fn from(id: AccountId) -> tendermint::account::Id {
tendermint::account::Id::from(&id)
}
}

impl From<&AccountId> for tendermint::account::Id {
fn from(id: &AccountId) -> tendermint::account::Id {
tendermint::account::Id::new(id.to_bytes())
}
}

/// 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 fmt::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 { name: s.to_owned() }.into())
}
}
}
Loading

0 comments on commit 386d7e0

Please sign in to comment.