From 8039d7afddcd08de852610df9af2cbda30484c07 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Sat, 21 Oct 2023 18:50:19 -0500 Subject: [PATCH 1/3] ci: Exclude prost types from nibiru-std/src/proto/buf from coverage + fmt --- .github/workflows/rust-test.yml | 29 +++++++++++++++++------------ Cargo.lock | 1 + contracts/bindings-perp/src/msg.rs | 2 +- contracts/nibi-stargate/README.md | 4 ++-- justfile | 8 ++++++++ nibiru-std/Cargo.toml | 1 + 6 files changed, 30 insertions(+), 15 deletions(-) diff --git a/.github/workflows/rust-test.yml b/.github/workflows/rust-test.yml index e7417c0..b9fee50 100644 --- a/.github/workflows/rust-test.yml +++ b/.github/workflows/rust-test.yml @@ -17,11 +17,11 @@ jobs: rust-test: runs-on: ubuntu-latest steps: - - name: Checkout branch + - name: "Checkout branch" uses: actions/checkout@v4 with: submodules: true - - name: Set up Rust caches + - name: "Set up Rust caches" uses: actions/cache@v3 id: rust-cache with: @@ -32,7 +32,7 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} - - name: Run tests + - name: "Run tests" # Test all packages run: cargo test --all --verbose # Test single package (e.g. contracts/whitelist) @@ -41,11 +41,11 @@ jobs: rust-build: runs-on: ubuntu-latest steps: - - name: Checkout branch + - name: "Checkout branch" uses: actions/checkout@v4 with: submodules: true - - name: Set up Rust caches + - name: "Set up Rust caches" uses: actions/cache@v3 id: rust-cache with: @@ -56,7 +56,7 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} - - name: Compile all Rust code in workspace + - name: "Compile all Rust code in workspace" run: cargo build coverage: @@ -67,7 +67,7 @@ jobs: - uses: actions/checkout@v4 with: submodules: true - - name: Set up Rust caches + - name: "Set up Rust caches" uses: actions/cache@v3 id: rust-cache with: @@ -78,18 +78,23 @@ jobs: ~/.cargo/git/db/ target/ key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} - - name: Install stable + - name: "Install stable rust-toolchain" uses: dtolnay/rust-toolchain@stable with: components: llvm-tools-preview - - name: cargo install cargo-llvm-cov + - name: "cargo install cargo-llvm-cov" uses: taiki-e/install-action@cargo-llvm-cov - - name: cargo generate-lockfile + - name: "cargo generate-lockfile" if: hashFiles('Cargo.lock') == '' run: cargo generate-lockfile - - name: cargo llvm-cov + + - name: "Install just" + # casey/just: https://just.systems/man/en/chapter_6.html + # taiki-e/install-action: https://github.com/taiki-e/install-action + uses: taiki-e/install-action@just + - name: "cargo llvm-cov" # Outputs the code coverage in a format that codecov.io can understand - run: cargo llvm-cov --lcov --output-path lcov.info + run: just test-coverage # run: cargo llvm-cov --locked --all-features --lcov --output-path lcov.info - name: Upload to codecov.io uses: codecov/codecov-action@v3 diff --git a/Cargo.lock b/Cargo.lock index 01c2dd5..98c6c33 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1497,6 +1497,7 @@ dependencies = [ "prost", "prost-types", "serde", + "serde_json", "thiserror", ] diff --git a/contracts/bindings-perp/src/msg.rs b/contracts/bindings-perp/src/msg.rs index afb7df3..2046662 100644 --- a/contracts/bindings-perp/src/msg.rs +++ b/contracts/bindings-perp/src/msg.rs @@ -229,4 +229,4 @@ impl CustomQuery for QueryMsg {} #[cw_serde] pub struct SudoersQueryResponse { pub sudoers: Sudoers, -} \ No newline at end of file +} diff --git a/contracts/nibi-stargate/README.md b/contracts/nibi-stargate/README.md index 66a5df3..d39d1c1 100644 --- a/contracts/nibi-stargate/README.md +++ b/contracts/nibi-stargate/README.md @@ -34,7 +34,7 @@ Table of Contents ```json { "mint": { - "coin": { "amount": "[amount]", "denom": "tc/[contract-addr]/[subdenom]" }, + "coin": { "amount": "[amount]", "denom": "tf/[contract-addr]/[subdenom]" }, "mint_to": "[mint-to-addr]" } } @@ -45,7 +45,7 @@ Table of Contents ```json { "burn": { - "coin": { "amount": "[amount]", "denom": "tc/[contract-addr]/[subdenom]" }, + "coin": { "amount": "[amount]", "denom": "tf/[contract-addr]/[subdenom]" }, "burn_from": "[burn-from-addr]" } } diff --git a/justfile b/justfile index 5136130..9e893cb 100644 --- a/justfile +++ b/justfile @@ -6,7 +6,10 @@ setup: just -l install: + # https://crates.io/crates/clippy rustup component add clippy + # https://crates.io/crates/cargo-llvm-cov + cargo install cargo-llvm-cov wasm-all: bash scripts/wasm-out.sh @@ -61,6 +64,11 @@ test *pkg: test-all: cargo test +# Test everything and output coverage report. +test-coverage: + cargo llvm-cov --lcov --output-path lcov.info \ + --ignore-filename-regex .*buf\/[^\/]+\.rs$ + # Format, lint, and test tidy: just fmt diff --git a/nibiru-std/Cargo.toml b/nibiru-std/Cargo.toml index 3aafbeb..aef1623 100644 --- a/nibiru-std/Cargo.toml +++ b/nibiru-std/Cargo.toml @@ -28,6 +28,7 @@ serde = "1.0.188" anyhow = "1" thiserror = "1.0.49" nibiru-macro = { path = "../packages/macro" } +serde_json = "1.0" # cargo run --bin script-name # [[bin]] From bfff63034b2ba7770b26f9458ba6c1bbb8093c23 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Sat, 21 Oct 2023 18:51:40 -0500 Subject: [PATCH 2/3] feat(nibiru-std): implement NibiruStargateQuery as an extension of prost::Nme --- nibiru-std/src/errors.rs | 12 ++++ nibiru-std/src/proto/mod.rs | 2 + nibiru-std/src/proto/traits.rs | 66 +++++++++++++++++++-- nibiru-std/src/proto/type_url_cosmos.rs | 79 +++++++++++++++++++++++++ nibiru-std/src/proto/type_url_nibiru.rs | 31 +++++++--- 5 files changed, 178 insertions(+), 12 deletions(-) create mode 100644 nibiru-std/src/proto/type_url_cosmos.rs diff --git a/nibiru-std/src/errors.rs b/nibiru-std/src/errors.rs index b90b9de..823fc33 100644 --- a/nibiru-std/src/errors.rs +++ b/nibiru-std/src/errors.rs @@ -2,6 +2,12 @@ use thiserror::Error; use cosmwasm_std as cw; +/// Shorthand for an empty anyhow::Result. Useful for idiomatic tests. +#[cfg(test)] +pub type TestResult = anyhow::Result<()>; + +pub type NibiruResult = Result; + #[derive(Error, Debug, PartialEq)] pub enum NibiruError { #[error("{0}")] @@ -9,6 +15,12 @@ pub enum NibiruError { #[error("no prost::Name implementation for type {}, where prost::Name.type_url() is needed.", type_name)] NoTypeUrl { type_name: String }, + + #[error("prost::Name::type_url {} does not correspond to a QueryRequest::Stargate path.", type_url)] + ProstNameisNotQuery { type_url: String }, + + #[error("prost::Name::type_url {} does not correspond to a CosmosMsg::Stargate type_url.", type_url)] + ProstNameisNotMsg { type_url: String }, } impl From for cw::StdError { diff --git a/nibiru-std/src/proto/mod.rs b/nibiru-std/src/proto/mod.rs index 96109b5..2ef90f6 100644 --- a/nibiru-std/src/proto/mod.rs +++ b/nibiru-std/src/proto/mod.rs @@ -1,7 +1,9 @@ mod traits; +mod type_url_cosmos; mod type_url_nibiru; pub use traits::*; +pub use type_url_cosmos::*; pub use type_url_nibiru::*; pub mod cosmos { diff --git a/nibiru-std/src/proto/traits.rs b/nibiru-std/src/proto/traits.rs index 3f0d20e..826579b 100644 --- a/nibiru-std/src/proto/traits.rs +++ b/nibiru-std/src/proto/traits.rs @@ -1,7 +1,9 @@ //! nibiru-std::proto - traits.rs : Implements extensions for prost::Message //! types for easy conversion to types needed for CosmWasm smart contracts. -use cosmwasm_std::{Binary, CosmosMsg}; +use cosmwasm_std::{Binary, CosmosMsg, QueryRequest}; + +use crate::errors::{NibiruError, NibiruResult}; pub trait NibiruProstMsg: prost::Message { /// Serialize this protobuf message as a byte vector @@ -60,10 +62,66 @@ where } } - /// Workaround for backwards type_url implementation. - /// This is fixed in a future version of prost but isn't yet available in - /// v0.12.1. See https://github.com/tokio-rs/prost/pull/923 + /// The "type URL" in the context of protobuf is used with a feature + /// called "Any", a type that allows one to serialize and embed proto + /// message (prost::Message) objects without as opaque values without having + /// to predefine the type in the original message declaration. + /// + /// For example, a protobuf definition like: + /// ```proto + /// message CustomProtoMsg { string name = 1; } + /// ``` + /// might have a type URL like "googleapis.com/package.name.CustomProtoMsg". + /// Usage of `Any` with type URLs enables dynamic message composition and + /// flexibility. + /// + /// We use these type URLs in CosmWasm and the Cosmos-SDK to classify + /// gRPC messages for transactions and queries because Tendermint ABCI + /// messages are protobuf objects. fn type_url(&self) -> String { format!("/{}.{}", Self::PACKAGE, Self::NAME) } } + +pub trait NibiruStargateQuery: prost::Message + prost::Name { + #![allow(clippy::wrong_self_convention)] + fn into_stargate_query( + &self, + ) -> NibiruResult>; + + fn path(&self) -> String; +} + +impl NibiruStargateQuery for M +where + M: prost::Message + prost::Name, +{ + /// Returns the `prost::Message` as a `QueryRequest::Stargate` object. + /// Errors if the `prost::Name::type_url` does not indicate the type is a + /// query. + fn into_stargate_query( + &self, + ) -> NibiruResult> { + if !self.type_url().contains("Query") { + return Err(NibiruError::ProstNameisNotQuery { + type_url: self.type_url(), + }); + } + Ok(QueryRequest::Stargate { + path: self.path(), + data: self.to_binary(), + }) + } + + /// Fully qualified gRPC service path used for routing. + /// Ex.: "/cosmos.bank.v1beta1.Query/SupplyOf" + fn path(&self) -> String { + let service_name = format!( + "Query/{}", + Self::NAME + .trim_start_matches("Query") + .trim_end_matches("Request") + ); + format!("/{}.{}", Self::PACKAGE, service_name) + } +} diff --git a/nibiru-std/src/proto/type_url_cosmos.rs b/nibiru-std/src/proto/type_url_cosmos.rs new file mode 100644 index 0000000..dce5c9b --- /dev/null +++ b/nibiru-std/src/proto/type_url_cosmos.rs @@ -0,0 +1,79 @@ +//! Implements the prost::Name trait for cosmos protobuf types, which defines +//! the prost::Message.type_url function needed for CosmWasm smart contracts. + +use prost::Name; + +use crate::proto::cosmos; + +const PACKAGE_BANK: &str = "cosmos.bank.v1beta1"; + +impl Name for cosmos::bank::v1beta1::QuerySupplyOfRequest { + const NAME: &'static str = "QuerySupplyOfRequest"; + const PACKAGE: &'static str = PACKAGE_BANK; +} + +impl Name for cosmos::bank::v1beta1::QueryBalanceRequest { + const NAME: &'static str = "QueryBalanceRequest"; + const PACKAGE: &'static str = PACKAGE_BANK; +} + +impl Name for cosmos::bank::v1beta1::QueryAllBalancesRequest { + const NAME: &'static str = "QueryAllBalancesRequest"; + const PACKAGE: &'static str = PACKAGE_BANK; +} + +impl Name for cosmos::bank::v1beta1::QueryDenomMetadataRequest { + const NAME: &'static str = "QueryDenomMetadataRequest"; + const PACKAGE: &'static str = PACKAGE_BANK; +} + +#[cfg(test)] +mod tests { + + use cosmwasm_std::{Empty, QueryRequest}; + + use crate::{ + errors::{NibiruResult, TestResult}, + proto::{cosmos, NibiruStargateQuery}, + }; + + #[test] + fn stargate_query_conversion() -> TestResult { + let test_cases: Vec<(&str, NibiruResult>)> = vec![ + ( + "/cosmos.bank.v1beta1.Query/SupplyOf", + cosmos::bank::v1beta1::QuerySupplyOfRequest { + denom: String::from("some_denom"), + } + .into_stargate_query(), + ), + ( + "/cosmos.bank.v1beta1.Query/Balance", + cosmos::bank::v1beta1::QueryBalanceRequest { + address: String::from("some_address"), + denom: String::from("some_denom"), + } + .into_stargate_query(), + ), + ( + "/cosmos.bank.v1beta1.Query/DenomMetadata", + cosmos::bank::v1beta1::QueryDenomMetadataRequest { + denom: String::from("some_denom"), + } + .into_stargate_query(), + ), + ]; + + for test_case in test_cases { + let test_case_path = test_case.0; + let pb_query = test_case.1?; + if let QueryRequest::Stargate { path, data: _data } = &pb_query { + assert_eq!(test_case_path, path) + } else { + panic!("failed test on case: {:#?} ", test_case_path) + } + } + + Ok(()) + } +} diff --git a/nibiru-std/src/proto/type_url_nibiru.rs b/nibiru-std/src/proto/type_url_nibiru.rs index 8027797..98f8a61 100644 --- a/nibiru-std/src/proto/type_url_nibiru.rs +++ b/nibiru-std/src/proto/type_url_nibiru.rs @@ -37,19 +37,34 @@ impl Name for nibiru::tokenfactory::MsgSetDenomMetadata { const PACKAGE: &'static str = PACKAGE_TOKENFACTORY; } -#[cfg(test)] -mod tests { +const PACKAGE_EPOCHS: &str = "nibiru.epochs.v1"; + +impl Name for nibiru::epochs::QueryEpochsInfoRequest { + const NAME: &'static str = "QueryEpochsInfoRequest"; + const PACKAGE: &'static str = PACKAGE_EPOCHS; +} - use crate::proto::{ - cosmos, - nibiru::{self, tokenfactory::MsgMint}, - NibiruProstMsg, NibiruStargateMsg, +impl Name for nibiru::epochs::QueryCurrentEpochRequest { + const NAME: &'static str = "QueryCurrentEpochRequest"; + const PACKAGE: &'static str = PACKAGE_EPOCHS; +} + +#[cfg(test)] +pub mod tests { + + use crate::{ + errors::TestResult, + proto::{ + cosmos, + nibiru::{self, tokenfactory::MsgMint}, + NibiruProstMsg, NibiruStargateMsg, + }, }; use cosmwasm_std as cw; #[test] - fn stargate_tokenfactory() -> anyhow::Result<()> { + fn stargate_tokenfactory() -> TestResult { let mut _pb: cw::CosmosMsg; let pb_msg: MsgMint = nibiru::tokenfactory::MsgMint::default(); _pb = pb_msg.into_stargate_msg(); @@ -84,7 +99,7 @@ mod tests { /// // which outputs "[10 6 115 101 110 100 101 114 18 8 115 117 98 100 101 110 111 109]" /// ``` #[test] - fn stargate_encoding() -> anyhow::Result<()> { + fn stargate_encoding() -> TestResult { let test_cases: Vec<(Box, Vec)> = vec![ ( Box::new(nibiru::tokenfactory::MsgCreateDenom { From 8b6f466b5f76ca587cc9c0b25ed4d1113aa34ff9 Mon Sep 17 00:00:00 2001 From: Unique-Divine Date: Sat, 21 Oct 2023 19:37:05 -0500 Subject: [PATCH 3/3] test(nibiru-std): verify that CosmosMsg::Stargate types error when converted to QueryRequest::Stargate --- nibiru-std/src/proto/type_url_nibiru.rs | 72 ++++++++++++++++++------- 1 file changed, 52 insertions(+), 20 deletions(-) diff --git a/nibiru-std/src/proto/type_url_nibiru.rs b/nibiru-std/src/proto/type_url_nibiru.rs index 98f8a61..a1e9541 100644 --- a/nibiru-std/src/proto/type_url_nibiru.rs +++ b/nibiru-std/src/proto/type_url_nibiru.rs @@ -56,33 +56,65 @@ pub mod tests { errors::TestResult, proto::{ cosmos, - nibiru::{self, tokenfactory::MsgMint}, - NibiruProstMsg, NibiruStargateMsg, + nibiru::{self}, + NibiruProstMsg, NibiruStargateMsg, NibiruStargateQuery, }, }; use cosmwasm_std as cw; #[test] - fn stargate_tokenfactory() -> TestResult { - let mut _pb: cw::CosmosMsg; - let pb_msg: MsgMint = nibiru::tokenfactory::MsgMint::default(); - _pb = pb_msg.into_stargate_msg(); - if let cw::CosmosMsg::Stargate { - type_url, - value: _value, - } = _pb - { - println!("full_name: {}", pb_msg.type_url()); - assert_eq!(type_url, "/nibiru.tokenfactory.v1.MsgMint") + fn stargate_tokenfactory_msgs() -> TestResult { + let test_cases: Vec<(&str, cw::CosmosMsg)> = vec![ + ( + "/nibiru.tokenfactory.v1.MsgMint", + nibiru::tokenfactory::MsgMint::default().into_stargate_msg(), + ), + ( + "/nibiru.tokenfactory.v1.MsgBurn", + nibiru::tokenfactory::MsgBurn::default().into_stargate_msg(), + ), + ( + "/nibiru.tokenfactory.v1.MsgChangeAdmin", + nibiru::tokenfactory::MsgChangeAdmin::default() + .into_stargate_msg(), + ), + ( + "/nibiru.tokenfactory.v1.MsgSetDenomMetadata", + nibiru::tokenfactory::MsgSetDenomMetadata::default() + .into_stargate_msg(), + ), + ( + "/nibiru.tokenfactory.v1.MsgUpdateModuleParams", + nibiru::tokenfactory::MsgUpdateModuleParams::default() + .into_stargate_msg(), + ), + ]; + + for test_case in test_cases { + let (tc_type_url, stargate_msg) = test_case; + if let cw::CosmosMsg::Stargate { + type_url, + value: _value, + } = stargate_msg.clone() + { + assert_eq!(tc_type_url, type_url) + } else { + panic!( + "Expected CosmosMsg::Stargate from CosmosMsg: {:#?}", + stargate_msg + ) + } } - _pb = nibiru::tokenfactory::MsgBurn::default().into_stargate_msg(); - _pb = - nibiru::tokenfactory::MsgChangeAdmin::default().into_stargate_msg(); - _pb = nibiru::tokenfactory::MsgUpdateModuleParams::default() - .into_stargate_msg(); - _pb = nibiru::tokenfactory::MsgSetDenomMetadata::default() - .into_stargate_msg(); + + println!( + "prost::Name corresponding to a CosmosMsg should error if we \ + try converting it to QueryRequest::Stargate" + ); + let pb_msg = nibiru::tokenfactory::MsgSetDenomMetadata::default(); + pb_msg + .into_stargate_query() + .expect_err("query is not a Msg"); Ok(()) }