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 fbcd9e8
Show file tree
Hide file tree
Showing 15 changed files with 780 additions and 60 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
16 changes: 14 additions & 2 deletions cosmos-tx/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,19 @@ 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"] }
subtle-encoding = { version = "0.5", features = ["bech32-preview"] }
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, 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()?,
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(),
}
}
}
182 changes: 182 additions & 0 deletions cosmos-tx/src/base.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
//! Base functionality.
use crate::{Decimal, Error, PublicKey, 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())
}
}

/// Create an [`AccountId`] for the given [`PublicKey`]
// TODO(tarcieri): extract our own public key type and move this method there
pub fn for_public_key(key: &PublicKey, prefix: &str) -> Result<Self> {
let account_id = match key {
PublicKey::Secp256k1(encoded_point) => tendermint::account::Id::from(*encoded_point),
_ => return Err(Error::Crypto.into()),
};

AccountId::new(prefix, account_id.as_bytes().try_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_proto_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())
}
}
}
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())
}
}
Loading

0 comments on commit fbcd9e8

Please sign in to comment.