diff --git a/CHANGELOG.md b/CHANGELOG.md index f46ce5bbe3..2a859fa4c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,10 +13,12 @@ and this project adheres to - cosmwasm-std: Make `abs_diff` const for `Uint{256,512}` and `Int{64,128,256,512}`. It is now const for all integer types. - cosmwasm-std: Implement `TryFrom` for `Decimal` ([#1832]) +- cosmwasm-std: Add `StdAck`. ([#1512]) - cosmwasm-std: Add new imports `db_next_{key, value}` for iterating storage keys / values only and make `Storage::{range_keys, range_values}` more efficient. This requires the `cosmwasm_1_4` feature to be enabled. ([#1834]) +[#1512]: https://github.com/CosmWasm/cosmwasm/issues/1512 [#1799]: https://github.com/CosmWasm/cosmwasm/pull/1799 [#1806]: https://github.com/CosmWasm/cosmwasm/pull/1806 [#1832]: https://github.com/CosmWasm/cosmwasm/pull/1832 diff --git a/Cargo.lock b/Cargo.lock index aa30b05347..4151125ce1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "addr2line" version = "0.19.0" @@ -776,7 +786,7 @@ dependencies = [ "ident_case", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -787,7 +797,7 @@ checksum = "29a358ff9f12ec09c3e61fef9b5a9902623a695a46a917b07f269bff1445611a" dependencies = [ "darling_core", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -987,7 +997,7 @@ dependencies = [ "darling", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -1126,6 +1136,18 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "go-gen" +version = "0.1.0" +dependencies = [ + "Inflector", + "anyhow", + "cosmwasm-schema", + "cosmwasm-std", + "indenter", + "schemars", +] + [[package]] name = "group" version = "0.13.0" @@ -1213,6 +1235,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -1578,9 +1606,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.57" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4ec6d5fe0b140acb27c9a0444118cf55bfbb4e0b259739429abb4521dd67c16" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" dependencies = [ "unicode-ident", ] @@ -1607,9 +1635,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.27" +version = "1.0.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f4f29d145265ec1c483c7c654450edde0bfe043d3938d6972630663356d9500" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" dependencies = [ "proc-macro2", ] @@ -1884,9 +1912,9 @@ checksum = "bebd363326d05ec3e2f532ab7660680f3b02130d780c299bca73469d521bc0ed" [[package]] name = "serde" -version = "1.0.163" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2113ab51b87a539ae008b5c6c02dc020ffa39afd2d83cffcb3f4eb2722cebec2" +checksum = "0ea67f183f058fe88a4e3ec6e2788e003840893b91bac4559cabedd00863b3ed" dependencies = [ "serde_derive", ] @@ -1913,13 +1941,13 @@ dependencies = [ [[package]] name = "serde_derive" -version = "1.0.163" +version = "1.0.180" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c805777e3930c8883389c602315a24224bcc738b63905ef87cd1420353ea93e" +checksum = "24e744d7782b686ab3b73267ef05697159cc0e5abbed3f47f9933165e5219036" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -1935,9 +1963,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.96" +version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "057d394a50403bcac12672b2b18fb387ab6d289d957dab67dd201875391e52f1" +checksum = "076066c5f1078eac5b722a31827a8832fe108bed65dfa75e233c89f8206e976c" dependencies = [ "itoa", "ryu", @@ -2037,9 +2065,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.16" +version = "2.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6f671d4b5ffdb8eadec19c0ae67fe2639df8684bd7bc4b83d986b8db549cf01" +checksum = "04361975b3f5e348b2189d8dc55bc942f278b2d482a6a0365de5bdd62d351567" dependencies = [ "proc-macro2", "quote", @@ -2100,7 +2128,7 @@ checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -2148,7 +2176,7 @@ checksum = "0f57e3ca2a01450b1a921183a9c9cbfda207fd822cef4ccb00a65402cbba7a74" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", ] [[package]] @@ -2268,7 +2296,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", "wasm-bindgen-shared", ] @@ -2313,7 +2341,7 @@ checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.16", + "syn 2.0.28", "wasm-bindgen-backend", "wasm-bindgen-shared", ] diff --git a/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_balances.json b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_balances.json new file mode 100644 index 0000000000..4bde7573d0 --- /dev/null +++ b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_balances.json @@ -0,0 +1,71 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AcknowledgementMsgBalances", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", + "oneOf": [ + { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "$ref": "#/definitions/BalancesResponse" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "BalancesResponse": { + "description": "This is the success response we send on ack for PacketMsg::Balance. Just acknowledge success or error", + "type": "object", + "required": [ + "account", + "balances" + ], + "properties": { + "account": { + "type": "string" + }, + "balances": { + "type": "array", + "items": { + "$ref": "#/definitions/Coin" + } + } + } + }, + "Coin": { + "type": "object", + "required": [ + "amount", + "denom" + ], + "properties": { + "amount": { + "$ref": "#/definitions/Uint128" + }, + "denom": { + "type": "string" + } + } + }, + "Uint128": { + "description": "A thin wrapper around u128 that is using strings for JSON encoding/decoding, such that the full u128 range can be used for clients that convert JSON numbers to floats, like JavaScript and jq.\n\n# Examples\n\nUse `from` to create instances of this and `u128` to get the value out:\n\n``` # use cosmwasm_std::Uint128; let a = Uint128::from(123u128); assert_eq!(a.u128(), 123);\n\nlet b = Uint128::from(42u64); assert_eq!(b.u128(), 42);\n\nlet c = Uint128::from(70u32); assert_eq!(c.u128(), 70); ```", + "type": "string" + } + } +} diff --git a/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_dispatch.json b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_dispatch.json new file mode 100644 index 0000000000..5c010f5bd1 --- /dev/null +++ b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_dispatch.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AcknowledgementMsgDispatch", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", + "oneOf": [ + { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "null" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_who_am_i.json b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_who_am_i.json new file mode 100644 index 0000000000..e4ad97424b --- /dev/null +++ b/contracts/ibc-reflect-send/schema/ibc/acknowledgement_msg_who_am_i.json @@ -0,0 +1,45 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AcknowledgementMsgWhoAmI", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", + "oneOf": [ + { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "$ref": "#/definitions/WhoAmIResponse" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "WhoAmIResponse": { + "description": "This is the success response we send on ack for PacketMsg::WhoAmI. Return the caller's account address on the remote chain", + "type": "object", + "required": [ + "account" + ], + "properties": { + "account": { + "type": "string" + } + } + } + } +} diff --git a/contracts/ibc-reflect-send/schema/packet_msg.json b/contracts/ibc-reflect-send/schema/ibc/packet_msg.json similarity index 100% rename from contracts/ibc-reflect-send/schema/packet_msg.json rename to contracts/ibc-reflect-send/schema/ibc/packet_msg.json diff --git a/contracts/ibc-reflect-send/src/bin/schema.rs b/contracts/ibc-reflect-send/src/bin/schema.rs index 2f8a8d3e72..cb7ea07166 100644 --- a/contracts/ibc-reflect-send/src/bin/schema.rs +++ b/contracts/ibc-reflect-send/src/bin/schema.rs @@ -1,8 +1,10 @@ use std::env::current_dir; -use cosmwasm_schema::{export_schema, schema_for, write_api}; +use cosmwasm_schema::{export_schema, export_schema_with_title, schema_for, write_api}; -use ibc_reflect_send::ibc_msg::PacketMsg; +use ibc_reflect_send::ibc_msg::{ + AcknowledgementMsg, BalancesResponse, DispatchResponse, PacketMsg, WhoAmIResponse, +}; use ibc_reflect_send::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; fn main() { @@ -16,5 +18,21 @@ fn main() { // Schemas for inter-contract communication let mut out_dir = current_dir().unwrap(); out_dir.push("schema"); + out_dir.push("ibc"); export_schema(&schema_for!(PacketMsg), &out_dir); + export_schema_with_title( + &schema_for!(AcknowledgementMsg), + &out_dir, + "AcknowledgementMsgBalances", + ); + export_schema_with_title( + &schema_for!(AcknowledgementMsg), + &out_dir, + "AcknowledgementMsgDispatch", + ); + export_schema_with_title( + &schema_for!(AcknowledgementMsg), + &out_dir, + "AcknowledgementMsgWhoAmI", + ); } diff --git a/contracts/ibc-reflect-send/src/ibc.rs b/contracts/ibc-reflect-send/src/ibc.rs index adede549eb..634fed3e81 100644 --- a/contracts/ibc-reflect-send/src/ibc.rs +++ b/contracts/ibc-reflect-send/src/ibc.rs @@ -146,7 +146,7 @@ fn acknowledge_who_am_i( // ignore errors (but mention in log) let WhoAmIResponse { account } = match ack { AcknowledgementMsg::Ok(res) => res, - AcknowledgementMsg::Err(e) => { + AcknowledgementMsg::Error(e) => { return Ok(IbcBasicResponse::new() .add_attribute("action", "acknowledge_who_am_i") .add_attribute("error", e)) @@ -176,7 +176,7 @@ fn acknowledge_balances( // ignore errors (but mention in log) let BalancesResponse { account, balances } = match ack { AcknowledgementMsg::Ok(res) => res, - AcknowledgementMsg::Err(e) => { + AcknowledgementMsg::Error(e) => { return Ok(IbcBasicResponse::new() .add_attribute("action", "acknowledge_balances") .add_attribute("error", e)) diff --git a/contracts/ibc-reflect-send/src/ibc_msg.rs b/contracts/ibc-reflect-send/src/ibc_msg.rs index d395388c3a..ca6041218c 100644 --- a/contracts/ibc-reflect-send/src/ibc_msg.rs +++ b/contracts/ibc-reflect-send/src/ibc_msg.rs @@ -1,4 +1,5 @@ -use cosmwasm_std::{Coin, ContractResult, CosmosMsg}; +use cosmwasm_schema::cw_serde; +use cosmwasm_std::{Coin, CosmosMsg}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; @@ -11,9 +12,35 @@ pub enum PacketMsg { Balances {}, } -/// All IBC acknowledgements are wrapped in `ContractResult`. -/// The success value depends on the PacketMsg variant. -pub type AcknowledgementMsg = ContractResult; +/// A custom acknowledgement type. +/// The success type `T` depends on the PacketMsg variant. +/// +/// This could be refactored to use [StdAck] at some point. However, +/// it has a different success variant name ("ok" vs. "result") and +/// a JSON payload instead of a binary payload. +/// +/// [StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512 +#[cw_serde] +pub enum AcknowledgementMsg { + Ok(S), + Error(String), +} + +impl AcknowledgementMsg { + pub fn unwrap(self) -> S { + match self { + AcknowledgementMsg::Ok(data) => data, + AcknowledgementMsg::Error(err) => panic!("{}", err), + } + } + + pub fn unwrap_err(self) -> String { + match self { + AcknowledgementMsg::Ok(_) => panic!("not an error"), + AcknowledgementMsg::Error(err) => err, + } + } +} /// This is the success response we send on ack for PacketMsg::Dispatch. /// Just acknowledge success or error diff --git a/contracts/ibc-reflect/schema/acknowledgement_msg_dispatch.json b/contracts/ibc-reflect/schema/acknowledgement_msg_dispatch.json deleted file mode 100644 index e89f54b65d..0000000000 --- a/contracts/ibc-reflect/schema/acknowledgement_msg_dispatch.json +++ /dev/null @@ -1,32 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AcknowledgementMsgDispatch", - "description": "This is the final result type that is created and serialized in a contract for every init/execute/migrate call. The VM then deserializes this type to distinguish between successful and failed executions.\n\nWe use a custom type here instead of Rust's Result because we want to be able to define the serialization, which is a public interface. Every language that compiles to Wasm and runs in the ComsWasm VM needs to create the same JSON representation.\n\n# Examples\n\nSuccess:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let response: Response = Response::default(); let result: ContractResult = ContractResult::Ok(response); assert_eq!(to_vec(&result).unwrap(), br#\"{\"ok\":{\"messages\":[],\"attributes\":[],\"events\":[],\"data\":null}}\"#); ```\n\nFailure:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let error_msg = String::from(\"Something went wrong\"); let result: ContractResult = ContractResult::Err(error_msg); assert_eq!(to_vec(&result).unwrap(), br#\"{\"error\":\"Something went wrong\"}\"#); ```", - "oneOf": [ - { - "type": "object", - "required": [ - "ok" - ], - "properties": { - "ok": { - "type": "null" - } - }, - "additionalProperties": false - }, - { - "description": "An error type that every custom error created by contract developers can be converted to. This could potientially have more structure, but String is the easiest.", - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" - } - }, - "additionalProperties": false - } - ] -} diff --git a/contracts/ibc-reflect/schema/acknowledgement_msg_who_am_i.json b/contracts/ibc-reflect/schema/acknowledgement_msg_who_am_i.json deleted file mode 100644 index 57c0933f8c..0000000000 --- a/contracts/ibc-reflect/schema/acknowledgement_msg_who_am_i.json +++ /dev/null @@ -1,47 +0,0 @@ -{ - "$schema": "http://json-schema.org/draft-07/schema#", - "title": "AcknowledgementMsgWhoAmI", - "description": "This is the final result type that is created and serialized in a contract for every init/execute/migrate call. The VM then deserializes this type to distinguish between successful and failed executions.\n\nWe use a custom type here instead of Rust's Result because we want to be able to define the serialization, which is a public interface. Every language that compiles to Wasm and runs in the ComsWasm VM needs to create the same JSON representation.\n\n# Examples\n\nSuccess:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let response: Response = Response::default(); let result: ContractResult = ContractResult::Ok(response); assert_eq!(to_vec(&result).unwrap(), br#\"{\"ok\":{\"messages\":[],\"attributes\":[],\"events\":[],\"data\":null}}\"#); ```\n\nFailure:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let error_msg = String::from(\"Something went wrong\"); let result: ContractResult = ContractResult::Err(error_msg); assert_eq!(to_vec(&result).unwrap(), br#\"{\"error\":\"Something went wrong\"}\"#); ```", - "oneOf": [ - { - "type": "object", - "required": [ - "ok" - ], - "properties": { - "ok": { - "$ref": "#/definitions/WhoAmIResponse" - } - }, - "additionalProperties": false - }, - { - "description": "An error type that every custom error created by contract developers can be converted to. This could potientially have more structure, but String is the easiest.", - "type": "object", - "required": [ - "error" - ], - "properties": { - "error": { - "type": "string" - } - }, - "additionalProperties": false - } - ], - "definitions": { - "WhoAmIResponse": { - "description": "This is the success response we send on ack for PacketMsg::WhoAmI. Return the caller's account address on the remote chain", - "type": "object", - "required": [ - "account" - ], - "properties": { - "account": { - "type": "string" - } - }, - "additionalProperties": false - } - } -} diff --git a/contracts/ibc-reflect/schema/acknowledgement_msg_balances.json b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_balances.json similarity index 59% rename from contracts/ibc-reflect/schema/acknowledgement_msg_balances.json rename to contracts/ibc-reflect/schema/ibc/acknowledgement_msg_balances.json index 20e956cd3f..b181c28559 100644 --- a/contracts/ibc-reflect/schema/acknowledgement_msg_balances.json +++ b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_balances.json @@ -1,7 +1,7 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "AcknowledgementMsgBalances", - "description": "This is the final result type that is created and serialized in a contract for every init/execute/migrate call. The VM then deserializes this type to distinguish between successful and failed executions.\n\nWe use a custom type here instead of Rust's Result because we want to be able to define the serialization, which is a public interface. Every language that compiles to Wasm and runs in the ComsWasm VM needs to create the same JSON representation.\n\n# Examples\n\nSuccess:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let response: Response = Response::default(); let result: ContractResult = ContractResult::Ok(response); assert_eq!(to_vec(&result).unwrap(), br#\"{\"ok\":{\"messages\":[],\"attributes\":[],\"events\":[],\"data\":null}}\"#); ```\n\nFailure:\n\n``` # use cosmwasm_std::{to_vec, ContractResult, Response}; let error_msg = String::from(\"Something went wrong\"); let result: ContractResult = ContractResult::Err(error_msg); assert_eq!(to_vec(&result).unwrap(), br#\"{\"error\":\"Something went wrong\"}\"#); ```", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", "oneOf": [ { "type": "object", @@ -16,7 +16,6 @@ "additionalProperties": false }, { - "description": "An error type that every custom error created by contract developers can be converted to. This could potientially have more structure, but String is the easiest.", "type": "object", "required": [ "error" diff --git a/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_dispatch.json b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_dispatch.json new file mode 100644 index 0000000000..5c010f5bd1 --- /dev/null +++ b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_dispatch.json @@ -0,0 +1,31 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AcknowledgementMsgDispatch", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", + "oneOf": [ + { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "type": "null" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + ] +} diff --git a/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_who_am_i.json b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_who_am_i.json new file mode 100644 index 0000000000..f706c655de --- /dev/null +++ b/contracts/ibc-reflect/schema/ibc/acknowledgement_msg_who_am_i.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "AcknowledgementMsgWhoAmI", + "description": "A custom acknowledgement type. The success type `T` depends on the PacketMsg variant.\n\nThis could be refactored to use [StdAck] at some point. However, it has a different success variant name (\"ok\" vs. \"result\") and a JSON payload instead of a binary payload.\n\n[StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512", + "oneOf": [ + { + "type": "object", + "required": [ + "ok" + ], + "properties": { + "ok": { + "$ref": "#/definitions/WhoAmIResponse" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "error" + ], + "properties": { + "error": { + "type": "string" + } + }, + "additionalProperties": false + } + ], + "definitions": { + "WhoAmIResponse": { + "description": "This is the success response we send on ack for PacketMsg::WhoAmI. Return the caller's account address on the remote chain", + "type": "object", + "required": [ + "account" + ], + "properties": { + "account": { + "type": "string" + } + }, + "additionalProperties": false + } + } +} diff --git a/contracts/ibc-reflect/schema/packet_msg.json b/contracts/ibc-reflect/schema/ibc/packet_msg.json similarity index 100% rename from contracts/ibc-reflect/schema/packet_msg.json rename to contracts/ibc-reflect/schema/ibc/packet_msg.json diff --git a/contracts/ibc-reflect/src/bin/schema.rs b/contracts/ibc-reflect/src/bin/schema.rs index 74b3408387..5103b778f8 100644 --- a/contracts/ibc-reflect/src/bin/schema.rs +++ b/contracts/ibc-reflect/src/bin/schema.rs @@ -19,6 +19,7 @@ fn main() { // Schemas for inter-contract communication let mut out_dir = current_dir().unwrap(); out_dir.push("schema"); + out_dir.push("ibc"); export_schema(&schema_for!(PacketMsg), &out_dir); export_schema_with_title( &schema_for!(AcknowledgementMsg), diff --git a/contracts/ibc-reflect/src/contract.rs b/contracts/ibc-reflect/src/contract.rs index 23fa7046a3..1af5d90e91 100644 --- a/contracts/ibc-reflect/src/contract.rs +++ b/contracts/ibc-reflect/src/contract.rs @@ -224,7 +224,7 @@ pub fn migrate(_deps: DepsMut, _env: Env, _msg: Empty) -> StdResult { // this encode an error or error message into a proper acknowledgement to the recevier fn encode_ibc_error(msg: impl Into) -> Binary { // this cannot error, unwrap to keep the interface simple - to_binary(&AcknowledgementMsg::<()>::Err(msg.into())).unwrap() + to_binary(&AcknowledgementMsg::<()>::Error(msg.into())).unwrap() } #[entry_point] diff --git a/contracts/ibc-reflect/src/msg.rs b/contracts/ibc-reflect/src/msg.rs index acd49baf63..6f72005f1c 100644 --- a/contracts/ibc-reflect/src/msg.rs +++ b/contracts/ibc-reflect/src/msg.rs @@ -1,5 +1,5 @@ use cosmwasm_schema::{cw_serde, QueryResponses}; -use cosmwasm_std::{Coin, ContractResult, CosmosMsg}; +use cosmwasm_std::{Coin, CosmosMsg}; /// Just needs to know the code_id of a reflect contract to spawn sub-accounts #[cw_serde] @@ -51,9 +51,35 @@ pub enum PacketMsg { ReturnMsgs { msgs: Vec }, } -/// All acknowledgements are wrapped in `ContractResult`. -/// The success value depends on the PacketMsg variant. -pub type AcknowledgementMsg = ContractResult; +/// A custom acknowledgement type. +/// The success type `T` depends on the PacketMsg variant. +/// +/// This could be refactored to use [StdAck] at some point. However, +/// it has a different success variant name ("ok" vs. "result") and +/// a JSON payload instead of a binary payload. +/// +/// [StdAck]: https://github.com/CosmWasm/cosmwasm/issues/1512 +#[cw_serde] +pub enum AcknowledgementMsg { + Ok(S), + Error(String), +} + +impl AcknowledgementMsg { + pub fn unwrap(self) -> S { + match self { + AcknowledgementMsg::Ok(data) => data, + AcknowledgementMsg::Error(err) => panic!("{}", err), + } + } + + pub fn unwrap_err(self) -> String { + match self { + AcknowledgementMsg::Ok(_) => panic!("not an error"), + AcknowledgementMsg::Error(err) => err, + } + } +} /// This is the success response we send on ack for PacketMsg::Dispatch. /// Just acknowledge success or error diff --git a/packages/go-gen/Cargo.toml b/packages/go-gen/Cargo.toml new file mode 100644 index 0000000000..89b1ae4715 --- /dev/null +++ b/packages/go-gen/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "go-gen" +authors = ["Christoph Otter "] +version = "0.1.0" +edition = "2021" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +schemars = "0.8.3" +cosmwasm-std = { path = "../std", version = "1.3.1", features = ["cosmwasm_1_3", "staking", "stargate", "ibc3"] } +cosmwasm-schema = { path = "../schema", version = "1.3.1" } +anyhow = "1" +Inflector = "0.11.4" +indenter = "0.3.3" diff --git a/packages/go-gen/README.md b/packages/go-gen/README.md new file mode 100644 index 0000000000..07d242a152 --- /dev/null +++ b/packages/go-gen/README.md @@ -0,0 +1,21 @@ +# JsonSchema Go Type Generator + +This is an internal utility to generate Go types from `cosmwasm-std`'s query +response types. These types can then be used in +[wasmvm](https://github.com/CosmWasm/wasmvm). + +## Usage + +Adjust the query / response type you want to generate in `src/main.rs` and run: +`cargo run -p go-gen` + +## Limitations + +Only basic structs and enums are supported. Tuples and enum variants with 0 or +more than 1 parameters don't work, for example. + +## License + +This package is part of the cosmwasm repository, licensed under the Apache +License 2.0 (see [NOTICE](https://github.com/CosmWasm/cosmwasm/blob/main/NOTICE) +and [LICENSE](https://github.com/CosmWasm/cosmwasm/blob/main/LICENSE)). diff --git a/packages/go-gen/src/go.rs b/packages/go-gen/src/go.rs new file mode 100644 index 0000000000..faead10df9 --- /dev/null +++ b/packages/go-gen/src/go.rs @@ -0,0 +1,237 @@ +use std::fmt::{self, Display, Write}; + +use indenter::indented; +use inflector::Inflector; + +use crate::utils::replace_acronyms; + +pub struct GoStruct { + pub name: String, + pub docs: Option, + pub fields: Vec, +} + +impl Display for GoStruct { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // generate documentation + format_docs(f, self.docs.as_deref())?; + // generate type + writeln!(f, "type {} struct {{", self.name)?; + // generate fields + { + let mut f = indented(f); + for field in &self.fields { + writeln!(f, "{}", field)?; + } + } + f.write_char('}')?; + Ok(()) + } +} + +pub struct GoField { + /// The name of the field in Rust (snake_case) + pub rust_name: String, + /// The documentation of the field + pub docs: Option, + /// The type of the field + pub ty: GoType, +} + +impl Display for GoField { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // documentation + format_docs(f, self.docs.as_deref())?; + // {field} {type} `json:"{field}"` + write!( + f, + "{} {} `json:\"{}", + replace_acronyms(self.rust_name.to_pascal_case()), + self.ty, + self.rust_name + )?; + if self.ty.is_nullable { + f.write_str(",omitempty")?; + } + f.write_str("\"`") + } +} + +pub struct GoType { + /// The name of the type in Go + pub name: String, + /// Whether the type should be nullable + /// This will add `omitempty` to the json tag and use a pointer type if + /// the type is not a basic type + pub is_nullable: bool, +} + +impl GoType { + pub fn is_basic_type(&self) -> bool { + const BASIC_GO_TYPES: &[&str] = &[ + "string", + "bool", + "int", + "int8", + "int16", + "int32", + "int64", + "uint", + "uint8", + "uint16", + "uint32", + "uint64", + "float32", + "float64", + "byte", + "rune", + "uintptr", + "complex64", + "complex128", + ]; + BASIC_GO_TYPES.contains(&&*self.name) + } +} + +impl Display for GoType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.is_nullable && !self.is_basic_type() { + // if the type is nullable and not a basic type, use a pointer + f.write_char('*')?; + } + f.write_str(&self.name) + } +} + +fn format_docs(f: &mut fmt::Formatter, docs: Option<&str>) -> fmt::Result { + if let Some(docs) = docs { + for line in docs.lines() { + f.write_str("// ")?; + f.write_str(line)?; + f.write_char('\n')?; + } + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn go_type_display_works() { + let ty = GoType { + name: "string".to_string(), + is_nullable: true, + }; + let ty2 = GoType { + name: "string".to_string(), + is_nullable: false, + }; + assert_eq!(format!("{}", ty), "string"); + assert_eq!(format!("{}", ty2), "string"); + + let ty = GoType { + name: "FooBar".to_string(), + is_nullable: true, + }; + assert_eq!(format!("{}", ty), "*FooBar"); + let ty = GoType { + name: "FooBar".to_string(), + is_nullable: false, + }; + assert_eq!(format!("{}", ty), "FooBar"); + } + + #[test] + fn go_field_display_works() { + let field = GoField { + rust_name: "foo_bar".to_string(), + docs: None, + ty: GoType { + name: "string".to_string(), + is_nullable: true, + }, + }; + assert_eq!( + format!("{}", field), + "FooBar string `json:\"foo_bar,omitempty\"`" + ); + + let field = GoField { + rust_name: "foo_bar".to_string(), + docs: None, + ty: GoType { + name: "string".to_string(), + is_nullable: false, + }, + }; + assert_eq!(format!("{}", field), "FooBar string `json:\"foo_bar\"`"); + + let field = GoField { + rust_name: "foo_bar".to_string(), + docs: None, + ty: GoType { + name: "FooBar".to_string(), + is_nullable: true, + }, + }; + assert_eq!( + format!("{}", field), + "FooBar *FooBar `json:\"foo_bar,omitempty\"`" + ); + } + + #[test] + fn go_field_docs_display_works() { + let field = GoField { + rust_name: "foo_bar".to_string(), + docs: Some("foo_bar is a test field".to_string()), + ty: GoType { + name: "string".to_string(), + is_nullable: true, + }, + }; + assert_eq!( + format!("{}", field), + "// foo_bar is a test field\nFooBar string `json:\"foo_bar,omitempty\"`" + ); + } + + #[test] + fn go_type_def_display_works() { + let ty = GoStruct { + name: "FooBar".to_string(), + docs: None, + fields: vec![GoField { + rust_name: "foo_bar".to_string(), + docs: None, + ty: GoType { + name: "string".to_string(), + is_nullable: true, + }, + }], + }; + assert_eq!( + format!("{}", ty), + "type FooBar struct {\n FooBar string `json:\"foo_bar,omitempty\"`\n}" + ); + + let ty = GoStruct { + name: "FooBar".to_string(), + docs: Some("FooBar is a test struct".to_string()), + fields: vec![GoField { + rust_name: "foo_bar".to_string(), + docs: None, + ty: GoType { + name: "string".to_string(), + is_nullable: true, + }, + }], + }; + assert_eq!( + format!("{}", ty), + "// FooBar is a test struct\ntype FooBar struct {\n FooBar string `json:\"foo_bar,omitempty\"`\n}" + ); + } +} diff --git a/packages/go-gen/src/main.rs b/packages/go-gen/src/main.rs new file mode 100644 index 0000000000..7688db1878 --- /dev/null +++ b/packages/go-gen/src/main.rs @@ -0,0 +1,466 @@ +use anyhow::{bail, ensure, Context, Result}; +use go::*; +use inflector::cases::pascalcase::to_pascal_case; +use schema::{documentation, schema_object_type, SchemaExt, TypeContext}; +use schemars::schema::{ObjectValidation, RootSchema, Schema, SchemaObject}; +use std::fmt::Write; +use utils::replace_acronyms; + +mod go; +mod schema; +mod utils; + +fn main() -> Result<()> { + let root = cosmwasm_schema::schema_for!(cosmwasm_std::BankQuery); + + let code = generate_go(root)?; + println!("{}", code); + + Ok(()) +} + +/// Generates the Go code for the given schema +fn generate_go(root: RootSchema) -> Result { + let title = replace_acronyms( + root.schema + .metadata + .as_ref() + .and_then(|m| m.title.as_ref()) + .context("failed to get type name")?, + ); + + let mut types = vec![]; + build_type(&title, &root.schema, &mut types) + .with_context(|| format!("failed to generate {title}"))?; + + // go through additional definitions + for (name, additional_type) in &root.definitions { + additional_type + .object() + .map(|def| build_type(&replace_acronyms(name), def, &mut types)) + .and_then(|r| r) + .context("failed to generate additional definitions")?; + } + let mut code = String::new(); + for ty in types { + writeln!(&mut code, "{ty}")?; + } + + Ok(code) +} + +/// Generates Go structs for the given schema and adds them to `structs`. +/// This will add more than one struct if the schema contains object types (anonymous structs). +fn build_type(name: &str, schema: &SchemaObject, structs: &mut Vec) -> Result<()> { + if schema::custom_type_of(name).is_some() { + // ignore custom types + return Ok(()); + } + + // first detect if we have a struct or enum + if let Some(obj) = schema.object.as_ref() { + let strct = build_struct(name, schema, obj, structs) + .map(Some) + .with_context(|| format!("failed to generate struct '{name}'"))?; + if let Some(strct) = strct { + structs.push(strct); + } + } else if let Some(variants) = schema::enum_variants(schema) { + let strct = build_enum(name, schema, variants, structs) + .map(Some) + .with_context(|| format!("failed to generate enum '{name}'"))?; + if let Some(strct) = strct { + structs.push(strct); + } + } else { + anyhow::bail!("failed to determine type for '{name}'"); + } + + Ok(()) +} + +/// Creates a Go struct for the given schema object and returns it. +/// This will also add any additional structs to `additional_structs` (but not the returned one). +pub fn build_struct( + name: &str, + strct: &SchemaObject, + obj: &ObjectValidation, + additional_structs: &mut Vec, +) -> Result { + let docs = documentation(strct); + + // go through all fields + let fields = obj.properties.iter().map(|(field, ty)| { + // get schema object + let schema = ty + .object() + .with_context(|| format!("expected schema object for field {field}"))?; + // extract type from schema object + let ty = schema_object_type(schema, TypeContext::new(name, field), additional_structs) + .with_context(|| format!("failed to get type of field '{field}'"))?; + Ok(GoField { + rust_name: field.clone(), + docs: documentation(schema), + ty, + }) + }); + let fields = fields.collect::>>()?; + + Ok(GoStruct { + name: to_pascal_case(name), + docs, + fields, + }) +} + +/// Creates a Go struct for the given schema object and returns it. +/// This will also add any additional structs to `additional_structs` (but not the returned one). +pub fn build_enum<'a>( + name: &str, + enm: &SchemaObject, + variants: impl Iterator, + additional_structs: &mut Vec, +) -> Result { + let docs = documentation(enm); + + // go through all fields + let fields = variants.map(|v| { + // get schema object + let v = v + .object() + .with_context(|| format!("expected schema object for enum variants of {name}"))?; + + // analyze the variant + let variant_field = build_enum_variant(v, name, additional_structs) + .context("failed to extract enum variant")?; + + anyhow::Ok(variant_field) + }); + let fields = fields.collect::>>()?; + + Ok(GoStruct { + name: name.to_string(), + docs, + fields, + }) +} + +/// Tries to extract the name and type of the given enum variant and returns it as a `GoField`. +pub fn build_enum_variant( + schema: &SchemaObject, + enum_name: &str, + additional_structs: &mut Vec, +) -> Result { + // for variants without inner data, there is an entry in `enum_variants` + // we are not interested in that case, so we error out + if let Some(values) = &schema.enum_values { + bail!( + "enum variants {} without inner data not supported", + values + .iter() + .map(|v| v.to_string()) + .collect::>() + .join(", ") + ); + } + + let docs = documentation(schema); + + // for variants with inner data, there is an object validation entry with a single property + // we extract the type of that property + let properties = &schema + .object + .as_ref() + .context("expected object validation for enum variant")? + .properties; + ensure!( + properties.len() == 1, + "expected exactly one property in enum variant" + ); + // we can unwrap here, because we checked the length above + let (name, schema) = properties.first_key_value().unwrap(); + let GoType { name: ty, .. } = schema_object_type( + schema.object()?, + TypeContext::new(enum_name, name), + additional_structs, + )?; + + Ok(GoField { + rust_name: name.to_string(), + docs, + ty: GoType { + name: ty, + is_nullable: true, // always nullable + }, + }) +} + +#[cfg(test)] +mod tests { + use cosmwasm_schema::cw_serde; + use cosmwasm_std::{Binary, Empty, HexBinary, Uint128}; + + use super::*; + + fn assert_code_eq(actual: String, expected: &str) { + let actual_no_ws = actual.split_whitespace().collect::>(); + let expected_no_ws = expected.split_whitespace().collect::>(); + + assert!( + actual_no_ws == expected_no_ws, + "assertion failed: `(actual == expected)`\nactual:\n`{}`,\nexpected:\n`\"{}\"`", + actual, + expected + ); + } + + fn assert_code_eq_ignore_docs(actual: String, expected: &str) { + let actual_filtered = actual + .lines() + .map(|line| line.split("//").next().unwrap()) // ignore comments + .flat_map(|line| line.split_whitespace()) + .collect::>(); + let expected_filtered = expected + .lines() + .map(|line| line.split("//").next().unwrap()) // ignore comments + .flat_map(|line| line.split_whitespace()) + .collect::>(); + + assert!( + actual_filtered == expected_filtered, + "assertion failed: `(actual == expected)`\nactual:\n`{}`,\nexpected:\n`\"{}\"`", + actual, + expected + ); + } + + #[test] + fn special_types() { + #[cw_serde] + struct SpecialTypes { + binary: Binary, + nested_binary: Vec>, + hex_binary: HexBinary, + uint128: Uint128, + } + + let schema = schemars::schema_for!(SpecialTypes); + let code = generate_go(schema).unwrap(); + + assert_code_eq( + code, + r#" + type SpecialTypes struct { + Binary []byte `json:"binary"` + HexBinary Checksum `json:"hex_binary"` + NestedBinary []*[]byte `json:"nested_binary"` + Uint128 string `json:"uint128"` + }"#, + ); + } + + #[test] + fn integers() { + #[cw_serde] + struct Integers { + a: u64, + b: i64, + c: u32, + d: i32, + e: u8, + f: i8, + g: u16, + h: i16, + } + + let schema = schemars::schema_for!(Integers); + let code = generate_go(schema).unwrap(); + + assert_code_eq( + code, + r#" + type Integers struct { + A uint64 `json:"a"` + B int64 `json:"b"` + C uint32 `json:"c"` + D int32 `json:"d"` + E uint8 `json:"e"` + F int8 `json:"f"` + G uint16 `json:"g"` + H int16 `json:"h"` + }"#, + ); + + #[cw_serde] + struct U128 { + a: u128, + } + #[cw_serde] + struct I128 { + a: i128, + } + let schema = schemars::schema_for!(U128); + assert!(generate_go(schema) + .unwrap_err() + .root_cause() + .to_string() + .contains("unsupported integer format: uint128")); + let schema = schemars::schema_for!(I128); + assert!(generate_go(schema) + .unwrap_err() + .root_cause() + .to_string() + .contains("unsupported integer format: int128")); + } + + #[test] + fn empty() { + #[cw_serde] + struct Empty {} + + let schema = schemars::schema_for!(Empty); + let code = generate_go(schema).unwrap(); + assert_code_eq(code, "type Empty struct { }"); + } + + /// Compares the generated code for a given type with the code in the corresponding file in + /// `tests/`. + /// The file name is derived from the type name by replacing `::` with `__` and adding `.go`. + macro_rules! compare_codes { + ($name:ty) => {{ + let filename = stringify!($name).replace("::", "__"); + let generated = generate_go(cosmwasm_schema::schema_for!($name)).unwrap(); + let expected = std::fs::read_to_string(format!("tests/{}.go", filename)).unwrap(); + + assert_code_eq_ignore_docs(generated, &expected); + }}; + } + + #[test] + fn responses_work() { + // bank + compare_codes!(cosmwasm_std::SupplyResponse); + compare_codes!(cosmwasm_std::BalanceResponse); + // compare_codes!(cosmwasm_std::AllBalanceResponse); // has different name in wasmvm + compare_codes!(cosmwasm_std::DenomMetadataResponse); + // compare_codes!(cosmwasm_std::AllDenomMetadataResponse); // uses `[]byte` instead of `*[]byte` + // staking + compare_codes!(cosmwasm_std::BondedDenomResponse); + compare_codes!(cosmwasm_std::AllDelegationsResponse); + compare_codes!(cosmwasm_std::DelegationResponse); + compare_codes!(cosmwasm_std::AllValidatorsResponse); + // compare_codes!(cosmwasm_std::ValidatorResponse); // does not use "omitempty" for `Validator` field + // distribution + compare_codes!(cosmwasm_std::DelegatorWithdrawAddressResponse); + // wasm + compare_codes!(cosmwasm_std::ContractInfoResponse); + // compare_codes!(cosmwasm_std::CodeInfoResponse); // TODO: Checksum type and "omitempty" + } + + #[test] + fn nested_enum_works() { + #[cw_serde] + struct Inner { + a: String, + } + + #[cw_serde] + enum MyEnum { + A(Inner), + B(String), + C { a: String }, + } + + let schema = schemars::schema_for!(MyEnum); + let code = generate_go(schema).unwrap(); + assert_code_eq( + code, + r#" + type CEnum struct { + A string `json:"a"` + } + type MyEnum struct { + A *Inner `json:"a,omitempty"` + B string `json:"b,omitempty"` + C *CEnum `json:"c,omitempty"` + } + type Inner struct { + A string `json:"a"` + } + "#, + ); + + #[cw_serde] + enum ShouldFail1 { + A(), + } + #[cw_serde] + enum ShouldFail2 { + A, + } + let schema = schemars::schema_for!(ShouldFail1); + assert!(generate_go(schema) + .unwrap_err() + .root_cause() + .to_string() + .contains("array type with non-singular item type is not supported")); + let schema = schemars::schema_for!(ShouldFail2); + assert!(generate_go(schema) + .unwrap_err() + .root_cause() + .to_string() + .contains("failed to determine type for 'ShouldFail2'")); + } + + #[test] + fn queries_work() { + // compare_codes!(cosmwasm_std::QueryRequest); // omit for now because it's huge + // just assert that it compiles + generate_go(cosmwasm_schema::schema_for!( + cosmwasm_std::QueryRequest + )) + .unwrap(); + // TODO: PageRequest.Key uses "omitempty" and no * + // compare_codes!(cosmwasm_std::BankQuery); + compare_codes!(cosmwasm_std::StakingQuery); + compare_codes!(cosmwasm_std::DistributionQuery); + compare_codes!(cosmwasm_std::IbcQuery); + compare_codes!(cosmwasm_std::WasmQuery); + } + + #[test] + fn array_item_type_works() { + #[cw_serde] + struct A { + a: Vec>>>>, + } + #[cw_serde] + struct B {} + + // example json: + // A { a: vec![vec![vec![None, Some(Some(B {})), Some(None)]]] } + // => {"a":[[[null,{},null]]]} + let code = generate_go(cosmwasm_schema::schema_for!(A)).unwrap(); + assert_code_eq( + code, + r#" + type A struct { + A [][][]*B `json:"a"` + } + type B struct { }"#, + ); + + #[cw_serde] + struct C { + c: Vec>>>>, + } + let code = generate_go(cosmwasm_schema::schema_for!(C)).unwrap(); + assert_code_eq( + code, + r#" + type C struct { + C [][][]*string `json:"c"` + }"#, + ); + } +} diff --git a/packages/go-gen/src/schema.rs b/packages/go-gen/src/schema.rs new file mode 100644 index 0000000000..22e6fd7c97 --- /dev/null +++ b/packages/go-gen/src/schema.rs @@ -0,0 +1,275 @@ +use anyhow::{bail, ensure, Context, Result}; + +use inflector::Inflector; +use schemars::schema::{InstanceType, Schema, SchemaObject, SingleOrVec}; + +use crate::{ + go::{GoField, GoStruct, GoType}, + utils::{replace_acronyms, suffixes}, +}; + +pub trait SchemaExt { + /// Returns a reference to the contained schema object, + /// or an error if the schema is not an object. + fn object(&self) -> anyhow::Result<&SchemaObject>; +} + +impl SchemaExt for Schema { + fn object(&self) -> anyhow::Result<&SchemaObject> { + match self { + Schema::Object(o) => Ok(o), + _ => bail!("expected schema object"), + } + } +} + +/// Returns the schemas of the variants of this enum, if it is an enum. +/// Returns `None` if the schema is not an enum. +pub fn enum_variants(schema: &SchemaObject) -> Option> { + Some(schema.subschemas.as_ref()?.one_of.as_ref()?.iter()) +} + +/// Returns the Go type for the given schema object and whether it is nullable. +/// May also add additional structs to the given `Vec` that need to be generated for this type. +pub fn schema_object_type( + schema: &SchemaObject, + type_context: TypeContext, + additional_structs: &mut Vec, +) -> Result { + let mut is_nullable = is_null(schema); + + // if it has a title, use that + let ty = if let Some(title) = schema.metadata.as_ref().and_then(|m| m.title.as_ref()) { + replace_custom_type(title) + } else if let Some(reference) = &schema.reference { + // if it has a reference, strip the path and use that + replace_custom_type( + reference + .split('/') + .last() + .expect("split should always return at least one item"), + ) + } else if let Some(t) = &schema.instance_type { + type_from_instance_type(schema, type_context, t, additional_structs)? + } else if let Some(subschemas) = schema.subschemas.as_ref().and_then(|s| s.any_of.as_ref()) { + // check if one of them is null + let nullable = nullable_type(subschemas)?; + if let Some(non_null) = nullable { + ensure!(subschemas.len() == 2, "multiple subschemas in anyOf"); + is_nullable = true; + // extract non-null type + let GoType { name, .. } = + schema_object_type(non_null, type_context, additional_structs)?; + replace_custom_type(&name) + } else { + subschema_type(subschemas, type_context, additional_structs) + .context("failed to get type of anyOf subschemas")? + } + } else if let Some(subschemas) = schema + .subschemas + .as_ref() + .and_then(|s| s.all_of.as_ref().or(s.one_of.as_ref())) + { + subschema_type(subschemas, type_context, additional_structs) + .context("failed to get type of allOf subschemas")? + } else { + bail!("no type for schema found: {:?}", schema); + }; + + Ok(GoType { + name: ty, + is_nullable, + }) +} + +/// Tries to extract the type of the non-null variant of an anyOf schema. +/// +/// Returns `Ok(None)` if the type is not nullable. +pub fn nullable_type(subschemas: &[Schema]) -> Result, anyhow::Error> { + let (found_null, nullable_type): (bool, Option<&SchemaObject>) = subschemas + .iter() + .fold(Ok((false, None)), |result: Result<_>, subschema| { + result.and_then(|(nullable, not_null)| { + let subschema = subschema.object()?; + if is_null(subschema) { + Ok((true, not_null)) + } else { + Ok((nullable, Some(subschema))) + } + }) + }) + .context("failed to get anyOf subschemas")?; + + Ok(if found_null { nullable_type } else { None }) +} + +/// The context for type extraction +#[derive(Clone, Copy, Debug)] +pub struct TypeContext<'a> { + /// The struct name + struct_name: &'a str, + /// The name of the field in the parent struct + field: &'a str, +} + +impl<'a> TypeContext<'a> { + pub fn new(parent: &'a str, field: &'a str) -> Self { + Self { + struct_name: parent, + field, + } + } +} + +/// Tries to extract a type name from the given instance type. +/// +/// Fails for unsupported instance types or integer formats. +pub fn type_from_instance_type( + schema: &SchemaObject, + type_context: TypeContext, + t: &SingleOrVec, + additional_structs: &mut Vec, +) -> Result { + // if it has an instance type, use that + Ok(if t.contains(&InstanceType::String) { + "string".to_string() + } else if t.contains(&InstanceType::Number) { + "float64".to_string() + } else if t.contains(&InstanceType::Integer) { + const AVAILABLE_INTS: &[&str] = &[ + "uint8", "int8", "uint16", "int16", "uint32", "int32", "uint64", "int64", + ]; + let format = schema.format.as_deref().unwrap_or("int64"); + if AVAILABLE_INTS.contains(&format) { + format.to_string() + } else { + bail!("unsupported integer format: {}", format); + } + } else if t.contains(&InstanceType::Boolean) { + "bool".to_string() + } else if t.contains(&InstanceType::Object) { + // generate a new struct for this object + // struct_name should be in PascalCase, so we detect the last word and use that as + // the suffix for the new struct name + let suffix = suffixes(type_context.struct_name) + .rev() + .find(|s| s.starts_with(char::is_uppercase)) + .unwrap_or(type_context.struct_name); + let new_struct_name = format!( + "{}{suffix}", + replace_acronyms(type_context.field.to_pascal_case()) + ); + + let fields = schema + .object + .as_ref() + .context("expected object validation")? + .properties + .iter() + .map(|(name, schema)| { + let schema = schema.object()?; + let ty = schema_object_type( + schema, + TypeContext::new(&new_struct_name, name), + additional_structs, + )?; + Ok(GoField { + rust_name: name.to_string(), + docs: documentation(schema), + ty, + }) + }) + .collect::>>()?; + + let strct = GoStruct { + name: new_struct_name.clone(), + docs: None, + fields, + }; + additional_structs.push(strct); + + new_struct_name + } else if t.contains(&InstanceType::Array) { + // get type of items + let item_type = array_item_type(schema, type_context, additional_structs) + .context("failed to get array item type")?; + + // for nullable array item types, we have to use a pointer type, even for basic types, + // so we can pass null as elements + // otherwise they would just be omitted from the array + replace_custom_type(&if item_type.is_nullable { + format!("[]*{}", item_type.name) + } else { + format!("[]{}", item_type.name) + }) + } else { + unreachable!("instance type should be one of the above") + }) +} + +/// Extract the type of the items of an array. +/// +/// This fails if the given schema object is not an array, +/// has multiple item types or other errors occur during type extraction of +/// the underlying schema. +pub fn array_item_type( + schema: &SchemaObject, + type_context: TypeContext, + additional_structs: &mut Vec, +) -> Result { + match schema.array.as_ref().and_then(|a| a.items.as_ref()) { + Some(SingleOrVec::Single(array_validation)) => { + schema_object_type(array_validation.object()?, type_context, additional_structs) + } + _ => bail!("array type with non-singular item type is not supported"), + } +} + +/// Tries to extract a type name from the given subschemas. +/// +/// This fails if there are multiple subschemas or other errors occur +/// during subschema type extraction. +pub fn subschema_type( + subschemas: &[Schema], + type_context: TypeContext, + additional_structs: &mut Vec, +) -> Result { + ensure!( + subschemas.len() == 1, + "multiple subschemas are not supported" + ); + let subschema = &subschemas[0]; + let GoType { name, .. } = + schema_object_type(subschema.object()?, type_context, additional_structs)?; + Ok(replace_custom_type(&name)) +} + +pub fn is_null(schema: &SchemaObject) -> bool { + schema + .instance_type + .as_ref() + .map_or(false, |s| s.contains(&InstanceType::Null)) +} + +pub fn documentation(schema: &SchemaObject) -> Option { + schema.metadata.as_ref()?.description.as_ref().cloned() +} + +/// Maps special types to their Go equivalents. +/// If the given type is not a special type, returns `None`. +pub fn custom_type_of(ty: &str) -> Option<&str> { + match ty { + "Uint128" => Some("string"), + "Binary" => Some("[]byte"), + "HexBinary" => Some("Checksum"), + "Addr" => Some("string"), + "Decimal" => Some("string"), + _ => None, + } +} + +pub fn replace_custom_type(ty: &str) -> String { + custom_type_of(ty) + .map(|ty| ty.to_string()) + .unwrap_or_else(|| ty.to_string()) +} diff --git a/packages/go-gen/src/utils.rs b/packages/go-gen/src/utils.rs new file mode 100644 index 0000000000..fab616c7ec --- /dev/null +++ b/packages/go-gen/src/utils.rs @@ -0,0 +1,80 @@ +/// An iterator that returns all suffixes of a string, excluding the empty string. +/// +/// It starts with the full string and ends with the last character. +/// It is a double-ended iterator and can be reversed. +pub fn suffixes(s: &str) -> impl Iterator + DoubleEndedIterator { + s.char_indices().map(|(pos, _)| &s[pos..]) +} + +/// Replaces common pascal-case acronyms with their uppercase counterparts. +pub fn replace_acronyms(ty: impl Into) -> String { + let mut ty = ty.into(); + replace_word_in_place(&mut ty, "Url", "URL"); + replace_word_in_place(&mut ty, "Uri", "URI"); + replace_word_in_place(&mut ty, "Id", "ID"); + replace_word_in_place(&mut ty, "Ibc", "IBC"); + ty +} + +fn replace_word_in_place(haystack: &mut String, from: &str, to: &str) { + assert_eq!(from.len(), to.len(), "from and to must be the same length"); + let mut start = 0; + while let Some(pos) = haystack[start..].find(from) { + let begin = start + pos; + let end = start + pos + from.len(); + let next_char = haystack.chars().nth(end); + match next_char { + Some(next_char) if next_char.is_ascii_lowercase() => {} + _ => { + // if the next character is uppercase or any non-ascii char or + // there is no next char, it's a full word + haystack.replace_range(begin..end, to); + } + } + start = end; + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + #[should_panic = "from and to must be the same length"] + fn replace_in_place_different_lengths() { + let mut s = "foo".to_string(); + replace_word_in_place(&mut s, "foo", "barbar"); + } + + #[test] + fn replace_in_place_multiple_works() { + let mut s = "FooFooFooFooFoo".to_string(); + replace_word_in_place(&mut s, "Foo", "bar"); + assert_eq!(s, "barbarbarbarbar"); + } + + #[test] + fn replace_in_place_single_works() { + let mut s = "foo".to_string(); + replace_word_in_place(&mut s, "foo", "bar"); + assert_eq!(s, "bar"); + } + + #[test] + fn replace_word_in_place_part() { + let mut s = "Foofoo".to_string(); + replace_word_in_place(&mut s, "Foo", "Bar"); + // should not replace, because it's not a full word + assert_eq!(s, "Foofoo"); + } + + #[test] + fn replace_acronyms_works() { + assert_eq!(replace_acronyms("MyIdentity"), "MyIdentity"); + assert_eq!(replace_acronyms("MyIdentityId"), "MyIdentityID"); + assert_eq!(replace_acronyms("MyUri"), "MyURI"); + assert_eq!(replace_acronyms("Url"), "URL"); + assert_eq!(replace_acronyms("A"), "A"); + assert_eq!(replace_acronyms("Url🦦"), "URL🦦"); + } +} diff --git a/packages/go-gen/tests/cosmwasm_std__AllBalanceResponse.go b/packages/go-gen/tests/cosmwasm_std__AllBalanceResponse.go new file mode 100644 index 0000000000..8d1ea81369 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__AllBalanceResponse.go @@ -0,0 +1,10 @@ +// AllBalancesResponse is the expected response to AllBalancesQuery +type AllBalancesResponse struct { + Amount []Coin `json:"amount"` // in wasmvm, there is an alias for `[]Coin` +} + +// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) +type Coin struct { + Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Denom string `json:"denom"` // type, eg. "ATOM" +} diff --git a/packages/go-gen/tests/cosmwasm_std__AllDelegationsResponse.go b/packages/go-gen/tests/cosmwasm_std__AllDelegationsResponse.go new file mode 100644 index 0000000000..ccebdbcd34 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__AllDelegationsResponse.go @@ -0,0 +1,16 @@ +// AllDelegationsResponse is the expected response to AllDelegationsQuery +type AllDelegationsResponse struct { + Delegations []Delegation `json:"delegations"` // in wasmvm, there is an alias for `[]Delegation` +} + +// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) +type Coin struct { + Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Denom string `json:"denom"` // type, eg. "ATOM" +} + +type Delegation struct { + Amount Coin `json:"amount"` + Delegator string `json:"delegator"` + Validator string `json:"validator"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__AllDenomMetadataResponse.go b/packages/go-gen/tests/cosmwasm_std__AllDenomMetadataResponse.go new file mode 100644 index 0000000000..d61cf12f3f --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__AllDenomMetadataResponse.go @@ -0,0 +1,51 @@ +type AllDenomMetadataResponse struct { + Metadata []DenomMetadata `json:"metadata"` + // NextKey is the key to be passed to PageRequest.key to + // query the next page most efficiently. It will be empty if + // there are no more results. + NextKey []byte `json:"next_key,omitempty"` +} + +// Replicating the cosmos-sdk bank module Metadata type +type DenomMetadata struct { + // Base represents the base denom (should be the DenomUnit with exponent = 0). + Base string `json:"base"` + // DenomUnits represents the list of DenomUnits for a given coin + DenomUnits []DenomUnit `json:"denom_units"` + Description string `json:"description"` + // Display indicates the suggested denom that should be + // displayed in clients. + Display string `json:"display"` + // Name defines the name of the token (eg: Cosmos Atom) + // + // Since: cosmos-sdk 0.43 + Name string `json:"name"` + // Symbol is the token symbol usually shown on exchanges (eg: ATOM). This can + // be the same as the display. + // + // Since: cosmos-sdk 0.43 + Symbol string `json:"symbol"` + // URI to a document (on or off-chain) that contains additional information. Optional. + // + // Since: cosmos-sdk 0.46 + URI string `json:"uri"` + // URIHash is a sha256 hash of a document pointed by URI. It's used to verify that + // the document didn't change. Optional. + // + // Since: cosmos-sdk 0.46 + URIHash string `json:"uri_hash"` +} + +// Replicating the cosmos-sdk bank module DenomUnit type +type DenomUnit struct { + // Aliases is a list of string aliases for the given denom + Aliases []string `json:"aliases"` + // Denom represents the string name of the given denom unit (e.g uatom). + Denom string `json:"denom"` + // Exponent represents power of 10 exponent that one must + // raise the base_denom to in order to equal the given DenomUnit's denom + // 1 denom = 10^exponent base_denom + // (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with + // exponent = 6, thus: 1 atom = 10^6 uatom). + Exponent uint32 `json:"exponent"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__AllValidatorsResponse.go b/packages/go-gen/tests/cosmwasm_std__AllValidatorsResponse.go new file mode 100644 index 0000000000..9280eda055 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__AllValidatorsResponse.go @@ -0,0 +1,14 @@ +// AllValidatorsResponse is the expected response to AllValidatorsQuery +type AllValidatorsResponse struct { + Validators []Validator `json:"validators"` // in wasmvm, there is an alias for `[]Validator` +} + +type Validator struct { + Address string `json:"address"` + // decimal string, eg "0.02" + Commission string `json:"commission"` + // decimal string, eg "0.02" + MaxChangeRate string `json:"max_change_rate"` + // decimal string, eg "0.02" + MaxCommission string `json:"max_commission"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__BalanceResponse.go b/packages/go-gen/tests/cosmwasm_std__BalanceResponse.go new file mode 100644 index 0000000000..a8735fd00f --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__BalanceResponse.go @@ -0,0 +1,10 @@ +// BalanceResponse is the expected response to BalanceQuery +type BalanceResponse struct { + Amount Coin `json:"amount"` +} + +// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) +type Coin struct { + Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Denom string `json:"denom"` // type, eg. "ATOM" +} diff --git a/packages/go-gen/tests/cosmwasm_std__BankQuery.go b/packages/go-gen/tests/cosmwasm_std__BankQuery.go new file mode 100644 index 0000000000..ef7729af81 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__BankQuery.go @@ -0,0 +1,43 @@ +type SupplyQuery struct { + Denom string `json:"denom"` +} + +type BalanceQuery struct { + Address string `json:"address"` + Denom string `json:"denom"` +} + +type AllBalancesQuery struct { + Address string `json:"address"` +} + +type DenomMetadataQuery struct { + Denom string `json:"denom"` +} + +type AllDenomMetadataQuery struct { + // Pagination is an optional argument. + // Default pagination will be used if this is omitted + Pagination *PageRequest `json:"pagination,omitempty"` +} + +type BankQuery struct { + Supply *SupplyQuery `json:"supply,omitempty"` + Balance *BalanceQuery `json:"balance,omitempty"` + AllBalances *AllBalancesQuery `json:"all_balances,omitempty"` + DenomMetadata *DenomMetadataQuery `json:"denom_metadata,omitempty"` + AllDenomMetadata *AllDenomMetadataQuery `json:"all_denom_metadata,omitempty"` +} + +// Simplified version of the cosmos-sdk PageRequest type +type PageRequest struct { + // Key is a value returned in PageResponse.next_key to begin + // querying the next page most efficiently. Only one of offset or key + // should be set. + Key []byte `json:"key"` + // Limit is the total number of results to be returned in the result page. + // If left empty it will default to a value to be set by each app. + Limit uint32 `json:"limit"` + // Reverse is set to true if results are to be returned in the descending order. + Reverse bool `json:"reverse"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__BondedDenomResponse.go b/packages/go-gen/tests/cosmwasm_std__BondedDenomResponse.go new file mode 100644 index 0000000000..cb16d22737 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__BondedDenomResponse.go @@ -0,0 +1,3 @@ +type BondedDenomResponse struct { + Denom string `json:"denom"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__CodeInfoResponse.go b/packages/go-gen/tests/cosmwasm_std__CodeInfoResponse.go new file mode 100644 index 0000000000..1173e9e7f9 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__CodeInfoResponse.go @@ -0,0 +1,5 @@ +type CodeInfoResponse struct { + Checksum Checksum `json:"checksum,omitempty"` + CodeID uint64 `json:"code_id"` + Creator string `json:"creator"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__ContractInfoResponse.go b/packages/go-gen/tests/cosmwasm_std__ContractInfoResponse.go new file mode 100644 index 0000000000..55ae866f55 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__ContractInfoResponse.go @@ -0,0 +1,9 @@ +type ContractInfoResponse struct { + // Set to the admin who can migrate contract, if any + Admin string `json:"admin,omitempty"` + CodeID uint64 `json:"code_id"` + Creator string `json:"creator"` + // Set if the contract is IBC enabled + IBCPort string `json:"ibc_port,omitempty"` + Pinned bool `json:"pinned"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__DelegationResponse.go b/packages/go-gen/tests/cosmwasm_std__DelegationResponse.go new file mode 100644 index 0000000000..9dcac82dfa --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__DelegationResponse.go @@ -0,0 +1,18 @@ +// DelegationResponse is the expected response to DelegationsQuery +type DelegationResponse struct { + Delegation *FullDelegation `json:"delegation,omitempty"` +} + +// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) +type Coin struct { + Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Denom string `json:"denom"` // type, eg. "ATOM" +} + +type FullDelegation struct { + AccumulatedRewards []Coin `json:"accumulated_rewards"` // in wasmvm, there is an alias for `[]Coin` + Amount Coin `json:"amount"` + CanRedelegate Coin `json:"can_redelegate"` + Delegator string `json:"delegator"` + Validator string `json:"validator"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__DelegatorWithdrawAddressResponse.go b/packages/go-gen/tests/cosmwasm_std__DelegatorWithdrawAddressResponse.go new file mode 100644 index 0000000000..0e46ed30e6 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__DelegatorWithdrawAddressResponse.go @@ -0,0 +1,3 @@ +type DelegatorWithdrawAddressResponse struct { + WithdrawAddress string `json:"withdraw_address"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__DenomMetadataResponse.go b/packages/go-gen/tests/cosmwasm_std__DenomMetadataResponse.go new file mode 100644 index 0000000000..a75ddaf21c --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__DenomMetadataResponse.go @@ -0,0 +1,47 @@ +type DenomMetadataResponse struct { + Metadata DenomMetadata `json:"metadata"` +} + +// Replicating the cosmos-sdk bank module Metadata type +type DenomMetadata struct { + // Base represents the base denom (should be the DenomUnit with exponent = 0). + Base string `json:"base"` + // DenomUnits represents the list of DenomUnits for a given coin + DenomUnits []DenomUnit `json:"denom_units"` + Description string `json:"description"` + // Display indicates the suggested denom that should be + // displayed in clients. + Display string `json:"display"` + // Name defines the name of the token (eg: Cosmos Atom) + // + // Since: cosmos-sdk 0.43 + Name string `json:"name"` + // Symbol is the token symbol usually shown on exchanges (eg: ATOM). This can + // be the same as the display. + // + // Since: cosmos-sdk 0.43 + Symbol string `json:"symbol"` + // URI to a document (on or off-chain) that contains additional information. Optional. + // + // Since: cosmos-sdk 0.46 + URI string `json:"uri"` + // URIHash is a sha256 hash of a document pointed by URI. It's used to verify that + // the document didn't change. Optional. + // + // Since: cosmos-sdk 0.46 + URIHash string `json:"uri_hash"` +} + +// Replicating the cosmos-sdk bank module DenomUnit type +type DenomUnit struct { + // Aliases is a list of string aliases for the given denom + Aliases []string `json:"aliases"` + // Denom represents the string name of the given denom unit (e.g uatom). + Denom string `json:"denom"` + // Exponent represents power of 10 exponent that one must + // raise the base_denom to in order to equal the given DenomUnit's denom + // 1 denom = 10^exponent base_denom + // (e.g. with a base_denom of uatom, one can create a DenomUnit of 'atom' with + // exponent = 6, thus: 1 atom = 10^6 uatom). + Exponent uint32 `json:"exponent"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__DistributionQuery.go b/packages/go-gen/tests/cosmwasm_std__DistributionQuery.go new file mode 100644 index 0000000000..79b541a7a1 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__DistributionQuery.go @@ -0,0 +1,8 @@ + +type DelegatorWithdrawAddressQuery struct { + DelegatorAddress string `json:"delegator_address"` +} + +type DistributionQuery struct { + DelegatorWithdrawAddress *DelegatorWithdrawAddressQuery `json:"delegator_withdraw_address,omitempty"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__IbcQuery.go b/packages/go-gen/tests/cosmwasm_std__IbcQuery.go new file mode 100644 index 0000000000..0bf4e30faa --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__IbcQuery.go @@ -0,0 +1,25 @@ +type PortIDQuery struct { +} + +// ListChannelsQuery is an IBCQuery that lists all channels that are bound to a given port. +// If `PortID` is unset, this list all channels bound to the contract's port. +// Returns a `ListChannelsResponse`. +// This is the counterpart of [IbcQuery::ListChannels](https://github.com/CosmWasm/cosmwasm/blob/v0.14.0-beta1/packages/std/src/ibc.rs#L70-L73). +type ListChannelsQuery struct { + // optional argument + PortID string `json:"port_id,omitempty"` +} + +type ChannelQuery struct { + ChannelID string `json:"channel_id"` + // optional argument + PortID string `json:"port_id,omitempty"` +} + +// IBCQuery defines a query request from the contract into the chain. +// This is the counterpart of [IbcQuery](https://github.com/CosmWasm/cosmwasm/blob/v0.14.0-beta1/packages/std/src/ibc.rs#L61-L83). +type IBCQuery struct { + PortID *PortIDQuery `json:"port_id,omitempty"` + ListChannels *ListChannelsQuery `json:"list_channels,omitempty"` + Channel *ChannelQuery `json:"channel,omitempty"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__StakingQuery.go b/packages/go-gen/tests/cosmwasm_std__StakingQuery.go new file mode 100644 index 0000000000..a9667033f9 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__StakingQuery.go @@ -0,0 +1,27 @@ +type BondedDenomQuery struct { // does not exist in wasmvm, but is an anonymous struct instead +} + +type AllDelegationsQuery struct { + Delegator string `json:"delegator"` +} + +type DelegationQuery struct { + Delegator string `json:"delegator"` + Validator string `json:"validator"` +} + +type AllValidatorsQuery struct { +} + +type ValidatorQuery struct { + /// Address is the validator's address (e.g. cosmosvaloper1...) + Address string `json:"address"` +} + +type StakingQuery struct { + BondedDenom *BondedDenomQuery `json:"bonded_denom,omitempty"` + AllDelegations *AllDelegationsQuery `json:"all_delegations,omitempty"` + Delegation *DelegationQuery `json:"delegation,omitempty"` + AllValidators *AllValidatorsQuery `json:"all_validators,omitempty"` + Validator *ValidatorQuery `json:"validator,omitempty"` +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__SupplyResponse.go b/packages/go-gen/tests/cosmwasm_std__SupplyResponse.go new file mode 100644 index 0000000000..c87e3dc2be --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__SupplyResponse.go @@ -0,0 +1,10 @@ +// SupplyResponse is the expected response to SupplyQuery +type SupplyResponse struct { + Amount Coin `json:"amount"` +} + +// Coin is a string representation of the sdk.Coin type (more portable than sdk.Int) +type Coin struct { + Amount string `json:"amount"` // string encoing of decimal value, eg. "12.3456" + Denom string `json:"denom"` // type, eg. "ATOM" +} \ No newline at end of file diff --git a/packages/go-gen/tests/cosmwasm_std__ValidatorResponse.go b/packages/go-gen/tests/cosmwasm_std__ValidatorResponse.go new file mode 100644 index 0000000000..21c24134ec --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__ValidatorResponse.go @@ -0,0 +1,14 @@ +// ValidatorResponse is the expected response to ValidatorQuery +type ValidatorResponse struct { + Validator *Validator `json:"validator"` // serializes to `null` when unset which matches Rust's Option::None serialization +} + +type Validator struct { + Address string `json:"address"` + // decimal string, eg "0.02" + Commission string `json:"commission"` + // decimal string, eg "0.02" + MaxChangeRate string `json:"max_change_rate"` + // decimal string, eg "0.02" + MaxCommission string `json:"max_commission"` +} diff --git a/packages/go-gen/tests/cosmwasm_std__WasmQuery.go b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go new file mode 100644 index 0000000000..84f3f453c1 --- /dev/null +++ b/packages/go-gen/tests/cosmwasm_std__WasmQuery.go @@ -0,0 +1,30 @@ + +// SmartQuery response is raw bytes ([]byte) +type SmartQuery struct { + // Bech32 encoded sdk.AccAddress of the contract + ContractAddr string `json:"contract_addr"` + Msg []byte `json:"msg"` +} + +// RawQuery response is raw bytes ([]byte) +type RawQuery struct { + // Bech32 encoded sdk.AccAddress of the contract + ContractAddr string `json:"contract_addr"` + Key []byte `json:"key"` +} + +type ContractInfoQuery struct { + // Bech32 encoded sdk.AccAddress of the contract + ContractAddr string `json:"contract_addr"` +} + +type CodeInfoQuery struct { + CodeID uint64 `json:"code_id"` +} + +type WasmQuery struct { + Smart *SmartQuery `json:"smart,omitempty"` + Raw *RawQuery `json:"raw,omitempty"` + ContractInfo *ContractInfoQuery `json:"contract_info,omitempty"` + CodeInfo *CodeInfoQuery `json:"code_info,omitempty"` +} \ No newline at end of file diff --git a/packages/std/src/ibc.rs b/packages/std/src/ibc.rs index f56944691c..d3afd68887 100644 --- a/packages/std/src/ibc.rs +++ b/packages/std/src/ibc.rs @@ -666,6 +666,17 @@ impl IbcReceiveResponse { } /// Set the acknowledgement for this response. + /// + /// ## Examples + /// + /// ``` + /// use cosmwasm_std::{StdAck, IbcReceiveResponse}; + /// + /// fn make_response_with_ack() -> IbcReceiveResponse { + /// let ack = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// IbcReceiveResponse::new().set_ack(ack) + /// } + /// ``` pub fn set_ack(mut self, ack: impl Into) -> Self { self.acknowledgement = ack.into(); self diff --git a/packages/std/src/lib.rs b/packages/std/src/lib.rs index d44d686da6..6bccc14595 100644 --- a/packages/std/src/lib.rs +++ b/packages/std/src/lib.rs @@ -28,6 +28,7 @@ mod query; mod results; mod sections; mod serde; +mod stdack; mod storage; mod timestamp; mod traits; @@ -87,6 +88,7 @@ pub use crate::results::{DistributionMsg, StakingMsg}; #[cfg(feature = "stargate")] pub use crate::results::{GovMsg, VoteOption}; pub use crate::serde::{from_binary, from_slice, to_binary, to_vec}; +pub use crate::stdack::StdAck; pub use crate::storage::MemoryStorage; pub use crate::timestamp::Timestamp; pub use crate::traits::{Api, Querier, QuerierResult, QuerierWrapper, Storage}; diff --git a/packages/std/src/stdack.rs b/packages/std/src/stdack.rs new file mode 100644 index 0000000000..e933257a08 --- /dev/null +++ b/packages/std/src/stdack.rs @@ -0,0 +1,163 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +use crate::binary::Binary; +use crate::to_binary; + +/// This is a standard IBC acknowledgement type. IBC application are free +/// to use any acknowledgement format they want. However, for compatibility +/// purposes it is recommended to use this. +/// +/// The original proto definition can be found at +/// and . +/// +/// In contrast to the original idea, [ICS-20](https://github.com/cosmos/ibc/tree/ed849c7bacf16204e9509f0f0df325391f3ce25c/spec/app/ics-020-fungible-token-transfer#technical-specification) and CosmWasm IBC protocols +/// use JSON instead of a protobuf serialization. +/// +/// For compatibility, we use the field name "result" for the success case in JSON. +/// However, all Rust APIs use the term "success" for clarity and discriminability from [Result]. +/// +/// If ibc_receive_packet returns Err(), then x/wasm runtime will rollback the state and +/// return an error message in this format. +/// +/// ## Examples +/// +/// For your convenience, there are success and error constructors. +/// +/// ``` +/// use cosmwasm_std::StdAck; +/// +/// let ack1 = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. +/// assert!(ack1.is_success()); +/// +/// let ack2 = StdAck::error("kaputt"); // Some free text error message +/// assert!(ack2.is_error()); +/// ``` +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum StdAck { + #[serde(rename = "result")] + Success(Binary), + Error(String), +} + +impl StdAck { + /// Creates a success ack with the given data + pub fn success(data: impl Into) -> Self { + StdAck::Success(data.into()) + } + + /// Creates an error ack + pub fn error(err: impl Into) -> Self { + StdAck::Error(err.into()) + } + + #[must_use = "if you intended to assert that this is a success, consider `.unwrap()` instead"] + #[inline] + pub const fn is_success(&self) -> bool { + matches!(*self, StdAck::Success(_)) + } + + #[must_use = "if you intended to assert that this is an error, consider `.unwrap_err()` instead"] + #[inline] + pub const fn is_error(&self) -> bool { + !self.is_success() + } + + /// Serialized the ack to binary using JSON. This used for setting the acknowledgement + /// field in IbcReceiveResponse. + /// + /// ## Examples + /// + /// Show how the acknowledgement looks on the write: + /// + /// ``` + /// # use cosmwasm_std::StdAck; + /// let ack1 = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// assert_eq!(ack1.to_binary(), br#"{"result":"AQ=="}"#); + /// + /// let ack2 = StdAck::error("kaputt"); // Some free text error message + /// assert_eq!(ack2.to_binary(), br#"{"error":"kaputt"}"#); + /// ``` + /// + /// Set acknowledgement field in `IbcReceiveResponse`: + /// + /// ``` + /// use cosmwasm_std::{StdAck, IbcReceiveResponse}; + /// + /// let ack = StdAck::success(b"\x01"); // 0x01 is a FungibleTokenPacketSuccess from ICS-20. + /// + /// let res: IbcReceiveResponse = IbcReceiveResponse::new().set_ack(ack.to_binary()); + /// let res: IbcReceiveResponse = IbcReceiveResponse::new().set_ack(ack); // Does the same but consumes the instance + /// ``` + pub fn to_binary(&self) -> Binary { + // We need a non-failing StdAck -> Binary conversion to allow using StdAck in + // `impl Into` arguments. + // Pretty sure this cannot fail. If that changes we can create a non-failing implementation here. + to_binary(&self).unwrap() + } + + pub fn unwrap(self) -> Binary { + match self { + StdAck::Success(data) => data, + StdAck::Error(err) => panic!("{}", err), + } + } + + pub fn unwrap_err(self) -> String { + match self { + StdAck::Success(_) => panic!("not an error"), + StdAck::Error(err) => err, + } + } +} + +impl From for Binary { + fn from(original: StdAck) -> Binary { + original.to_binary() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn stdack_success_works() { + let success = StdAck::success(b"foo"); + match success { + StdAck::Success(data) => assert_eq!(data, b"foo"), + StdAck::Error(_err) => panic!("must not be an error"), + } + } + + #[test] + fn stdack_error_works() { + let err = StdAck::error("bar"); + match err { + StdAck::Success(_data) => panic!("must not be a success"), + StdAck::Error(err) => assert_eq!(err, "bar"), + } + } + + #[test] + fn stdack_is_success_is_error_work() { + let success = StdAck::success(b"foo"); + let err = StdAck::error("bar"); + // is_success + assert!(success.is_success()); + assert!(!err.is_success()); + // is_eror + assert!(!success.is_error()); + assert!(err.is_error()); + } + + #[test] + fn stdack_to_binary_works() { + let ack1 = StdAck::success(b"\x01"); + assert_eq!(ack1.to_binary(), br#"{"result":"AQ=="}"#); + + let ack2 = StdAck::error("kaputt"); + assert_eq!(ack2.to_binary(), br#"{"error":"kaputt"}"#); + } +}