diff --git a/docs/tutorial/learning-resources.md b/docs/tutorial/learning-resources.md new file mode 100644 index 0000000..580a38c --- /dev/null +++ b/docs/tutorial/learning-resources.md @@ -0,0 +1,63 @@ +# Learning Resources + +This page contains curated learning resources to help you on your CosmWasm journey. Whether you're just getting started or looking to deepen your knowledge, these resources will help you master smart contract development with CosmWasm. + +## Video Tutorials + +### LearnWeb3Dev (Odie) + +Comprehensive video series covering CosmWasm development from basics to deployment: + +**Quick Start & Rust for Smart Contracts** +- 8-video playlist covering fundamentals +- [Watch on YouTube](https://www.youtube.com/watch?v=k55wD8p7AfY&list=PLdXjxuqotg2gahVYpCwfLcaD1ViZgCiQi) + +**CosmWasm** +- 6 videos on core CosmWasm concepts +- [Watch on YouTube](https://www.youtube.com/watch?v=i0ij_eYHJv4&list=PLdXjxuqotg2gI7ZpREJrVMGtvZsG4RjL7) + +**Deploying Smart Contracts To Osmosis** +- 5-video playlist for Osmosis deployment +- [Watch on YouTube](https://www.youtube.com/watch?v=MnyuVvdA6n4&list=PLdXjxuqotg2iDzmRQuSfU7MHU0FzSnuO1) + +**Deploying Smart Contracts To Neutron** +- 5 videos for Neutron deployment +- [Watch on YouTube](https://www.youtube.com/watch?v=vLkJLUwklYY&list=PLdXjxuqotg2g2lkyPMtXd5chNOo6Lw7ke) + +## Interactive Courses + +### B9Lab + +Comprehensive CosmWasm development course covering smart contract development from the ground up. + +- [CosmWasm Course](https://cosmwasm.b9lab.com/) + +### Area-52 + +Advanced training and certification for CosmWasm developers. + +- [Area-52 Platform](https://area-52.io/) + +## Comprehensive Tutorial Series + +### CosmWasm Developer Platform Tutorials + +A complete step-by-step series covering CosmWasm development from fundamentals to advanced topics, with 19 progressive tutorials. + +- [View Platform Tutorials](./platform/) - Comprehensive hands-on learning path + +## Chain-Specific Tutorials + +### Neutron + +Comprehensive introduction to developing CosmWasm contracts on Neutron. + +- [Introduction to CosmWasm on Neutron](https://docs.neutron.org/developers/tutorials/introduction_to_cosmwasm) + +## Community + +Looking to connect with other CosmWasm developers? Consider joining the CosmWasm community to share knowledge, get help, and collaborate on projects. + +## Next Steps + +After exploring these resources, check out our [tutorials](./introduction.mdx) to start building your first CosmWasm smart contract. diff --git a/docs/tutorial/platform/01-intro.md b/docs/tutorial/platform/01-intro.md new file mode 100644 index 0000000..557a350 --- /dev/null +++ b/docs/tutorial/platform/01-intro.md @@ -0,0 +1,54 @@ +--- +title: Introduction +description: Introduction to CosmWasm +--- + +# Introduction + +## From smart contracts to CosmWasm + +A smart contract is a piece of code that runs automatically in a provable way. The joke is that it is neither smart, as it only does what it is instructed to do, nor a contract, as it is not legally binding. + +This originally-abstract concept was made real with the advent of blockchains, and in particular of Ethereum. + +On a typical Ethereum platform, anyone is free to deploy a smart contract as long as they have the tokens to do so, and no validators are censoring them. A set of smart contracts plus related off-chain elements has come to be called a decentralized application, or dApp. However, a platform open to all introduces trade-offs, such as congestion that has nothing to do with the success of your application. This is why other platforms introduced the concept of application-specific blockchains, or app-chains for short, most notably the **Interchain**, a constellation of app-chains, most of them built off of the **Cosmos SDK**. + +The code that makes a given app-chain, or at least the part that impacts network consensus, can still be considered a smart contract, or a set thereof, as it runs automatically in a provable way. The difference is that to change a piece of code on an app-chain, or to add a new one, you need to make an on-chain proposal and reach consensus over it. This hurdle is in fact one of the points of an app-chain. + +Inevitably, this free-for-all / strict-app-chain dichotomy is not perfect, and some app-chains have had a need to reintroduce free (as in free speech, not free beer) smart contracts, for instance to **foster experimentation**. For this to be possible, an app-chain needs to have **components** that impact the network consensus, and that allow the deployment of **Turing-complete smart contracts**. Or at least complete within the limited resources of a blockchain. If these specific components are not present at the genesis of the app-chain, they too need to be introduced like any other with an on-chain proposal. After the relevant proposal has passed, then it becomes possible to deploy _free_ smart contracts. + +:::info + +This is where **CosmWasm** comes in, as the set of components that allow the deployment of Turing-complete smart contracts. + +::: + +The **Cosm** part of its name refers to the [Cosmos SDK](https://docs.cosmos.network). So unsurprisingly, the [CosmWasm _module_](https://github.com/CosmWasm/wasmd/tree/main/x/wasm) is a Cosmos SDK module, which may be present at genesis, or may be introduced as part of an upgrade proposal. This module allows any developer, at a minimum, to deploy smart contracts within it, and, more importantly, to have them **interact** with the rest of the app-chain in a controlled and controllable way. + +## CosmWasm tool chain + +Now that you understand the big picture of what CosmWasm is from the point of view of an app-chain, let's look at it from the point of view of a smart contract developer. To be able to work, as a smart contract developer you need to understand: + +* What languages are available. +* What are the interfaces and communication protocols you have to use and follow. +* What resources are available to the deployed smart contract, in this case, app-chain resources. +* What are the tools that will assist you or improve your productivity. + +The smart contract language chosen for CosmWasm is [WebAssembly](https://webassembly.org/), which explains the **Wasm** part of its name. WebAssembly is a stack-based binary instruction format associated with a set of low-level instructions. There exist WebAssembly virtual machines (WAVM) that are able to execute this binary, including in Web browsers. If you come from the Java or the Ethereum worlds, these concepts map directly to the JVM/EVM, bytecodes, and Assembly languages. + +:::info + +As such, the CosmWasm module, runs, connects, and instruments a WebAssembly VM variant, also known as _runtime_, currently [Wasmer](https://github.com/CosmWasm/cosmwasm/blob/main/packages/vm/Cargo.toml#L59). It also instruments it in order to, for instance, meter operations with a gas mechanism as a denial-of-service countermeasure. + +::: + +And just as in the Ethereum and Java worlds, developers code in higher-level languages, such as Solidity or Java, which are then compiled to their respective bytecodes. For CosmWasm, the currently preferred higher-level language is [**Rust**](https://www.rust-lang.org/). + +Rust exists and grows independently of the blockchain world, therefore the larger ecosystem can benefit CosmWasm developers, with important exceptions such as non-deterministic functions. This is why, within CosmWasm's set of components, you can find: + +* A set of interfaces that help you code your smart contracts as per the expectations of the CosmWasm module. +* An extensible set of messages defined in Rust that, when serialized and then interpreted by the CosmWasm module, allow your smart contract to communicate with the app-chain's other modules. +* Further libraries to handle storage, testing, and more. +* A compilation target, bytecode optimizer and checker to account for blockchains' particular situation, especially their limited resources. + +In addition, given that it is built for the Interchain, CosmWasm is ready for [IBC](https://www.ibcprotocol.dev), the Inter-Blockchain Protocol. Your CosmWasm smart contracts can even exchange messages with other CosmWasm smart contracts, or its own clones, on other app-chains. diff --git a/docs/tutorial/platform/02-concepts-overview.md b/docs/tutorial/platform/02-concepts-overview.md new file mode 100644 index 0000000..e7071bf --- /dev/null +++ b/docs/tutorial/platform/02-concepts-overview.md @@ -0,0 +1,825 @@ +--- +title: Concepts +description: CosmWasm concepts overview +--- + +# CosmWasm concepts overview + +The creators of the CosmWasm pieces had the benefit of hindsight and so reused and/or modified concepts found in other smart contracting systems, and invented others. + +## Bytecode lifecycle + +At the risk of pushing an open door, a smart contract has an associated **bytecode**, and that bytecode has to be stored somewhere. + +If you come from Ethereum, then it sounds natural to you that each smart contract instance has its own bytecode. + + + + +Indeed, with the EVM, the bytecode is stored in the `code` sub-area of the smart contract instance's account. Different instances may effectively have the same bytecode, as can be confirmed by their identical bytecode hashes. And although identical bytecode may be stored only once at the node level, it still costs users in full for each instance to deploy their own bytecode, although to a lesser extent when using bytecode-reuse techniques such as _proxying_. + +```mermaid +--- +config: {} +title: The Ethereum Way +--- +flowchart TB + subgraph Tx1["Signed Deploy Tx1"] + BytecodeTx1["Bytecode"] + end + subgraph Instance1["Smart Contract Instance1"] + direction TB + State1["Instance Store"] + Bytecode1["Stored Bytecode"] + Bal1["Eth Balance"] + Stor1["State"] + end + subgraph Tx2["Signed Deploy Tx2"] + BytecodeTx2["Bytecode"] + end + subgraph Instance2["Smart Contract Instance2"] + direction TB + State2["Instance Store"] + Bytecode2["Stored Bytecode"] + Bal2["Eth Balance"] + Stor2["State"] + end + Tx1 --> Deploy1{"Instantiate"} + Tx2 --> Deploy2{"Instantiate"} + Deploy1 --> Instance1 + Deploy2 --> Instance2 + State1 --> Bytecode1 & Bal1 & Stor1 + State2 --> Bytecode2 & Bal2 & Stor2 + Bytecode["Bytecode"] --> Tx1 & Tx2 + Code["Code"] --> Compile + Compile{"Compile"} --> Bytecode + style Bytecode1 fill:#E1BEE7 + style BytecodeTx1 fill:#E1BEE7 + style Bytecode2 fill:#E1BEE7 + style BytecodeTx2 fill:#E1BEE7 +``` + + + + +CosmWasm, on the other hand, **separates [storing](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L89)** of the bytecode on chain, **and [instantiating](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L113)** a smart contract instance using said bytecode. + +A stored bytecode is identified by an **id**, which is just a good old [auto-incrementing integer](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/keeper.go#L183). Optionally, you can also apply permissions to [control the use](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/keeper.go#L152-L159) of this stored bytecode, and [change these permissions](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L273-L283) at a later stage. Thereafter, when you instantiate a smart contract instance, you mention the [bytecode id](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L122) to use. + +Here is what these actions would look like on the command line: + + + + Sending a transaction to store a bytecode on-chain looks like this: + + ```sh + wasmd tx wasm store path_to/compiled_smart_contract.wasm \ + --from ... + ``` + + After the transaction has been validated, you can find the bytecode id in the events, as it looks something like that: + + ```json + ... + "type": "store_code", + "attributes": [ + { + "key": "code_id", + "value": "8", + "index": true + }, + ] + ... + ``` + This tells you that the just-stored bytecode has the id `8`. + + + + Sending a transaction to instantiate a smart contract looks like this: + + ```sh + wasmd tx wasm instantiate 8 '{"constructor_field1":...}' \ + --from ... + ``` + + Where `8` is the bytecode id that your instance will use. + After the transaction has been validated, you can find the smart contract's address in the events, as it looks something like that: + + ```json + ... + "type": "instantiate", + "attributes": [ + { + "key": "_contract_address", + "value": "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d", + "index": true + }, + ] + ... + ``` + + To understand how the `wasm14hj...` address was created for your smart contract instance, head to the [hello world](./04-hello-world.html) where these is a deep-dive named "How was this address computed?". + + + +A non-negligeable side benefit of having the bytecode stored separately is that when one [smart contract deploys another](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/results/cosmos_msg.rs#L207), it only needs to mention the bytecode id to use, instead of passing the whole bytecode. From the smart contract's point-of-view, it would look like this: + + +```rust +return Ok(response.add_message( + SubMsg::reply_on_success( // The eventual reply contains the new address + WasmMsg::Instantiate { // Instruct the CosmWasm module + code_id: 8u64, // 8 written with 64 bits + msg: to_json_binary( // Serialize the instantiate message + &InstantiateMsg { // An instantiate message valid for this bytecode + constructor_field1: ... + } + )?, + ... + }, + ... + ) +)); +``` + + +If you want to store code and instantiate within the same transaction, you can also do that with [a special message](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L387), although it is not available from the command-line out of the box. Unless you add the command-line bindings yourself, that is. + +Once stored, a bytecode cannot be modified. In particular, you cannot upgrade, or migrate, a bytecode as part of an upgrade of the underlying app chain. On the other hand, a smart contract instance's id of the bytecode in use can either be immutable or [upgradeable](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L227); you decide at deployment which one it shall be by setting or ommitting the [admin](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L120). + +```mermaid +--- +config: {} +title: The CosmWasm Way +--- +flowchart TB + subgraph StoreTx["Signed Store Tx"] + BytecodeTx["Bytecode"] + end + subgraph BytecodeStore["CosmWasm Bytecode Store"] + direction LR + CodeId["code_id"] + BytecodeStored["Bytecode"] + end + subgraph Tx1["Signed Instantiate Tx1"] + CodeIdTx1["code_id"] + end + subgraph Instance1["Smart Contract Instance1"] + direction TB + State1["Instance Store"] + StoredCode1["code_id"] + Stor1["State"] + end + subgraph Tx2["Signed Instantiate Tx2"] + CodeIdTx2["code_id"] + end + subgraph Instance2["Smart Contract Instance2"] + direction TB + State2["Instance Store"] + StoredCode2["code_id"] + Stor2["State"] + end + Code["Code"] --> Compile{"Compile"} + Compile --> Bytecode["Bytecode"] + Bytecode -->|Copy| BytecodeTx + StoreTx --> BytecodeStore + CodeId -->|Maps to| BytecodeStored + Tx1 --> Deploy1{"Instantiate"} + Tx2 --> Deploy2{"Instantiate"} + Deploy1 --> Instance1 + Deploy2 --> Instance2 + CodeId -.->|Copy| CodeIdTx1 & CodeIdTx2 + State1 -->|Contains| StoredCode1 & Stor1 + State2 -->|Contains| StoredCode2 & Stor2 + style BytecodeTx fill:#E1BEE7 + style BytecodeStored fill:#E1BEE7 + style CodeId fill:#BEE1E7 + style StoredCode1 fill:#BEE1E7 + style StoredCode2 fill:#BEE1E7 +``` + +If you come from Ethereum, you expect a deterministic compilation step, which makes it possible to verify that a given bytecode is the product of a given code. CosmWasm offers the same tool chain, with a [verifier](https://medium.com/cosmwasm/dont-trust-cosmwasm-verify-db1caac2d335) for Rust code. + +## The module's messages + + + + +The Cosmos SDK defines a transaction type: + + +```go +type Tx struct { + // body is the processable content of the transaction + Body *TxBody `protobuf:"bytes,1,opt,name=body,proto3" json:"body,omitempty"` + ... +} +``` + + +Where the body is defined as: + + +```go +type TxBody struct { + // messages is a list of messages to be executed. The required signers of + // those messages define the number and order of elements in AuthInfo's + // signer_infos and Tx's signatures. Each required signer address is added to + // the list only the first time it occurs. + // By convention, the first required signer (usually from the first message) + // is referred to as the primary signer and pays the fee for the whole + // transaction. + Messages []*types.Any `protobuf:"bytes,1,rep,name=messages,proto3" json:"messages,omitempty"` + ... +} +``` + + +In there, `types.Any` can be any Cosmos module's message, including those for [Bank](https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/x/bank/types/tx.pb.go#L37-L41) or [CosmWasm](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L342-L351). + + + + +Because the CosmWasm module is a **Cosmos module**, it is called to action with its own queries, messages, and hooks. As examples of Cosmos messages, you have: + +* [`MsgStoreCode`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L40-L48) is used to store a bytecode inside the CosmWasm bytecode store. When sending it, you just put the bytecode in [`bytes wasm_byte_code`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L96). To see it in action, go to _Store your contract code_ part of the [hello world](./04-hello-world.html#store-your-contract-code). +* [`MsgExecuteContract`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L342-L351) is used to instruct the CosmWasm module to call the [_execute_ entry point](https://docs.cosmwasm.com/core/entrypoints/execute), a.k.a. function, on the contract instance identified by [`string contract`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L196), and with the entry point arguments serialized in [`bytes msg`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L198-L201). + + + ```protobuf + message MsgExecuteContract { + ... + // Sender is the that actor that signed the messages + string sender = 1 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; + // Contract is the address of the smart contract + string contract = 2 [ (cosmos_proto.scalar) = "cosmos.AddressString" ]; + // Msg json encoded message to be passed to the contract + bytes msg = 3 [ + (gogoproto.casttype) = "RawContractMessage", + (amino.encoding) = "inline_json" + ]; + ... + } + ``` + + To belabor the above point: + + 1. `message MsgExecuteContract` is a Cosmos SDK message that the app-chain sends to its own CosmWasm module. + 2. `bytes msg` is the serialized CosmWasm `ExecuteMsg` that the CosmWasm module sends to the `execute` entry point of one of the smart contracts instantiated within the CosmWasm module. Its interpretation is up to the smart contract itself. + + To see it in action, go to _Send a transaction to your contract_ part of the [hello world](./04-hello-world.html#send-a-transaction-to-your-contract). To see how the smart contract handles the `ExecuteMsg`, go to the practical exercise's [First Execute Transaction](./06-first-contract-register.html). + +## A smart contract's messages + +When it comes to the smart contract messages, the role of the CosmWasm module is: + +* To pass these pieces of information onwards to the proper smart contract(s)' entry points, +* and in Rust form (in its WebAssembly form actually), +* with **certain guarantees** about the information passed along. + +For instance: + +* The smart contract can know with certainty: + * Among other message-related information, which account sent the message: + + ```rust + pub struct MessageInfo { + /// The `sender` field from `MsgInstantiateContract` and `MsgExecuteContract`. + /// You can think of this as the address that initiated the action (i.e. the message). What that + /// means exactly heavily depends on the application. + /// + /// The x/wasm module ensures that the sender address signed the transaction or + /// is otherwise authorized to send the message. + pub sender: Addr, + ... + } + ``` + + This sender may be the signer of a transaction or may be another smart contract within the same CosmWasm module (think Ethereum Solidity's `msg.sender`). Your smart contract ought to be agnostic as to which one it is. + * And, among other block-related information, at [what height](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/types.rs#L32) the whole chain is/was at the time of execution: + + ```rust + pub struct BlockInfo { + /// The height of a block is the number of blocks preceding it in the blockchain. + pub height: u64, + ... + } + ``` + + (Think Ethereum Solidity's `block.height`). +* When a user instructs the CosmWasm module that a smart contract call needs to include a certain **amount of tokens**, the CosmWasm module will take these tokens from the sender and inform the smart contract that it has in fact taken these tokens and credited them to the smart contract's address (Think if there was an Ethereum Solidity's `msg.value` for ERC-20s). The smart contract can then work with this **guaranteed assumption**. + + ```rust + pub struct MessageInfo { + ... + /// The funds that are sent to the contract as part of `MsgInstantiateContract` + /// or `MsgExecuteContract`. The transfer is processed in bank before the contract + /// is executed such that the new balance is visible during contract execution. + pub funds: Vec, + } + ``` + + See [the hello world's](./04-hello-world.html#send-a-transaction-to-your-contract) _Send a transaction to your contract_ to pass funds along with a contract call, and [the practical exercise's](./16-fund-handling.html) _Proper Fund Handling_ to see it implemented in your own smart contract. +* When a smart contract sends a message to another one, and this other contract fails, the originating smart contract does not by default need to roll back any state changes. Instead, the CosmWasm module provides this guarantee of atomicity, unless instructed otherwise. See [the practical exercise's](./14-contract-reply.html) _First Contract Reply Integration_ for an example of the default behavior. +* Additionally, when a smart contract sends a message to another and expects a reply, the payload returned (CosmWasm v2 only) is the same as the one that was sent initially, guaranteed by the CosmWasm module. + +The structure from a Cosmos transaction to a message received by a CosmWasm smart contract can be summarized as follows: + +```mermaid +--- +title: Cosmos SDK to CosmWasm module's Messages Structure (Clickable) +--- +classDiagram + namespace CosmosSDK { + class Tx { + +TxBody body + ... + } + + class TxBody { + +Message[] messages + ... + } + + class CosmosMessage { + +String type + +[]Byte value + ... + } + } + + namespace Bank { + class BankModuleMessage { + type = "cosmos.bank.v1beta1..." + ... + } + + class MsgSend { + +Coins amount + ... + } + } + + namespace CosmWasm { + class CosmWasmModuleMessage { + type = "cosmwasm.wasm.v1..." + ... + } + + class MsgStoreCode { + +[]Byte wasm_bytecode + ... + } + + class MsgExecuteContract { + +String contract_address + +[]Byte inner_msg + ... + } + } + + namespace SmartContractX { + class ExecuteMsg { + +String type + +Any optional_data + ... + } + } + + link Tx "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/types/tx/tx.pb.go#L34-L44" "Tx in Cosmos SDK" + link TxBody "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/types/tx/tx.pb.go#L348-L372" "TxBody in Cosmos SDK" + link CosmosMessage "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/codec/types/any.go#L15-L56" "CosmosMessage in Cosmos SDK" + link BankModuleMessage "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/x/bank/types/tx.pb.go" "Bank messages in Cosmos SDK" + link MsgSend "https://github.com/cosmos/cosmos-sdk/blob/v0.50.9/x/bank/types/tx.pb.go#L37-L41" "Bank MsgSend in Cosmos SDK" + link CosmWasmModuleMessage "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go" "CosmWasm messages in Wasmd" + link MsgStoreCode "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L40-L48" "MsgStoreCode in Wasmd" + link MsgExecuteContract "https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L342-L351" "MsgExecuteContract in Wasmd" + link ExecuteMsg "https://github.com/b9lab/cw-my-collection-manager/blob/main/src/msg.rs#L31-L36" "ExecuteMsg in smart contract" + Tx --* TxBody : contains 1 + TxBody --* CosmosMessage : contains many + CosmosMessage <|-- BankModuleMessage : is a + BankModuleMessage --* MsgSend : contains 1 (example) + CosmosMessage <|-- CosmWasmModuleMessage : is a + CosmWasmModuleMessage --* MsgStoreCode : contains 1 (example) + CosmWasmModuleMessage --* MsgExecuteContract : contains 1 (example) + MsgExecuteContract --* ExecuteMsg : contains 1 (example) +``` + +## Entry points + +As mentioned earlier, a CosmWasm smart contract can be deployed and called with a message, a reply or a query. These concepts are in fact identified in the smart contract interface as different methods called [entry points](https://docs.cosmwasm.com/core/entrypoints). + + + + +If you come from the EVM world, a smart contract instantiation is the result of a specific type of transactions where the recipient is missing. + +And a smart contract instance has a single point of entry, _behind_ which there is a dispatcher that jumps to the code of the available methods as identified by their respective function selector, or the _fallback_ when the provided selector is not found. In effect, the program counter always starts at `0` in the bytecode. + + + + +A WebAssembly binary instead _exports_ a set of functions that can be called, not unlike what a library would do. The Wasm VM of the CosmWasm module can call such a function. So the purpose of the entry point is to identify a function that should be exported by the WebAssembly compiler, and to confirm that the function signature conforms to the expectations of CosmWasm. + + +```rust +#[cfg_attr(not(feature = "library"), entry_point)] +pub fn execute( + mut deps: DepsMut, + env: Env, + info: MessageInfo, + msg: ExecuteMsg, +) -> ContractResult { + ... +} +``` + + +The role of the CosmWasm module is to: + +* **Call** the relevant entry points of relevant smart contracts in the relevant situations. +* **Populate** elements such as `env` and `info` with the right values. +* **Verify** and then **pass** on the `msg` or other value coming from elsewhere. +* **Run** the code and **handle** all returns, including potential rollbacks. + +If you don't implement a given entry point, then it is closed and attempting to call it results in an error; there are no fallbacks. For instance, if your smart contract does not implement the _sudo_, or the _migrate_, entry point, then these functions are unavailable although your smart contract can still _execute_ and respond to _query_. + +```mermaid +--- +title: Not all entry points need to be implemented +--- +classDiagram + class ContractA { + +execute() + +query() + +sudo() + +migrate() + +ibc_packet_receive() + } + + class ContractB { + +execute() + +query() + +sudo() + } + + class ContractC { + +execute() + +query() + } +``` + +## Smart contract execution + +### Invocation + +When a CosmWasm smart contract is invoked: + +* Its code is loaded into memory, either from the node's cache or from the app-chain state. +* It is allocated some configurable amount of memory, typically [32 MB](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/keeper.go#L40) for the stack and the heap. +* It is given access to its state. +* It is given the arguments to run on. These arguments can come from various places, and are not limited to: + * The message itself in the case of an `ExecuteMsg` or `SudoMsg`, + * Or from IBC packets if the smart contract is configured for IBC. +* Out of the execution come further messages and IBC packets, plus a possibly updated state. + +```mermaid +--- +title: State Machine - Execution Simplified +--- +flowchart TD + MessageIn["Message In"] --> Execute + StateIn["State In"] --> Execute + StateIn --> HandlePacket + subgraph ExecutionEnv["Execution Environment"] + Execute + HandlePacket["Handle IBC Packet"] + end + PacketIn["IBC Packet In"] --> HandlePacket + subgraph ResponseOut["Response Out"] + MessageOut["Messages Out"] + PacketOut["IBC Packets Out"] + end + Execute --> ResponseOut + HandlePacket --> ResponseOut + Execute --> StateOut["State Out"] + HandlePacket --> StateOut +``` + +### Determinism + +To achieve consensus on the blockchain, smart contracting platforms need to have deterministic execution, in the sense that non-validating nodes should be able to verify blocks, transactions and state at any later time. If you are familiar with the EVM, you know that all available opcodes are deterministic, and in fact have been created from scratch with determinism as a core requirement. + +On the other hand, WebAssembly has a [degree of nondeterminism](https://github.com/WebAssembly/design/blob/main/Nondeterminism.md), and Rust has libraries that give access to nondeterministic aspects, such as CPU time or network I/O. CosmWasm mitigates this issue by not exposing the OS layer to smart contracts. + +### DoS protection + +In terms of gas, because each metered action in Web Assembly, for instance adding 2 integers, is potentially quite insignificant, the [module meters gas](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/keeper.go#L414-L415) in its own unit: _wasm gas_. At the moment, 1 wasm gas [converts](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/gas_register.go#L264) to [1/140,000th](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/gas_register.go#L34) _regular Cosmos gas_. Also, the module does not double-count what is anyway measured at the SDK level, such as accessing the on-chain storage. + + + + +A node running a blockchain with the CosmWasm module meters the smart contract when it encounters its code for the first time. After that, it keeps the relevant information in its local cache. + +This design was the reason behind [an incomplete patching event](https://medium.com/cosmwasm/the-incomplete-gas-patch-and-why-it-caused-consensus-failures-173547ef02de), where even after the software patch had been applied, the nodes still relied on their old caches, making it look like no upgrade had taken place. Clearing the nodes' caches, fixed that issue. + + + + +### Deterministic invocation + +Looking back at the execution arguments, wheverer they come from, they too are always part of the consensus: + +* Either because the values are inscribed in a transaction's message, +* Or they have been computed by another smart contract, in the case of cross-contract messages. + +### Receiving tokens + +A CosmWasm smart contract has an address and as such, you can send tokens to it in the same way that you send tokens to a Cosmos account, with the use of the bank module's `SendMsg`. And unlike what happens in Ethereum, **this action does not trigger code execution**. A smart contract can also send out tokens it owns as part of its execution. + + + + +For instance, to prepare the message in order to send tokens using the Rust-to-bank _bindings_ would look like this: + + +```rust +let bank_msg = BankMsg::Send { + to_address: sender, + amount: coins(amount.u128(), token_denom), +}; +``` + + + + + +Of course, it is possible to have code execute in the same transaction by sending a `MsgExecute` alongside the `MsgSend`. However, in this case, both messages are uncorrelated, and the smart contract cannot verify that the adequate transfer has been made. It can only check its own balance, which would be an attack vector. + +However, throwing tokens _over the fence_ to the smart contract is the lesser-used way of sending tokens to a smart contract. You should preferably use the [`funds`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/types/tx.pb.go#L350) feature of `MsgExecute`. As mentioned in the messages section above, the CosmWasm module will do the requested fund transfer prior to the execution, so that the smart contract has guarantees that the adequate token transfer has been done to its benefit. See [this hands-on exercise](./16-fund-handling.html) for an example. + +On the other hand, for IBC token transfers, it is possible for a smart contract to subscribe to IBC callbacks such that it can be notified as soon as it receives tokens or when the tokens it has sent cross-chain have been received or have timed out. + +For a token transfer to be handled by the CosmWasm module, and the smart contract to have guarantees that the token transfer was done, a user has to create a `MsgExecute` message that carries funds information. + +## Smart contract instance storage + +As mentioned earlier, bytecode is stored separately from smart contract instances. Now, each instance has its own on-chain storage, separate from each other. It is the role of the CosmWasm module to give access to its **own storage**, and only it, to a smart contract: + +* In read/write mode when passing a message or an IBC packet. +* In read-only mode when passing a query. + + + + +If you come from the EVM world, then you know that the EVM has primitive commands like `sstore` that implicitly access the smart contract's storage. The EVM also makes sure that a given smart contract has access to its own storage only, and that it is read-only when accessed during a query, or with a `staticcall` for that matter. + + + + +The WebAssembly VM has no primitives that give access to a hypothetical on-chain storage. Instead, when the CosmWasm module calls one of the smart contract's functions, it passes along a [**context object**](https://github.com/b9lab/cw-my-nameservice/blob/first-execute-message/src/contract.rs#L22) with [fields](https://github.com/b9lab/cw-my-nameservice/blob/first-execute-message/src/contract.rs#L36) and methods that give access to the on-chain storage in read-only or read/write, depending on the context of the call, i.e. [query](https://github.com/b9lab/cw-my-nameservice/blob/first-query-message/src/contract.rs#L48) or [execution](https://github.com/b9lab/cw-my-nameservice/blob/first-query-message/src/contract.rs#L24) respectively. + + + + + ```rust + pub fn query(deps: Deps, ...) ... + ``` + + `Deps` gives access to immutable, i.e. unmodifiable, state. + + + + ```rust + pub fn execute(deps: DepsMut, ...) ... + ``` + + `DepsMut` gives access to mutable, i.e. modifiable, state. + + + +Unsurprisingly, the CosmWasm module arranges each smart contract's storage (think `contract1337/`) within its own module's storage (think `wasm/`), which is itself part of the on-chain storage (think `/`). If you need to, [refresh](https://tutorials.cosmos.network/academy/2-cosmos-concepts/7-multistore-keepers.html) yourself on Cosmos SDK keeper storage. + +```mermaid +--- +title: Chain Storage Sub-division +--- +flowchart TB + subgraph ChainStorage["Chain Storage '/'"] + direction TB + + subgraph WasmModule["CosmWasm Module Storage 'wasm/'"] + direction TB + + subgraph BytecodeStorage["Bytecode Storage 'bytecode/'"] + direction TB + + map["code_id → wasm_bytes"] + end + + subgraph Contracts["Instances Storage 'contract'"] + CONTRACT1["Contract #1337 '1337/' + • State + • Config"] + + CONTRACT2["Contract #1338 '1338/' + • State + • Config"] + + CONTRACT3["Contract #1339 '1339/' + • State + • Config"] + end + end + + OTHER_MODULES["Other Modules' Storage + • 'bank/' + • 'staking/' + • Custom..."] + end + + %% Styling + classDef chainBox fill:#e6f3ff,stroke:#99ccff + classDef moduleBox fill:#fff0f5,stroke:#ffb6c1 + classDef contractBox fill:#f0fff0,stroke:#99ff99 + classDef accessBox fill:#fffae6,stroke:#ffd700 + classDef evmBox fill:#f0e6ff,stroke:#cc99ff + classDef implBox fill:#ffe6e6,stroke:#ff9999 + + class ChainStorage chainBox + class WasmModule,BYTECODE moduleBox + class Contracts,CONTRACT1,CONTRACT2,CONTRACT3 contractBox + class AccessControl,MSG_ACCESS,QUERY_ACCESS accessBox + class ComparisonEVM,EVM_CONTRACT,EVM_OPS,EVM_SCOPE evmBox + class Implementation,PREFIX,KEY_VALUE,ISOLATION implBox +``` + +## Calling out from the smart contract + +An interesting feature of smart contracts is that they can **call out to other parts** of the system. CosmWasm smart contracts do too. + +### Almost absent reentrancy risk + +If you come from the EVM world, you ought to be well acquainted with the risks associated with reentrancy. This risk is facilitated by smart contracts calling each other mid-execution. Eventually, you learn to implement the checks-effects-interactions design pattern as a protection mechanism. + +CosmWasm **reduces** the risk of reentrancy with a different design at the platform level: a smart contract instance can only call out to other systems, including other smart contracts, only after it has exited its own invocation. In effect, this forces the _interactions_ part to come last. Of course, it is still on you, the smart-contract developer, to correctly implement the [_checks_](https://github.com/b9lab/cw-my-nameservice/blob/first-multi-test/src/contract.rs#L38-L40) and [_effects_](https://github.com/b9lab/cw-my-nameservice/blob/first-multi-test/src/contract.rs#L42) parts within the function's body. The call mechanism is as follows. + +### Invocation mechanism + +A successful smart contract execution can return a [list of messages](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L103) that will be acted upon in turn _within the same transaction_, therefore atomically. For instance, to invoke another smart contract, the invoking smart contract can return a [`WasmMsg::Execute`](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L83) as part of its returned messages. This sequential design approach ensures that the execution of the invoked smart contract does not insert itself in the middle of the execution of the invoker, all the while preserving guarantees of atomicity. + + +```rust +pub fn execute(...) -> ContractResult { + // Checks + // Effects + Ok(response.add_submessage(onward_sub_msg)) +} +``` + + +Note that if there are nested returned messages, messages are evaluated [depth-first](https://docs.cosmwasm.com/docs/smart-contracts/contract-semantics/#dispatching-messages) as this cleaves to the intent of the caller. + +Go to [this exercise](./12-cross-contract.html) to see it in action. + +### Reply mechanism + +Of course, in some situations, your smart contract may invoke another and expect to receive a value in return, such as a newly created identifier. A fire-and-forget message at the end of the execution will not work. In this case, instead of returning a _simple_ message, you need to wrap the message to [create a sub-message](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L88-L93). The mechanism for this is to mention, as part of the sub-message, that [a reply is expected](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L91). + + +```rust +pub fn execute(...) -> ContractResult { + // Checks + // Effects + let onward_sub_msg = SubMsg { + id: 1u64, + msg: CosmosMsg::::Wasm(onward_exec_msg), + reply_on: ReplyOn::Success, + gas_limit: None, + }; + Ok(response.add_submessage(onward_sub_msg)) +} +``` + + +And to code the **`reply`** entry point (function) such that it handles all expected replies. Your sub-message contains an [id](https://github.com/CosmWasm/cosmwasm/blob/v2.1.0/packages/std/src/results/submessages.rs#L35) for the smart contract to [match the reply](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L165) to the original sub-message, in order to carry on with the execution. The sub-message also contains a [binary payload](https://github.com/CosmWasm/cosmwasm/blob/v2.1.0/packages/std/src/results/submessages.rs#L49) (in CosmWasm 2.0) that you can use as call context instead of using the storage temporarily, thereby saving gas. Go to [this exercise](./14-contract-reply.html) to see it in action. + + +```rust +pub fn reply(..., msg: Reply) -> ContractResult { + match ReplyCode::try_from(msg.id)? { + 1u64 => reply_pass_through(..., msg), + } +} +``` + + + + +Take note of how the `reply` entry point is deliberately different from the `execute` entry point, always with a view of reducing the risk of reentrancy. It is incumbent on you, the developer, to **not re-introduce an attack vector** by moving _checks_ or _effects_ from `execute` to `reply`. + + + +### Composing invocations + +Moreover, there is a mechanism to let you, the developer, substitute the returned value of the original `execute` call with the returned value of the corresponding `reply` call. This substitution makes it possible to have the combination of `execute` and `reply` act as as single invocation from the point of view of the original caller. + +This lets you achieve cross-contract communication and cooperation, also called _composition_, as with the EVM, but with a reduced risk of reentrancy. This does not eliminate it entirely, though, as you still have to code all of your checks-effects into the body of the `execute` function, and avoid implementing the _effects_ in the `reply` function. + +In a sense, with this message-passing architecture, the set of CosmWasm smart contract instances follows the principles of an actor model. + +### Cross query + +The above is about messages, which have the potential to change contracts' states, and where attacks may happen. On the other hand, if a smart contract only needs a value in a read-only mode, this is a different and safer situation. For this situation, the context object [has the method](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L95-L96) `deps.querier.query` that lets the smart contract call another synchronously in a read-only mode, where the read-only part is enforced at the VM level. + + +```rust +let token_count_result = deps.querier + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + contract_addr: collection, + msg: to_json_binary(&CollectionQueryMsg::NumTokens {})?, + })); +``` + + +If your smart contract calls others, you can also [include the target's message types](https://docs.cosmwasm.com/docs/architecture/composition#type-safe-wrappers) in order to benefit from compile-time type checking, instead of passing along serialized binaries. + +If you want to learn more about learnings from Ethereum, head [here](https://docs.cosmwasm.com/docs/architecture/smart-contracts#lessons-learned-from-ethereum). + +### To app-chain modules + +Smart contract can also call modules found in the underlying Cosmos SDK app-chain, and vice versa. + +So that you don't have to reinvent the wheel, the Rust library offers traits that expose frequently-used Cosmos SDK modules, such as [bank](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/results/cosmos_msg.rs#L93-L106). It also has base types that help you create custom messages and queries, called _bindings_, to access custom modules of your app chain. + + +```rust +BankMsg::Send { + to_address: payment_params.beneficiary.to_string(), + amount: vec![paid], +} +``` + + +And to mitigate potential mishaps, smart contracts can check at instantiation that the underlying app chain [supports their requirements](https://docs.cosmwasm.com/docs/architecture/composition#checking-for-support). + +It is also possible for app-chain modules to call smart contracts. A simple way would be to have an app-chain module's keeper have access to the CosmWasm module's keeper (or message server) so as to call the [`Execute`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/msg_server.go#L110) or [`SmartQuery`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/x/wasm/keeper/keeper.go#L823) functions on it. + +### Sending tokens + +You have seen that a smart contract can receive tokens, just as any other on-chain account but also, and preferably, via the `funds` feature of `MsgExecute`. Now because a smart contract can: + +* Send a `MsgExecute` as part of its return calls, therefore it can also set the `funds` field so that the target smart contract receives the funds and gets the assurance it received the funds. +* Send a bank `MsgSend` as part of its return calls, therefore it can also send tokens to whatever on-chain account, including another smart contract. + +### Tying it together + +As an example, if you have Contract A's execution that makes calls to other smart contracts and modules, it could look like this: + +```mermaid +--- +title: Cross elements execution +--- +sequenceDiagram + participant System + box Contract A + participant Contract A Execution as Execute + participant Contract A Reply as Reply + end + participant Contract B as Contract B Query + participant Contract C as Contract C Execute + participant Other Cosmos Module + + System->>Contract A Execution: Invoke execute + activate Contract A Execution + Contract A Execution->>Contract B: Query for value + activate Contract B + Note over Contract A Execution,Contract B: Read-only! + Contract B->>Contract A Execution: Receive queried value + deactivate Contract B + Contract A Execution->>Contract A Execution: Return response with onward messages + Contract A Execution->>Contract C: Execute message 1 with reply + deactivate Contract A Execution + activate Contract C + Contract C->>Contract C: Return response with reply result + Contract C->>Contract A Reply: + deactivate Contract C + activate Contract A Reply + Contract A Reply->>Contract A Reply: Return response without further messages + Contract A Reply-->>System: Success! + deactivate Contract A Reply + Contract A Execution->>Other Cosmos Module: Execute message 2 without reply + Other Cosmos Module-->>System: Success! +``` + +## Testing + +Testing your smart contracts, as for all your software projects, should be part of your development. Their are different levels of testing, and you benefit from existing tools at each level. + +* Rust **unit tests**. They reside in your code, and are tested purely in a Rust way, without even any conversion to WebAssembly. See [this exercise](./05-first-contract.html#unit-testing) for an introduction and [this one](./06-first-contract-register.html#unit-testing) to experience it in more details. +* **Mocked-app tests**, happening all in Rust with the use of the cw-multi-test library to test your contract's interactions with a mocked CosmWasm module. See [this exercise](./08-first-contract-test.html) for an introduction and [this one](./12-cross-contract.html#mocked-app-tests) to experience it in more details. +* Message mocked tests. You create mocks of your SDK modules, against which your smart contracts communicate. + +## Client side + +You can easily create UIs for your smart contracts, all the more so that they encode their messages in JSON. This JSON encoding also allows you to easily debug messages when the time comes. The CosmJS library offers CosmWasm bindings that you can extend with your own types. diff --git a/docs/tutorial/platform/03-integration.md b/docs/tutorial/platform/03-integration.md new file mode 100644 index 0000000..937534c --- /dev/null +++ b/docs/tutorial/platform/03-integration.md @@ -0,0 +1,47 @@ +--- +title: Integration into Cosmos +description: Add the CosmWasm module to your app-chain +--- + +# Integration into Cosmos + +You have decided to add CosmWasm to your app-chain, now what? You need to add the CosmWasm module to the code of your Cosmos SDK app-chain, and integrate it as tightly as desirable. The document source for this process can be found [here](https://github.com/CosmWasm/wasmd/blob/main/INTEGRATION.md), where you can also find more detailed steps and corner cases. + +The Cosmos SDK is highly customizable. Not only can you add your own modules, you can also replace stock modules with your modified ones. Because of this customizability, the amount of work required to integrate CosmWasm into your app-chain will vary. + +This section is not about experimenting with CosmWasm (see [here](./04-hello-world.html) for that) or learning how to write CosmWasm smart contracts (see [here](./05-first-contract.html) for that). + +## Prerequisites + +CosmWasm supports different Cosmos SDK versions, but you nonetheless have a limited choice of CosmWasm versions per Cosmos SDK versions. See the list [here](https://github.com/CosmWasm/wasmd/blob/main/INTEGRATION.md#prerequisites). + +Additionally, because of current limitations coming from `wasmvm`, only nodes running Linux or Mac on Intel CPUs are supported for production, although Mac on ARM CPUs are supported for testing. + +## If you have a stock app-chain + +If you have the standard modules, the standard Proof-of-Stake, the standard Merkle tree storage, have not modified any modules, then these would be your steps. If this does not describe your case, this part can still inform you about the steps you will need to complete before moving on to the customized parts. + +1. You [declare the `wasmd` dependency](https://github.com/osmosis-labs/osmosis/blob/v9.0.0-rc0/go.mod#L6) just like any other. +2. You [import the `x/wasm` module](https://github.com/osmosis-labs/osmosis/blob/v9.0.0-rc0/app/app.go#L11), and wire it up in `app.go`. +3. You add the [two necessary ante handlers](https://github.com/osmosis-labs/osmosis/blob/v9.0.0-rc0/app/ante.go#L42-L43). + +The [`wasmd`](https://github.com/CosmWasm/wasmd/tree/main) repository itself is an example of an integration into Gaia, the code behind the Cosmos Hub. Except that it has the `x/wasm` as code instead of a dependency. You might choose to copy the `x/wasm` folder too, but this would cost you a lot when updating. + +## If you have modified stock modules + +This really is case by case. For instance: + +* You may have changed the underlying Merkle tree structure for storage. In this case, you need to [remove the `"iterator"`](https://github.com/osmosis-labs/osmosis/blob/v25.2.0/app/keepers/keepers.go#L563) capability from the integration. +* You may have swapped Proof-of-Stake with a Proof-of-Authority. In this case, you need to [remove the `"staking"`](https://github.com/osmosis-labs/osmosis/blob/v25.2.0/app/keepers/keepers.go#L563) capability. + +## If you have custom Cosmos SDK modules + +In this case, it makes sense to make it easy for smart contract developers to access your custom modules with the use of your custom messages. What you create for this purpose are called _bindings_. + +The standard CosmWasm library offers `CosmosMsg::Custom` and `QueryRequest::Custom` objects for you to extend and define access to SDK modules. So first, on the Rust end, you expose [the available messages](https://github.com/osmosis-labs/bindings/blob/main/packages/bindings/src/msg.rs). + +Then, on the app-chain end, typically in a separate module that you would call `wasmbindings`, you create a [`CustomQuerier`](https://github.com/osmosis-labs/osmosis/blob/v25.2.0/wasmbinding/query_plugin.go) and [`CustomMessenger`](https://github.com/osmosis-labs/osmosis/blob/v25.2.0/wasmbinding/message_plugin.go) that bind your custom messages and queries to their actual actions in your module. Then you pass these with [the CosmWasm options](https://github.com/osmosis-labs/osmosis/blob/v25.2.0/wasmbinding/wasm.go), to be used at the [`app` wiring](https://github.com/osmosis-labs/osmosis/blob/188abfcd15544ca07d468c0dc0169876ffde6079/app/keepers/keepers.go#L576). + +At this point, it is possible for any smart contract to call into your custom modules. In fact, you ought to let smart contracts confirm that they can by telling the CosmWasm module to expose a `requires_MY_MODULE` command, [`"osmosis"` in this example](https://github.com/osmosis-labs/osmosis/blob/188abfcd15544ca07d468c0dc0169876ffde6079/app/keepers/keepers.go#L574). This command can be checked at smart contract instantiation to avoid smart contracts with effectively un-runnable code. + +Additionally, to improve the smart contract developers' experience, you ought to create valid mocks of query replies so that developers can run accurate tests. \ No newline at end of file diff --git a/docs/tutorial/platform/04-hello-world.md b/docs/tutorial/platform/04-hello-world.md new file mode 100644 index 0000000..79df5e9 --- /dev/null +++ b/docs/tutorial/platform/04-hello-world.md @@ -0,0 +1,1551 @@ +--- +title: Hello World +description: All on your machine +--- + +# Hello World + +As always, the goal of this hello world exercise is to give you a feel of the technology, but also to superficially experience how the concepts you learned map to something more tangible. Don't hesitate to expand the collapsed sections if you want to dive deep on certain points, with a view to better understand the mechanics of it. + +These first steps take inspiration from the CosmWasm [docs' hello world](https://docs.cosmwasm.com/docs/getting-started/intro), and from the [Cosmos SDK's basic tutorial](https://tutorials.cosmos.network/tutorials/3-run-node/). The difference is that after you have downloaded all the packages and dependencies, you no longer need access to other online resources. + +It offers two tracks, you can either do all your compilations and running natively on your computer, or achieve the same in Docker. If you choose the Docker path, that allows you to hold off installing anything other than Docker. + +## What will happen + +Here are the steps you are going to complete: + +* Build the blockchain code. +* Create a running Cosmos blockchain: + * With a single running node. + * With CosmWasm installed +* Compile a smart contract code. +* Store the code and deploy a smart contract instance. +* Interact with it. + +Let's get started. + +[Install pre-requisites](https://docs.cosmwasm.com/core/installation). Either on your computer, or install Docker. Including [JQ](https://jqlang.github.io/jq/). + +## Build Your blockchain code + +Clone wasmd, which is the _simd_ of CosmWasm, at [v0.53.2](https://github.com/CosmWasm/wasmd/tree/v0.53.2). + +```sh +git clone https://github.com/CosmWasm/wasmd --branch v0.53.2 +cd wasmd +``` + +Build the blockchain application. + + + + ```sh + make build + ``` + + + ```sh + docker build --tag wasmd:0.53.2 . + ``` + + + +With this done, `.build/wasmd` or Docker's `wasmd:0.53.2` is your blockchain executable. + +## Prepare your blockchain + +After you have compiled your blockchain code, and before you can turn your focus to CosmWasm, you need a running blockchain. This step has you prepare the blockchain application, test keys and a genesis file. + + + +If you have previously used wasmd, you may already have data in your `~/.wasmd` folder. It is typically safe to erase the `config` sub-folder. But **if you are a validator** on one of the wasmd networks, this exercise is not safe, so stop right there or proceed at your own risks. + +The Docker track also uses your local folder, by sharing it as a volume with the flag `-v $HOME/.wasmd:/root/.wasmd`, so that you can switch to the local track at a later stage. Make space in the `~/.wasmd` folder: + +```sh +rm -rf ~/.wasmd/config \ + ~/.wasmd/data \ + ~/.wasmd/wasm +``` + + + +Initialize the blockchain application's genesis file and other configuration files. + + + + ```sh + ./build/wasmd init validator-1 \ + --chain-id learning-chain-1 + ``` + + + ```sh + docker run --rm -it \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd init validator-1 \ + --chain-id learning-chain-1 + ``` + + + +If you are curious, you can see what was created in `~/.wasmd`. + +Then you can add a convenient but low-safety key for Alice, who shall become the validator operator. + + + + ```sh + ./build/wasmd keys add alice \ + --keyring-backend test + ``` + + + ```sh + docker run --rm -it \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd keys add alice \ + --keyring-backend test + ``` + + + +Make a note of the seed phrase so that you can reuse it in a Node.js REPL console. + +To become a validator, Alice needs an initial balance in the staking token. Give her one: + + + + ```sh + ./build/wasmd genesis \ + add-genesis-account alice 100000000stake \ + --keyring-backend test + ``` + + + ```sh + docker run --rm -it \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd genesis \ + add-genesis-account alice 100000000stake \ + --keyring-backend test + ``` + + + +Have Alice create and sign the token-staking transaction that will make Alice an initial validator. + + + + ```sh + ./build/wasmd genesis \ + gentx alice 70000000stake \ + --keyring-backend test \ + --chain-id learning-chain-1 + ``` + + + ```sh + docker run --rm -it \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd genesis \ + gentx alice 70000000stake \ + --keyring-backend test \ + --chain-id learning-chain-1 + ``` + + + +The system tells you that a file has been created with a signed transaction in it. Add this signed transaction to the genesis file so that Alice starts indeed as a validator. + + + + ```sh + ./build/wasmd genesis collect-gentxs + ``` + + + ```sh + docker run --rm -it \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd genesis collect-gentxs + ``` + + + + + +The blockchain preparation is now done, and you need not redo the above steps if you restart this exercise at a later date. + + + +## Run your blockchain + + + +If you stopped `wasmd` earlier, you can come back here and restart `wasmd` where you left it off. + + + +Run Alice's validating node to start the blockchain system and to interact with it. + + + + ```sh + ./build/wasmd start + ``` + + + ```sh + docker run --rm -it \ + --name val-alice-1 \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd start + ``` + + + +Your node is now running and ready to receive smart contracts. + + + + +If you are new to Cosmos, you may want to confirm that you are correctly set up. Other than looking at the block height increasing in the log, a simple verification is to check Alice's balance is as expected in another terminal. First, store her address: + + + + ```sh + alice=$(./build/wasmd keys show alice \ + --keyring-backend test \ + --address) + ``` + + + ```sh + alice=$(docker run --rm -i \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd keys show alice \ + --keyring-backend test \ + --address | tr -d '\r') + ``` + + + +Confirm you have the correct address with: + +```sh +echo -n $alice +``` + +Which returns something like `wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as`. With that, you can query for her balance: + + + + ```sh + ./build/wasmd query bank balances $alice + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query bank balances $alice + ``` + + + +The expected answer: + +```yaml +balances: +- amount: "30000000" + denom: stake +pagination: + total: "1" +``` + +Indeed, she started with `100,000,000` and staked `70,000,000`, so she now has only `30,000,000` remaining available. The staking rewards that she has accummulated are not available until they are collected. + + + + +## Compile your smart contract + +With a running chain, you can start interacting with the CosmWasm module. First you are going to download and compile a smart contract. + +We will use the same nameservice smart contract of the [long running exercise](./05-first-contract.html) but at the intermediate [`add-first-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-first-library) branch. In another terminal, start by cloning it and changing the directory. + +```sh +git clone https://github.com/b9lab/cw-my-nameservice --branch add-first-library +cd cw-my-nameservice/contracts/nameservice +``` + +At this version of the contract, it compiles with Rust v1.80.1 to the Wasm target. + + + + ```sh + rustup install 1.80.1 + rustup target add wasm32-unknown-unknown --toolchain 1.80.1 + RUSTFLAGS='-C link-arg=-s' cargo +1.80.1 wasm + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root \ + -w /root \ + rust:1.80.1 \ + sh -c "rustup target add wasm32-unknown-unknown && \ + RUSTFLAGS='-C link-arg=-s' cargo +1.80.1 wasm" + ``` + + + +The compiled WebAssembly is located relative to the contract dir in `./target/wasm32-unknown-unknown/release/cw_my_nameservice.wasm`. The file ends up at about 221 KB in size. In fact, the `RUSTFLAGS='-C link-arg=-s'` flag is there to reduce its size, always a concern in blockchain. You can remove the flag as a test, and you should see that it then ends up at 1.5 MB in size. + +Copy the file to the `~/.wasmd` so as to reuse it when storing the code on-chain. Make the folder if it is missing: + +```sh +mkdir -p $HOME/.wasmd/wasm/code +cp $(pwd)/target/wasm32-unknown-unknown/release/cw_my_nameservice.wasm \ + $HOME/.wasmd/wasm/code/cw_my_nameservice.wasm +``` + +Note that the `~/.wasmd` path is also accessible by the Docker container running the blockchain app, which makes it convenient for this exercise. + +## Store your contract code + +With the bytecode of the smart contract ready, you are about to interact with the running chain again. Return to the `wasmd` directory, in a new shell. + +Start with a simple initial CosmWasm query to see what code, if any, has already been stored. + + + + ```sh + ./build/wasmd query wasm list-code + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm list-code + ``` + + + +As expected, it returns: + +```yaml +code_infos: [] +pagination: ... +``` + +It is time to store your first CosmWasm code with the `tx wasm store` command. Alice, who owns `stake` tokens, can do it. + + + + ```sh + ./build/wasmd tx wasm store $HOME/.wasmd/wasm/code/cw_my_nameservice.wasm \ + --from alice --keyring-backend test \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --chain-id learning-chain-1 \ + --yes --output json --broadcast-mode sync + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd tx wasm store /root/.wasmd/wasm/code/cw_my_nameservice.wasm \ + --from alice --keyring-backend test \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --chain-id learning-chain-1 \ + --yes --output json --broadcast-mode sync + ``` + + + + + + +The structure of these commands is that: + +1. `wasmd` is the name of the executable. It is already running with the `start` command. However, here you are executing it separately with different commands. +2. `query` or `tx` directs the execution to create a query object or a transaction object instead of running a blockchain. +3. `wasm` directs the execution to create query/tx objects for the `wasm` module. +4. The other parameters are those that are required to create the query/tx object. + +After it has created and signed the object, `wasmd` sends it to the running blockchain through the standard local port, unless you specify another host and port. + + + + +With `--broadcast-mode sync`, the command sends a transaction, but does not wait for it to be confirmed. Instead, you get succinct information, including the transaction hash: + +```json +{"height":"0","txhash":"34087EB0B74233E7E3C3AA9CE6EFCB4279130AF1C2BCAE992DD1E1D1775D02ED","codespace":"","code":0,"data":"","raw_log":"","logs":[],"info":"","gas_wanted":"0","gas_used":"0","tx":null,"timestamp":"","events":[]} +``` + +Make a note of this `txhash`, such as: + +```sh +ns_store_txhash=34087EB0B74233E7E3C3AA9CE6EFCB4279130AF1C2BCAE992DD1E1D1775D02ED +``` + +With your specific value. + +## Verify your stored code + +The newly stored code has a newly created `id` that you need to know in order to use it. The authoritative way is to retrieve the code information from the transaction's events itself. The event of interest has the `type: "store_code"`: + + + + ```sh + ./build/wasmd query tx $ns_store_txhash --output json \ + | jq '.events[] | select(.type == "store_code")' + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query tx $ns_store_txhash --output json \ + | jq '.events[] | select(.type == "store_code")' + ``` + + + +Which returns something like: + +```json +{ + "type": "store_code", + "attributes": [ + { + "key": "code_checksum", + "value": "98f9924c5fbe94dd6ad24d71f2352593e54aac6aabcfaa9b1bf000f64b33992d", + "index": true + }, + { + "key": "code_id", + "value": "1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] +} +``` + +There is a code id, predictably at `1`, and a code checksum. + + + +The `msg_index` is here to assist you with identifying which code is which, in the rare case where you store two or more codes in a single transaction. + + + +Make a note of the code id as you will use it during instantiation: + + + + ```sh + ns_code_id=$(./build/wasmd query tx $ns_store_txhash --output json \ + | jq -r '.events[] | select(.type == "store_code") .attributes[] | select(.key == "code_id") .value') + ``` + + + ```sh + ns_code_id=$(docker exec val-alice-1 \ + wasmd query tx $ns_store_txhash --output json \ + | jq -r '.events[] | select(.type == "store_code") .attributes[] | select(.key == "code_id") .value') + ``` + + + +Does the code checksum match? Let's check: + + + + ```sh + sha256sum $HOME/.wasmd/wasm/code/cw_my_nameservice.wasm + ``` + + + ```sh + docker exec val-alice-1 \ + sha256sum /root/.wasmd/wasm/code/cw_my_nameservice.wasm + ``` + + + +You should see the same value as the one emitted in the event. + + + + +You can run a diff of the stored code and the one you have: + + + + ```sh + ./build/wasmd query wasm code $ns_code_id \ + $HOME/.wasmd/wasm/code/downloaded_cw_my_nameservice.wasm + diff $HOME/.wasmd/wasm/code/cw_my_nameservice.wasm \ + $HOME/.wasmd/wasm/code/downloaded_cw_my_nameservice.wasm + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm code $ns_code_id \ + /root/.wasmd/wasm/code/downloaded_cw_my_nameservice.wasm + docker exec val-alice-1 \ + diff /root/.wasmd/wasm/code/cw_my_nameservice.wasm \ + /root/.wasmd/wasm/code/downloaded_cw_my_nameservice.wasm + ``` + + + +No message on the `diff` means that they are identical. + + + + +At any time, you can use the Cosmos SDK convention to get information: + + + + ```sh + ./build/wasmd query wasm --help + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm --help + ``` + + + +You should get this: + +```txt +Querying commands for the wasm module + +Usage: + wasmd query wasm [flags] + wasmd query wasm [command] + +Available Commands: + build-address build contract address + code Downloads wasm bytecode for given code id + code-info Prints out metadata of a code id + contract Prints out metadata of a contract given its address + contract-history Prints out the code history for a contract given its address + contract-state Querying commands for the wasm module + libwasmvm-version Get libwasmvm version + list-code List all wasm bytecode on the chain + list-contract-by-code List wasm all bytecode on the chain for given code id + list-contracts-by-creator List all contracts by creator + params Query the current wasm parameters + pinned List all pinned code ids + +Flags: + -h, --help help for wasm + +Global Flags: + --home string directory for config and data (default "/root/.wasmd") + --log_format string The logging format (json|plain) (default "plain") + --log_level string The logging level (trace|debug|info|warn|error|fatal|panic|disabled or '*:,:') (default "info") + --log_no_color Disable colored logs + --trace print out full stack trace on errors + +Use "wasmd query wasm [command] --help" for more information about a command. +``` + + + + +You already have, but if you need it later on, you can simply call: + + + + ```sh + ./build/wasmd query wasm code-info $ns_code_id + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm code-info $ns_code_id + ``` + + + +Which returns something like: + +```yaml +code_id: "1" +creator: wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as +data_hash: 98F9924C5FBE94DD6AD24D71F2352593E54AAC6AABCFAA9B1BF000F64B33992D +instantiate_permission: + addresses: [] + permission: Everybody +``` + +Make a mental note of the `instantiate_permission` as this is an advanced feature you can enable. If you are curious, you can have a look at it starting with: + + + + ```sh + ./build/wasmd tx wasm store --help | grep -e --instantiate + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd tx wasm store --help | grep -e --instantiate + ``` + + + +Which yields: + +```txt +--instantiate-anyof-addresses strings Any of the addresses can instantiate a contract from the code, optional +--instantiate-everybody string Everybody can instantiate a contract from the code, optional +--instantiate-nobody string Nobody except the governance process can instantiate a contract from the code, optional +--instantiate-only-address string Removed: use instantiate-anyof-addresses instead +``` + + + + +Earlier, when you tried retrieving all code infos, it returned `[]`. Try again: + + + + ```sh + ./build/wasmd query wasm list-code + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm list-code + ``` + + + +Now, you get something like: + +```yaml +code_infos: +- code_id: "1" + creator: wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as + data_hash: 98F9924C5FBE94DD6AD24D71F2352593E54AAC6AABCFAA9B1BF000F64B33992D + instantiate_permission: + addresses: [] + permission: Everybody +pagination: ... +``` + +For the avoidance of doubt, trying to get the code id of your just-deployed bytecode is wrong. In a real setting, you are not alone on the blockchain and you can easily get confused as to which code is yours. + + + + +## Deploy your contract instance + +Now you have your bytecode stored on-chain, but no smart contract has been deployed using this code. You can confirm this with: + + + + ```sh + ./build/wasmd query wasm list-contract-by-code $ns_code_id + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm list-contract-by-code $ns_code_id + ``` + + + +This returns: + +```yaml +contracts: [] +pagination: ... +``` + +Time to instantiate your first CosmWasm smart contract. The [constructor requires](https://github.com/b9lab/cw-my-nameservice/blob/add-first-library/src/contract.rs#L18) a [specific message](https://github.com/b9lab/cw-my-nameservice/blob/add-first-library/src/msg.rs#L5-L7): + + +```rust +pub struct InstantiateMsg { + pub minter: String, +} +``` + + +Which has to be serialized as JSON. The `minter` is the account that will be allowed to register new names. To make it easy, you pick Alice as the minter. Get her address: + + + + + ```sh + alice=$(./build/wasmd keys show alice \ + --keyring-backend test \ + --address) + ``` + + + ```sh + alice=$(docker run --rm -i \ + -v $HOME/.wasmd:/root/.wasmd \ + wasmd:0.53.2 \ + wasmd keys show alice \ + --keyring-backend test \ + --address | tr -d '\r') + ``` + + + +The next action is to prepare the instantiate message by setting the right value: + +```sh +ns_init_msg_1='{"minter":"'$alice'"}' +``` + + + + +With: + +```sh +echo $ns_init_msg_1 +``` + +Which should return something like: + +```txt +{"minter":"wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as"} +``` + + + + +With the message ready, you can send the comand to instantiate your first smart contract: + + + + ```sh + ./build/wasmd tx wasm instantiate $ns_code_id "$ns_init_msg_1" \ + --label "name service" --no-admin \ + --from alice --keyring-backend test \ + --chain-id learning-chain-1 \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --yes + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd tx wasm instantiate $ns_code_id "$ns_init_msg_1" \ + --label "name service" --no-admin \ + --from alice --keyring-backend test \ + --chain-id learning-chain-1 \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --yes + ``` + + + +Note that the meat of the command is `instantiate $ns_code_id "$ns_init_msg_1"`, which would read as `instantiate 1 "{"minter":"wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as"}"`. + +Once again, you get a transaction hash. Make a note of it with your own hash, for instance: + +```sh +ns_instantiate_txhash_1=9881879B2A7663638D6DA81D6F9ECC7DBF8AA12B105817B06A761749A22764E1 +``` + + + + +Now that the transaction has likely been confirmed, you can retrieve it: + + + + ```sh + ./build/wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq + ``` + + + +This returns a lot of elements, but don't miss the event of type `"instantiate"`. + + + + +## Retrieve your contract address + +At this stage, what is important is the **address** at which your contract instance resides. This is the address you will use to interact with your instantiated contract. The authoritative way to get this information is to get it from the events, more precisely at the event of type `"instantiate"`: + + + + ```sh + ./build/wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq '.events[] | select(.type == "store_code") .attributes[] | select(.key == "code_id") .value' + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq '.events[] | select(.type == "instantiate")' + ``` + + + +This returns something like: + +```json +{ + "type": "instantiate", + "attributes": [ + { + "key": "_contract_address", + "value": "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d", + "index": true + }, + { + "key": "code_id", + "value": "1", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] +} +``` + +Here too, `msg_index` assists you when you have more than one instantiation in one transaction. Your smart contract address is `wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d`, which you can retrieve with: + + + + ```sh + ns_addr1=$(./build/wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq -r '.events[] | select(.type == "instantiate") .attributes[] | select(.key == "_contract_address") .value') + ``` + + + ```sh + ns_addr1=$(docker exec val-alice-1 \ + wasmd query tx $ns_instantiate_txhash_1 \ + --output json | jq -r '.events[] | select(.type == "instantiate") .attributes[] | select(.key == "_contract_address") .value') + ``` + + + +Note how the address is much longer than a _regular_ address, like Alice's. With the contract instantiated, you can query a few things about it. + + + + +It is possible to get the same information by code id. + + + + ```sh + ./build/wasmd query wasm list-contract-by-code $ns_code_id + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm list-contract-by-code $ns_code_id + ``` + + + +This returns something like: + +```yaml +contracts: +- wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d +pagination: ... +``` + +Of course, if you have more than one smart contracts here, you could easily get confused; so the correct method is to use the transaction's event proper. + + + + +CosmWasm has kept some information about your new instance. At any time, you can retrieve it with: + + + + ```sh + ./build/wasmd query wasm contract $ns_addr1 + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract $ns_addr1 + ``` + + + +Which returns something similar to: + +```yaml +address: wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d +contract_info: + admin: "" + code_id: "1" + created: + block_height: "6596" + tx_index: "0" + creator: wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as + extension: null + ibc_port_id: "" + label: name service +``` + + + + +At a later stage, your contract instances may hold a token balance. At any time, you can fetch this information with: + + + + ```sh + ./build/wasmd query bank balances $ns_addr1 + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query bank balances $ns_addr1 + ``` + + + +Which initially returns: + +```yaml +balances: [] +pagination: {} +``` + +Note how the `query bank balances` is purely a bank-module command, it does not involve the CosmWasm module. That's because the smart contract instance has an address that the bank module recognizes as valid, and that's all the bank module asks to start counting tokens. + + + + +The instance keeps its state in storage. If you come from Ethereum, you are familiar with the `web3.getStorageAt()` command, which returns a specific storage slot of a specific smart contract. + +The equivalent command in CosmWasm is `query wasm contract-state`. Conveniently, it also has the `all` subcommand. So let's see what the instance has in storage with: + + + + ```sh + ./build/wasmd query wasm contract-state all $ns_addr1 + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract-state all $ns_addr1 + ``` + + + +Which returns: + +```yaml +models: +- key: 6E616D655F6D696E746572 + value: eyJvd25lciI6Indhc20xa2htdjZxbDRwa2h4azVyOThtNXp6NHRldHQ1NWRzbjYzdm1tNzYiLCJwZW5kaW5nX293bmVyIjpudWxsLCJwZW5kaW5nX2V4cGlyeSI6bnVsbH0= +pagination: + next_key: null + total: "0" +``` + +The `key` looks like ASCII. Convert it: + +```sh +echo 6E616D655F6D696E746572 | xxd -r -p +``` + +This returns `name_minter`. The value looks like Base64. Convert it: + +```sh +echo eyJvd25lciI6Indhc20xa2htdjZxbDRwa2h4azVyOThtNXp6NHRldHQ1NWRzbjYzdm1tNzYiLCJwZW5kaW5nX293bmVyIjpudWxsLCJwZW5kaW5nX2V4cGlyeSI6bnVsbH0= | base64 -D +``` + +This returns + +```json +{"owner":"wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as","pending_owner":null,"pending_expiry":null} +``` + +This is reassuringly consistent with: + +1. The declaration of the `MINTER` state element: + + + ```rust + pub const MINTER: OwnershipStore = OwnershipStore::new("name_minter"); + ``` + + +2. The `Ownership`'s definition: + + + ```rust + pub struct Ownership { + pub owner: Option, + pub pending_owner: Option, + pub pending_expiry: Option, + } + ``` + + +3. The instantiation message plus what is in the constructor: + + + ```rust + let _ = MINTER.initialize_owner(deps.storage, deps.api, Some(msg.minter.as_str()))?; + ``` + + +Now that you know that: + +1. The minter information is saved at the key `name_minter`. +2. The minter address proper is under `owner`. +3. The value proper is saved in Base64. + +You can query the instance's state directly, without the `all` keyword. Either with the key in ASCII: + + + + ```sh + ./build/wasmd query wasm contract-state raw $ns_addr1 \ + name_minter --ascii \ + --output json \ + | jq -r '.data' \ + | base64 -D + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract-state raw $ns_addr1 \ + name_minter --ascii \ + --output json \ + | jq -r '.data' \ + | base64 -D + ``` + + + +Or with the key in hex: + + + + ```sh + ./build/wasmd query wasm contract-state raw $ns_addr1 \ + 6E616D655F6D696E746572 --hex \ + --output json \ + | jq -r '.data' \ + | base64 -D + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract-state raw $ns_addr1 \ + 6E616D655F6D696E746572 --hex \ + --output json \ + | jq -r '.data' \ + | base64 -D + ``` + + + + + +Note that, as always in blockchain, you can query for storage values at different block heights with the `--height` flag; provided the content was not pruned from storage. + + + + + + +Your new smart contract's address is probably exactly `wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d` too, unlike your Alice's address, which is different. This is no lucky guess. + +The new address computation takes place during the transaction execution, so let's dive into the Go code of wasmd. + +The message server uses the [`ClassicAddressGenerator`](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/msg_server.go#L67). This generator uses a [simply-incrementing 64-bit number](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/addresses.go#L20) instance id to count all the instances created by this method. + +Then it: + +* Takes the [hash](https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/types/address/hash.go#L32) of the string [`module`](https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/types/address/hash.go#L81). +* Appends the [module name](https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/types/address/hash.go#L74) (i.e. [`wasm`](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/addresses.go#L40)). +* Appends the [`0` byte](https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/types/address/hash.go#L80). +* Appends the [8 bytes of the code id](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/addresses.go#L38) big-endian style. +* And appends the [8 bytes of this instance id](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/addresses.go#L39), which [starts at `1`](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/keeper.go#L1256). +* [Hashes](https://github.com/cosmos/cosmos-sdk/blob/v0.50.7/types/address/hash.go#L28) the lot with sha256. + +And voila. After a conversion to bech32, you get your address. + +Try it yourself with the help of online tools: + +1. The string `module` hashes to: `120970d812836f19888625587a4606a5ad23cef31c8684e601771552548fc6b9` as seen [here](https://emn178.github.io/online-tools/sha256.html?input=module&input_type=utf-8&output_type=hex&hmac_enabled=0&hmac_input_type=hex) or with the command: + + ```sh + echo -n module | sha256sum --text + ``` + +2. `wasm` hex-encodes to `7761736d` as you can confirm [here](http://www.unit-conversion.info/texttools/hexadecimal/#data), or with the command: + + ```sh + echo -n wasm | xxd -p + ``` + +3. The `0` byte encodes to `00`. +4. The instance id of 1 goes first and is, as a 64 bit number, written as `0000000000000001`. +5. The code id, also of 1, goes second and is also written as `0000000000000001`. + +Putting it all together, what you need to hash is: + +```txt +120970d812836f19888625587a4606a5ad23cef31c8684e601771552548fc6b97761736d0000000000000000010000000000000001 +``` + +This yields `ade4a5f5803a439835c636395a8d648dee57b2fc90d98dc17fa887159b69638b` as seen [here](https://emn178.github.io/online-tools/sha256.html?input=120970d812836f19888625587a4606a5ad23cef31c8684e601771552548fc6b97761736d0000000000000000010000000000000001&input_type=hex&output_type=hex&hmac_enabled=0&hmac_input_type=hex) or with the command: + +```sh +echo -n 120970d812836f19888625587a4606a5ad23cef31c8684e601771552548fc6b97761736d0000000000000000010000000000000001 \ + | xxd -r -p \ + | sha256sum --binary +``` + +Now, with this hashed result, you need to compute the bech32 address. Head [here](https://blockchain-academy.hs-mittweida.de/bech32-tool/) and, in the _Encoder_ part, put: + +* `wasm` +* `ade4a5f5803a439835c636395a8d648dee57b2fc90d98dc17fa887159b69638b` + +Press _Encode_ and on the right you see `wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d`. + +Alternatively, you can do the same with wasmd: + + + + ```sh + ./build/wasmd keys parse ade4a5f5803a439835c636395a8d648dee57b2fc90d98dc17fa887159b69638b + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd keys parse ade4a5f5803a439835c636395a8d648dee57b2fc90d98dc17fa887159b69638b + ``` + + + +Congratulations! You have recomputed your smart contract instance address. + + + +As a side-note, CosmWasm implements another contract address computation function. If you come from Ethereum, this is similar to `CREATE2`. To use it, you would invoke the `tx wasm instantiate2` command, which, in turn, is using the aptly-named [`PredictableAddressGenerator`](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/addresses.go#L26). If you want to pre-calculate a future address, you can use the command: + + + + ```sh + ./build/wasmd query wasm build-address --help + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd keys query wasm build-address --help + ``` + + + + + + + + +## Send a transaction to your contract + +The smart contract you just instantiated is made to register names. As your first transaction, you will register the name `"queen-of-the-hill"` and map it to Alice. What message does the [`execute` function](https://github.com/b9lab/cw-my-nameservice/blob/add-first-library/src/contract.rs#L29) expect? It expects this: + + +```rust +pub enum ExecuteMsg { + Register { name: String, owner: Addr }, +} +``` + + +CosmWasm serializes an enum such as `ExecuteMsg` by prefixing the value proper with the type, here `Register`. Since you want to register Alice at the name, your register message, with its two fields, is: + +```sh +ns_register_queen_to_alice='{"register":{"name":"queen-of-the-hill","owner":"'$alice'"}}' +``` + + + + +With: + +```sh +echo $ns_register_queen_to_alice +``` + +Which should return something like: + +```txt +{"register":{"name":"queen-of-the-hill","owner":"wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as"}} +``` + + + + +As can be seen in the `execute_register` function, only the minter can send an `ExecuteMsg::Register` message: + + +```rust +MINTER + .assert_owner(deps.storage, &info.sender) + .map_err(ContractError::from_minter(&info.sender))?; +``` + + +And as you recall from the instantiation message, Alice is the minter. So Alice has to send this transaction. This smart contract does not need funds, but as a vehicle to demonstrate the concept, you attach funds of `100 stake` to the call: + + + + ```sh + ./build/wasmd tx wasm execute $ns_addr1 "$ns_register_queen_to_alice" \ + --amount 100stake \ + --from alice --keyring-backend test \ + --chain-id learning-chain-1 \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --yes + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd tx wasm execute $ns_addr1 "$ns_register_queen_to_alice" \ + --amount 100stake \ + --from alice --keyring-backend test \ + --chain-id learning-chain-1 \ + --gas-prices 0.25stake --gas auto --gas-adjustment 1.3 \ + --yes + ``` + + + +Once more, make a note of the transaction hash. For instance: + +```sh +ns_register_queen_to_alice_txhash=7966EBDD3766243FFFFE70D0A360305DE11B0BE77A305470D23D376B65432451 +``` + + + + +What happened? Let's look at the events that were emitted as part of this registration: + + + + ```sh + ./build/wasmd query tx $ns_register_queen_to_alice_txhash \ + --output json \ + | jq ".events" + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query tx $ns_register_queen_to_alice_txhash \ + --output json \ + | jq ".events" + ``` + + + +There comes a long list of events. Note this particular one: + +```json +{ + "type": "transfer", + "attributes": [ + { + "key": "recipient", + "value": "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d", + "index": true + }, + { + "key": "sender", + "value": "wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as", + "index": true + }, + { + "key": "amount", + "value": "100stake", + "index": true + }, + { + "key": "msg_index", + "value": "0", + "index": true + } + ] +} +``` + +It is emitted by the bank module and is the trace that tells you that Alice paid the name service contract `100stake`. And indeed, you can confirm that now the smart contract instance holds tokens: + + + + ```sh + ./build/wasmd query bank balance $ns_addr1 stake + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query bank balance $ns_addr1 stake + ``` + + + +Which returns: + +```yaml +balance: + amount: "100" + denom: stake +``` + + + +This version of the code of the smart contract cannot send tokens away, so its balance is in effect stranded. Better care next time... + + + +Now, if you look at how the transaction is built: + + + + ```sh + ./build/wasmd query tx $ns_register_queen_to_alice_txhash \ + --output json \ + | jq ".tx.body.messages" + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query tx $ns_register_queen_to_alice_txhash \ + --output json \ + | jq ".tx.body.messages" + ``` + + + +You see a single message: + +```json +[ + { + "@type": "/cosmwasm.wasm.v1.MsgExecuteContract", + "sender": "wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as", + "contract": "wasm14hj2tavq8fpesdwxxcu44rty3hh90vhujrvcmstl4zr3txmfvw9s0phg4d", + "msg": { + "register": { + "name": "queen-of-the-hill", + "owner": "wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as" + } + }, + "funds": [ + { + "denom": "stake", + "amount": "100" + } + ] + } +] +``` + +Although you see the word `funds` as a field of the message, it does not mean that the bank module will automagically handle that. Instead: + +1. At the transaction creation, when you included the extra information `--amount 100stake` in the command line, `wasmd` knew to [add the `funds` field](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/client/cli/gov_tx.go#L437) to the message. +2. The wasm module's message server [extracts the `funds` information](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/msg_server.go#L124) from the message and passes it on to the execution proper. +3. As part of the execution, the module [transfers the coins using the bank module](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/keeper.go#L402); and only continues if these transfers are successful. +4. The module then [crafts the info](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/keeper.go#L408), which includes the implied fact that the funds were collected. +5. The info is [passed to the Wasm VM](https://github.com/CosmWasm/wasmd/blob/v0.52.0/x/wasm/keeper/keeper.go#L413). +6. This jumps to the smart contract, which [receives an info object](https://github.com/b9lab/cw-my-nameservice/blob/add-first-library/src/contract.rs#L28) that contains the [`funds`](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/types.rs#L105) detail. +7. The contract could validate that it was paid enough for the minting. In fact, if you go to [this part](./16-fund-handling.html) of the long-running exercise, you see [it checking](https://github.com/b9lab/cw-my-collection-manager/blob/main/src/contract.rs#L107-L146) exeactly that in the list of funds that have been collected. + + + + +You can use the same way you used earlier. Call up all storage: + + + + ```sh + ./build/wasmd query wasm contract-state all $ns_addr1 + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract-state all $ns_addr1 + ``` + + + +Which returns: + +```yaml +models: +- key: 000D6E616D655F7265736F6C766572717565656E2D6F662D7468652D68696C6C + value: eyJvd25lciI6Indhc20xdGV2Nnh0NHBncm5qcHh3bXZ2N2pybDhwaDR4NDd3ZzV2Y2QwYXMifQ== +- key: 6E616D655F6D696E746572 + ... +``` + +If you remove the `000D` prefix from the new key and decode it as ASCII: + +```sh +echo 6E616D655F7265736F6C766572717565656E2D6F662D7468652D68696C6C | xxd -r -p +``` + +You get: + +```txt +name_resolverqueen-of-the-hill +``` + +This is a concatenation of [the map's name](https://github.com/b9lab/cw-my-nameservice/blob/add-first-library/src/state.rs#L11) and the item's key. + +The `00` prefix denotes a key part of a more complex type, while the next `0D` identifies this complex type as a map. + +Now if you Base64-decode the value with: + +```sh +echo eyJvd25lciI6Indhc20xdGV2Nnh0NHBncm5qcHh3bXZ2N2pybDhwaDR4NDd3ZzV2Y2QwYXMifQ== | base64 -d +``` + +You get: + +```json +{"owner":"wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as"} +``` + +Which is consistent with the `NameRecord`: + + +```rust +pub struct NameRecord { + pub owner: Addr, +} +``` + + + + + +## Send a query to your contract + +Has the name been duly registered, and is there a convenient way to verify? Yes, first create the resolve message so that it follows the expected query type: + + +```rust +pub enum QueryMsg { + ResolveRecord { name: String }, +} +``` + + +This is another `enum` which again is serialized by prefixing with the snake-case variant: + +```sh +ns_resolve_queen='{"resolve_record":{"name":"queen-of-the-hill"}}' +``` + +Then you pass it as a query to the smart contract: + + + + ```sh + ./build/wasmd query wasm contract-state smart $ns_addr1 "$ns_resolve_queen" + ``` + + + ```sh + docker exec val-alice-1 \ + wasmd query wasm contract-state smart $ns_addr1 "$ns_resolve_queen" + ``` + + + +Which returns as expected: + +```yaml +data: + address: wasm1tev6xt4pgrnjpxwmvv7jrl8ph4x47wg5vcd0as +``` + +Congratulations! You have updated the name service smart contract and confirmed it. + +## Conclusion + +Here is a summary of what you accomplished: + +* You compiled a blockchain that supports CosmWasm. +* You initialized and ran it. +* You compiled a smart contract. +* You stored a bytecode on-chain. +* You instantiated a smart contract using your bytecode. +* You had your smart contract save information in its state with the use of a transaction. +* You interrogated your smart contract about its state with the use of a query. + +If you did not on the first pass, go back and expand the collapsed sections to learn more. + +If you are keen on doing a couple of other hello-world-like exercises before plunging into smart contract writing, try Neutron's [Remix IDE's tutorial](https://docs.neutron.org/tutorials/cosmwasm_remix) or [WasmKit's tutorial](https://docs.neutron.org/tutorials/cosmwasm_wasmkit). + +If you feel ready to start an exercise that will take you from creating your own smart contract, to testing it and managing it, head to the next section: [first contract](./05-first-contract.md). diff --git a/docs/tutorial/platform/05-first-contract.md b/docs/tutorial/platform/05-first-contract.md new file mode 100644 index 0000000..5eb1465 --- /dev/null +++ b/docs/tutorial/platform/05-first-contract.md @@ -0,0 +1,487 @@ +--- +title: First contract +description: Write your first smart contract +--- + +# First contract + +In the hello world, you used an already-made smart contract that implements a name service. It is a [rudimentary one](https://github.com/deus-labs/cw-contracts/blob/v0.11.0/contracts/nameservice/src/contract.rs). This somewhat longer-running exercise intends to build progressively a better nameservice, which could be [this one](https://github.com/public-awesome/names/blob/v1.2.8/contracts/name-minter/src/contract.rs), from the ground up. + + + +In practice, you will progressively build [this name service](https://github.com/b9lab/cw-my-nameservice). The exercise is built such that you can skip ahead by switching to the appropriate [branch](https://github.com/b9lab/cw-my-nameservice/branches) as mentioned at the top of the page of each exercise section. + + + +It offers two tracks, one local and the other with Docker, so that you can postpone installing the prerequisites. + +It was built with Rust 1.80.1 for CosmWasm 2.1.3. It may work with other versions, but breaking changes happen. + +## The Rust project + +Most likely, you will start your CosmWasm project as a Rust project. Use `cargo` to initialize a new one. + + + + ```sh + cargo new my-nameservice --lib --edition 2021 + ``` + + + ```sh + docker run --rm --interactive --tty \ + --volume $(pwd):/root/ --workdir /root \ + rust:1.80.1 \ + cargo new my-nameservice --lib --edition 2021 + ``` + + + +Move into the project directory. + +```sh +cd my-nameservice +``` + + + +At this stage, you should have something similar to the [`initial-cargo`](https://github.com/b9lab/cw-my-nameservice/tree/initial-cargo) branch. + + + +If you are using VisualStudio Code, feel free to copy the `.vscode` content you see [here]((https://github.com/b9lab/cw-my-nameservice/tree/initial-cargo). + +## The instantiation message + +With the base project ready, you can move to your first message. Your smart contract will be instantiated, and the `instantiate` function needs a message. Create it in a new `src/msg.rs` file: + + +```rust +use cosmwasm_schema::cw_serde; + +#[cw_serde] +pub struct InstantiateMsg {} +``` + + +You use the attribute macro [`cw_serde`](https://docs.cosmwasm.com/core/entrypoints#defining-your-own-messages) in order to make your for-now-empty _instantiate_ message serializable. Make its content available to the Rust project by replacing the sample code in `src/lib.rs` with: + + + ```diff-rs + + pub mod msg; + - pub fn add(left: u64, right: u64) -> u64 { + - left + right + - } + - + - #[cfg(test)] + - mod tests { + - ... + - } + ``` + + +Note that it says `pub` as the message needs to be known outside of the project, including tests. + +Back in `src/msg.rs` you will notice that `cosmwasm_schema` now appears as an `unresolved import`. You see the same message if you try to build: + + + + ```sh + cargo build + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo build + ``` + + + +Returns: + +```txt +error[E0432]: unresolved import `cosmwasm_schema` + --> src/msg.rs:1:5 + | +1 | use cosmwasm_schema::cw_serde; + | ^^^^^^^^^^^^^^^ use of undeclared crate or module `cosmwasm_schema` +``` + +Indeed, you need to add the relevant dependency: + + + + ```sh + cargo add cosmwasm-schema@2.1.3 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cosmwasm-schema@2.1.3 + ``` + + + + + +At this stage, you should have something similar to the [`instantiation-message`](https://github.com/b9lab/cw-my-nameservice/tree/instantiation-message) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/initial-cargo..instantiation-message) as the diff. + + + +## The instantiation function + +With the message declared, you can move on to the function that will instantiate your smart contract. + +Create a new file `src/contract.rs` with: + + + ```rust + use crate::msg::InstantiateMsg; + use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdError}; + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn instantiate( + _: DepsMut, + _: Env, + _: MessageInfo, + _: InstantiateMsg, + ) -> Result { + Ok(Response::default()) + } + ``` + + +Note how: + +* It does not do much beyond returning a default `Ok` response. +* The `#[entry_point]` attribute macro marks the function as a public contract function. +* It is [a convention](https://docs.cosmwasm.com/core/conventions/library-feature) to make it conditional on not being a library, to facilitate reuse. +* The instantiation usage is inferred by the name `instantiate` of this function, which matters. +* The order and types of the parameters matter and need to match exactly. Their names do not. +* It gets a [`DepsMut`](https://docs.cosmwasm.com/core/entrypoints#depsdepsmut), which is a mutable dependency that gives read&write access to storage. This makes sense as the constructor may need to write to storage. +* The return type also matters. + +Also make it available to the Rust project by adding the following line to `src/lib.rs`: + + + ```diff-rs + + pub mod contract; + pub mod msg; + ``` + + +The module is also marked as public because the CosmWasm system needs to be able to call its function(s). + +Once again, there is a missing dependency: `cosmwasm_std`. Add it: + + + + ```sh + cargo add cosmwasm-std@2.1.3 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cosmwasm-std@2.1.3 + ``` + + + + + +At this stage, you should have something similar to the [`instantiation-function`](https://github.com/b9lab/cw-my-nameservice/tree/instantiation-function) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/instantiation-message..instantiation-function) as the diff. + + + +## Improve error reporting + +With a view to improving error reporting as you progress, you introduce your own error type. In a new `src/error.rs`, add: + + + ```rust + use cosmwasm_std::StdError; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + } + ``` + + +Note that it uses the popular [thiserror package](https://docs.rs/thiserror/latest/thiserror). Again, add the following line to `src/lib.rs`. + + + ```diff-rs + pub mod contract; + + mod error; + pub mod msg; + ``` + + +Note that it is not `pub` as it only needs to be available within the Rust library project. + +And don't forget to add the corresponding dependency: + + + + ```sh + cargo add thiserror@1.0.63 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add thiserror@1.0.63 + ``` + + + +Now that the new error type has been declared, you can use it in `src/contract.rs`: + + + ```diff-rs + - use crate::msg::InstantiateMsg; + - use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response, StdError}; + + use crate::{error::ContractError, msg::InstantiateMsg}; + + use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; + + + + type ContractResult = Result; + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn instantiate( + ... + - ) -> Result { + + ) -> ContractResult { + ... + } + ``` + + +Note how: + +* It uses the new error type in a new alias type for the oft-used `Result` type. +* It uses the new type as the return of the `instantiate` function. + + + +At this stage, you should have something similar to the [`improve-error-reporting`](https://github.com/b9lab/cw-my-nameservice/tree/improve-error-reporting) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/instantiation-function..improve-error-reporting) as the diff. + + + +## Compilation to WebAssembly + +You can already build with the `cargo build` command. How about building to WebAssembly? You need to add the WebAssembly compiling target for that, if it was not yet installed. + + + + ```sh + rustup target add wasm32-unknown-unknown + ``` + + + To avoid downloading the `wasm32` target every time, it is good to create a new Docker image that includes it. Create a new file `builder.dockerfile`: + + + ```Dockerfile + FROM rust:1.80.1 + + RUN rustup target add wasm32-unknown-unknown + ``` + + + And build the image to be named `rust-cosmwasm:1.80.1`: + + ```sh + docker build . --file builder.dockerfile --tag rust-cosmwasm:1.80.1 + ``` + + + +With the target installed, you can compile to WebAssembly with: + + + + ```sh + cargo build --release --target wasm32-unknown-unknown + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root -w /root \ + rust-cosmwasm:1.80.1 \ + cargo build --release --target wasm32-unknown-unknown + ``` + + + +This `cargo build` command is a bit verbose so it pays to create an alias. The right place for that is in `.cargo/config.toml`. Create the folder and the file: + +```sh +mkdir .cargo +touch .cargo/config.toml +``` + +And in it, put: + + +```toml +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +``` + + +With this alias defined, you can now use `cargo wasm` instead of writing `cargo build --release --target wasm32-unknown-unknown`. Change your command to: + + + + ```sh + cargo wasm + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root -w /root \ + rust-cosmwasm:1.80.1 \ + cargo wasm + ``` + + + +## Compilation to CosmWasm + +While you are working on the configuration, you might as well add some elements necessary to compile to a type amenable to the CosmWasm module, along with flags curated by the CosmWasm team. Add the following lines in `Cargo.toml` below the `[package]` section: + + + ```diff + [package] + name = "my-nameservice" + version = "0.1.0" + edition = "2021" + + + # Linkage options. More information: https://doc.rust-lang.org/reference/linkage.html + + [lib] + + crate-type = ["cdylib", "rlib"] + + + [features] + + # Use library feature to disable all instantiate/execute/query exports + + library = [] + + + # Optimizations in release builds. More information: https://doc.rust-lang.org/cargo/reference/profiles.html + + [profile.release] + + opt-level = "z" + + debug = false + + rpath = false + + lto = true + + debug-assertions = false + + codegen-units = 1 + + panic = 'abort' + + incremental = false + + overflow-checks = true + + [dependencies] + ... + ``` + + +You can now build your smart contract, then store and deploy on-chain the generated wasm found in `target/wasm32-unknown-unknown/release/my_nameservice.wasm`. Refer to the hello world for how to do it. + + + +At this stage, you should have something similar to the [`compilation-elements`](https://github.com/b9lab/cw-my-nameservice/tree/compilation-elements) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/improve-error-reporting..compilation-elements) as the diff. + + + +## Unit testing + +You have not written much of a smart contract. However it is still useful to prepare the unit testing elements that will come in handy when your smart contract becomes larger. Unit testing does not touch the WebAssembly target or the CosmWasm module. See the integration tests for that. The tests are run purely to test your functions in isolation within Rust. + +In `src/contract.rs`, add this at the end of the file: + + + ```rust + #[cfg(test)] + mod tests { + use crate::msg::InstantiateMsg; + use cosmwasm_std::{testing, Addr, Response}; + + #[test] + fn test_instantiate() { + // Arrange + let mut mocked_deps_mut = testing::mock_dependencies(); + let mocked_env = testing::mock_env(); + let mocked_addr = Addr::unchecked("addr"); + let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let instantiate_msg = InstantiateMsg {}; + + // Act + let contract_result = super::instantiate( + mocked_deps_mut.as_mut(), + mocked_env, + mocked_msg_info, + instantiate_msg, + ); + + // Assert + assert!(contract_result.is_ok(), "Failed to instantiate"); + assert_eq!(contract_result.unwrap(), Response::default()) + } + } + ``` + + +Note how: + +* It follows unit testing conventions. +* `cosmwasm_std::testing` provides a set of mocking functions for each of `instantiate`'s argument. In its current form the function expects but does not use the arguments, therefore you don't need to configure the mocks further. +* It tests the return type, but also its content. + +With the test ready, you can run it with the following command: + + + + ```sh + cargo test + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo test + ``` + + + +Which should print its success in the output: + +```txt +... +running 1 test +test contract::tests::test_instantiate ... ok +... +``` + +## Conclusion + + + +At this stage, you should have something similar to the [`first-unit-test`](https://github.com/b9lab/cw-my-nameservice/tree/first-unit-test) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/compilation-elements..first-unit-test) as the diff. + + diff --git a/docs/tutorial/platform/06-first-contract-register.md b/docs/tutorial/platform/06-first-contract-register.md new file mode 100644 index 0000000..bd13cc5 --- /dev/null +++ b/docs/tutorial/platform/06-first-contract-register.md @@ -0,0 +1,254 @@ +--- +title: First Execute Transaction +description: Add a function to your smart contract so you can register a name to an address via a transaction. +--- + +# First Execute Message + +In the previous section you created a barebones smart contract. It exists but does not do much. In this section, you are going to have it do something: your smart contract is going to handle a transaction message. + + + +If you skipped the previous section, you can just switch the project to its [`first-unit-test`](https://github.com/b9lab/cw-my-nameservice/tree/first-unit-test) branch and take it from there. + + + +## The execute message + +First off, you have to decide what message it is going to execute. You are implementing a nameservice, so it is reasonable to pick a message that will **register a name**. + +Add the following code in `src/msg.rs`: + + + ```diff-rs + use cosmwasm_schema::cw_serde; + + #[cw_serde] + pub struct InstantiateMsg {} + + + #[cw_serde] + + pub enum ExecuteMsg { + + Register { name: String }, + + } + ``` + + +Note that: + +* Your transaction message, and all future variants, will fall under `enum ExecuteMsg`. +* It too is serialized using `cw_serde`. +* Your first variant is `Register`. +* This variant only carries a `name: String`. Implicit here is that the sender's address is the other important parameter. + +## The storage definition + +Because your smart contract is meant to store the incoming information for future reference, you are about to keep in storage what the names map to. To help you define storage elements, you ought to use a library created to that effect. There are currently two of them, [StoragePlus](https://docs.cosmwasm.com/cw-storage-plus) and [Storey](https://docs.cosmwasm.com/storey). Here you add a new dependency for the CosmWasm StoragePlus elements. + + + + ```sh + cargo add cw-storage-plus2.0.0 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw-storage-plus@2.0.0 + ``` + + + +Then you need to describe the layout of the storage. Create the file `src/state.rs` with: + + + ```rust + use cosmwasm_schema::cw_serde; + use cosmwasm_std::Addr; + use cw_storage_plus::Map; + + #[cw_serde] + pub struct NameRecord { + pub owner: Addr, + } + + pub const NAME_RESOLVER: Map<&[u8], NameRecord> = Map::new("name_resolver"); + ``` + + +Note how: + +* You declare a new [`Map`](https://docs.cosmwasm.com/cw-storage-plus/containers/map), a type defined in `cw_storage_plus`. +* This map uses the [namespace](https://docs.cosmwasm.com/cw-storage-plus/basics#keys-and-prefixes) `"name_resolver"`, which will be used as a [prefix](https://github.com/CosmWasm/cw-storage-plus/blob/v2.0.0/src/map.rs#L64) for all the item keys in it. +* You will use the variable named `NAME_RESOLVER` to access items in the map. +* Keys are declared as pure bytes, `[u8]`, for more versatility. +* A `NameRecord` is the type of values that will be in this map. +* Values are stored as [serialized JSON](https://github.com/CosmWasm/cw-storage-plus/blob/v2.0.0/src/item.rs#L53), so you need to flag that as well with `cw_serde` on `NameRecord`. + +Don't forget to make the state locally accessible to your project: add the following line to `src/lib.rs`. + + + ```diff-rs + pub mod contract; + mod error; + pub mod msg; + + mod state; + ``` + + +## A new error + +Before moving on to the execution code, you can foresee that you will forbid registering a name that is already taken. Add an error for this situation: the value `NameTaken` to the enum `ContractError` in `src/error.rs`: + + + ```diff-rs + #[derive(Error, Debug)] + pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("Name already taken ({name})")] + + NameTaken { name: String }, + } + ``` + + +Note that it accepts the `name` as a string in order to provide better error formatting. + +## The execute function + +You add the handling in `src/contract.rs`. Adjust the imports: + + + ```diff-rs + - use crate::{error::ContractError, msg::InstantiateMsg}; + + use crate::{ + + error::ContractError, + + msg::{ExecuteMsg, InstantiateMsg}, + + state::{NameRecord, NAME_RESOLVER}, + + }; + ``` + + +And add two new functions: + + + ```rust + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn execute(deps: DepsMut, _: Env, info: MessageInfo, msg: ExecuteMsg) -> ContractResult { + match msg { + ExecuteMsg::Register { name } => execute_register(deps, info, name), + } + } + + fn execute_register(deps: DepsMut, info: MessageInfo, name: String) -> ContractResult { + let key = name.as_bytes(); + let record = NameRecord { owner: info.sender }; + + if NAME_RESOLVER.has(deps.storage, key) { + return Err(ContractError::NameTaken { name }); + } + + NAME_RESOLVER.save(deps.storage, key, &record)?; + + Ok(Response::default()) + } + ``` + + +Note how: + +* The `execute` function only cares to dispatch the messages according to their variant. This is the [conventional way](https://docs.cosmwasm.com/core/conventions/enum-dispatch) of handling execution in CosmWasm, so that the function body's size remains manageable as more message variants are added. +* The `execute_register` is where the proper implementation of `Register` is handled. +* The implementation is quite typical, you only save to storage if there are no pre-existing values. +* The sender information is found in `info: MessageInfo`. +* `NAME_RESOLVER` has functions named `has` and `save`, where the function parameter is `deps.storage`. This is the CosmWasm way, and it may look like an inversion of responsibility if you come from other platforms. + +Your contract can now register names with the sender adresses while rejecting already registered names. + + + +If you wanted to make it possible for _someone_ to register names for someone else, you would need to add an address to the message too. In particular, if you introduced auctions on names, the sender would be the auction smart contract, and the address in the message, i.e. the owner, would be the auction winner. + + + +## Unit testing + +With a new message and code, it is time to unit test it. + +In `src/contract.rs`, add the following: + + + ```diff-rs + ... + + #[cfg(test)] + mod tests { + - use crate::msg::InstantiateMsg; + + use crate::{ + + msg::{ExecuteMsg, InstantiateMsg}, + + state::{NameRecord, NAME_RESOLVER}, + + }; + use cosmwasm_std::{testing, Addr, Response}; + + #[test] + fn test_instantiate() { + ... + } + + + #[test] + + fn test_execute() { + + // Arrange + + let mut mocked_deps_mut = testing::mock_dependencies(); + + let mocked_env = testing::mock_env(); + + let mocked_addr = Addr::unchecked("addr"); + + let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let name = "alice".to_owned(); + + let execute_msg = ExecuteMsg::Register { name: name.clone() }; + + + + // Act + + let contract_result = super::execute( + + mocked_deps_mut.as_mut(), + + mocked_env, + + mocked_msg_info, + + execute_msg, + + ); + + + + // Assert + + assert!(contract_result.is_ok(), "Failed to register alice"); + + assert_eq!(contract_result.unwrap(), Response::default()); + + assert!(NAME_RESOLVER.has(mocked_deps_mut.as_ref().storage, name.as_bytes())); + + let stored = NAME_RESOLVER.load(mocked_deps_mut.as_ref().storage, name.as_bytes()); + + assert!(stored.is_ok()); + + assert_eq!(stored.unwrap(), NameRecord { owner: mocked_addr }); + + } + } + ``` + + +Note how: + +* You can mock all the elements that go into the call. +* It tests the return values of the call. +* It tests that the expected value is found in storage. + +After you run the tests, it should print: + +```txt +... +running 2 tests +test contract::tests::test_instantiate ... ok +test contract::tests::test_execute ... ok +... +``` + +## Conclusion + + + +At this stage, you should have something similar to the [`first-execute-message`](https://github.com/b9lab/cw-my-nameservice/tree/first-execute-message) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/first-unit-test..first-execute-message) as the diff. + + + +It is possible to store names and their addresses, but it is not possible to retrieve them, other than querying the storage natively. You fix that in the next section. + diff --git a/docs/tutorial/platform/07-first-contract-query.md b/docs/tutorial/platform/07-first-contract-query.md new file mode 100644 index 0000000..3e62be0 --- /dev/null +++ b/docs/tutorial/platform/07-first-contract-query.md @@ -0,0 +1,185 @@ +--- +title: First Contract Query +description: Add a function to your smart contract so you can query addresses by name. +--- + +# First contract Query + +In the previous section, you added a message to register an address under a name in storage. In this section, you make it possible to easily query for said stored values. + + + +If you skipped the previous section, you can just switch the project to its [`first-execute-message`](https://github.com/b9lab/cw-my-nameservice/tree/first-execute-message) branch and take it from there. + + + +## The query message and response + +To query from storage, you add a specific query message and its corresponding response. Add a `QueryResponse` import to `src/msg.rs`: + + + ```diff-rs + - use cosmwasm_schema::cw_serde; + + use cosmwasm_schema::{cw_serde, QueryResponses}; + ``` + + +Then add the new enum and struct: + + + ```rust + #[cw_serde] + #[derive(QueryResponses)] + pub enum QueryMsg { + #[returns(ResolveRecordResponse)] + ResolveRecord { name: String }, + } + + #[cw_serde] + pub struct ResolveRecordResponse { + pub address: Option, + } + ``` + + +Note how: + +* As with the transaction message, the `QueryMsg` is an enum. +* The `ResolveRecord` type mentions the type it returns with the use of the `returns` macro. +* `QueryResponses` is a [macro](https://github.com/CosmWasm/cosmwasm/blob/main/packages/schema-derive/src/lib.rs#L12). +* `ResolveRecordResponse` contains an `Option` to account for the fact that a missing owner is a valid result when resolving a name. +* `ResolveRecordResponse` otherwise looks very much like a `NameRecord`, but it could be different and collect different values from different places, depending on what is needed with this query. + +## The query function + +You define the message handling in `src/contract.rs`. Adjust the imports: + + + ```diff-rs + use crate::{ + error::ContractError, + - msg::{ExecuteMsg, InstantiateMsg}, + + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ResolveRecordResponse}, + state::{NameRecord, NAME_RESOLVER}, + }; + - use cosmwasm_std::{entry_point, DepsMut, Env, MessageInfo, Response}; + + use cosmwasm_std::{ + + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + + }; +``` + + +Then, below the `execute` function, you add the query functions: + + + ```rust + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn query(deps: Deps, _: Env, msg: QueryMsg) -> StdResult { + match msg { + QueryMsg::ResolveRecord { name } => query_resolve_record(deps, name), + } + } + + fn query_resolve_record(deps: Deps, name: String) -> StdResult { + let key = name.as_bytes(); + + let address = NAME_RESOLVER + .may_load(deps.storage, key)? + .map(|record| record.owner.to_string()); + + let resp = ResolveRecordResponse { address }; + + to_json_binary(&resp) + } + ``` + + +Note how: + +* Just as for the `execute` function, the `query` function is only here to dispatch to other functions depending on the message variant. +* Unlike the `execute` function, it takes a non-mutable `Deps`. Indeed, a query is not meant to modify storage, and Rust can catch such errors at compilation instead of run time. +* The function uses the `.may_load` method to handle potential errors gracefully. See Rust's `?` conditional `return`. +* The `address` variable is an `Option` because a missing value in storage is a valid response. +* The return type is JSON binary, that you create by calling the standard `serde` function `to_json_binary`. + +With this done, you can now query registered addresses by their names. + +## Unit testing + +It's time for your third unit test. In `src/contract.rs`, add the following: + + + ```diff-rs + ... + + #[cfg(test)] + mod tests { + use crate::{ + - msg::{ExecuteMsg, InstantiateMsg}, + + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + state::{NameRecord, NAME_RESOLVER}, + }; + - use cosmwasm_std::{testing, Addr, Response}; + + use cosmwasm_std::{testing, Addr, Binary, Response}; + + ... + + #[test] + fn test_execute() { + ... + } + + + #[test] + + fn test_query() { + + // Arrange + + let mut mocked_deps_mut = testing::mock_dependencies(); + + let mocked_env = testing::mock_env(); + + let name = "alice".to_owned(); + + let mocked_addr_value = "addr".to_owned(); + + let mocked_addr = Addr::unchecked(mocked_addr_value.clone()); + + let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let _ = super::execute_register(mocked_deps_mut.as_mut(), mocked_msg_info, name.clone()) + + .expect("Failed to register alice"); + + let query_msg = QueryMsg::ResolveRecord { name }; + + + + // Act + + let query_result = super::query(mocked_deps_mut.as_ref(), mocked_env, query_msg); + + + + // Assert + + assert!(query_result.is_ok(), "Failed to query alice name"); + + let expected_response = format!(r#"{{"address":"{mocked_addr_value}"}}"#); + + let expected = Binary::new(expected_response.as_bytes().to_vec()); + + assert_eq!(query_result.unwrap(), expected); + + } + } + ``` + + +Note that: + +* When arranging for the test, you keep the address string value for reuse. +* The expected string reads as a JSON, and that it is created with escaped characters made possible with `{{` and the raw string marker `r#...#`. +* `Binary::new(expected_response.as_bytes().to_vec())` converts to a binary. + +After you run `cargo test`, it should print its success in the output: + +```txt +... +running 3 tests +test contract::tests::test_instantiate ... ok +test contract::tests::test_execute ... ok +test contract::tests::test_query ... ok +... +``` + +## Conclusion + +You have created a query message and added its handling so that users and other smart contracts can check the registration status of names. + + + +At this stage, you should have something similar to the [`first-query-message`](https://github.com/b9lab/cw-my-nameservice/tree/first-query-message) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/first-execute-message..first-query-message) as the diff. + + + +So far your unit tests have only tested functions in isolation and run within Rust, without touching WebAssembly or CosmWasm. You expand a bit in the next section by having your functions interact with a mocked app chain. diff --git a/docs/tutorial/platform/08-first-contract-test.md b/docs/tutorial/platform/08-first-contract-test.md new file mode 100644 index 0000000..19dd802 --- /dev/null +++ b/docs/tutorial/platform/08-first-contract-test.md @@ -0,0 +1,336 @@ +--- +title: First Integration Test +description: Get closer to simulating your smart contract on a CosmWasm blockchain. +--- + +# First Integration Test + +So far, you have tested your smart contract with unit tests. These unit tests help you verify that your functions work as expected in isolation. However, they do not let you check whether your smart contract would work correctly on a blockchain, or with each other. + +You are fixing that in this section. + + + +If you skipped the previous section, you can just switch the project to its [`first-query-message`](https://github.com/b9lab/cw-my-nameservice/tree/first-query-message) branch and take it from there. + + + +## Structure + +You are going to use [MultiTest](https://docs.cosmwasm.com/cw-multi-test), which mocks an underlying blockchain, complete with mocked modules such as _Bank_. These mocks and tools are still all in Rust, which ensures speed. They would also allow you to test cross-contract communication. + +Because the tests take place in Rust, there is no compilation to WebAssembly. So to mimic a compiled object, there is a [`ContractWrapper`](https://github.com/CosmWasm/cw-multi-test/blob/v2.1.1/src/contracts.rs#L161) that exposes functions as if they were your smart contract's entry points. + +There is neither networking, consensus nor block creation. + +In this section, each of your integration test will: + +* Mock an underlying app chain. +* Store your smart contract code. +* Deploy a smart contract instance. +* Test something specific on this instance. +* Verify that it happened as per the expectations. + +## Dependencies + +You start by adding MultiTest as a development dependency to your project: + + + + ```sh + cargo add --dev cw-multi-test@2.1.1 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add --dev cw-multi-test@2.1.1 + ``` + + + +You are going to put your integration tests into a new folder: `tests`. In this folder, create a `contract.rs` file where you start by adding your dependencies: + + + ```rust + use cosmwasm_std::Addr; + use cw_multi_test::{App, ContractWrapper, Executor}; + use cw_my_nameservice::{ + contract::{execute, instantiate, query}, + msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ResolveRecordResponse}, + }; + ``` + + +Note that: + +* `cw_multi_test::App` mocks an underlying Cosmos app chain. +* `cw_multi_test::ContractWrapper` mocks a compiled smart contract, without actually compiling it to WebAssembly. +* `cw_multi_test::Executor` imports functions that allow you to execute actions on your mocked App. +* You import your smart contract's functions and messages. You may have to rename from `my_nameservice` if you picked a different name for your project. + +## Preparation + +When you mock your underlying app chain, you can choose [which features](https://docs.cosmwasm.com/cw-multi-test/features) it should implement. In this case, [the defaults](https://github.com/CosmWasm/cw-multi-test/blob/v2.1.1/src/app.rs#L92-L99) will be enough. So to mock an App, you simply call: + +```rust +let mut mock_app = App::default(); +``` + +Each of your tests will repeat similar steps, namely: + +1. Wrap the smart contract functions, to simulate a compilation. +2. Store the code on the mocked app chain. +3. Deploy an instance of your smart contract. + +So it is worth creating a function that you can call to do that. Add to `tests/contract.rs`: + + + ```rust + fn instantiate_nameservice(mock_app: &mut App) -> (u64, Addr) { + let nameservice_code = Box::new(ContractWrapper::new(execute, instantiate, query)); + let nameservice_code_id = mock_app.store_code(nameservice_code); + return ( + nameservice_code_id, + mock_app + .instantiate_contract( + nameservice_code_id, + Addr::unchecked("deployer"), + &InstantiateMsg {}, + &[], + "nameservice", + None, + ) + .expect("Failed to instantiate nameservice"), + ); + } + ``` + + +Note how: + +* Your smart contract is "compiled" into `ContractWrapper`. +* It is then stored on-chain, at a code id. +* The address of the deployer is not important, but could be in future iterations of your smart contract. +* The [`instantiate_contract`](https://github.com/CosmWasm/cw-multi-test/blob/v2.1.1/src/executor.rs#L84) function is actually defined in `Executor`. + +## Name register test + +With this, you can add a test of a name register. You want to make sure that it is saved to storage. Add: + + + ```rust + #[test] + fn test_register() { + // Arrange + let mut mock_app = App::default(); + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + let owner_addr_value = "owner".to_owned(); + let owner_addr = Addr::unchecked(owner_addr_value.clone()); + let name_alice = "alice".to_owned(); + let register_msg = ExecuteMsg::Register { + name: name_alice.to_owned(), + }; + + // Act + let result = mock_app.execute_contract( + owner_addr.clone(), + contract_addr.clone(), + ®ister_msg, + &[], + ); + + // Assert + assert!(result.is_ok(), "Failed to register alice"); + let stored_addr_bytes = mock_app + .contract_storage(&contract_addr) + .get(format!("\0\rname_resolver{name_alice}").as_bytes()) + .expect("Failed to load from name alice"); + let stored_addr = String::from_utf8(stored_addr_bytes).unwrap(); + assert_eq!(stored_addr, format!(r#"{{"owner":"{owner_addr_value}"}}"#)); + } + ``` + + +Note that: + +* It looks very much like what you did in unit tests. +* The [`execute_contract`](https://github.com/CosmWasm/cw-multi-test/blob/v2.1.1/src/executor.rs#L145) is declared in `Executor`. +* You are accessing directly to storage, which is a bit arduous but could come in handy at times, instead of relying on the query message. The `"alice"` key is prefixed with the `0` and `\r` bytes and the name of the storage map. + + + + +You can retrieve them all, as long as there are not too many of them, with: + +```rust +let store_dump = mock_app.dump_wasm_raw(&contract_addr); +println!("Length {}. Keys:", store_dump.len()); +for (key, _) in store_dump { + println!("{} / {:?}", str::from_utf8(&key).unwrap(), key); +} +``` + +Then, in order to get the logs while testing, you add the `-- --nocapture` flag like so: + + + + ```sh + cargo test -- --nocapture + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo test -- --nocapture + ``` + + + +Which prints something along the lines of: + +```txt +Length 1. Keys: +name_resolveralice / [0, 13, 110, 97, 109, 101, 95, 114, 101, 115, 111, 108, 118, 101, 114, 97, 108, 105, 99, 101] +``` + +The numerical values make it clear that the first two characters are `0` and [`13`](https://www.ascii-code.com/13), i.e. `\r`. + + + + +To confirm that it works, you run the same way you did for unit tests: + + + + ```sh + cargo test + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo test + ``` + + + +Which should print something like: + +```txt +... + Running tests/contract.rs (target/debug/deps/contract-d6161d38a3b0d331) + +running 1 test +test test_register ... ok + +test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +... +``` + +## Name query test + +With the name register test in place, you can add another test to confirm that the query works too. Add: + + + ```rust + #[test] + fn test_query() { + // Arrange + let mut mock_app = App::default(); + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + let owner_addr = Addr::unchecked("owner"); + let name_alice = "alice".to_owned(); + let register_msg = ExecuteMsg::Register { + name: name_alice.to_owned(), + }; + let _ = mock_app + .execute_contract( + owner_addr.clone(), + contract_addr.clone(), + ®ister_msg, + &[], + ) + .expect("Failed to register alice"); + let resolve_record_query_msg = QueryMsg::ResolveRecord { + name: name_alice.to_owned(), + }; + + // Act + let result = mock_app + .wrap() + .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + + // Assert + assert!(result.is_ok(), "Failed to query alice name"); + assert_eq!( + result.unwrap(), + ResolveRecordResponse { + address: Some(owner_addr.to_string()) + } + ) + } + ``` + + +Note that: + +* This time you execute the register command and expect a positive result, instead of checking it with an `assert!`. +* You access the query functions by wrapping the app: [`.wrap()`](https://github.com/CosmWasm/cw-multi-test/blob/v2.1.1/src/app.rs#L433). +* There are a lot of [possible query functions](https://github.com/CosmWasm/cosmwasm/blob/v2.1.3/packages/std/src/traits.rs#L355). So as to handle the fewer de/serialization matters, you can call `query_wasm_smart`. +* [`query_wasm_smart`](https://github.com/CosmWasm/cosmwasm/blob/v2.1.3/packages/std/src/traits.rs#L521) is a function of the mocked app, so it expects you to pass the address of the contract to query. +* You also need to specify the expected `ResolveRecordResponse` type because the compiler cannot otherwise infer it. + +Once you run `cargo test` again, you should see: + +```sh +... +running 2 tests +test test_register ... ok +test test_query ... ok + +test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s +... +``` + +For good measure, you can add a test that makes sure there are no results when querying on an unregistered name: + + + ```rust + #[test] + fn test_query_empty() { + // Arrange + let mut mock_app = App::default(); + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + let name_alice = "alice".to_owned(); + let resolve_record_query_msg = QueryMsg::ResolveRecord { + name: name_alice.to_owned(), + }; + + // Act + let result = mock_app + .wrap() + .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + + // Assert + assert!(result.is_ok(), "Failed to query alice name"); + assert_eq!(result.unwrap(), ResolveRecordResponse { address: None }) + } + ``` + + +## Conclusion + +You have created your first mocked-app test whereby your smart contract is tested against a mocked CosmWasm module. + + + +At this stage, you should have something similar to the [`first-multi-test`](https://github.com/b9lab/cw-my-nameservice/tree/first-multi-test) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/first-query-message..first-multi-test) as the diff. + + diff --git a/docs/tutorial/platform/09-first-response.md b/docs/tutorial/platform/09-first-response.md new file mode 100644 index 0000000..5bee909 --- /dev/null +++ b/docs/tutorial/platform/09-first-response.md @@ -0,0 +1,153 @@ +--- +title: First Composed Response +description: Return something more after execution. +--- + +# First Composed Response + +As it stands, your `execute` function only returns a `Response::default()`. That is the bare minimum. Eventually, as your smart contract communicates with others or even other modules, it needs to pass more information as part of its response. This section is a step in this direction. + + + +If you skipped the previous section, you can just switch the project to its [`first-multi-test`](https://github.com/b9lab/cw-my-nameservice/tree/first-multi-test) branch and take it from there. + + + +## Add an event + +A very simple thing to add to your response is an [event](https://docs.cosmwasm.com/core/architecture/events). It is the same concept as [events in Cosmos](https://tutorials.cosmos.network/academy/2-cosmos-concepts/10-events.html). You can add attributes to the `wasm` event that is added by the CosmWasm module itself, and is always present. + +Better yet, you add your own event, which will mean something to those who interact with your smart contract. Update your `src/contract.rs` with: + + + ```diff-rust + ... + + use cosmwasm_std::{ + - entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, StdResult, + + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, + + StdResult, + }; + + ... + + fn execute_register(deps: DepsMut, info: MessageInfo, name: String) -> ContractResult { + let key = name.as_bytes(); + - let record = NameRecord { owner: info.sender }; + + let record = NameRecord { + + owner: info.sender.to_owned(), + + }; + + ... + + - Ok(Response::default()) + + let registration_event = Event::new("name-register") + + .add_attribute("name", name) + + .add_attribute("owner", info.sender); + + let resp = Response::default().add_event(registration_event); + + Ok(resp) + } + + ... + ``` + + +Note that: + +* You are free to choose any event name other than `name-register`. You ought to make it a visible constant too. +* The CosmWasm module will prefix it with `wasm-` before sending the event as a Cosmos one. +* You can add more attributes in your event if the need arises. +* You can add more events to the response if you need or want to. + +## Adjust the unit test + +The response has changed, so must the unit test: + + + ```diff-rust + ... + + mod tests { + + ... + + - use cosmwasm_std::{testing, Addr, Binary, Response}; + + use cosmwasm_std::{testing, Addr, Binary, Event, Response}; + + ... + + fn test_execute() { + + ... + + - assert_eq!(contract_result.unwrap(), Response::default()); + + let received_response = contract_result.unwrap(); + + let expected_event = Event::new("name-register") + + .add_attribute("name", name.to_owned()) + + .add_attribute("owner", mocked_addr.to_string()); + + let expected_response = Response::default().add_event(expected_event); + + assert_eq!(received_response, expected_response); + + ... + + } + + ... + + } + + ... + ``` + + +Note that: + +* The `assert_eq!` macro does a deep equal between the _received_ and _expected_ responses. +* Because you have only tested the function in isolation, the event name is not prefixed with `wasm-`. + +## Adjust the mocked-app test + +It too needs modifying: + + + ```diff-rust + - use cosmwasm_std::Addr; + + use cosmwasm_std::{Addr, Event}; + + ... + + fn test_register() { + + ... + + assert!(result.is_ok(), "Failed to register alice"); + - let received_response = result.unwrap(); + + let expected_event = Event::new("wasm-name-register") + + .add_attribute("name", name_alice.to_owned()) + + .add_attribute("owner", owner_addr_value.to_owned()); + + received_response.assert_event(&expected_event); + + assert_eq!(received_response.data, None); + + ... + + } + + ... + ``` + + +Note that: + +* Here, the received response is of type `AppResponse`, which contains your new event, and may eventually contain other events that would be emitted by other smart contracts that may have been called as part of `ExecuteMsg::Register`. +* This time, the mocked app prefixed the event with `wasm-`. +* The response has an `assert_event` function that calls `assert!` and `has_event` with a nice message so that you don't have to write it yourself. + +## Conclusion + +There is more to execution responses than events, as you will learn in subsequent sections. + + + +At this stage, you should have something similar to the [`first-event`](https://github.com/b9lab/cw-my-nameservice/tree/first-event) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/first-multi-test..first-event) as the diff. + + diff --git a/docs/tutorial/platform/10-use-library.md b/docs/tutorial/platform/10-use-library.md new file mode 100644 index 0000000..a018325 --- /dev/null +++ b/docs/tutorial/platform/10-use-library.md @@ -0,0 +1,532 @@ +--- +title: Use the Ownable Library +description: Add a feature by incorporating code from elsewhere. +--- + +# Use the Ownable Library + +Your smart contract lets anyone register names. This sounds a bit too much like the dot-com rush and its notorious domain parking. You may want to introduce some order. For instance, in the future, only an auction smart contract may eventually be allowed to register names to auction winners. + +A regular word used for such a _name registerer_ is **minter**. In this section you add a minter to your smart contract, and have it gatekeep the register function. + +Ideally, your smart contract should make it possible to update the minter, or have the minter be able to pass the baton. This sounds a lot like the _ownable_ pattern found in blockchain, for instance in Ethereum. + +In fact, there is [such a thing](https://github.com/larry0x/cw-plus-plus/tree/main/packages/ownable) too in the CosmWasm ecosystem. In this section, you delegate to it: + +* The storage definition. +* The update mechanics. +* The message types. + + + +If you skipped the previous section, you can just switch the project to its [`first-event`](https://github.com/b9lab/cw-my-nameservice/tree/first-event) branch and take it from there. + + + +## Add the dependency + + + + ```sh + cargo add cw-ownable@2.1.0 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw-ownable@2.1.0 + ``` + + + +## Add the storage element + +The object that defines access to the minter in storage is [`OwnershipStore`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L31). Note how it not only stores the [`owner`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L19), but also a [`pending_owner`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L23), which has to accept an invitation to become the new minter. + +In your situation, the variables are named `owner` inside the library, but when used in your smart contract, you will use the word _minter_ to avoid confusion. Update `src/state.rs`: + + + ```diff-rust + ... + + use cosmwasm_std::Addr; + + use cw_ownable::OwnershipStore; + use cw_storage_plus::Map; + + ... + + pub const NAME_RESOLVER: Map<&[u8], NameRecord> = Map::new("name_resolver"); + + pub const MINTER: OwnershipStore = OwnershipStore::new("name_minter"); + + ... + ``` + + +## Add new message variants + +Only the new minter will be allowed to register new names. So it is a good first step to have this address set when deploying the instance. Add it to `InstantiateMsg`: + + + ```diff-rust + ... + + #[cw_serde] + - pub struct InstantiateMsg {} + + pub struct InstantiateMsg { + + pub minter: String, + + } + + ... + ``` + + +Eventually, you could imagine making this optional and have another function that lets one set the minter. A straight string keeps things simple for now. + +When time comes to register a name, the message will have to be sent from the minter. However, at the moment, the smart contract takes the sender of the message as the eventual name owner. So you need to change the message so that the owner is mentioned: + + + ```diff-rust + use cosmwasm_schema::{cw_serde, QueryResponses}; + + use cosmwasm_std::Addr; + + ... + + pub struct ExecuteMsg { + - Register { name: String }, + + Register { name: String, owner: Addr }, + } + + ... + ``` + + +The `QueryMsg` does not need to change as querying does not involve the minter. + +## Add handling at instantiation + +This is where the smart contract saves to storage the _minter_ information it received: + + + ```diff-rust + use crate::{ + - state::{NameRecord, NAME_RESOLVER}, + + state::{NameRecord, MINTER, NAME_RESOLVER}, + } + + ... + + pub fn instantiate( + - _deps_: DepsMut, + + deps: DepsMut, + _: Env, + _: MessageInfo, + - _msg_: InstantiateMsg + + msg: InstantiateMsg + ) -> ContractResult { + + let _ = MINTER.initialize_owner(deps.storage, deps.api, Some(msg.minter.as_str()))?; + Ok(Response::default()) + } + + ... + ``` + + +Note how: + +* It uses this [`initialiaze_owner`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L45) function defined in the library. +* The function can erase the minter if you pass `None` instead of `Some`. +* It may return a `StdErr`, in which case the error is returned thanks to the trailing `?`. +* The returned `StdErr` is still transformed into a `ContractError::Std` thanks to the `from` macro `Std(#[from] StdError)`. +* You do not use the returned `Ownership` since you know what it is. + +## Add handling at name registration + +This is where the smart contract verifies that it is the minter that is sending the message. You need to: + +1. Destructure the message to extract the eventual name owner. +2. Verify that the message sender is the minter. +3. Adjust the record and the event with the proper owner. + +The verification may yield an error. So you add a new error type to make it explicit, and add a convenience curried function that will come in handy when propagating errors: + + + ```diff-rust + use cosmwasm_std::StdError; + + use cw_ownable::OwnershipError; + use thiserror::Error; + + ... + + pub enum ContractError { + ... + NameTaken { name: String }, + + #[error("Caller ({caller}) is not minter")] + + Minter { + + caller: String, + + inner: OwnershipError, + + }, + } + + + + impl ContractError { + + pub fn from_minter<'a>(caller: &'a Addr) -> impl Fn(OwnershipError) -> ContractError + 'a { + + move |inner: OwnershipError| ContractError::Minter { + + caller: caller.to_string(), + + inner, + + } + + } + + } + ``` + + +Note that: + +* The message could be refined eventually, but it will do for now. The error message mentions the caller for convenience. +* The `from_minter` function returns a closure. + +Now you can update the handling: + + + ```diff-rust + use cosmwasm_std::{ + - entry_point, to_json_binary, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, + + entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, + StdResult, + }; + + ... + + pub fn execute( + ... + ) -> ContractResult { + match msg { + - ExecuteMsg::Register { name } => execute_register(deps, info, name), + + ExecuteMsg::Register { name, owner } => execute_register(deps, info, name, &owner), + } + } + + ... + + fn execute_register( + ... + name: String, + + owner: &Addr, + ) -> ContractResult { + + MINTER + + .assert_owner(deps.storage, &info.sender) + + .map_err(ContractError::from_minter(&info.sender))?; + let key = name.as_bytes(); + let record = NameRecord { + - owner: info.sender.to_owned(), + + owner: owner.to_owned(), + }; + + ... + + let registration_event = Event::new("name-register") + .add_attribute("name", name) + - .add_attribute("owner", info.sender); + + .add_attribute("owner", owner.to_string()); + let resp = Response::default().add_event(registration_event); + + ... + } + ``` + + +Note how: + +* It is using the [`assert_owner`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L77) function of the library. This is production function, now one reserved for tests. +* This function potentially returns a `OwnershipError`, which is why you use a `map_error` to transform the `Result` into a `Err(ContractError::Minter)` using the curried function defined earlier. +* Here too, the trailing `?` is used to return the eventual error. +* Other than that, it is just a matter of replacing the `sender` with the `owner`. + +## Adjust unit tests + +With the handling done, you need to update tests, starting with the unit tests. + + + +The Ownable library makes calls to [`Api.addr_validate`](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L52). Unfortunately in CosmWasm 2.0, the `MockApi.addr_validate` does not replace this function with a dummy check. So `Addr::unchecked` will not work. Fortunately, it is still possible to create relatively dummy addresses. + + + +To test the instantiation you will: + +1. Create a quasi-proper minter address. +2. Confirm it was recorded in storage. + + + ```diff-rust + ... + + mod tests { + use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + - state::{NameRecord, NAME_RESOLVER}, + + state::{NameRecord, MINTER, NAME_RESOLVER}, + }; + - use cosmwasm_std::{testing, Addr, Binary, Event, Response}; + + use cosmwasm_std::{testing, Addr, Api, Binary, CanonicalAddr, Event, Response}; + + ... + + fn test_instantiate() { + // Arrange + ... + let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let minter = mocked_deps_mut + + .api + + .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + + .expect("Failed to create minter address"); + - let instantiate_msg = InstantiateMsg {}; + + let instantiate_msg = InstantiateMsg { + + minter: minter.to_string(), + + }; + ... + + // Assert + ... + assert_eq!(contract_result.unwrap(), Response::default()); + + assert!(MINTER + + .assert_owner(&mocked_deps_mut.storage, &minter) + + .is_ok()); + } + } + ``` + + +Note how: + +* You need to import `cosmwasm_std::Api` to have access to `addr_humanize` +* The `Act` part remains unchanged. + +Similarly, you adjust the `test_execute`. You can choose to mimic a proper instantiation or directly manipulate the `MINTER` object. Here, it is mimicking an instantiation: + + + ```diff-rust + ... + + mod tests { + ... + + fn test_execute() { + // Arrange + ... + let mocked_addr = Addr::unchecked("addr"); + + let minter = mocked_deps_mut + + .api + + .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + + .expect("Failed to create minter address"); + + let _ = super::instantiate( + + mocked_deps_mut.as_mut(), + + mocked_env.to_owned(), + + testing::message_info(&mocked_addr, &[]), + + InstantiateMsg { + + minter: minter.to_string(), + + }, + + ) + + .expect("Failed to instantiate"); + - let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let mocked_msg_info = testing::message_info(&minter, &[]); + let name = "alice".to_owned(); + + let owner = Addr::unchecked("owner"); + - let execute_msg = ExecuteMsg::Register { name: name.clone() }; + + let execute_msg = ExecuteMsg::Register { + + name: name.clone(), + + owner: owner.to_owned(), + + }; + ... + + // Assert + ... + let expected_event = Event::new("name-register") + .add_attribute("name", name.to_owned()) + - .add_attribute("owner", mocked_addr.to_string()); + + .add_attribute("owner", owner.to_string()); + ... + - assert_eq!(stored.unwrap(), NameRecord { owner: mocked_addr }); + + assert_eq!(stored.unwrap(), NameRecord { owner: owner }); + } + } + ``` + + +Note how: + +* The arrange part is much longer. + +As for the `test_query`, you have to add more preparation: + + + ```diff-rust + ... + + mod tests { + ... + + fn test_query() { + // Arrange + ... + let mocked_addr = Addr::unchecked(mocked_addr_value.clone()); + + let minter = mocked_deps_mut + + .api + + .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + + .expect("Failed to create minter address"); + + let _ = super::instantiate( + + mocked_deps_mut.as_mut(), + + mocked_env.to_owned(), + + testing::message_info(&mocked_addr, &[]), + + InstantiateMsg { + + minter: minter.to_string(), + + }, + + ) + + .expect("Failed to instantiate"); + - let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let mocked_msg_info = testing::message_info(&minter, &[]); + - let _ = super::execute_register(mocked_deps_mut.as_mut(), mocked_msg_info, name.clone()) + - .expect("Failed to register alice"); + + let _ = super::execute_register( + + mocked_deps_mut.as_mut(), + + mocked_msg_info, + + name.clone(), + + &mocked_addr, + + ) + + .expect("Failed to register alice"); + ... + } + } + ``` + + +Note how: + +* Only the _arrange_ part is modified. + +## Add to unit tests + +To complete the picture, you ought to add tests to cover the case where an account other than the minter tries to register a name. + +## Adjust mocked app tests + +Similarly, the mocked app tests need to be adjusted. In fact, you do not have much to modify as it is mostly a matter of setting a proper minter to permit actions. You modify the `instantiate_nameservice` convenience function to also return the minter, for reuse from the test proper: + + + ```diff-rust + - use cosmwasm_std::{Addr, Event}; + + use cosmwasm_std::{Addr, Api, CanonicalAddr, Event}; + + + type ContractAddr = Addr; + + type MinterAddr = Addr; + + - fn instantiate_nameservice(mock_app: &mut App) -> (u64, Addr) { + + fn instantiate_nameservice(mock_app: &mut App) -> (u64, ContractAddr, MinterAddr) { + ... + let nameservice_code_id = mock_app.store_code(nameservice_code); + + let minter = mock_app + + .api() + + .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + + .unwrap(); + return ( + nameservice_code_id, + mock_app + .instantiate_contract( + nameservice_code_id, + Addr::unchecked("deployer"), + - &InstantiateMsg {}, + + &InstantiateMsg { + + minter: minter.to_string(), + + }, + &[], + "nameservice", + None, + ) + .expect("Failed to instantiate nameservice"), + + minter, + ); + } + ``` + + +Note that the type aliases are here only as syntactic sugar to disambiguate the two returned `Addr`. + +With this done, you can adjust `test_register`: + + + ```diff-rust + fn test_register() { + ... + - let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr, minter) = instantiate_nameservice(&mut mock_app); + ... + let register_msg = ExecuteMsg::Register { + name: name_alice.to_owned(), + + owner: owner_addr.to_owned(), + }; + ... + let result = mock_app.execute_contract( + - owner_addr.clone(), + + minter, + ... + ); + ... + } + ``` + + +And both `test_query` functions: + + + ```diff-rust + fn test_query() { + ... + - let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr, minter) = instantiate_nameservice(&mut mock_app); + ... + let register_msg = ExecuteMsg::Register { + name: name_alice.to_owned(), + + owner: owner_addr.to_owned(), + }; + ... + let _ = mock_app + .execute_contract( + - owner_addr.clone(), + + minter, + ... + ) + ... + } + ... + fn test_query_empty() { + ... + - let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr, _) = instantiate_nameservice(&mut mock_app); + ... + } + ``` + + +## Add to mocked app tests + +The introduction of the minter warrants further testing. In particular: + +* Test that the minter was saved at instantiation. +* Test that it is not possible to register a name from another account than the minter. + +This is left as an exercise. + +## Conclusion + +You have used a library that embeds some assumptions about access to storage, delegated some operations to it, and confirmed with tests that it works. This library can do a lot more, including modifying the minter. As an exercise, you may want to: + +* Add a `QueryMsg` variant to query the minter's current status. +* Add a `ExecuteMsg` variant to pass an [Action](https://github.com/larry0x/cw-plus-plus/blob/ownable-v2.1.0/packages/ownable/src/lib.rs#L211) to the minter ownership object. + +What you have done is all within a single smart contract, it is not cross-contract message exchange. + + + +At this stage, you should have something similar to the [`add-first-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-first-library) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/first-event..add-first-library) as the diff. + + diff --git a/docs/tutorial/platform/11-use-large-library.md b/docs/tutorial/platform/11-use-large-library.md new file mode 100644 index 0000000..47616a1 --- /dev/null +++ b/docs/tutorial/platform/11-use-large-library.md @@ -0,0 +1,813 @@ +--- +title: Use the NFT Library +description: Instead of reinventing the wheel. +--- + +# Use the NFT Library + +Your smart contract that can register names sounds an awful lot like the _mint_ funtion of non-fungible tokens (NFTs). You even added a minter that gatekeeps the minting. Eventually, you can imagine adding functionality so that the name owners can transfer, or sell, those names. + + + +If you skipped the previous section, you can just switch the project to its [`add-first-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-first-library) branch and take it from there. + + + +Instead of reinventing the wheel, you could reuse an NFT library. [`cw721`](https://crates.io/crates/cw721) is one such library. Its code is [here](https://github.com/public-awesome/cw-nfts). An additional advantage of using a library that acts close to a standard is that your smart contract is going to be compatible with other smart contracts that are compatible with the standard. + +Let's refactor in order to use it. You are going to: + +1. Change the dependencies. +2. Decide how much of the library you are going to use. +3. Decide what to still declare in storage and what to delegate. +4. Ditto for messages. +5. Update the smart contract handling of messages. +6. Update the tests. + +## Add the dependency + + + + ```sh + cargo add cw721 --git https://github.com/public-awesome/cw-nfts --tag "v0.19.0" + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw721 --git https://github.com/public-awesome/cw-nfts --tag "v0.19.0" + ``` + + + +Note: + +* At the time of writing, the 0.19.0 version is not yet published, which is why you have to call it via Github. + +Additionally: + +* The [`ownable`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/Cargo.toml#L29) and [`cw-storage-plus`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/Cargo.toml#L31) libraries come with `cw721`, so you don't need to mention them on their own: + + + ```diff-toml + ... + [dependencies] + - cw-ownable = "2.1.0" + - cw-storage-plus = "2.0.0" + ... + ``` + + +* The current version requires [CosmWasm v1.5+](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/Cargo.toml#L16-L17), so you have to downgrade your versions, including `cw-multi-test`, even though it is [used by `cw721`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/Cargo.toml#L28): + + + ```diff-toml + ... + [dependencies] + - cosmwasm-schema = "2.1.3" + - cosmwasm-std = "2.1.3" + + cosmwasm-schema = "1.5.8" + + cosmwasm-std = "1.5.8" + ... + + [dev-dependencies] + - cw-multi-test = "2.1.1" + + cw-multi-test = "1.2.0" + ``` + + +## Decide on the types + +This NFT library is itself a large body of work and you have to decide on how you are going to use it. In its jargon, an _extension_ is the additional information relative to the NFT, such as URL or even content, which could be stored on- or off-chain. In order to cleave as close as possible to what you have already done, you pick an [empty extension](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/extension.rs#L95), so that what remains are: + +* _Token id_, which maps directly to the registered name. +* _Owner_, which is the same concept as previously. + +## How state changes + +Since the NFT library takes care of the records and the minter, you do not need to declare anything: + + + ```diff-rust + - use cosmwasm_schema::cw_serde; + - use cosmwasm_std::Addr; + - use cw_ownable::OwnershipStore; + - use cw_storage_plus::Map; + + - #[cw_serde] + - pub struct NameRecord { + - pub owner: Addr, + - } + + - pub const NAME_RESOLVER: Map<&[u8], NameRecord> = Map::new("name_resolver"); + - pub const MINTER: OwnershipStore = OwnershipStore::new("name_minter"); + ``` + + +It is ok to keep an empty file to signify that having nothing is indeed a decision and not an omission. + +## How errors change + +You get a new class of errors, those of the library. And since you delegate registration and minter actions, all you have to do is wrap the new class of errors: + + + ```diff-rust + - use cosmwasm_std::{Addr, StdError}; + - use cw_ownable::OwnershipError; + + use cosmwasm_std::StdError; + + use cw721::error::Cw721ContractError; + use thiserror::Error; + + pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + - #[error("Name already taken ({name})")] + - NameTaken { name: String }, + - #[error("Caller ({caller}) is not minter")] + - Minter { + - caller: String, + - inner: OwnershipError, + - }, + + #[error("{0}")] + + Cw721(#[from] Cw721ContractError), + } + + - impl ContractError { + - pub fn from_minter<'a>(caller: &'a Addr) -> impl Fn(OwnershipError) -> ContractError + 'a { + - move |inner: OwnershipError| ContractError::Minter { + - caller: caller.to_string(), + - inner, + - } + - } + - } + ``` + + +Note how, thanks to the `#from` macro, `Cw721ContractError` can also be _automagically_ converted to `ContractError`. + +## How messages change + +Your goal with this change is also to maximize compatilibity. So to make sure that other smart contracts can communicate with yours, you keep the standard's messages unchanged, with the knowledge that you have picked an empty extension: + + + ```diff-rust + - use cosmwasm_schema::{cw_serde, QueryResponses}; + - use cosmwasm_std::Addr; + + use cosmwasm_std::Empty; + + use cw721::msg::{Cw721ExecuteMsg, Cw721InstantiateMsg, Cw721QueryMsg}; + + - #[cw_serde] + - pub struct InstantiateMsg { + - pub minter: String, + - } + + pub type InstantiateMsg = Cw721InstantiateMsg>; + + - #[cw_serde] + - pub enum ExecuteMsg { + - Register { name: String, owner: Addr }, + - } + + pub type ExecuteMsg = Cw721ExecuteMsg, Option, Empty>; + + - #[cw_serde] + - #[derive(QueryResponses)] + - pub enum QueryMsg { + - #[returns(ResolveRecordResponse)] + - ResolveRecord { name: String }, + - } + + pub type QueryMsg = Cw721QueryMsg, Option, Empty>; + + - #[cw_serde] + - pub struct ResolveRecordResponse { + - pub address: Option, + - } + ``` + + +Note that defining type aliases is a convenience rather than a necessity. + +## How the contract changes + +From here, all your smart contract has to do is to delegate actions to the equivalent ones from the library, with the knowledge that you have picked an empty extension. The library allows you a certain degree of configuration, but for your purposes, invoking the similarly-named functions on `Cw721EmptyExtensions::default()` gets you what you need. + + + ```diff-rust + - use crate::{ + error::ContractError, + - msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ResolveRecordResponse}, + - state::{NameRecord, MINTER, NAME_RESOLVER}, + + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + }; + use cosmwasm_std::{ + - entry_point, to_json_binary, Addr, Binary, Deps, DepsMut, Env, Event, MessageInfo, Response, StdResult, + + entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, + }; + + use cw721::{ + + extension::Cw721EmptyExtensions, + + traits::{Cw721Execute, Cw721Query}, + + }; + + type ContractResult = Result; + + type BinaryResult = Result; + + pub fn instantiate( + deps: DepsMut, + - _: Env, + - _: MessageInfo, + + env: Env, + + info: MessageInfo, + msg: InstantiateMsg, + ) -> ContractResult { + - let _ = MINTER.initialize_owner(deps.storage, deps.api, Some(msg.minter.as_str()))?; + - Ok(Response::default()) + + Ok(Cw721EmptyExtensions::default().instantiate(deps, &env, &info, msg)?) + } + + pub fn execute( + deps: DepsMut, + - _: Env, + + env: Env, + info: MessageInfo, + msg: ExecuteMsg, + ) -> ContractResult { + - match msg { + - ExecuteMsg::Register { name, owner } => execute_register(deps, info, name, &owner), + - } + + Ok(Cw721EmptyExtensions::default().execute(deps, &env, &info, msg)?) + } + + - fn execute_register(...) { + - ... + - } + + ... + + pub fn query( + deps: Deps, + - _: Env, + + env: Env, + msg: QueryMsg, + - ) -> StdResult { + - match msg { + - QueryMsg::ResolveRecord { name } => query_resolve_record(deps, name), + - } + + ) -> BinaryResult { + + Ok(Cw721EmptyExtensions::default().query(deps, &env, msg)?) + } + + - fn query_resolve_record() { + - ... + - } + ``` + + +Note how: + +* It's a matter of passing the error along with a `?` to benefit from the `#from` macro constructor. +* And a matter of wrapping the values in an `Ok` result. +* You introduce the type `BinaryResult` so as to benefit succinctly from the error's `#from` constructor when querying. +* You do not add your own event for convenience. To do so meaningfully, you would have to first find out which message type is passing through. +* The `Register` equivalent in the library is `Mint`, which already emits its relevant events. + +## Update your unit tests + +Many things have changed, but in essence, you mostly have to: + +* Adjust for the change of CosmWasm version from 2 to 1. +* Change how your messages are built. +* Change the storage checks with newly appropriate ones, including the storage keys. + +### The dummy instantiation message + +The new [`InstantiateMsg`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/msg.rs#L126-L143) has a long list of attributes, most of which you do not care much about in unit tests. It is worthwhile taking this into a separate function: + + + ```diff-rust + ... + mod tests { + ... + + fn simple_instantiate_msg(minter: String) -> InstantiateMsg { + + InstantiateMsg { + + name: "my names".to_owned(), + + symbol: "MYN".to_owned(), + + creator: None, + + minter: Some(minter.to_string()), + + collection_info_extension: None, + + withdraw_address: None, + + } + + } + ... + + #[test] + fn test_instantiate() { + ... + } + } + ``` + + +### Instantiate + +What you want to test is that you can instantiate and have the minter set as expected. + + + ```diff-rust + ... + mod tests { + use crate::{ + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + - state::{NameRecord, MINTER, NAME_RESOLVER}, + } + - use cosmwasm_std::{testing, Addr, Api, Binary, CanonicalAddr, Event, Response}; + + use cosmwasm_std::{testing, Addr, Binary, Response}; + + use cw721::{ + + extension::Cw721EmptyExtensions, + + state::{NftInfo, MINTER}, + + }; + + ... + + #[test] + fn test_instantiate() { + ... + - let mocked_msg_info = testing::message_info(&mocked_addr, &[]); + + let mocked_msg_info = testing::mock_info(&mocked_addr.to_string(), &[]); + - let minter = mocked_deps_mut + - .api + - .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + - .expect("Failed to create minter address"); + + let minter = Addr::unchecked("minter"); + - let instantiate_msg = InstantiateMsg { + - minter: minter.to_string(), + - }; + + let instantiate_msg = simple_instantiate_msg(minter.to_string()); + ... + - assert_eq!(contract_result.unwrap(), Response::default()); + + assert_eq!( + + contract_result.unwrap(), + + Response::default() + + .add_attribute("minter", "minter") + + .add_attribute("creator", "addr") + + ); + ... + } + } + ``` + + +Note how: + +* Changes look mostly cosmetic. +* There is no change to the assertion on `MINTER` other than now the `use` statement: + * Refers to `cw721::state::MINTER`, + * Instead of `crate::state::MINTER`. +* The assertion on `MINTER` is unchanged because it uses `.assert_owner`, which is found in the `ownable` library, whether you import it yourself, or `cw721` does. +* The NFT library does not create an elegantly separate event, instead it adds straight attributes to the response. This behaviour depends on the library you use. + +### Execute + +Here, you have to bring the same changes to the _arrange_ part, then verify that the data was saved by using the library. It is a bit more involved. + +The information about NFTs is stored in a map named [`.nft_info`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/state.rs#L58), and because of your choices here, the values stored are deserialized to the type `NftInfo>`. You access the map with `Cw721EmptyExtensions::default().config.nft_info`. + + + ```diff-rust + ... + mod tests { + ... + + #[test] + fn test_execute() { + ... + - let minter = mocked_deps_mut + - .api + - .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + - .expect("Failed to create minter address"); + + let minter = Addr::unchecked("minter"); + let _ = super::instantiate( + mocked_deps_mut.as_mut(), + mocked_env.to_owned(), + - testing::message_info(&mocked_addr, &[]), + + testing::mock_info(&mocked_addr.to_string(), &[]), + - InstantiateMsg { + - minter: minter.to_string(), + - }, + + simple_instantiate_msg(minter.to_string()), + ) + ... + - let mocked_msg_info = testing::message_info(&minter, &[]); + + let mocked_msg_info = testing::mock_info(&minter.to_string(), &[]); + ... + - let execute_msg = ExecuteMsg::Register { + - name: name.clone(), + - owner: owner.to_owned(), + + let execute_msg = ExecuteMsg::Mint { + + token_id: name.to_owned(), + + owner: owner.to_string(), + + token_uri: None, + + extension: None, + }; + ... + - let expected_event = Event::new("name-register") + - .add_attribute("name", name.to_owned()) + - .add_attribute("owner", owner.to_string()); + - let expected_response = Response::default().add_event(expected_event); + + let expected_response = Response::default() + + .add_attribute("action", "mint") + + .add_attribute("minter", "minter") + + .add_attribute("owner", "owner") + + .add_attribute("token_id", "alice"); + ... + - assert!(NAME_RESOLVER.has(mocked_deps_mut.as_ref().storage, name.as_bytes())); + + assert!(Cw721EmptyExtensions::default() + + .config + + .nft_info + + .has(mocked_deps_mut.as_ref().storage, name.as_str())); + - let stored = NAME_RESOLVER.load(mocked_deps_mut.as_ref().storage, name.as_bytes()); + + let stored = Cw721EmptyExtensions::default() + + .config + + .nft_info + + .load(mocked_deps_mut.as_ref().storage, name.as_str()); + assert!(stored.is_ok()); + assert_eq!( + stored.unwrap(), + - NameRecord { owner: owner } + + NftInfo { + + owner: owner, + + approvals: [].to_vec(), + + token_uri: None, + + extension: None, + + } + ); + } + } + ``` + + +Note how: + +* The `Register` message is now `Mint`. +* Here too, there is no separate event but the attributes are added to the main result. +* Previously you accessed the storage map with `NAME_RESOLVER`, now you achieve the same be digging a bit to `nft_info`. +* The `Mint` message and the `stored` object both mention `token_uri` and `extension` as `None`. That's a result of your choice of picking `Cw721EmptyExtensions`. + +### Query + +To test the query, you need to retrace both the instantiate and execute steps. + + + ```diff-rust + ... + mod tests { + ... + + #[test] + fn test_query() { + ... + - let minter = mocked_deps_mut + - .api + - .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + - .expect("Failed to create minter address"); + + let minter = Addr::unchecked("minter"); + let _ = super::instantiate( + mocked_deps_mut.as_mut(), + mocked_env.to_owned(), + - testing::message_info(&mocked_addr, &[]), + + testing::mock_info(&mocked_addr.to_string(), &[]), + - InstantiateMsg { + - minter: minter.to_string(), + - }, + + simple_instantiate_msg(minter.to_string()), + ) + ... + - let mocked_msg_info = testing::message_info(&minter, &[]); + + let mocked_msg_info = testing::mock_info(&minter.to_string(), &[]); + + let execute_msg = ExecuteMsg::Mint { + + token_id: name.to_owned(), + + owner: mocked_addr.to_string(), + + token_uri: None, + + extension: None, + + }; + ... + - let _ = super::execute_register( + + let _ = super::execute( + mocked_deps_mut.as_mut(), + + mocked_env.to_owned(), + mocked_msg_info, + - name.clone(), + - &mocked_addr, + + execute_msg, + ); + ... + - let query_msg = QueryMsg::ResolveRecord { name }; + + let query_msg = QueryMsg::OwnerOf { + + token_id: name, + + include_expired: None, + + }; + ... + - let expected_response = format!(r#"{{"address":"{mocked_addr_value}"}}"#); + + let expected_response = format!(r#"{{"owner":"{mocked_addr_value}","approvals":[]}}"#); + - let expected = Binary::new(expected_response.as_bytes().to_vec()); + + let expected = Binary::from(expected_response.as_bytes()); + ... + } + } + ``` + + +Note how: + +* `QueryMsg` now offers a long list of variants, of which the most succinct for the test is `OwnerOf`. + +## Update your mocked app tests + +Here, as for the unit tests you need to: + +* Adjust for the change of CosmWasm version from 2 to 1. +* Change how your messages are built. +* Change the event and storage checks with newly appropriate ones, including the storage keys. + +### The deploy helper + +This function exists to assist you in proper tests. Without surprise, it changes to reflect the current status: + + + ```diff-rust + ... + - type ContractAddr = Addr; + - type MinterAddr = Addr; + + - fn instantiate_nameservice(mock_app: &mut App) -> (u64, ContractAddr, MinterAddr) { + + fn instantiate_nameservice(mock_app: &mut App) -> (u64, Addr) { + ... + - let minter = mock_app + - .api() + - .addr_humanize(&CanonicalAddr::from("minter".as_bytes())) + - .unwrap(); + return ( + nameservice_code_id, + mock_app + .instantiate_contract( + ... + &InstantiateMsg { + + name: "my names".to_owned(), + + symbol: "MYN".to_owned(), + + creator: None, + - minter: minter.to_string(), + + minter: Some("minter".to_owned()), + + collection_info_extension: None, + + withdraw_address: None, + }, + ... + ) + .expect("Failed to instantiate nameservice"), + - minter + ); + } + ``` + + +Note that: + +* You no longer need to disambiguate the 2 returned `Addr`. +* As in the unit tests, the `InstantiateMsg` is longer but full of dummy data or `None`s. + +### Execute + +The difficulty here is to get access to the right values in storage when accessing it directly. + + + ```diff-rust + - use cosmwasm_std::{Addr, Api, CanonicalAddr, Event}; + + use cosmwasm_std::{Addr, Event, StdError, Storage}; + + use cw721::msg::OwnerOfResponse; + use cw_multi_test::{App, ContractWrapper, Executor}; + use cw_my_nameservice::{ + contract::{execute, instantiate, query}, + - msg::{ExecuteMsg, InstantiateMsg, QueryMsg, ResolveRecordResponse}, + + msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + }; + + ... + + fn test_register() { + ... + - let (_, contract_addr, minter) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + ... + - let register_msg = ExecuteMsg::Register { + + let register_msg = ExecuteMsg::Mint { + - name: name_alice.to_owned(), + + token_id: name_alice.to_owned(), + - owner: owner_addr.to_owned(), + + owner: owner_addr.to_string(), + + extension: None, + + token_uri: None, + }; + ... + let result = mock_app.execute_contract( + - minter, + + Addr::unchecked("minter"), + ... + ); + ... + - let expected_event = Event::new("wasm-name-register") + + let expected_event = Event::new("wasm") + + .add_attribute("_contract_address", "contract0".to_owned()) + + .add_attribute("action", "mint".to_owned()) + + .add_attribute("minter", "minter".to_owned()) + - .add_attribute("name", name_alice.to_owned()) + - .add_attribute("owner", owner_addr_value.to_owned()); + + .add_attribute("owner", owner_addr_value.to_owned()) + + .add_attribute("token_id", name_alice.to_owned()); + ... + + // Global storage + + let expected_key_main = + + format!("\0\u{4}wasm\0\u{17}contract_data/contract0\0\u{6}tokens{name_alice}",); + + let stored_addr_bytes = mock_app + + .storage() + + .get(expected_key_main.as_bytes()) + + .expect("Failed to load from name alice"); + + let stored_addr = String::from_utf8(stored_addr_bytes).unwrap(); + + assert_eq!( + + stored_addr, + + format!( + + r#"{{"owner":"{owner_addr_value}","approvals":[],"token_uri":null,"extension":null}}"# + + ) + + ); + + // Storage local to contract + let stored_addr_bytes = mock_app + .contract_storage(&contract_addr) + - .get(format!("\0\rname_resolver{name_alice}").as_bytes()) + + .get(format!("\0\u{6}tokens{name_alice}").as_bytes()) + .expect("Failed to load from name alice"); + ... + assert_eq!( + stored_addr, + - format!(r#"{{"owner":"{owner_addr_value}"}}"#) + + format!( + + r#"{{"owner":"{owner_addr_value}","approvals":[],"token_uri":null,"extension":null}}"# + + ) + ); + } + ``` + + +Note how: + +* The attributes are now piled int the `wasm` event, which is always there as it is added by the CosmWasm module, or the mocked app as seen here. +* You confirm that you can access the NFT info with a global storage key. +* You also check that you can access the same info from a key relative to the smart contract's storage area. That assists in visualizing what is taking place within the library. +* These long keys can be explained: + * `"\0\u{4}wasm"` is the prefix of all storage handled by CosmWasm. + * `"\0\u{17}contract_data/"` is the next prefix that CosmWasm reserves to store all smart contract data. + * `"contract0"` is the next prefix that CosmWasm uses for all the storage of your smart contract being tested. `0` is the instance id, this strictly incrementing number. + * `"\0\u{6}tokens"` is the [next prefix](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/state.rs#L73) that the library uses to store `nft_info`, and where `"\0\u{6}"` identifies an [`IndexedMap`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/state.rs#L100). + * `"alice"`, the last element, is the key of the value in the indexed map. + + + + +You can retrieve them all, as long as there are not too many of them, with: + +```rust +println!("{:?}", mock_app.storage()); +``` + +Then, in order to get the logs while testing, you add the `-- --nocapture` flag like so: + + + + ```sh + cargo test -- --nocapture + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo test -- --nocapture + ``` + + + +Which prints something along the lines of: + +```txt +MemoryStorage (7 entries) { + 0x00047761736d0009636f6e747261637473636f6e747261637430: 0x7b22636f64655f6964223a312c2263726561746f72223a226465706c6f796572222c2261646d696e223a6e756c6c2c226c6162656c223a226e616d6573657276696365222c2263726561746564223a31323334357d + 0x00047761736d0017636f6e74726163745f646174612f636f6e7472616374300006746f6b656e73616c696365: 0x7b226f776e6572223a226f776e6572222c22617070726f76616c73223a5b5d2c22746f6b656e5f757269223a6e756c6c2c22657874656e73696f6e223a6e756c6c7d + 0x00047761736d0017636f6e74726163745f646174612f636f6e747261637430000d746f6b656e735f5f6f776e657200056f776e6572616c696365: 0x35 + 0x00047761736d0017636f6e74726163745f646174612f636f6e747261637430636f6c6c656374696f6e5f6d696e746572: 0x7b226f776e6572223a226d696e746572222c2270656e64696e675f6f776e6572223a6e756c6c2c2270656e64696e675f657870697279223a6e756c6c7d + 0x00047761736d0017636f6e74726163745f646174612f636f6e74726163743063773732315f636f6c6c656374696f6e5f696e666f: 0x7b226e616d65223a226d79206e616d6573222c2273796d626f6c223a224d594e222c22757064617465645f6174223a2231353731373937343139383739333035353333227d + 0x00047761736d0017636f6e74726163745f646174612f636f6e7472616374306e756d5f746f6b656e73: 0x31 + 0x00047761736d0017636f6e74726163745f646174612f636f6e7472616374306f776e657273686970: 0x7b226f776e6572223a226465706c6f796572222c2270656e64696e675f6f776e6572223a6e756c6c2c2270656e64696e675f657870697279223a6e756c6c7d +} +``` + +For instance, on the second line: + +* `0x00047761736d0017636f6e74726163745f646174612f636f6e7472616374300006746f6b656e73616c696365` is `"\0\u{4}wasm\0\u{17}contract_data/contract0\0\u{6}tokensalice"`, which you can confirm with converters such as [this one](https://www.duplichecker.com/ascii-to-text.php). +* And the value to its right is `{"owner":"owner","approvals":[],"token_uri":null,"extension":null}`. + + + + +### Query(s) + +To be able to get to the query, you have to retrace the same steps as when testing the execution. Then it is only a matter of changing the `QueryMsg` types. + + + ```diff-rust + + fn test_query() { + ... + - let (_, contract_addr, minter) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + ... + - let register_msg = ExecuteMsg::Register { + + let register_msg = ExecuteMsg::Mint { + - name: name_alice.to_owned(), + + token_id: name_alice.to_owned(), + - owner: owner_addr.to_owned(), + + owner: owner_addr.to_string(), + + extension: None, + + token_uri: None, + }; + ... + let _ = mock_app + .execute_contract( + - minter, + + Addr::unchecked("minter"), + ... + ) + .expect("Failed to register alice"); + - let resolve_record_query_msg = QueryMsg::ResolveRecord { + + let resolve_record_query_msg = QueryMsg::OwnerOf { + - name: name_alice.to_owned(), + + token_id: name_alice.to_owned(), + + include_expired: None, + + }; + ... + let result = mock_app + .wrap() + - .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + + .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + ... + assert_eq!( + result.unwrap(), + - ResolveRecordResponse { + + OwnerOfResponse { + - address: Some(owner_addr.to_string()) + + owner: owner_addr.to_string(), + + approvals: [].to_vec(), + } + ); + } + + fn test_query_empty() { + ... + - let (_, contract_addr, _) = instantiate_nameservice(&mut mock_app); + + let (_, contract_addr) = instantiate_nameservice(&mut mock_app); + let name_alice = "alice".to_owned(); + - let resolve_record_query_msg = QueryMsg::ResolveRecord { + + let resolve_record_query_msg = QueryMsg::OwnerOf { + - name: name_alice.to_owned(), + + token_id: name_alice.to_owned(), + + include_expired: None, + }; + ... + let result = mock_app + .wrap() + - .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + + .query_wasm_smart::(&contract_addr, &resolve_record_query_msg); + ... + - assert!(result.is_ok(), "Failed to query alice name"); + + assert!(result.is_err(), "There was an unexpected value"); + - assert_eq!(result.unwrap(), ResolveRecordResponse { address: None }) + + assert_eq!(result.unwrap_err(), StdError::GenericErr { + + msg: "Querier contract error: type: cw721::state::NftInfo>; key: [00, 06, 74, 6F, 6B, 65, 6E, 73, 61, 6C, 69, 63, 65] not found".to_owned(), + + }); + } + ``` + + +Note that: + +* The new response type is [`OwnerOfResponse`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/msg.rs#L155-L157). +* If a record is missing it returns an error instead of a `None` as you did earlier. +* Said error is quite verbose and cannot really be guessed. Its list of bytes represents `"\0\u{6}tokensalice"`. + +## Conclusion + +Now that you delegate to the NFT library, it could be worhwhile to test that the other features you expect are present, such as approvals and transfers. It would be good to also confirm that the minter indeed gatekeeps the minting call. This is left as an exercise. + + + +At this stage, you should have something similar to the [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/add-first-library..add-nft-library) as the diff. + + + +You have added the NFT library to increase your compatibility with other smart contracts. In the next section, you learn how to have cross-contract communication. diff --git a/docs/tutorial/platform/12-cross-contract.md b/docs/tutorial/platform/12-cross-contract.md new file mode 100644 index 0000000..88fb3f5 --- /dev/null +++ b/docs/tutorial/platform/12-cross-contract.md @@ -0,0 +1,618 @@ +--- +title: First Contract Integration +description: Send a message from one smart contract to another. +--- + +# First Contract Integration + +You have created a name-registering smart contract that is compatible with the NFT standard. This is a good time to have it receive messages from another smart contract. + + + +If you skipped the previous section, you can just switch the `my-nameservice` project to its [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch and take it from there. + + + +## The use-case + +A typical use-case is when an NFT collection delegates the minter to another smart contract. This _minter_ smart contract could for instance implement an auction, at the end of which, the auction winner can instruct the minter contract to mint the name auctioned. + +In this section, to start with cross-contract communication, you create a manager smart contract that only passes NFT commands through. Because NFTs are grouped into collections, you call it the collection manager. + +## The collection manager project + +In another folder, preferrably alongisde (not inside) your `my-nameservice` folder, you create another Rust project: + + + + ```sh + cargo new my-collection-manager --lib --edition 2021 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo new my-collection-manager --lib --edition 2021 + ``` + + + +Move into the project directory. + +```sh +cd my-collection-manager +``` + +This project is a CosmWasm one and a smart contract that needs to understand the NFT standard so: + + + + ```sh + cargo add cosmwasm-schema@1.5.8 cosmwasm-std@1.5.8 thiserror@1.0.63 + cargo add cw721 --git https://github.com/public-awesome/cw-nfts --tag "v0.19.0" + cargo add --dev cw-multi-test@1.2.0 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cosmwasm-schema@1.5.8 cosmwasm-std@1.5.8 thiserror@1.0.63 + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw721 --git https://github.com/public-awesome/cw-nfts --tag "v0.19.0" + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add --dev cw-multi-test@1.2.0 + ``` + + + +Add the WebAssembly alias: + +```sh +mkdir .cargo +touch .cargo/config.toml +``` + +And in it, put: + + +```toml +[alias] +wasm = "build --release --target wasm32-unknown-unknown" +``` + + +Plus the flags for CosmWasm: + + + ```diff + [package] + name = "my-collection-manager" + version = "0.1.0" + edition = "2021" + + + # Linkage options. More information: https://doc.rust-lang.org/reference/linkage.html + + [lib] + + crate-type = ["cdylib", "rlib"] + + + [features] + + # Use library feature to disable all instantiate/execute/query exports + + library = [] + + + # Optimizations in release builds. More information: https://doc.rust-lang.org/cargo/reference/profiles.html + + [profile.release] + + opt-level = "z" + + debug = false + + rpath = false + + lto = true + + debug-assertions = false + + codegen-units = 1 + + panic = 'abort' + + incremental = false + + overflow-checks = true + + [dependencies] + ... + ``` + + +Note how: + +* This is just a condensed repeat of what was done to prepare `my-nameservice`. +* It does not have `my-nameservice` as a dependency, on `cw721` as it is meant to remain versatile. + +## The messages + +With a view to make this smart contract versatile, you do not store on-chain the NFT collection's address, but instead pass it as part of calls. + +So your instantiate message does not contain anything: + + + ```rust + use cosmwasm_schema::cw_serde; + use cosmwasm_std::Empty; + use cw721::msg::Cw721ExecuteMsg; + + #[cw_serde] + pub struct InstantiateMsg {} + ``` + + +As a pass-through smart contract, for now, there should at least be an execute message variant that contains: + +* The target collection's address. +* The message to pass through to it. + +In fact, it can apply to any NFT message. So you add: + + + ```rust + #[cw_serde] + pub enum ExecuteMsg { + PassThrough { + collection: String, + message: Cw721ExecuteMsg, Option, Empty>, + }, + } + ``` + + +Note that: + +* It uses the empty extension, which limits its full usability. +* At this stage, there is no point in declaring any query messages. + +## The errors + +As you did before with `my-nameservice`, you add your simple error messages: + + + ```rust + use cosmwasm_std::StdError; + use thiserror::Error; + + #[derive(Error, Debug)] + pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + } + ``` + + +## The contract + +With these minimal declarations, you have nothing to prepare in `instantiate`, and you only have to forward the message when finding a `PassThrough`: + + + ```rust + use crate::{ + error::ContractError, + msg::{CollectionExecuteMsg, ExecuteMsg, InstantiateMsg}, + }; + #[cfg(not(feature = "library"))] + use cosmwasm_std::entry_point; + use cosmwasm_std::{to_json_binary, DepsMut, Env, MessageInfo, Response, WasmMsg}; + + type ContractResult = Result; + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn instantiate(_: DepsMut, _: Env, _: MessageInfo, _: InstantiateMsg) -> ContractResult { + Ok(Response::default()) + } + + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn execute(deps: DepsMut, env: Env, info: MessageInfo, msg: ExecuteMsg) -> ContractResult { + match msg { + ExecuteMsg::PassThrough { + collection, + message, + } => execute_pass_through(deps, env, info, collection, message), + } + } + + fn execute_pass_through( + _: DepsMut, + _: Env, + info: MessageInfo, + collection: String, + message: CollectionExecuteMsg, + ) -> ContractResult { + let onward_exec_msg = WasmMsg::Execute { + contract_addr: collection, + msg: to_json_binary(&message)?, + funds: info.funds, + }; + Ok(Response::default().add_message(onward_exec_msg)) + } + ``` + + +Note how: + +* To pass the message onwards to the collection you compose a `WasmMsg::Execute` that mentions: + * The target address, in this case the smart contract that represents the NFT collection. + * The message to be received, which here has to be a `Cw721ExecuteMsg` of the correct type. + * A list of the funds that you want the CosmWasm module to: + * Take from the balance of the sender, here the collection manager. + * Give them to the message target, here the NFT collection contract itself. + * Inform the recipient about the funds in the `MessageInfo.funds`. +* Then you add this Wasm message to the response. +* At this stage, your collection manager does not deal with its own funds, so, to avoid leaving funds stranded on its balance, it just forwards them on to the NFT collection. +* This type of message passing is of the _fire and forget_ type. In effect, the CosmWasm module will enforce the transaction's atomicity: you do not have to handle the error cases coming from the NFT collection. Any error coming from the collection will: + * Revert the actions of the manager, which includes the passing-on of funds, which will be returned to the original message sender. + * Revert any state changes the function may have done. + + + +Take note of how the onward message is sent as part of the returned response. This ensures that your function has completed its own actions before the next actions are considered. + +Of course it is on you to make sure that the function completes all necessary actions before the (implicit) `return` statement. + + + + + +It is as true with CosmWasm as it is [with Ethereum](https://trustchain.medium.com/ethereum-msg-value-reuse-vulnerability-5afd0aa2bcef): beware blindly reusing `info.funds`. Indeed you may send more funds than you have received, or run into analogous situations. This would result in, for instance, a possible theft of other people's escrows. + +Consider this contrived example: + +```rust +let onward_exec_msg = WasmMsg::Execute { + contract_addr: collection.to_owned(), + msg: to_json_binary(&message)?, + funds: info.funds.to_owned(), // <-- First time +}; +let onward_exec_msg2 = WasmMsg::Execute { + contract_addr: collection, + msg: to_json_binary(&message)?, + funds: info.funds, // <-- Second time +}; +Ok(Response::default() + .add_message(onward_exec_msg) + .add_message(onward_exec_msg2)) +``` + +Both your messages instruct the CosmWasm module to forward the funds received. But the smart contract received said funds only once. So the second time it sends funds, it will have to pick them from its pre-existing balance, i.e. the balance it had before the message was received. + +The pre-existing balance can be _other people's escrows_, who store value in this smart contract. + +In this contrived example it is somewhat evident, but be mindful that it could be more hidden, as in this pseudo-code: + +```rust +ExecuteMsg::PassThrough { + collection, + message, +} => { + execute_pass_through(deps, env, info, collection, message); + execute_pass_through(deps, env, info, collection, message); +}, +``` + +In this second example, it is not immediately visible that funds are sent twice. + + + +## The library project + +Don't forget to put the Rust modules together into `lib.rs`: + + + ```rust + pub mod contract; + mod error; + pub mod msg; + ``` + + +## Unit test + +At this stage, there is not much to test. The only thing to test is that the response is as expected: + + + ```rust + #[cfg(test)] + mod tests { + use crate::msg::{CollectionExecuteMsg, ExecuteMsg}; + use cosmwasm_std::{testing, to_json_binary, Addr, Coin, Response, Uint128, WasmMsg}; + + #[test] + fn test_pass_through() { + // Arrange + let mut mocked_deps_mut = testing::mock_dependencies(); + let mocked_env = testing::mock_env(); + let executer = Addr::unchecked("executer"); + let fund_sent = Coin { + denom: "gold".to_owned(), + amount: Uint128::from(335u128), + }; + let mocked_msg_info = testing::mock_info(executer.as_ref(), &[fund_sent.to_owned()]); + let name = "alice".to_owned(); + let owner = Addr::unchecked("owner"); + let inner_msg = CollectionExecuteMsg::Mint { + token_id: name.to_owned(), + owner: owner.to_string(), + token_uri: None, + extension: None, + }; + let execute_msg = ExecuteMsg::PassThrough { + collection: "collection".to_owned(), + message: inner_msg.to_owned(), + }; + + // Act + let contract_result = super::execute( + mocked_deps_mut.as_mut(), + mocked_env, + mocked_msg_info, + execute_msg, + ); + + // Assert + assert!(contract_result.is_ok(), "Failed to pass message through"); + let received_response = contract_result.unwrap(); + let expected_response = Response::default().add_message(WasmMsg::Execute { + contract_addr: "collection".to_owned(), + msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"), + funds: vec![fund_sent], + }); + assert_eq!(received_response, expected_response); + } + } + ``` + + +Note that: + +* There is no need to deploy an NFT collection. Not even this manager smart contract. +* You pass _pretend_ funds to the message info. And because the smart contract does not check its balance, no extra mocking is necessary. + +## Mocked app tests + +Now it becomes more interesting. This is where you test the interaction between two smart contracts, within a mocked CosmWasm module. + +You are about to instantiate two smart contracts: + +* An NFT collection using `my-nameservice`. +* Your collection manager. + +### Considerations + +You ought to follow a certain sequence of actions. The NFT collection will be instantiated with the minter's address, this will save you an extra use of [`Cw721ExecuteMsg::UpdateMinterOwnership`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/msg.rs#L37). So you will instantiate the manager first in order to use its address as the minter, so that it can pass through `Mint` calls. + + + +It is a good time to remind you that in the `MessageInfo`, the `sender` may be the smart contract that sent this message. In our case, when passing through, the `sender` will always be the collection manager. + + + +You do not yet have a dependency on your `my-nameservice` project. You want it only for tests, that means you add it as a _dev_ dependency, taking care to adjust if your paths are different: + + + + ```sh + cargo add my-nameservice --dev --path ../my-nameservice --rename my-nameservice + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd)/..:/root/ -w /root/my-collection-manager \ + rust:1.80.1 \ + cargo add my-nameservice --dev --path ../my-nameservice --rename my-nameservice + ``` + + + +These tests are another way to confirm that your name service conforms to the expectations of the NFTs library. To confirm it does, you will: + +* Deploy the collection smart contract from the code of `my-nameservice`, and its own `InstantiateMsg`, since we can expect the owner of the collection to proceed like this. +* Build the _execute_ amd _query_ messages only from the NFT library's own execute messages, as this would be what a user of the collection manager would do. + +Create the `tests/contract.rs` file. + +### Helpers + +As when preparing mocked app tests earlier, you can have helpers to deploy your smart contracts. The collection, using the name service code: + + + ```rust + use cosmwasm_std::{to_json_binary, Addr, Empty, Event}; + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg, OwnerOfResponse}; + use cw_multi_test::{App, ContractWrapper, Executor}; + use cw_my_collection_manager::{ + contract::{execute, instantiate}, + msg::{ExecuteMsg, InstantiateMsg}, + }; + use cw_my_nameservice::{ + contract::{ + execute as execute_my_nameservice, instantiate as instantiate_my_nameservice, + query as query_my_nameservice, + }, + msg::InstantiateMsg as MyNameserviceInstantiateMsg, + }; + + pub type CollectionExecuteMsg = Cw721ExecuteMsg, Option, Empty>; + pub type CollectionQueryMsg = Cw721QueryMsg, Option, Empty>; + + fn instantiate_nameservice(mock_app: &mut App, minter: String) -> (u64, Addr) { + let nameservice_code = Box::new(ContractWrapper::new( + execute_my_nameservice, + instantiate_my_nameservice, + query_my_nameservice, + )); + let nameservice_code_id = mock_app.store_code(nameservice_code); + ( + nameservice_code_id, + mock_app + .instantiate_contract( + nameservice_code_id, + Addr::unchecked("deployer-my-nameservice"), + &MyNameserviceInstantiateMsg { + name: "my names".to_owned(), + symbol: "MYN".to_owned(), + creator: None, + minter: Some(minter), + collection_info_extension: None, + withdraw_address: None, + }, + &[], + "nameservice", + None, + ) + .expect("Failed to instantiate my nameservice"), + ) + } + ``` + + +Note that: + +* It contains all the imports for the upcoming test functions too. +* Imports coming from `my-nameservice` are aliased to avoid confusion. + +Also add a helper to instantiate your collection manager: + + + ```rust + fn instantiate_collection_manager(mock_app: &mut App) -> (u64, Addr) { + let code = Box::new(ContractWrapper::new(execute, instantiate, |_, _, _: ()| { + to_json_binary("mocked_manager_query") + })); + let manager_code_id = mock_app.store_code(code); + + ( + manager_code_id, + mock_app + .instantiate_contract( + manager_code_id, + Addr::unchecked("deployer-manager"), + &InstantiateMsg {}, + &[], + "my-collection-manager", + None, + ) + .expect("Failed to instantiate collection manager"), + ) + } + ``` + + +Note that: + +* There is a dummy lambda in place of the missing query function. + +### Test the pass-through + +In this test, you want to confirm that the collection correctly minted the name that the collection manager forwarded to it: + + + ```rust + #[test] + fn test_mint_through() { + // Arrange + let mut mock_app = App::default(); + let (_, addr_manager) = instantiate_collection_manager(&mut mock_app); + let (_, addr_collection) = instantiate_nameservice(&mut mock_app, addr_manager.to_string()); + let owner_addr = Addr::unchecked("owner"); + let name_alice = "alice".to_owned(); + let sender_addr = Addr::unchecked("sender"); + let register_msg = ExecuteMsg::PassThrough { + collection: addr_collection.to_string(), + message: CollectionExecuteMsg::Mint { + token_id: name_alice.clone(), + owner: owner_addr.to_string(), + token_uri: None, + extension: None, + }, + }; + + // Act + let result = mock_app.execute_contract( + sender_addr.clone(), + addr_manager.clone(), + ®ister_msg, + &[], + ); + + // Assert + assert!(result.is_ok(), "Failed to pass through the message"); + let result = result.unwrap(); + let expected_cw721_event = Event::new("wasm") + .add_attribute("_contract_address", addr_collection.to_string()) + .add_attribute("action", "mint") + .add_attribute("token_id", name_alice.to_string()) + .add_attribute("owner", owner_addr.to_string()); + result.assert_event(&expected_cw721_event); + let owner_query = CollectionQueryMsg::OwnerOf { + token_id: name_alice.to_string(), + include_expired: None, + }; + let result = mock_app + .wrap() + .query_wasm_smart::(addr_collection, &owner_query); + assert!(result.is_ok(), "Failed to query alice name"); + assert_eq!( + result.unwrap(), + OwnerOfResponse { + owner: owner_addr.to_string(), + approvals: vec![], + } + ); + } + ``` + + +Note that: + +* The collection is deployed after the manager and uses its address. +* In mocked app tests, you could in fact guess the addresses of the deployed instances. They are `contract0`, `contract1` and so forth purely depending on the order of deployment. +* After the collection is deployed, there is no use of anything imported from `my-nameservice`. +* You query the collection independently of the collection manager. + +### Run the tests + +To run the tests, it is the same command as before, with the caveat that the `my-nameservice` folder has to be accessible. From within `my-collection-manager`, you run: + + + + ```sh + cargo test + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd)/..:/root/ -w /root/my-collection-manager \ + rust:1.80.1 \ + cargo test + ``` + + + +## Conclusion + +You have built a smart contract that sends messages to another in order to execute an action on the remote one. + +You could test more things such as: + +* Confirm that a single message with two `PassThrough` messages for two different collections works as expected. +* When the manager contract is made the owner of a name, it is able to transfer it to another address with a different pass-through message. +* Confirm that an invalid message, such a non-owner trying to transfer a name, results in an error. + +These are left as an exercise. + + + +At this stage: + +* The `my-nameservice` project should have something similar to the [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch. +* The `my-collection-manager` project should have something similar to the [`initial-pass-through`](https://github.com/b9lab/cw-my-collection-manager/tree/initial-pass-through) branch. + + diff --git a/docs/tutorial/platform/13-cross-query.md b/docs/tutorial/platform/13-cross-query.md new file mode 100644 index 0000000..7ead248 --- /dev/null +++ b/docs/tutorial/platform/13-cross-query.md @@ -0,0 +1,351 @@ +--- +title: First Contract Query Integration +description: Send a synchronous and read-only query from one smart contract to another. +--- + +# First Contract Query Integration + +In the previous section you had your _collection manager_ smart contract send a message to your _name service_ smart contract so that the latter executes something. The _manager_ sends this message at the end of its own execution. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch. +* The `my-collection-manager` project to its [`initial-pass-through`](https://github.com/b9lab/cw-my-collection-manager/tree/initial-pass-through) branch. + +And take it from there. + + + +What if your collection manager wants to query a value from the collection in order to make a decision? It would need to make a synchronous query. This is possible. For that, your collection manager assembles a query message and sends it synchronously, with the certainty that it all happens in a read-only way. + +## The use-case + +The NFT library keeps track of how many tokens it has minted. It is possible to query this information with the use of [`QueryMsg::NumTokens`](https://github.com/public-awesome/cw-nfts/blob/v0.19.0/packages/cw721/src/msg.rs#L191-L193). To demonstrate its use, your collection manager will emit an event about the current number of tokens before passing the message through. + +## Update `execute` + +It is a matter of preparing the `NumTokens` query and interpreting the result: + + + ```diff-rust + use crate::{ + ... + - msg::{CollectionExecuteMsg, ExecuteMsg, InstantiateMsg}, + + msg::{CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg}, + } + ... + use cosmwasm_std::{ + - to_json_binary, DepsMut, Env, MessageInfo, Response, WasmMsg, + + to_json_binary, DepsMut, Env, Event, MessageInfo, QueryRequest, Response, WasmMsg, WasmQuery, + }; + + use cw721::msg::NumTokensResponse; + ... + + fn execute_pass_through( + - _: DepsMut, + + deps: DepsMut, + ... + ) -> ContractResult { + ... + + let token_count_result = + + deps.querier + + .query::(&QueryRequest::Wasm(WasmQuery::Smart { + + contract_addr: collection, + + msg: to_json_binary(&CollectionQueryMsg::NumTokens {})?, + + })); + + let token_count_event = Event::new("my-collection-manager") + + .add_attribute("token-count-before", token_count_result?.count.to_string()); + Ok(Response::default() + .add_message(onward_exec_msg) + + .add_event(token_count_event)) + ) + ... + } + ``` + + +Note how: + +* The `querier` gives read-only access. +* The preparing and sending very much looks like what you did with mocked-app tests of the query function. +* The event is `token-count-before` to make it clear that it takes place before any action, which may be a minting. Given the design of CosmWasm it is not possible to query after the message from within the `execute` function. + +## Unit tests + +Updating the unit test is arduous. Indeed, your default mocked dependencies created with `testing::mock_dependencies()` do not mock any values on your dummy collection. You have to prepare the mocks yourself. + +### Add a mock querier + +The following is inspired form the [work of Stargaze](https://github.com/public-awesome/names/blob/v2.2.0/contracts/sg721-name/src/unit_tests.rs#L22-L67). Add your own `MockQuerier` which returns predefined responses: + + + ```diff-rust + ... + mod tests { + - use crate::msg::{CollectionExecuteMsg, ExecuteMsg}; + + use crate::msg::{CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg}; + use cosmwasm_std::{ + + from_json, + - testing, + + testing::{self, MockApi, MockQuerier, MockStorage}, + - to_json_binary, Addr, Coin, Response, Uint128, WasmMsg, + + to_json_binary, Addr, Coin, ContractResult, Empty, Event, OwnedDeps, Querier, + + QuerierResult, QueryRequest, Response, SystemError, SystemResult, Uint128, WasmMsg, + + WasmQuery, + }; + + use cw721::msg::NumTokensResponse; + + use std::marker::PhantomData; + + + pub fn mock_deps( + + response: NumTokensResponse, + + ) -> OwnedDeps { + + OwnedDeps { + + storage: MockStorage::default(), + + api: MockApi::default(), + + querier: NumTokensMockQuerier::new(MockQuerier::new(&[]), response), + + custom_query_type: PhantomData, + + } + + } + + + + pub struct NumTokensMockQuerier { + + base: MockQuerier, + + response: NumTokensResponse, + + } + + + + impl Querier for NumTokensMockQuerier { + + fn raw_query(&self, bin_request: &[u8]) -> QuerierResult { + + match from_json(bin_request) { + + Ok(request) => self.handle_query(&request), + + Err(e) => SystemResult::Err(SystemError::InvalidRequest { + + error: format!("Parsing query request: {}", e), + + request: bin_request.into(), + + }), + + } + + } + + } + + + + impl NumTokensMockQuerier { + + pub fn handle_query(&self, request: &QueryRequest) -> QuerierResult { + + match request { + + QueryRequest::Wasm(wasm_query) => match wasm_query { + + WasmQuery::Smart { + + contract_addr: _, + + msg, + + } => { + + let serialized = from_json::(msg) + + .map(|collection_query| match collection_query { + + CollectionQueryMsg::NumTokens {} => to_json_binary(&self.response) + + .expect("Failed to serialize num tokens response"), + + _ => unimplemented!("{:?}", collection_query), + + }) + + .expect("Failed to find serialised type"); + + SystemResult::Ok(ContractResult::Ok(serialized)) + + } + + _ => unimplemented!("{:?}", wasm_query), + + }, + + _ => self.base.handle_query(request), + + } + + } + + + + pub fn new(base: MockQuerier, response: NumTokensResponse) -> Self { + + NumTokensMockQuerier { base, response } + + } + + } + ... + } + ``` + + +Note that: + +* You define a brand new `MockQuerier`, one that fits your narrow purpose of returning one predefined response. +* Your `NumTokensMockQuerier` is a simple mock that returns the provided `NumTokensResponse`, without really verifying that it is called only once. +* The mock querier still fails if it receives a query other than `NumTokens`. If you wanted to handle more queries, you would have to adjust it. +* You also have to redefine the `mock_deps` function because its return value is strongly typed, and you need `OwnedDeps`. +* The content of `handle_query` is long but it is mostly about deserializing, picking and serializing. + +### Update execute + +With this, you need to call `mock_deps` with dummy response values `count: 3`, and then update the expected message with the expected event: + + + ```diff-rust + ... + mod tests { + ... + fn test_pass_through() { + // Arrange + - let mut mocked_deps_mut = testing::mock_dependencies(); + + let mut mocked_deps_mut = mock_deps(NumTokensResponse { count: 3 }); + ... + let expected_response = Response::default() + .add_message(WasmMsg::Execute { + contract_addr: "collection".to_owned(), + msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"), + funds: vec![fund_sent], + - }); + + }) + + .add_event( + + Event::new("my-collection-manager").add_attribute("token-count-before", "3"), + + ); + ... + } + } + ``` + + +Note that: + +* The difficult work was creating the mock querier. + +## Mocked app tests + +In these tests, you test the integration between the collection smart contract and the manager, so you do not even need to mock the querier, but instead rely on the mocked app to direct your query as expected. + +### Update the existing test + + + ```diff-rust + ... + fn test_mint_through() { + ... + result.assert_event(&expected_cw721_event); + + let expected_manager_event = + + Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "0"); + + result.assert_event(&expected_manager_event); + let owner_query = CollectionQueryMsg::OwnerOf + ... + } + ``` + + +As expected, there are no tokens when you do your first _mint_. + +### Add a more relevant test + +To make the tests more meaningful, you also add a test for when two mint commands have been sent. + + + ```rust + #[test] + fn test_mint_num_tokens() { + // Arrange + let mut mock_app = App::default(); + let (_, addr_manager) = instantiate_collection_manager(&mut mock_app); + let (_, addr_collection) = instantiate_nameservice(&mut mock_app, addr_manager.to_string()); + let owner_addr = Addr::unchecked("owner"); + let name_alice = "alice".to_owned(); + let name_bob = "bob".to_owned(); + let sender_addr = Addr::unchecked("sender"); + let register_msg = ExecuteMsg::PassThrough { + collection: addr_collection.to_string(), + message: CollectionExecuteMsg::Mint { + token_id: name_alice.clone(), + owner: owner_addr.to_string(), + token_uri: None, + extension: None, + }, + }; + let _ = mock_app + .execute_contract( + sender_addr.clone(), + addr_manager.clone(), + ®ister_msg, + &[], + ) + .expect("Failed to pass through the first mint message"); + let register_msg = ExecuteMsg::PassThrough { + collection: addr_collection.to_string(), + message: CollectionExecuteMsg::Mint { + token_id: name_bob.clone(), + owner: owner_addr.to_string(), + token_uri: None, + extension: None, + }, + }; + + // Act + let result = mock_app.execute_contract( + sender_addr.clone(), + addr_manager.clone(), + ®ister_msg, + &[], + ); + + // Assert + assert!( + result.is_ok(), + "Failed to pass through the second mint message" + ); + let result = result.unwrap(); + let expected_cw721_event = Event::new("wasm") + .add_attribute("_contract_address", addr_collection.to_string()) + .add_attribute("action", "mint") + .add_attribute("token_id", name_bob.to_string()) + .add_attribute("owner", owner_addr.to_string()); + result.assert_event(&expected_cw721_event); + let expected_manager_event = + Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "1"); + result.assert_event(&expected_manager_event); + assert_eq!( + mock_app + .wrap() + .query_wasm_smart::( + addr_collection.to_owned(), + &CollectionQueryMsg::OwnerOf { + token_id: name_alice.to_string(), + include_expired: None, + } + ) + .expect("Failed to query alice name"), + OwnerOfResponse { + owner: owner_addr.to_string(), + approvals: vec![], + } + ); + assert_eq!( + mock_app + .wrap() + .query_wasm_smart::( + addr_collection, + &CollectionQueryMsg::OwnerOf { + token_id: name_bob.to_string(), + include_expired: None, + } + ) + .expect("Failed to query bob name"), + OwnerOfResponse { + owner: owner_addr.to_string(), + approvals: vec![], + } + ); + } + ``` + + +Note how: + +* You mint both the names `alice` and `bob`. +* The event when minting `bob` has a `token-count-before` of `1` because `alice`, and only `alice`, exists. + +## Conclusion + +Your manager smart contract now: + +* Queries another smart contract synchronously in a read-only mode. +* Sends a message to another smart contract asynchronously at the end of its own execution. + + + +At this stage: + +* The `my-nameservice` project should still have something similar to the [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch. +* The `my-collection-manager` project should have something similar to the [`cross-contract-query`](https://github.com/b9lab/cw-my-collection-manager/tree/cross-contract-query) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/initial-pass-through..cross-contract-query) as the diff. + + + +What if the message to the other smart contract needed a reply? For instance, the message could create a new item, whose ID your smart contract needs to know for future reference. + +This is the object of the reply mechanism in the next section. \ No newline at end of file diff --git a/docs/tutorial/platform/14-contract-reply.md b/docs/tutorial/platform/14-contract-reply.md new file mode 100644 index 0000000..2ddffaa --- /dev/null +++ b/docs/tutorial/platform/14-contract-reply.md @@ -0,0 +1,499 @@ +--- +title: First Contract Reply Integration +description: Receive an asynchronous reply from a message sent to another smart contract. +--- + +# First Contract Reply Integration + +In a previous section, your _manager_ smart contract sent an asynchronous message to the _collection_ smart contract, in a fire-and-forget manner. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`add-nft-library`](https://github.com/b9lab/cw-my-nameservice/tree/add-nft-library) branch. +* The `my-collection-manager` project to its [`cross-contract-query`](https://github.com/b9lab/cw-my-collection-manager/tree/cross-contract-query) branch. + +And take it from there. + + + +In fact, it is possible for the _caller_ to receive a response from the _callee_. This is the object of this section. Your _name service_ smart contract is going to return some data after its execution. And your _collection manager_ smart contract is going to receive and emit it. + +## The use-case + +After execution, the collection manager smart contract is going to emit how many tokens exist in the collection after it has executed the latest command. Instead of launching another query to the collection, as it does before returning the message, the manager is going to rely on the collection returning this information. And because the manager has to remain able to work even with collections that don't return this information, you will return default values it some situations. + +It uses the [reply mechanism](https://docs.cosmwasm.com/core/entrypoints/reply) of the actor model. + +## Update `my-nameservice` + +Your name service is a particular implementation of the NFT library. It adds elements but remains compatible with it. Returning some data at the end of the execution preserves compatibility with the NFT library. So go back to the `my-nameservice` project. + +### The returned type + +Add the future returned information in `src/msg.rs`: + + + ```diff-rust + + use cosmwasm_schema::cw_serde; + use cosmwasm_std::Empty; + ... + pub type QueryMsg = Cw721QueryMsg, Option, Empty>; + + + #[cw_serde] + + pub struct ExecuteMsgResponse { + + pub num_tokens: u64, + + } + ``` + + +Note that: + +* You name it neutrally such that it could conceivably be expanded. +* You could have used the library's `NumTokensResponse`, although it could become an issue when expanding with more fields in the future. And its field is named simply `count`, which could be confusing. + +### Return from `execute` + +With the type defined, you can now add a `.data` to the response: + + + ```diff-rust + use crate::{ + error::ContractError, + - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + + msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg}, + } + use cosmwasm_std::{ + - entry_point, Binary, Deps, DepsMut, Env, MessageInfo, Response, + + entry_point, to_json_binary, Binary, Deps, DepsMut, Env, MessageInfo, Response, + }; + ... + pub fn execute( + - deps: DepsMut, + + mut deps: DepsMut, + env: Env, + ... + ) -> ContractResult { + - Ok(Cw721EmptyExtensions::default().execute(deps, &env, &info, msg)?) + + let library = Cw721EmptyExtensions::default(); + + Ok(library + + .execute(deps.branch(), &env, &info, msg) + + .inspect(|response| assert_eq!(response.data, None))? + + .set_data(to_json_binary(&ExecuteMsgResponse { + + num_tokens: library.query_num_tokens(deps.storage)?.count, + + })?)) + } + ``` + + +Note how: + +* You make the `deps` mutable. That's to be able to compile, because of the quirks of Rust. Open to a more elegant way. +* With `assert_eq!(response.data, None)`, you make sure that the NFT library returned no data. That's an insurance policy against overwriting if and when the library changes in the future. +* You do a direct access to `.query_num_tokens`, which returns the convenient `NumTokensResponse`. +* You serialize the data to binary, as seen many times. + +### Adjust the unit test + +With a minor change to the returned object, you only have to adjust your expected response when testing `execute`: + + + ```diff-rust + mod tests { + - use crate::msg::{ExecuteMsg, InstantiateMsg, QueryMsg}; + + use crate::msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg}; + - use cosmwasm_std::{testing, Addr, Binary, Response}; + + use cosmwasm_std::{testing, to_json_binary, Addr, Binary, Response}; + ... + fn test_execute() { + ... + let expected_response = Response::default() + + .set_data( + + to_json_binary(&ExecuteMsgResponse { num_tokens: 1 }) + + .expect("Failed to serialize counter"), + + ) + .add_attribute("action", "mint") + ... + } + } + ``` + + +Note that: + +* After minting once on an empty library, you end up with a single token. + +### Adjust the mocked-app test + +On this test too, you just confirm that the data returned is as expected: + + + ```diff-rust + - use cosmwasm_std::{Addr, Event, StdError, Storage}; + + use cosmwasm_std::{to_json_binary, Addr, Event, StdError, Storage}; + ... + use cw_my_nameservice::{ + contract::{execute, instantiate, query}, + - msg::{ExecuteMsg, InstantiateMsg, QueryMsg}, + + msg::{ExecuteMsg, ExecuteMsgResponse, InstantiateMsg, QueryMsg}, + } + ... + fn test_register() { + ... + assert_eq!( + received_response.data, + - None, + + Some(to_json_binary(&ExecuteMsgResponse { num_tokens: 1 }) + + .expect("Failed to serialize counter")), + ); + ... + } + ``` + + +### Intermediate conclusion on `my-name-service` + +This completes this section's changes on `my-nameservice`. + + + +At this stage the `my-nameservice` project should have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/add-nft-library..execute-return-data) as the diff. + + + +## Using it in the collection manager + +Your update of my name service was rather straightforward. Back in `my-collection-manager`, you now need to make use of it as part of the reply mechanism. You are going to: + +* Define a new data carrying type. +* Define a set of return codes for branching. +* Instruct the system that you expect a reply. +* Add the `reply` entry point. +* Adjust and add to your tests. + +### The expected type + +Similarly to the type you defined in `my-nameservice`, you define it here a second time. By re-defining it, you avoid having to import `my-nameservice`, which would be overkill. + + + ```rust + #[cw_serde] + pub struct NameServiceExecuteMsgResponse { + pub num_tokens: u64, + } + ``` + + +Note how: + +* The name is pointedly specific as it is indeed copied from `my-nameservice`. + +### Define the reply codes + +Because all replies come through the same `reply` entry point, there needs to be a mechanism to distinguish between call types. The library does this by way of an `id: u64`. You provide this `id` when sending a message that expects a reply, and the CosmWasm module will ensure that the same `id` is part of the reply object. + +It is in your interest to clearly identify what each value mean. One way to achieve it is with constants. Another, more elegant, way is with an enum that maps to a number: + + + ```diff-rust + use crate::{ + error::ContractError, + msg::{ + - CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, + + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, NameServiceExecuteMsgResponse, + }, + } + ... + use cosmwasm_std::{ + - to_json_binary, DepsMut, Env, Event, MessageInfo, QueryRequest, Response, WasmMsg, WasmQuery, + + from_json, to_json_binary, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, QueryRequest, + + Reply, ReplyOn, Response, StdError, SubMsg, WasmMsg, WasmQuery, + } + ... + type ContractResult = Result; + + + enum ReplyCode { + + PassThrough = 1, + + } + + + + impl TryFrom for ReplyCode { + + type Error = ContractError; + + + + fn try_from(item: u64) -> Result { + + match item { + + 1 => Ok(ReplyCode::PassThrough), + + _ => panic!("invalid ReplyCode({})", item), + + } + + } + + } + ... + ``` + + +Note that: + +* The name `PassThrough` harks back to its `ExecuteMsg` namesake variant, but it need not be. It just so happens to be pertinent in this case. +* The unsightly unknown case `_ =>` is extracted in the `From` implementation so that, when using `::from`, you can use a succinct `match` on the enum that ensure exhaustiveness at compile time. +* You panic in the unknown case, instead of returning an error, because this case reveals a developer error, not a user error: + * Either you defined a new id value without creating its corresponding entry in the enum. + * Or the CosmWasm module unexpectedly called your smart contract on `reply`. +* Unlike when developing with the Cosmos SDK, a panic in a CosmWasm smart contract does not stop the consensus dead. That would be too easy a vector of attack. Instead, it reverts the transaction. +* The `= 1`, truly is of type `isize`, not `u64`, but at this small scale, there is zero risk of incompatibility. + +### Adjust `execute_pass_through` + +In the current use case, only the pass-through command expects a reply, so you make the change in `execute_pass_through`. Earlier you added a message to the response. Now you have to wrap this message to make it a sub-message, with additional reply information: + + + ```diff-rust + ... + pub fn execute_pass_through( + ... + ) -> ContractResult { + ... + let onward_exec_msg = WasmMsg::Execute { + ... + }; + + let onward_sub_msg = SubMsg { + + id: ReplyCode::PassThrough as u64, + + msg: CosmosMsg::::Wasm(onward_exec_msg), + + reply_on: ReplyOn::Success, + + gas_limit: None, + + }; + let token_count_result = + ... + Ok(Response::default() + - .add_message(onward_exec_msg) + + .add_submessage(onward_sub_msg) + .add_event(token_count_event)) + } + ... + ``` + + +Note how: + +* You set the `id` in the `SubMsg` to identify the type of reply, as explained earlier. +* The sub-message contains the original message in `.msg`. +* With the use of `CosmosMsg::Wasm`, you can already foresee that a sub-message can be used to send a message to something other than another smart contract. It can be sent to another Cosmos module. +* You are asking for a reply only in case of a `Success`. That's because you want to add information to the transaction in case of success. In case of failure, you do not care, other than that the system rolls back everything as it is designed to do by default. You can learn more about the different reply cases [here](https://docs.cosmwasm.com/core/entrypoints/reply). +* You can define a gas limit. In fact, if your goal is to add a gas limit to the original message, using a sub-message is the way to go, whether you intend on receiving a reply or not. + +### Introduce the reply entry point + +With the request for a reply prepared, you have to create the entry point proper. This is a new entry point, just like `instantiate` and `execute`. As in `execute`, you want to keep it expandable and readable. Therefore you only keep a `match` statement in it: + + + ```rust + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn reply(deps: DepsMut, env: Env, msg: Reply) -> ContractResult { + match ReplyCode::try_from(msg.id)? { + ReplyCode::PassThrough => reply_pass_through(deps, env, msg), + } + } + + fn reply_pass_through(_deps: DepsMut, _env: Env, msg: Reply) -> ContractResult { + let resp = msg.result.into_result().map_err(StdError::generic_err)?; + let data = if let Some(data) = resp.data { + data.0[2..].to_vec() + } else { + return Ok(Response::default()); + }; + let value = if let Ok(value) = from_json::(data) { + value + } else { + return Ok(Response::default()); + }; + let event = Event::new("my-collection-manager") + .add_attribute("token-count-after", value.num_tokens.to_string()); + Ok(Response::default().add_event(event)) + } + ``` + + +Note that: + +* It panics if: + * It cannot identify the message `id`. + * The data contains less than 2 bytes. +* It fails with an error if: + * The `msg.result` has an error indicating that the remote execution failed. In practice this should not happen, because you sent the sub-message with `ReplyOn::Success`. +* On the other hand, it does not fail, but instead returns an `Ok(Response::default())`, if: + * The response's data is empty. This is a valid situation, for instance, when the reply comes from a contract that uses an unmodified NFT library, unlike `my-nameservice`. + * The response data cannot be deserialized to a `NameServiceExecuteMsgResponse`. This is a valid situation whereby the reply comes from a smart contract that sends something but unknown as of now. +* The binary returned is prefixed with two bytes: `0A` and the number of bytes that follow. This explains the `[2..]` operation to get rid of these first 2 bytes. This part could be improved to be more idiomatic to CosmWasm. And perhaps also more performant. +* The `.map_err(StdError::generic_err)` is there to convert a `String` into a `StdErr` so that you can benefit from the automatic conversion on `?`. +* The new event has a `token-count-after` attribute, as opposed to the previously seen `token-count-before`. + +### Adjust the `execute` unit test + +You modified the message returned in `execute`, so you need to adjust the corresponding assertions: + + + ```diff-rust + mod tests { + - use crate::msg::{CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg}; + + use crate::{ + + contract::ReplyCode, + + msg::{ + + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, NameServiceExecuteMsgResponse, + + }, + + }; + use cosmwasm_std::{ + ... + testing::{self, MockApi, MockQuerier, MockStorage}, + - to_json_binary, Addr, Coin, ContractResult, Empty, Event, OwnedDeps, Querier, + - QuerierResult, QueryRequest, Response, SystemError, SystemResult, Uint128, WasmMsg, + - WasmQuery, + + to_json_binary, Addr, Binary, Coin, ContractResult, CosmosMsg, Empty, Event, OwnedDeps, + + Querier, QuerierResult, QueryRequest, Reply, ReplyOn, Response, SubMsg, SubMsgResponse, + + SubMsgResult, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery, + } + ... + fn test_pass_through() { + ... + let expected_response = Response::default() + - .add_message(WasmMsg::Execute { + - contract_addr: "collection".to_owned(), + - msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"), + - funds: vec![fund_sent], + + .add_submessage(SubMsg { + + id: ReplyCode::PassThrough as u64, + + msg: CosmosMsg::::Wasm(WasmMsg::Execute { + + contract_addr: "collection".to_owned(), + + msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"), + + funds: vec![fund_sent], + + }), + + reply_on: ReplyOn::Success, + + gas_limit: None, + }) + ... + } + } + ``` + + +Note that: + +* You reused the expected message but also wrapped it in a sub-message. + +### Unit-test the reply + +With a new entry point, it is worth testing it in isolation. You check that your new reply function returns the expected response. Add a brand-new test: + + + ```rust + #[test] + fn test_reply_pass_through() { + // Arrange + let mut mocked_deps_mut = mock_deps(NumTokensResponse { count: 3 }); + let mocked_env = testing::mock_env(); + let num_tokens = to_json_binary(&NameServiceExecuteMsgResponse { num_tokens: 4 }) + .expect("Failed to serialize counter"); + let mut prefixed_num_tokens = vec![10, 16]; + prefixed_num_tokens.extend_from_slice(num_tokens.as_slice()); + let reply = Reply { + id: ReplyCode::PassThrough as u64, + result: SubMsgResult::Ok(SubMsgResponse { + data: Some(Binary::from(prefixed_num_tokens)), + events: vec![], + }), + }; + + // Act + let contract_result = super::reply(mocked_deps_mut.as_mut(), mocked_env, reply); + + // Assert + assert!(contract_result.is_ok(), "Failed to pass reply through"); + let received_response = contract_result.unwrap(); + let expected_response = Response::default() + .add_event(Event::new("my-collection-manager").add_attribute("token-count-after", "4")); + assert_eq!(received_response, expected_response); + } + ``` + + +Note that: + +* There is some trickery to account for the fact that 2 bytes are removed. The test prepends those 2 bytes. +* When unit testing your reply in isolation, you do not need to have had an `execute` beforehand. + +### Adjust your mocked-app test + +A mocked app test gets you closer to how it would behave with the CosmWasm module. In effect, the mocked app will call `reply` on your smart contract as necessary. That is, if you _compile_ your smart contract with the `reply` function too. + +So the updates are minor, plus you do not need to add a test specifically for the reply function. + + + ```diff-rust + ... + use cw_my_collection_manager::{ + - contract::{execute, instantiate}, + + contract::{execute, instantiate, reply}, + msg::{ExecuteMsg, InstantiateMsg}, + }; + ... + fn instantiate_collection_manager(mock_app: &mut App) -> (u64, Addr) { + let code = Box::new( + ContractWrapper::new(execute, instantiate, |_, _, _: ()| { + to_json_binary("mocked_manager_query") + - }), + + }) + + .with_reply(reply), + ); + ... + } + ... + fn test_mint_through() { + ... + let expected_manager_event = + Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "0"); + result.assert_event(&expected_manager_event); + + let expected_manager_event = + + Event::new("wasm-my-collection-manager").add_attribute("token-count-after", "1"); + + result.assert_event(&expected_manager_event); + ... + } + ... + fn test_mint_num_tokens() { + ... + let expected_manager_event = + Event::new("wasm-my-collection-manager").add_attribute("token-count-before", "1"); + result.assert_event(&expected_manager_event); + + let expected_manager_event = + + Event::new("wasm-my-collection-manager").add_attribute("token-count-after", "2"); + + result.assert_event(&expected_manager_event); + ... + } + ``` + + +Note that: + +* The `token-count-before` event emitted in the `execute` and the `token-count-after` in `reply` are separate and not merged, although they have the same type. This is how the mocked app is implemented in this version. + +## Conclusion + +Your manager smart contract now: + +* Sends a sub-message to another smart contract, with the expectation of a reply. +* Receives and understands the reply, which it uses to emit an event. + +You could add more tests to verify that: + +* It panics when receiving a bad reply `id`. +* I can handle replies with empty data. + +This is left as an exercise. + + + +At this stage: + +* The `my-nameservice` project should have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch, with [this](https://github.com/b9lab/cw-my-nameservice/compare/add-nft-library..execute-return-data) as the diff. +* The `my-collection-manager` project should have something similar to the [`reply-from-execute`](https://github.com/b9lab/cw-my-collection-manager/tree/reply-from-execute) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/cross-contract-query..reply-from-execute) as the diff. + + + +You just saw the use of `CosmosMsg::Wasm`. In the next section, you use other variants of `CosmosMsg` to interact with Cosmos modules. diff --git a/docs/tutorial/platform/15-cross-module.md b/docs/tutorial/platform/15-cross-module.md new file mode 100644 index 0000000..df4ddc2 --- /dev/null +++ b/docs/tutorial/platform/15-cross-module.md @@ -0,0 +1,419 @@ +--- +title: First Cross-Module Integration +description: Send a message to another Cosmos module, break the CosmWasm barrier. +--- + +# First Cross-Module Integration + +In the previous sections, you made your _collection manager_ smart contract sends funds onwards to the _NFT collection_ smart contract. That's convenient from the point of view of the manager, although this kicks the can down to the NFT collection. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project to its [`reply-from-execute`](https://github.com/b9lab/cw-my-collection-manager/tree/reply-from-execute) branch. + +And take it from there. + + + +## The use-case + +You foresee your collection manager smart contract becoming a market place where owners sell their registered names. For this to happen, your smart contract needs to handle funds properly and therefore to be able to send appropriate messages to the app-chain's bank module. + +As a first step, you change your manager contract so that it sends the received funds to a beneficiary address, instead of sending them to the NFT collection. + +## Add a storage library + +The address of the beneficiary of these funds is information that needs to be to be stored in storage and so set at instantiation. Add the `cw-storage-plus` library: + + + + ```sh + cargo add cw-storage-plus@1.2.0 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw-storage-plus@1.2.0 + ``` + + + +## New elements + +The beneficiary is an address that is valid for the whole contract. As you keep an eye on future expansion, it is worth making a small structure. Update `src/msg.rs`: + + + ```diff-rust + use cosmwasm_schema::cw_serde; + - use cosmwasm_std::Empty; + + use cosmwasm_std::{Addr, Empty}; + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg}; + + #[cw_serde] + pub struct InstantiateMsg { + + pub payment_params: PaymentParams, + } + + + #[cw_serde] + + pub struct PaymentParams { + + pub beneficiary: Addr, + + } + + + pub type CollectionExecuteMsg = Cw721ExecuteMsg, Option, Empty>; + ... + ``` + + +And define its storage location. Create a new `src/state.rs` with a single stored item in it: + + + ```rust + use cw_storage_plus::Item; + + use crate::msg::PaymentParams; + + pub const PAYMENT_PARAMS: Item = Item::new("payment_params"); + ``` + + +Then tie it back into the library: + + + ```diff-rust + pub mod contract; + mod error; + pub mod msg; + + mod state; + ``` + + +And of course, update your `instantiate` method to use it: + + + ```diff-rust + use crate::{ + ... + state::PAYMENT_PARAMS, + } + ... + pub fn instantiate( + - _deps_: DepsMut, + + deps: DepsMut, + _: Env, + _: MessageInfo, + msg: InstantiateMsg, + ) -> ContractResult { + + PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?; + Ok(Response::default()) + } + ``` + + +## Forward funds in `execute` + +With the straightforward stuff taken care of, it is time to properly handle fund forwarding in the `execute` method: + + + ```diff-rust + ... + use cosmwasm_std::{ + - from_json, to_json_binary, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, QueryRequest, + - Reply, ReplyOn, Response, StdError, SubMsg, WasmMsg, WasmQuery, + + from_json, to_json_binary, BankMsg, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, + + QueryRequest, Reply, ReplyOn, Response, StdError, SubMsg, WasmMsg, WasmQuery, + } + ... + fn execute_pass_through( + ... + ) -> ContractResult { + + let response = Response::default(); + + let response = if !info.funds.is_empty() { + + let payment_params = PAYMENT_PARAMS.load(deps.storage)?; + + let forward_funds_msg = BankMsg::Send { + + to_address: payment_params.beneficiary.to_string(), + + amount: info.funds, + + }; + + response.add_message(forward_funds_msg) + + } else { + + response + + }; + let onward_exec_msg = WasmMsg::Execute { + contract_addr: collection.to_owned(), + msg: to_json_binary(&message)?, + - funds: info.funds, + + funds: vec![], + }; + ... + - Ok(Response::default() + + Ok(response + .add_submessage(onward_sub_msg) + .add_event(token_count_event)) + } + ``` + + +Note how: + +* The `WasmMsg::Execute` message now no longer forwards any funds to the NFT collection, but otherwise stays the same. +* It only loads the parameters from storage if there are funds to forward. This is to reduce unnecessary gas costs. +* Sending a [`BankMsg`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.8/packages/std/src/results/cosmos_msg.rs#L56) is how you talk to the bank module. +* The `.add_message` takes an [`msg: impl Into>`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.8/packages/std/src/results/response.rs#L114). +* Thankfully, `CosmosMsg`'s [implements `From`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.8/packages/std/src/results/cosmos_msg.rs#L363-L367), so the `BankMsg` is converted into a[`CosmosMsg::Bank(BankMsg)`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.8/packages/std/src/results/cosmos_msg.rs#L28). + +## Update the unit tests + +Now that there needs to be something in storage, it is better to call instantiate as part of the tests, and to update the expectations on the response: + + + ```diff-rust + ... + mod tests { + use crate::{ + ... + msg::{ + - CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, NameServiceExecuteMsgResponse, + + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, + + NameServiceExecuteMsgResponse, msg::PaymentParams, + }, + } + use cosmwasm_std::{ + from_json, + testing::{self, MockApi, MockQuerier, MockStorage}, + - to_json_binary, Addr, Binary, Coin, ContractResult, CosmosMsg, Empty, Event, OwnedDeps, + - Querier, QuerierResult, QueryRequest, Reply, ReplyOn, Response, SubMsg, SubMsgResponse, + - SubMsgResult, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery, + + to_json_binary, Addr, BankMsg, Binary, Coin, ContractResult, CosmosMsg, Empty, Event, + + OwnedDeps, Querier, QuerierResult, QueryRequest, Reply, ReplyOn, Response, SubMsg, + + SubMsgResponse, SubMsgResult, SystemError, SystemResult, Uint128, WasmMsg, WasmQuery, + } + ... + fn test_pass_through() { + ... + let mocked_env = testing::mock_env(); + + let deployer = Addr::unchecked("deployer"); + + let mocked_msg_info = testing::mock_info(deployer.as_ref(), &[]); + + let instantiate_msg = InstantiateMsg { + + payment_params: PaymentParams { beneficiary: deployer.to_owned() }, + + }; + + let _ = super::instantiate( + + mocked_deps_mut.as_mut(), + + mocked_env.to_owned(), + + mocked_msg_info, + + instantiate_msg, + + ) + + .expect("Failed to instantiate manager"); + let executer = Addr::unchecked("executer"); + ... + let expected_response = Response::default() + + .add_message(BankMsg::Send { + + to_address: executer.to_string(), + + amount: vec![fund_sent], + + }) + .add_submessage(SubMsg { + ... + msg: CosmosMsg::::Wasm(WasmMsg::Execute { + ... + - funds: vec![fund_sent], + + funds: vec![], + }), + }) + ... + } + } + ``` + + +Note that: + +* You removed the funds expectation on the `WasmMsg::Execute` message. + +## Update the mocked-app tests + +Your current test with the mocked app does not send coins along with the mint call. And indeed, the mocked addresses you use have no balances of their own to send from. Before you make some serious changes with balances, you can update your test that does not handle coins. Other than modifying the way you instantiate your collection manager, there is no change in the rest of the test. + + + ```diff-rust + - use cosmwasm_std::{to_json_binary, Addr, Empty, Event}; + + use cosmwasm_std::{to_json_binary, Addr, Coin, Empty, Event, Uint128}; + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg, OwnerOfResponse}; + - use cw_multi_test::{App, ContractWrapper, Executor}; + + use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor}; + use cw_my_collection_manager::{ + contract::{execute, instantiate, reply}, + - msg::{ExecuteMsg, InstantiateMsg}, + + msg::{ExecuteMsg, InstantiateMsg, PaymentParams}, + }; + ... + fn instantiate_collection_manager( + mock_app: &mut App, + + payment_params: PaymentParams, + ) -> (u64, Addr) { + ... + return ( + manager_code_id, + mock_app + .instantiate_contract( + manager_code_id, + Addr::unchecked("deployer-manager"), + - &InstantiateMsg {}, + + &InstantiateMsg { payment_params }, + &[], + "my-collection-manager", + None, + ) + .expect("Failed to instantiate collection manager"), + ); + } + ... + fn test_mint_through() { + // Arrange + let mut mock_app = App::default(); + + let beneficiary_addr = Addr::unchecked("beneficiary"); + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + + PaymentParams { + + beneficiary: beneficiary_addr.to_owned(), + + }, + ); + ... + } + ... + fn test_mint_num_tokens() { + // Arrange + let mut mock_app = App::default(); + + let beneficiary_addr = Addr::unchecked("beneficiary"); + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + + PaymentParams { + + beneficiary: beneficiary_addr.to_owned(), + + }, + ); + ... + } + ``` + + +Note that: + +* Only the instantiation changes. +* It includes imports used below here. + +## Add a mocked app test with tokens + +Let's make it interesting and have the sender send tokens along with the pass-through mint transaction. To be able to send tokens with the mocked app, you have to set some balances when mocking. That's where the [`AppBuilder`](https://github.com/CosmWasm/cw-multi-test/blob/v1.2.0/src/app_builder.rs) comes in. + + + ```rust + #[test] + fn test_paid_mint_through() { + // Arrange + let sender_addr = Addr::unchecked("sender"); + let extra_fund_sent = Coin { + denom: "gold".to_owned(), + amount: Uint128::from(335u128), + }; + let mut mock_app = AppBuilder::default().build(|router, _api, storage| { + router + .bank + .init_balance( + storage, + &sender_addr, + vec![extra_fund_sent.to_owned()], + ) + .expect("Failed to init bank balances"); + }); + let beneficiary = Addr::unchecked("beneficiary"); + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + PaymentParams { + beneficiary: beneficiary.to_owned(), + }, + ); + let (_, addr_collection) = instantiate_nameservice(&mut mock_app, addr_manager.to_string()); + let owner_addr = Addr::unchecked("owner"); + let name_alice = "alice".to_owned(); + let register_msg = ExecuteMsg::PassThrough { + collection: addr_collection.to_string(), + message: CollectionExecuteMsg::Mint { + token_id: name_alice.clone(), + owner: owner_addr.to_string(), + token_uri: None, + extension: None, + }, + }; + + // Act + let result = mock_app.execute_contract( + sender_addr.clone(), + addr_manager.clone(), + ®ister_msg, + &[extra_fund_sent.to_owned()], + ); + + // Assert + assert!(result.is_ok(), "Failed to pass through the message"); + let result = result.unwrap(); + let expected_beneficiary_bank_event = Event::new("transfer") + .add_attribute("recipient", "beneficiary") + .add_attribute("sender", "contract0") + .add_attribute("amount", "335gold"); + result.assert_event(&expected_beneficiary_bank_event); + assert_eq!( + Vec::::new(), + mock_app + .wrap() + .query_all_balances(sender_addr) + .expect("Failed to get sender balances") + ); + assert_eq!( + vec![extra_fund_sent], + mock_app + .wrap() + .query_all_balances(beneficiary) + .expect("Failed to get beneficiary balances") + ); + assert_eq!( + Vec::::new(), + mock_app + .wrap() + .query_all_balances(addr_manager) + .expect("Failed to get manager balances") + ); + assert_eq!( + Vec::::new(), + mock_app + .wrap() + .query_all_balances(addr_collection) + .expect("Failed to get collection balances") + ); + } + ``` + + +Note that: + +* It is inside the [`build` function](https://github.com/CosmWasm/cw-multi-test/blob/v1.2.0/src/app_builder.rs#L536) that you access the `storage` element necessary to call up the mocked balances feature. +* The sender is only credicted with `extra_fund_sent` so it has no remaining balance, which is asserted too. +* Only the bank message triggered an event, unlike the funds forwarded by the CosmWasm module when using the `funds` feature. +* Checking the balances of the smart contracts is just here as a belt-and-braces idea because that's akin to verifying the mocked app has correctly implemented the conservation of funds. + +## Conclusion + +You smart contract now sends a message across the CosmWasm module barrier and into a Cosmos module, the bank. It does so by forwarding all funds received. This is a rudimentary way of handling funds. In particular, for a collection manager that plans on eventually being a marketplace. + + + +At this stage: + +* The `my-nameservice` project should still have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project should have something similar to the [`cross-module-message`](https://github.com/b9lab/cw-my-collection-manager/tree/cross-module-message) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/reply-from-execute..cross-module-message) as the diff. + + + +In the next section, you have your collection manager smart contract handle funds more elaborately. diff --git a/docs/tutorial/platform/16-fund-handling.md b/docs/tutorial/platform/16-fund-handling.md new file mode 100644 index 0000000..62e5a36 --- /dev/null +++ b/docs/tutorial/platform/16-fund-handling.md @@ -0,0 +1,585 @@ +--- +title: Proper Funds Handling +description: Expect a payment, and return the change. +--- + +# Proper Funds Handling + +Your _collection manager_ smart contract can forward all funds it receives to a beneficiary. That's a good way to avoid stranding funds on its balance, but this is level 1 of handling payments. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project to its [`cross-module-message`](https://github.com/b9lab/cw-my-collection-manager/tree/cross-module-message) branch. + +And take it from there. + + + +## The use-case + +As a first step towards being a market place, you modify your collection manager contract such that it expects a fixed payment for minting a new name. It will also send the payment, and nothing more, to the beneficiary. All extra funds will be sent back to the sender, in effect, returning the _change_. The contract will keep its balance to zero. To increase compatibility, the smart contract is configured to make the payment optional, in which case it returns all funds to the sender. + +## New elements + +The expected payment is defined as a `Coin`, and since it is optional, you wrap it into an `Option`: + + + ```diff-rust + use cosmwasm_schema::cw_serde; + - use cosmwasm_std::{Addr, Empty}; + + use cosmwasm_std::{Addr, Coin, Empty}; + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg}; + ... + #[cw_serde] + pub struct PaymentParams { + pub beneficiary: Addr, + + pub mint_price: Option, + } + ... + ``` + + +There will be an expected payment, so this introduces a new kind of possible user error: + + + ```diff-rust + - use cosmwasm_std::{StdErr}; + + use cosmwasm_std::{Coin, StdError}; + use thiserror::Error; + ... + pub enum ContractError { + #[error("{0}")] + Std(#[from] StdError), + + #[error("price cannot be zero")] + + ZeroPrice, + + #[error("missing payment {:?}", missing_payment)] + + MissingPayment { missing_payment: Coin }, + } + ... + ``` + + +As evidenced here, you will also reject zero-valued prices, as this should be covered by `None` with less storage space used. You can encapsulate this information back in `src/msg.rs`: + + + ```diff-rust + use cosmwasm_schema::cw_serde; + - use cosmwasm_std::{Addr, Coin, Empty}; + + use cosmwasm_std::{Addr, Coin, Empty, Uint128}; + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg}; + + + + use crate::error::ContractError; + ... + #[cw_serde] + pub struct PaymentParams { + ... + } + + + impl PaymentParams { + + pub fn validate(&self) -> Result<(), ContractError> { + + match &self.mint_price { + + Some(coin) if coin.amount.le(&Uint128::zero()) => Err(ContractError::ZeroPrice), + + None | Some(_) => Ok(()), + + } + + } + + } + ... + ``` + + +## Update `instantiate` + +The `instantiate` function can already handle the modified `PaymentParams`, but it would be nice that it does not allow a price of `0`, as conceptually, this is already covered by the `Option` part: + + + ```diff-rust + ... + use cosmwasm_std::{ + - from_json, to_json_binary, BankMsg, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, + - QueryRequest, Reply, ReplyOn, Response, StdError, SubMsg, WasmMsg, WasmQuery, + + from_json, to_json_binary, BankMsg, Coin, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, + + QueryRequest, Reply, ReplyOn, Response, StdError, SubMsg, Uint128, WasmMsg, WasmQuery, + }; + ... + pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, msg: InstantiateMsg) -> ContractResult { + + msg.payment_params.validate()?; + PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?; + Ok(Response::default()) + } + ... + ``` + + +## Update `execute` + +Now comes the meat of fund handling. When in `execute`, your smart contract receives a [`MessageInfo`](https://github.com/CosmWasm/cosmwasm/blob/v1.5.8/packages/std/src/types.rs#L91-L106) with a `funds: Vec` field that indicates what tokens have been sent as part of the message. The funds have been made available to your smart contract, with an assurance provided by the CosmWasm module. This is an assurance akin to that of `msg.value` in Ethereum's Solidity. + +A small difficulty in our use-case is that CosmWasm populates `funds` as it is instructed by the maker of the message. In particular, if you send a message from the command like so: + +```sh +wasmd tx wasm execute --amount 30silver,30silver ... +``` + +The `funds` field will contain two identical elements `30 silver` `Coin` objects, as it does not do any `Coin` aggregation. If your smart contract expects to be paid `55 silver`, two `Coin` objecs of `30 silver` each ought to be valid payment. So the `execute` function needs to: + +1. Identify a valid payment possibly spread through multiple `Coin` objects. +2. Pay the beneficiary the agreed amount. +3. Calculate the change to return. +4. Return the change and unrelated `Coin`s back to the sender. + +### An aggregating function + +Start by adding in `src/contract.rs` a function that aggregates the coins for a denom of interest: + + + ```rust + fn split_fund_denom(denom: &String, funds: &[Coin]) -> (Uint128, Vec) { + let (amount, others) = funds.iter().fold( + (Uint128::zero(), Vec::with_capacity(funds.len())), + |(aggregated, mut others), fund| { + if &fund.denom == denom { + (aggregated.strict_add(fund.amount), others) + } else { + others.push(fund.clone()); + (aggregated, others) + } + }, + ); + (amount, others) + } + ``` + + +Note that: + +* The goal is to give it the minting price denom. +* Then it returns an aggregated `Coin` for the denom, and collects the other denominated coins in a vector without any aggregation. +* The `fold` function of an iterator takes an initial value, which here is a tuple with: + * The value `0` to aggregate all coins of the given denom. + * An empty coin list to collect coins of other denoms. +* This function does not deal about returning change of the denom, since it does not know the price. +* The `clone()` call takes place on the `fund`, not `funds`. This could make it more gas efficient. + +### Fund handling only for minting + +With the aggregation done, it is possible to create a function that returns the relevant bank messages to add to the response, with the assumption that this is only for when a minting is taking place: + + + ```rust + fn handle_pre_mint_funds( + deps: &DepsMut, + info: &MessageInfo, + ) -> Result, ContractError> { + let payment_params = PAYMENT_PARAMS.load(deps.storage)?; + let (payment, change) = match payment_params.mint_price { + None => (None, info.funds.to_owned()), + Some(minting_price) if minting_price.amount.le(&Uint128::zero()) => { + Err(ContractError::ZeroPrice)? + } + Some(minting_price) => { + let (aggregated, mut others) = split_fund_denom(&minting_price.denom, &info.funds); + match aggregated.checked_sub(minting_price.amount) { + Err(_) => Err(ContractError::MissingPayment { + missing_payment: minting_price.to_owned(), + })?, + Ok(change_in_denom) if change_in_denom.le(&Uint128::zero()) => {} + Ok(change_in_denom) => others.push(Coin { + denom: minting_price.denom.clone(), + amount: change_in_denom, + }), + }; + (Some(minting_price), others) + } + }; + let mut bank_msgs = Vec::::new(); + if let Some(paid) = payment { + bank_msgs.push(BankMsg::Send { + to_address: payment_params.beneficiary.to_string(), + amount: vec![paid], + }); + } + if !change.is_empty() { + bank_msgs.push(BankMsg::Send { + to_address: info.sender.to_string(), + amount: change, + }) + }; + Ok(bank_msgs) + } + ``` + + +Note that: + +* It avoids adding any bank messages where the amount is `0` as this can trigger errors, depending on the implementation of the bank module. +* It also avoids setting an empty list of `Coin`s to the bank message. +* Even if there is no minting price, it has to handle returning all the funds to the sender. +* It also triggers an error in case of a `0` price. This case should not happen if the smart contract was instantiated correctly. This is using a composed `match` branch: + + ```rust + Some(minting_price) if minting_price.amount.le(&Uint128::zero()) => + ``` + +* With the use of `Uint128::checked_sub`, you can elegantly handle the case where not enough was paid and preparing the message. Here too it uses a composed `match` branch; + + ```rust + Ok(change_in_denom) if change_in_denom.le(&Uint128::zero()) => + ``` + +### Update `execute` proper + +With these functions prepared, you can now go back to the main function and: + +* Introduce the case when it is a mint and use the messages that have been prepared. +* Otherwise keep the hard refund. + + + ```diff-rust + ... + fn execute_pass_through( + ... + ) -> ContractResult { + let response = Response::default(); + - let response = if !info.funds.is_empty() { + - let payment_params = PAYMENT_PARAMS.load(deps.storage)?; + - let forward_funds_msg = BankMsg::Send { + - to_address: payment_params.beneficiary.to_string(), + - amount: info.funds, + - }; + - response.add_message(forward_funds_msg) + - } else { + - response + + let response = match message { + + CollectionExecuteMsg::Mint { .. } => match handle_pre_mint_funds(&deps, &info) { + + Err(err) => Err(err)?, + + Ok(bank_msgs) => response.add_messages(bank_msgs), + + }, + + _ => { + + if !info.funds.is_empty() { + + let refund_msg = BankMsg::Send { + + to_address: info.sender.to_string(), + + amount: info.funds, + + }; + + response.add_message(refund_msg) + + } else { + + response + + } + + } + }; + ... + } + ``` + + +Note how: + +* It only calls the new fund handling function in case of a mint message, and returns everything to sender otherwise. + +## Unit tests + +You are going to update the existing unit tests and add one that checks the proper handling of funds on mint. + +### Update unit test + +There is not much to do here. You can decide that minting is free, and test that all funds are returned: + + + ```diff-rust + ... + mod tests { + ... + fn test_pass_through() { + ... + let instantiate_msg = InstantiateMsg { + payment_params: PaymentParams { + beneficiary: deployer.to_owned(), + + mint_price: None, + }, + }; + ... + let expected_response = Response::default() + .add_message(BankMsg::Send { + - to_address: deployer.to_string(), + + to_address: executer.to_string(), + amount: vec![fund_sent], + }) + ... + } + ... + } + ``` + + +Note that it is just confirming that, absent a minting price, the funds are no longer sent to the beneficiary but returned to the sender. + +### Add one with complex funds + +To make things more interesting you create a new unit test where: + +* You set a minting price of `55 silver`. +* Send a mint command with two funds of `30 silver` each, ensuring it is a valid payment that expects some change. +* Also send an unnecessary fund of `335 gold`. + +With this, you expect the beneficiary to receive `55 silver`, and the sender to be returned `5 silver` and `335 gold`. Let's add this brand new test function: + + + ```rust + #[test] + fn test_paid_mint_pass_through() { + // Arrange + let mut mocked_deps_mut = mock_deps(NumTokensResponse { count: 3 }); + let mocked_env = testing::mock_env(); + let beneficiary = Addr::unchecked("beneficiary"); + let deployer = Addr::unchecked("deployer"); + let mocked_msg_info = testing::mock_info(deployer.as_ref(), &[]); + let minting_price = Coin { + amount: Uint128::from(55u16), + denom: "silver".to_owned(), + }; + let instantiate_msg = InstantiateMsg { + payment_params: PaymentParams { + beneficiary: beneficiary.to_owned(), + mint_price: Some(minting_price.to_owned()), + }, + }; + let _ = super::instantiate( + mocked_deps_mut.as_mut(), + mocked_env.to_owned(), + mocked_msg_info, + instantiate_msg, + ) + .expect("Failed to instantiate manager"); + let executer = Addr::unchecked("executer"); + let extra_fund_sent = Coin { + denom: "gold".to_owned(), + amount: Uint128::from(335u128), + }; + let fistful_silver = Coin { + amount: Uint128::from(30u16), + denom: "silver".to_owned(), + }; + let mocked_msg_info = testing::mock_info( + executer.as_ref(), + &[ + extra_fund_sent.to_owned(), + fistful_silver.to_owned(), + fistful_silver, + ], + ); + let name = "alice".to_owned(); + let owner = Addr::unchecked("owner"); + let inner_msg = CollectionExecuteMsg::Mint { + token_id: name.to_owned(), + owner: owner.to_string(), + token_uri: None, + extension: None, + }; + let execute_msg = ExecuteMsg::PassThrough { + collection: "collection".to_owned(), + message: inner_msg.to_owned(), + }; + + // Act + let contract_result = super::execute( + mocked_deps_mut.as_mut(), + mocked_env, + mocked_msg_info.to_owned(), + execute_msg, + ); + + // Assert + assert!(contract_result.is_ok(), "Failed to pass message through"); + let received_response = contract_result.unwrap(); + let expected_denom_change = Coin { + amount: Uint128::from(5u16), + denom: "silver".to_owned(), + }; + let expected_response = Response::default() + .add_message(BankMsg::Send { + to_address: beneficiary.to_string(), + amount: vec![minting_price], + }) + .add_message(BankMsg::Send { + to_address: mocked_msg_info.sender.to_string(), + amount: vec![extra_fund_sent, expected_denom_change], + }) + .add_submessage(SubMsg { + id: ReplyCode::PassThrough as u64, + msg: CosmosMsg::::Wasm(WasmMsg::Execute { + contract_addr: "collection".to_owned(), + msg: to_json_binary(&inner_msg).expect("Failed to serialize inner message"), + funds: vec![], + }), + reply_on: ReplyOn::Success, + gas_limit: None, + }) + .add_event( + Event::new("my-collection-manager").add_attribute("token-count-before", "3"), + ); + assert_eq!(received_response, expected_response); + } + ``` + + +Note that: + +* Most of the space is taken building the funds and asserting them. + +## Mocked app tests + +Here you only need to make some updates. First on the functions that test without a minting price: + + + ```diff-rust + ... + fn test_mint_through() { + ... + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + PaymentParams { + beneficiary: beneficiary_addr.to_owned(), + + mint_price: None, + }, + ); + ... + } + ... + fn test_mint_num_tokens() { + ... + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + PaymentParams { + beneficiary: beneficiary_addr.to_owned(), + + mint_price: None, + }, + ); + ... + } + ``` + + +Not much to show here. + +Then for the more interesting one: + +* You set a minting price of `55 silver`. +* Send a mint command with two funds of `30 silver` each, ensuring it is a valid payment that expects some change. +* Also send an unnecessary fund of `335 gold`. + +With this, you expect the beneficiary to receive `55 silver`, and the sender to be returned `5 silver` and `335 gold`. But for all this to happen, the sender needs to start with `60 silver` and `335 gold` at least at _genesis_. + +Let's adjust: + + + ```diff-rust + ... + fn test_paid_mint_through() { + // Arrange + let sender_addr = Addr::unchecked("sender"); + + let minting_price = Coin { + + amount: Uint128::from(55u16), + + denom: "silver".to_owned(), + + }; + ... + let mut mock_app = AppBuilder::default().build(|router, _api, storage| { + + let original_silver = Coin { + + amount: Uint128::from(60u16), + + denom: "silver".to_owned(), + + }; + router + ... + .init_balance( + ... + - vec![extra_fund_sent.to_owned()], + + vec![extra_fund_sent.to_owned(), original_silver], + ) + ... + }); + ... + let (_, addr_manager) = instantiate_collection_manager( + &mut mock_app, + PaymentParams { + beneficiary: beneficiary.to_owned(), + + mint_price: Some(minting_price.to_owned()), + }, + ); + ... + let register_msg = ExecuteMsg::PassThrough { + ... + }; + + let half_silver = Coin { + + amount: Uint128::from(30u16), + + denom: "silver".to_owned(), + + }; + ... + let result = mock_app.execute_contract( + sender_addr.clone(), + addr_manager.clone(), + ®ister_msg, + &[ + extra_fund_sent.to_owned(), + + half_silver.to_owned(), + + half_silver, + ], + ); + ... + let expected_beneficiary_bank_event = Event::new("transfer") + ... + - .add_attribute("amount", "335gold"); + + .add_attribute("amount", "55silver"); + result.assert_event(&expected_beneficiary_bank_event); + + let expected_sender_bank_event = Event::new("transfer") + + .add_attribute("recipient", "sender") + + .add_attribute("sender", "contract0") + + .add_attribute("amount", "335gold,5silver"); + + result.assert_event(&expected_sender_bank_event); + + let expected_silver_change = Coin { + + amount: Uint128::from(5u16), + + denom: "silver".to_owned(), + + }; + assert_eq!( + - Vec::::new(), + + vec![extra_fund_sent, expected_silver_change], + mock_app + .wrap() + .query_all_balances(sender_addr) + .expect("Failed to get sender balances") + ); + assert_eq!( + - vec![extra_fund_sent], + + vec![minting_price], + mock_app + .wrap() + .query_all_balances(beneficiary) + .expect("Failed to get beneficiary balances") + ); + ... + } + ... + ``` + + +Note that: + +* There was not too much to change as the test was already mostly set up. +* The main point is to correctly keep track of the monies. +* The beneficiary receives only the minting price as evidenced by the event and the balance. +* The bank event's `amount` attribute concatenates the different coins in this manner: `"335gold,5silver"`. + +## Conclusion + +Your smart contract is now able to manipulate funds received in a manner consistent with an expected payment, and to send messages to the bank to make token transfers. + +You could add tests that test the new private functions in isolation, or that funds are returned for non-mint operations. This is left as an exercise. + + + +At this stage: + +* The `my-nameservice` project should still have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project should have something similar to the [`proper-fund-handling`](https://github.com/b9lab/cw-my-collection-manager/tree/proper-fund-handling) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/cross-module-message..proper-fund-handling) as the diff. + + diff --git a/docs/tutorial/platform/17-sudo-msg.md b/docs/tutorial/platform/17-sudo-msg.md new file mode 100644 index 0000000..803cca2 --- /dev/null +++ b/docs/tutorial/platform/17-sudo-msg.md @@ -0,0 +1,396 @@ +--- +title: First Sudo Message +description: The Cosmos way to manage your smart contract. +--- + +# First Sudo Message + +Your _collection manager_ smart contract can now impose a payment when minting a new name. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project to its [`proper-fund-handling`](https://github.com/b9lab/cw-my-collection-manager/tree/proper-fund-handling) branch. + +And take it from there. + + + +What if you want to change the price or the beneficiary? You could add a new `ExecuteMsg` variant, and gateway it with some parameters. Perhaps you may want to have a vote on it. All this sounds a lot like a governance proposal as it can be implemented on a Cosmos app-chain. What if you could create a governance proposal whose execution is deterministically executed by your smart contract? + +That's one of the purposes of **sudo** messages. A sudo message is one that comes from the underlying app-chain. This is not a user-generated message coming with a transaction. + +## The mechanism + +When running `wasmd` or your own app chain you can launch a governance proposal by using the command: + + ```sh + wasmd tx gov submit-proposal sudo-contract --help + ``` + +This creates a [`MsgSudoContract`](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L314-L328) that very much looks like [`WasmSudo`](https://github.com/CosmWasm/cw-multi-test/blob/v1.2.0/src/wasm.rs#L45-L53) that you use in the mocked app tests below. + +When the proposal is voted in, your smart contract gets called with the `bytes msg` part. + +## The use-case + +You implement a sudo message that lets the app-chain change the payment parameters. + +## Add the payment params query + +As it is going to be useful, add a query to get the current payment parameters. In a previous section, you saw in detail how to do that, so here we add them without much explanations: + + + ```diff-rust + - use cosmwasm_schema::cw_serde; + + use cosmwasm_schema::{cw_serde, QueryResponses}; + use cosmwasm_std::{Addr, Coin, Empty, Uint128}; + ... + pub struct NameServiceExecuteMsgResponse { + pub num_tokens: u64, + } + + + + #[cw_serde] + + #[derive(QueryResponses)] + + pub enum QueryMsg { + + #[returns(GetPaymentParamsResponse)] + + GetPaymentParams, + + } + + + + #[cw_serde] + + pub struct GetPaymentParamsResponse { + + pub payment_params: PaymentParams, + + } + ``` + + + + ```diff-rust + use crate::{ + error::ContractError, + msg::{ + - CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, + - NameServiceExecuteMsgResponse, + + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, GetPaymentParamsResponse, + + InstantiateMsg, NameServiceExecuteMsgResponse, QueryMsg, + }, + state::PAYMENT_PARAMS, + }; + ... + use cosmwasm_std::{ + - from_json, to_json_binary, BankMsg, Coin, CosmosMsg, DepsMut, Empty, Env, Event, MessageInfo, + - QueryRequest, Reply, ReplyOn, Response, StdError, SubMsg, Uint128, WasmMsg, WasmQuery, + + from_json, to_json_binary, BankMsg, Coin, CosmosMsg, Deps, DepsMut, Empty, Env, Event, + + MessageInfo, QueryRequest, QueryResponse, Reply, ReplyOn, Response, StdError, SubMsg, Uint128, + + WasmMsg, WasmQuery, + }; + ... + fn reply_pass_through(...) -> ContractResult { + ... + } + + + #[cfg_attr(not(feature = "library"), entry_point)] + + pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + + match msg { + + QueryMsg::GetPaymentParams {} => Ok(to_json_binary(&GetPaymentParamsResponse { + + payment_params: PAYMENT_PARAMS.load(deps.storage)?, + + })?), + + } + + } + ... + ``` + + +Take this opportunity to replace your dummy query lambda, used in the mocked app test, with the real function: + + + ```diff-rust + - use cosmwasm_std::{to_json_binary, Addr, Coin, Empty, Event, Uint128}; + + use cosmwasm_std::{Addr, Coin, Empty, Event, Uint128}; + ... + use cw_my_collection_manager::{ + - contract::{execute, instantiate, reply}, + + contract::{execute, instantiate, query, reply}, + msg::{ExecuteMsg, InstantiateMsg, PaymentParams}, + }; + ... + fn instantiate_collection_manager( + ... + ) -> (u64, Addr) { + - let code = Box::new( + - ContractWrapper::new(execute, instantiate, |_, _, _: ()| { + - to_json_binary("mocked_manager_query") + - }) + - .with_reply(reply), + - ); + + let code = Box::new(ContractWrapper::new(execute, instantiate, query).with_reply(reply)); + let manager_code_id = mock_app.store_code(code); + ... + } + ... + ``` + + + + +At this intermediate stage the `my-collection-manager` project should have something similar to the [`payment-params-query`](https://github.com/b9lab/cw-my-collection-manager/tree/payment-params-query) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/proper-fund-handling..payment-params-query) as the diff. + + + +## The sudo message + +You define your own sudo messages separately from the others. They are not a subset of, say, `ExecuteMsg`: + + + ```diff-rust + ... + pub struct GetPaymentParamsResponse { + pub payment_params: PaymentParams, + } + + + + #[cw_serde] + + pub enum SudoMsg { + + UpdatePaymentParams(PaymentParams), + + } + ``` + + +## Sudo handling + +To handle sudo messages, you need to add the `sudo` entry point. This is the one that the CosmWasm module will invoke when the system instructs it to handle a sudo message. Here too, the `sudo` function only matches the variant and then invokes a specialized sudo function: + + + ```diff-rust + use crate::{ + msg::{ + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, GetPaymentParamsResponse, + - InstantiateMsg, NameServiceExecuteMsgResponse, QueryMsg, + + InstantiateMsg, NameServiceExecuteMsgResponse, PaymentParams, QueryMsg, SudoMsg, + }, + } + ... + + #[cfg_attr(not(feature = "library"), entry_point)] + + pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> ContractResult { + + match msg { + + SudoMsg::UpdatePaymentParams(payment_params) => { + + sudo_update_payment_params(deps, payment_params) + + } + + } + + } + + + + fn sudo_update_payment_params(deps: DepsMut, payment_params: PaymentParams) -> ContractResult { + + payment_params.validate()?; + + PAYMENT_PARAMS.save(deps.storage, &payment_params)?; + + let sudo_event = Event::new("my-collection-manager"); + + let sudo_event = append_payment_params_attributes(sudo_event, payment_params); + + Ok(Response::default().add_event(sudo_event)) + + } + + + fn append_payment_params_attributes(my_event: Event, payment_params: PaymentParams) -> Event { + + let my_event = my_event.add_attribute( + + "update-payment-params-beneficiary", + + payment_params.beneficiary, + + ); + + match payment_params.mint_price { + + None => my_event.add_attribute("update-payment-params-mint-price", "none"), + + Some(mint_price) => my_event + + .add_attribute("update-payment-params-mint-price-denom", mint_price.denom) + + .add_attribute( + + "update-payment-params-mint-price-amount", + + mint_price.amount.to_string(), + + ), + + } + + } + + #[cfg(test)] + mod tests... + ``` + + +Note that: + +* Just as in instantiate, it verifies that the new parameters are valid. +* Just as in execute, it emits an event to inform on the change. +* The new function that appends values to the event can be reused. + +## Adjust instantiate for good measure + +Now that the payment parameters can change, and with a goal of symmetry, you can adjust the `instantiate` function to also emit an event. This has the added benefit that the history of the payment parameters value can be reconstructed from the events alone. + + + ```diff-rust + .. + #[cfg_attr(not(feature = "library"), entry_point)] + pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, msg: InstantiateMsg) -> ContractResult { + msg.payment_params.validate()?; + PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?; + + let instantiate_event = Event::new("my-collection-manager"); + + let instantiate_event = append_payment_params_attributes(instantiate_event, msg.payment_params); + - Ok(Response::default()) + + Ok(Response::default().add_event(instantiate_event)) + } + ... + ``` + + +## Unit tests + +It is worth adding a unit test that calls the function in isolation and confirms it returns as expected. + + + ```diff-rust + ... + mod tests { + use crate::{ + contract::ReplyCode, + msg::{ + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, InstantiateMsg, + - NameServiceExecuteMsgResponse, PaymentParams, + + NameServiceExecuteMsgResponse, PaymentParams, SudoMsg, + }, + + state::PAYMENT_PARAMS, + }; + ... + fn test_reply_pass_through() { + ... + } + + + + #[test] + + fn test_sudo_update_payment_params() { + + // Arrange + + let mut mocked_deps_mut = testing::mock_dependencies(); + + let mocked_env = testing::mock_env(); + + let beneficiary = Addr::unchecked("beneficiary"); + + let new_payment_params = PaymentParams { + + beneficiary: beneficiary.to_owned(), + + mint_price: Some(Coin { + + denom: "silver".to_owned(), + + amount: Uint128::one(), + + }), + + }; + + let sudo_msg = SudoMsg::UpdatePaymentParams(new_payment_params.to_owned()); + + + + // Act + + let contract_result = super::sudo(mocked_deps_mut.as_mut(), mocked_env, sudo_msg); + + + + // Assert + + assert!(contract_result.is_ok(), "Failed to sudo"); + + let received_response = contract_result.unwrap(); + + let expected_response = Response::default().add_event( + + Event::new("my-collection-manager") + + .add_attribute("update-payment-params-beneficiary", beneficiary) + + .add_attribute("update-payment-params-mint-price-denom", "silver") + + .add_attribute("update-payment-params-mint-price-amount", "1"), + + ); + + assert_eq!(received_response, expected_response); + + let payment_params = PAYMENT_PARAMS + + .load(&mocked_deps_mut.storage) + + .expect("Failed to load payment params"); + + assert_eq!(payment_params, new_payment_params); + + } + } + ``` + + +Note that: + +* It is not strictly necessary to first call the instantiate as the test only writes to storage. + +## Mocked app tests + +You can instruct the mocked app to pass a sudo msg to your _compiled_ smart contract, as long as you compiled it with the `sudo` function. + + + ```diff-rust + ... + use cw721::msg::{Cw721ExecuteMsg, Cw721QueryMsg, OwnerOfResponse}; + - use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor}; + + use cw_multi_test::{App, AppBuilder, ContractWrapper, Executor, WasmSudo}; + use cw_my_collection_manager::{ + - contract::{execute, instantiate, query, reply}, + - msg::{ExecuteMsg, InstantiateMsg, PaymentParams}, + + contract::{execute, instantiate, query, reply, sudo}, + + msg::{ExecuteMsg, GetPaymentParamsResponse, InstantiateMsg, PaymentParams, QueryMsg, SudoMsg}, + }; + ... + fn instantiate_collection_manager( + ... + ) -> (u64, Addr) { + let code = Box::new( + ContractWrapper::new(execute, instantiate, query) + - .with_reply(reply), + + .with_reply(reply) + + .with_sudo(sudo), + ); + ... + } + ... + + #[test] + + fn test_sudo_update_payment_params() { + + // Arrange + + let mut mock_app = App::default(); + + let beneficiary_addr = Addr::unchecked("beneficiary"); + + let (_, addr_manager) = instantiate_collection_manager( + + &mut mock_app, + + PaymentParams { + + beneficiary: beneficiary_addr.to_owned(), + + mint_price: None, + + }, + + ); + + let new_payment_params = PaymentParams { + + beneficiary: beneficiary_addr.to_owned(), + + mint_price: Some(Coin { + + denom: "silver".to_owned(), + + amount: Uint128::from(23u16), + + }), + + }; + + let update_sudo_msg = SudoMsg::UpdatePaymentParams(new_payment_params.to_owned()); + + let sudo_msg = cw_multi_test::SudoMsg::Wasm( + + WasmSudo::new(&addr_manager, &update_sudo_msg).expect("Failed to serialize sudo message"), + + ); + + + + // Act + + let result = mock_app.sudo(sudo_msg); + + + + // Assert + + assert!(result.is_ok(), "Failed to pass through the message"); + + let result = result.unwrap(); + + let expected_sudo_event = Event::new("wasm-my-collection-manager") + + .add_attribute("_contract_address", addr_manager.to_owned()) + + .add_attribute("update-payment-params-beneficiary", beneficiary_addr) + + .add_attribute("update-payment-params-mint-price-denom", "silver") + + .add_attribute("update-payment-params-mint-price-amount", "23"); + + result.assert_event(&expected_sudo_event); + + let result = mock_app + + .wrap() + + .query_wasm_smart::(&addr_manager, &QueryMsg::GetPaymentParams); + + assert!(result.is_ok(), "Failed to query payment params"); + + assert_eq!( + + result.unwrap(), + + GetPaymentParamsResponse { + + payment_params: new_payment_params + + } + + ); + + } + ``` + + +Note that: + +* Note that your smart contract's sudo message is first wrapped into the testing frameworks sudo message. +* The mocked app tests only test that the contract handles a sudo message coming from the app. It does not test that, given a properly formed governance proposal, the smart contract is called on sudo. Testing that would be like testing a mocked app feature. + +## Conclusion + +Now your smart contract is able to receive instructions from the underlying app chain to update the payment parameters. By leveraging the Cosmos SDK's governance module, you only have to code the effect of successful proposals. + + + +At this stage: + +* The `my-nameservice` project should still have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project should have something similar to the [`sudo-message`](https://github.com/b9lab/cw-my-collection-manager/tree/sudo-message) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/payment-params-query..sudo-message) as the diff from the payment params query and [this](https://github.com/b9lab/cw-my-collection-manager/compare/proper-fund-handling..sudo-message) as the larger diff from the previous section. + + diff --git a/docs/tutorial/platform/18-migration.md b/docs/tutorial/platform/18-migration.md new file mode 100644 index 0000000..f313e87 --- /dev/null +++ b/docs/tutorial/platform/18-migration.md @@ -0,0 +1,529 @@ +--- +title: First Migration +description: Introduce a change in-flight. +--- + +# First Migration + +Your smart contract can now change its parameters via a Cosmos SDK governance proposal. + + + +If you skipped the previous section, you can just switch: + +* The `my-nameservice` project to its [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project to its [`sudo-message`](https://github.com/b9lab/cw-my-collection-manager/tree/sudo-message) branch. + +And take it from there. + + + +When you introduced the payment parameters into your smart contract, the change not only changed its code, it also modified its storage layout. Because of that, swapping the code id of your smart contract would not have re-executed the `instantiate` function. Therefore an `execute` would always fail at the `PAYMENT_PARAMS.load(..)?` line. + +You need something akin to the `instantiate` function, but for a smart contract that already exists. That, and more, is the objective of a migration. + +## The mechanism + +A migration changes **atomically** two elements of a smart contract: + +* Its code, which can be swapped at any time, as long as an administrator has been defined at instantiation. +* Its storage, which can be changed within the scope of a transaction; one that effects the migration. + +You will define a new `migrate` entry point and, inside it, do the storage adjustments. + +In this exercise, you have introduced `PaymentParams` in two steps. + +* You added the storage element with just the `beneficiary: Addr` [here](./15-cross-module.html). +* You added `mint_price: Option` to the existing object [here](./16-fund-handling.html). + +You can imagine two different migration situations: + +1. From no payment params, to a fully formed one. +2. From a half payment params, to a fully formed one. + +_Fixing_ storage as part of the migration is fraught business. That's why, in this section: + +* We handle the first case of going from no payment params to a fully formed one. +* We introduce versioning so that future migrations have a single value to check. + +## A new dependency + +To assist you with semantic versioning, you add the [`cw2` library](https://docs.rs/crate/cw2/1.1.2): + + + + ```sh + cargo add cw2@1.1.2 + ``` + + + ```sh + docker run --rm -it \ + -v $(pwd):/root/ -w /root \ + rust:1.80.1 \ + cargo add cw2@1.1.2 + ``` + + + +The `cw2` library offers more than that, and in particular, it expects your smart contract to store information about itself in storage. You will add that shortly. + +## The message + +Since the `migrate` function will store a new `PaymentParams`, your migration message needs to carry one: + + + ```diff-rust + ... + pub enum SudoMsg { + UpdatePaymentParams(PaymentParams), + } + + + + #[cw_serde] + + pub struct MigrateMsg { + + pub payment_params: PaymentParams, + + } + ``` + + +Note that although its content looks identical to the `InstantiateMsg`, it is better to keep both message types separate so as to avoid confusion. + +## A new error + +Because a migrate function expects certain initial conditions, it should return an error if it does not recognize them. The `cw2` library defines a `VersionError`, which it would be good to already include, even if, at this stage, its use looks premature. Add a new error: + + + ```diff-rust + use cosmwasm_std::{Coin, StdError}; + + use cw2::VersionError; + use thiserror::Error; + ... + pub enum ContractError { + ... + MissingPayment { missing_payment: Coin }, + + #[error("{0}")] + + Version(#[from] VersionError), + } + ``` + + +## Adjust `instantiate` with the contract version + +This will be convenient for **future** migrations, not this one. Add two constants: + + + ```diff-rust + ... + use crate::msg::PaymentParams; + + + pub const CONTRACT_NAME: &str = "my-collection-manager"; + + pub const CONTRACT_VERSION: &str = env!("CARGO_PKG_VERSION"); + + pub const PAYMENT_PARAMS: Item = Item::new("payment_params"); + ``` + + +Note that: + +* The version is taken from the Cargo package. This is up to you to change. +* You could also take the name from the Cargo package with `env!("CARGO_PKG_NAME")`. + + + ```diff-rust + use crate::{ + ... + msg::{ + ... + }, + - state::PAYMENT_PARAMS, + + state::{CONTRACT_NAME, CONTRACT_VERSION, PAYMENT_PARAMS}, + }; + ... + use cosmwasm_std::{ + ... + }; + + use cw2::set_contract_version; + use cw721::msg::NumTokensResponse; + + type ContractResult = Result; + ... + pub fn instantiate(deps: DepsMut, _: Env, _: MessageInfo, msg: InstantiateMsg) -> ContractResult { + msg.payment_params.validate()?; + PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + - let instantiate_event = Event::new("my-collection-manager"); + + let instantiate_event = Event::new("my-collection-manager") + + .add_attribute("update-contract-version", CONTRACT_VERSION); + let instantiate_event = append_payment_params_attributes(instantiate_event, msg.payment_params); + ... + } + ... + ``` + + +Note that: + +* As always when using a library, you need to make sure that you do not overwrite what the library is writing, in this case at the `"contract_info"` storage key. + + + +You added the `set_contract_version` line only now. However, this is only a tutorial constraint, so that you discover what relates to migration in a single place. For your next smart contract, you ought to add the `set_contract_version` line right at the beginning of the project. + + + +## The `migrate` entry point + +This is where you change the storage layout of your smart contract as part of the migration: + + + ```diff-rust + use crate::{ + ... + msg::{ + CollectionExecuteMsg, CollectionQueryMsg, ExecuteMsg, GetPaymentParamsResponse, + - InstantiateMsg, NameServiceExecuteMsgResponse, PaymentParams, QueryMsg, SudoMsg, + + InstantiateMsg, MigrateMsg, NameServiceExecuteMsgResponse, PaymentParams, QueryMsg, SudoMsg, + }, + ... + }; + ... + use cosmwasm_std::{ + ... + }; + - use cw2::set_contract_version; + + use cw2::{get_contract_version, set_contract_version, ContractVersion, VersionError}; + use cw721::msg::NumTokensResponse; + + fn sudo_update_payment_params(...) -> ContractResult { + ... + } + + + + #[cfg_attr(not(feature = "library"), entry_point)] + + pub fn migrate(deps: DepsMut, _env: Env, msg: MigrateMsg) -> ContractResult { + + if let Ok(ContractVersion { + + contract: _, + + version, + + }) = get_contract_version(deps.storage) + + { + + return Err(ContractError::Version(VersionError::WrongVersion { + + expected: "0.0.0".to_owned(), + + found: version, + + })); + + } + + msg.payment_params.validate()?; + + PAYMENT_PARAMS.save(deps.storage, &msg.payment_params)?; + + set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; + + let migrate_event = Event::new("my-collection-manager") + + .add_attribute("update-contract-version", CONTRACT_VERSION); + + let migrate_event = append_payment_params_attributes(migrate_event, msg.payment_params); + + Ok(Response::default().add_event(migrate_event)) + + } + ... + ``` + + +Note that: + +* The `if let Ok(ContractVersion {...}) = get_contract_version(...)` branch is unusual as the function returns an error when it finds an `ok` value. This reflects the fact that there was no contract version initially. A subsequent migration ought to either: + * Use `assert_contract_version` to confirm that the smart contract is at the expected version before proceeding. + * Or use `get_contract_version` to then branch depending on the value found. The older the version, the more changes would have to be applied. +* The rest of the actions otherwise look identical to the `instantiate` function. This may not always be the case however. Each situation is unique. +* It emits an event for convenience. + +## Unit tests + +### On `instantiate` + +Now that you save the version in storage, depending on the `cw2` library, it is worth checking that it stored the right values: + + + ```diff-rust + ... + mod tests { + ... + use cosmwasm_std::{ + ... + } + + use cw2::{assert_contract_version, ContractVersion}; + use cw721::msg::NumTokensResponse; + ... + impl NumTokensMockQuerier { + ... + } + + + #[test] + + fn test_instantiate() { + + // Arrange + + let mut mocked_deps_mut = mock_deps(NumTokensResponse { count: 3 }); + + let mocked_env = testing::mock_env(); + + let deployer = Addr::unchecked("deployer"); + + let mocked_msg_info = testing::mock_info(deployer.as_ref(), &[]); + + let instantiate_msg = InstantiateMsg { + + payment_params: PaymentParams { + + beneficiary: deployer, + + mint_price: None, + + }, + + }; + + + + // Act + + let result = super::instantiate( + + mocked_deps_mut.as_mut(), + + mocked_env.to_owned(), + + mocked_msg_info, + + instantiate_msg, + + ); + + + + // Assert + + assert!(result.is_ok(), "Failed to instantiate manager"); + + let received_response = result.unwrap(); + + let expected_response = Response::default().add_event( + + Event::new("my-collection-manager") + + .add_attribute("update-contract-version", "0.1.0") + + .add_attribute("update-payment-params-beneficiary", deployer) + + .add_attribute("update-payment-params-mint-price", "none"), + + ); + + assert_eq!(received_response, expected_response); + + let saved_payment_params = PAYMENT_PARAMS + + .load(&mocked_deps_mut.storage) + + .expect("Failed to load payment params"); + + assert_eq!(saved_payment_params, payment_params); + + assert_contract_version(&mocked_deps_mut.storage, "my-collection-manager", "0.1.0") + + .expect("Failed to assert contract version"); + + let contract_info = cw2::CONTRACT + + .load(&mocked_deps_mut.storage) + + .expect("Failed to load contract info"); + + assert_eq!( + + contract_info, + + ContractVersion { + + contract: "my-collection-manager".to_owned(), + + version: "0.1.0".to_owned(), + + } + + ); + + } + ... + } + ``` + + +Note that: + +* It contains the imports for all the new tests. +* The test checks the contract version in two different ways. The `assert_contract_version` is one you can actually call from a smart contract function if you want your migrate function to be valid for a single version value. +* It also checks the event introduced earlier. + +### On `migrate` + + + ```diff-rust + ... + mod tests { + ... + fn test_sudo_update_payment_params() { + ... + } + + + + #[test] + + fn test_migrate_payment_params() { + + // Arrange + + let mut mocked_deps_mut = testing::mock_dependencies(); + + let mocked_env = testing::mock_env(); + + let beneficiary = Addr::unchecked("beneficiary"); + + let new_payment_params = PaymentParams { + + beneficiary: beneficiary.to_owned(), + + mint_price: Some(Coin { + + denom: "silver".to_owned(), + + amount: Uint128::one(), + + }), + + }; + + let migrate_msg = MigrateMsg { + + payment_params: new_payment_params.to_owned(), + + }; + + + + // Act + + let result = super::migrate(mocked_deps_mut.as_mut(), mocked_env, migrate_msg); + + + + // Assert + + assert!(result.is_ok(), "Failed to migrate manager"); + + let received_response = result.unwrap(); + + let expected_response = Response::default().add_event( + + Event::new("my-collection-manager") + + .add_attribute("update-contract-version", "0.1.0") + + .add_attribute("update-payment-params-beneficiary", beneficiary) + + .add_attribute("update-payment-params-mint-price-denom", "silver") + + .add_attribute("update-payment-params-mint-price-amount", "1"), + + ); + + assert_eq!(received_response, expected_response); + + let saved_payment_params = PAYMENT_PARAMS + + .load(&mocked_deps_mut.storage) + + .expect("Failed to load payment params"); + + assert_eq!(new_payment_params, saved_payment_params); + + assert_contract_version(&mocked_deps_mut.storage, "my-collection-manager", "0.1.0") + + .expect("Failed to assert contract version"); + + let contract_info = cw2::CONTRACT + + .load(&mocked_deps_mut.storage) + + .expect("Failed to load contract info"); + + assert_eq!( + + contract_info, + + ContractVersion { + + contract: "my-collection-manager".to_owned(), + + version: "0.1.0".to_owned(), + + } + + ); + + } + } + ``` + + +Note that: + +* The _old_ `instantiate` function, pre-migration, did not save anything to storage, so when testing the migration, we do not need to run a mocked _old_ `instantiate`. +* It looks very much like `test_sudo_update_payment_params`. + +## Mocked app tests + +Conveniently, the `cw-multi-test` library offers a [`migrate_contract`](https://github.com/CosmWasm/cw-multi-test/blob/v1.2.0/src/executor.rs#L168) function that lets you atomically change the code and run the `migrate` function. + +In this test, you need to create two _bytecodes_: + +1. The initial one: + * With an instantiate function that does not store any payment params. + * Without migrate function. +2. The new one: + * With an instantiate function that stores payment params. + * With a migrate function. + +The goal is to create the smart contract instance with the first bytecode, then swap the code to the second one with `migrate_contract`, thereby: + +* Skipping the instantiate function on the first bytecode. +* Calling the migrate function on the second bytecode. + +The _Arrange_ part reflects these two steps: + + + ```diff-rust + + use std::fmt::Error; + + + use cosmwasm_schema::cw_serde; + - use cosmwasm_std::{Addr, Coin, Empty, Event, Uint128}; + + use cosmwasm_std::{Addr, Coin, DepsMut, Empty, Env, Event, MessageInfo, Response, Uint128}; + ... + use cw_my_collection_manager::{ + - contract::{execute, instantiate, query, reply, sudo}, + - msg::{ExecuteMsg, GetPaymentParamsResponse, InstantiateMsg, PaymentParams, QueryMsg, SudoMsg} + + contract::{execute, instantiate, migrate, query, reply, sudo}, + + msg::{ExecuteMsg, GetPaymentParamsResponse, InstantiateMsg, MigrateMsg, PaymentParams, QueryMsg, SudoMsg}, + } + ... + fn test_sudo_update_payment_params() { + ... + } + + + + #[test] + + fn test_migrate_payment_params() { + + // Arrange old smart contract + + #[cw_serde] + + struct OldInstantiateMsg {} + + let mut mock_app = App::default(); + + let admin_addr = Addr::unchecked("admin"); + + let old_code = Box::new( + + ContractWrapper::new( + + execute, + + |_: DepsMut, _: Env, _: MessageInfo, _: OldInstantiateMsg| -> Result { + + Ok(Response::default()) + + }, + + query, + + ) + + .with_reply(reply) + + .with_sudo(sudo), + + ); + + let manager_old_code_id = mock_app.store_code(old_code); + + let addr_manager = mock_app + + .instantiate_contract( + + manager_old_code_id, + + Addr::unchecked("deployer-manager"), + + &OldInstantiateMsg {}, + + &[], + + "my-collection-manager", + + Some(admin_addr.to_string()), + + ) + + .expect("Failed to instantiate old collection manager"); + + // Arrange migration + + let new_code = Box::new( + + ContractWrapper::new(execute, instantiate, query) + + .with_reply(reply) + + .with_sudo(sudo) + + .with_migrate(migrate), + + ); + + let manager_new_code_id = mock_app.store_code(new_code); + + let beneficiary_addr = Addr::unchecked("beneficiary"); + + let new_payment_params = PaymentParams { + + beneficiary: beneficiary_addr.to_owned(), + + mint_price: Some(Coin { + + denom: "silver".to_owned(), + + amount: Uint128::from(23u16), + + }), + + }; + + let migrate_msg = MigrateMsg { + + payment_params: new_payment_params.to_owned(), + + }; + + + + // Act + + let result = mock_app.migrate_contract( + + admin_addr, + + addr_manager.to_owned(), + + &migrate_msg, + + manager_new_code_id, + + ); + + + + // Assert + + assert!(result.is_ok(), "Failed to migrate the contract"); + + let result = result.unwrap(); + + let expected_migrate_event = Event::new("migrate") + + .add_attribute("_contract_address", addr_manager.to_owned()) + + .add_attribute("code_id", "2".to_owned()); + + result.assert_event(&expected_migrate_event); + + let expected_migrate_event2 = Event::new("wasm-my-collection-manager") + + .add_attribute("_contract_address", addr_manager.to_owned()) + + .add_attribute("update-contract-version", "0.1.0") + + .add_attribute("update-payment-params-beneficiary", beneficiary_addr) + + .add_attribute("update-payment-params-mint-price-denom", "silver") + + .add_attribute("update-payment-params-mint-price-amount", "23"); + + result.assert_event(&expected_migrate_event2); + + let result = mock_app + + .wrap() + + .query_wasm_smart::(&addr_manager, &QueryMsg::GetPaymentParams); + + assert!(result.is_ok(), "Failed to query payment params"); + + assert_eq!( + + result.unwrap(), + + GetPaymentParamsResponse { + + payment_params: new_payment_params + + } + + ); + + } + ``` + + +Note how: + +* The function defines a `struct OldInstantiateMsg` that reflects the state of `InstantiateMsg` before the change. In a larger project, you may want to have it as a clearly defined type. +* The function passes a lambda `|_, _, _, _| { ... }` as the `instantiate` function. It works here because the old `instantiate` function did not do anything special. In a larger project, you may want to define it inside another package imported as a development dependency. +* The old code is created without a `migrate` function, for a better simulation. +* The smart contract is instantiated with an admin. If it was left as `None`, like in the other test functions, then it would not be possible to swap the code. +* The new code contains the latest `instantiate` and `migrate` functions. +* The migrate call is made from the admin address. +* The assertions on events look a lot like `test_sudo_update_payment_params`, apart form the attribute on the new code id: `"2"`. + +## Conclusion + +You can now upgrade your smart contract from an earlier version. + + + +At this stage: + +* The `my-nameservice` project should still have something similar to the [`execute-return-data`](https://github.com/b9lab/cw-my-nameservice/tree/execute-return-data) branch. +* The `my-collection-manager` project should have something similar to the [`migrate-function`](https://github.com/b9lab/cw-my-collection-manager/tree/migrate-function) branch, with [this](https://github.com/b9lab/cw-my-collection-manager/compare/sudo-message..migrate-function) as the diff. + + + +There is no test that shows the smart contract cannot be twice migrated. This is left as an exercise. \ No newline at end of file diff --git a/docs/tutorial/platform/19-best-practices.md b/docs/tutorial/platform/19-best-practices.md new file mode 100644 index 0000000..4bc1fe4 --- /dev/null +++ b/docs/tutorial/platform/19-best-practices.md @@ -0,0 +1,95 @@ +--- +title: Best Practices +description: A collection of what to do and not to do. +--- + +# Best Practices + +Going through the concepts and the exercises should have given you an idea of what to do and not to do. Nonetheless, it is always a good idea to collect these ideas into a singular place, here. + +## Preparing code + +When creating your project, you should make sure that its code can be reused as a crate by others. This means that in particular, you ought to add the entry point flag _conditionally_ like so: + +```rust +#[cfg_attr(not(feature = "library"), entry_point)] +``` + +Also, you should inscribe your smart contract's version in the `instantiate` function from the start with: + +```rust +set_contract_version(deps.storage, CONTRACT_NAME, CONTRACT_VERSION)?; +``` + +To see it being done (though a bit too late), see the exercise's [migration part](./18-migration.html). + +Your code is always at risk of becoming large and unwieldy, which is one way bugs hide. To avoid this fate, you ought to split code elements judiciously. In particular, you may consider a combination of the following: + +* Sending Rust unit tests into their own module file. +* Keeping the `execute` function short by having only a `match` statement in it. +* Sending the _sub_-execute functions into their own files. + +## State handling + +Do not forget to update all the relevant state before exiting. + +* There is of course the classic case where a single state entry needs to be updated (think balance update). +* But there is also the case where two state entries need to be updated when something changes on either one ([think sale and trade](https://github.com/oak-security/cosmwasm-security-dojo/blob/68527006200e269fc8386a3e1b7c4799e2a6cd19/challenges/04-nft/src/contract.rs#L282-L285)). + +## Addresses + +Addresses and strings tend to be used interchangeably in CosmWasm but there are still risks. + +1. Ensure input addresses are valid. In particular, you should [deserialize user inputs](https://github.com/DA0-DA0/dao-contracts/wiki/CosmWasm-security-best-practices#dont-deserialize-into-addr) into `String`, and then use [`Api.addr_validate`](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/traits.rs#L157) before going further. +2. Beware un-normalized addresses. In particular, you should use [`Api.addr_validate`](https://github.com/CosmWasm/cosmwasm/blob/v2.1.4/packages/std/src/traits.rs#L157) so as to normalize addresses if any comparison is required. Otherwise, for instance, a different capitalization can help [escape detection](https://github.com/oak-security/cosmwasm-security-dojo/blob/main/challenges/05-addressing/src/exploit.rs#L82). + +## Funds handling + +When expecting and receiving funds, your code should never confuse between: + +1. The value transferred as part of a transaction. +2. The total amount that is saved to state after the transaction is complete. + +In particular: + +* If you are receiving payment, then what matters is the value that comes with the transaction. +* If you are verifying a status, such as staked amount, what matters is the stored state, perhaps even one snapshotted in the past. In particular you want to be [resistant to flash loans](https://github.com/DA0-DA0/dao-contracts/wiki/CosmWasm-security-best-practices#your-attackers-have-unlimited-capital). + +In practice, this means: + +* If your code holds funds on behalf of other users, it should not have to query its own balance with the bank module as its balance represents an aggregate. Instead it should store in its own state the relevant values. See [this hacking challenge](https://github.com/oak-security/cosmwasm-security-dojo/tree/main/challenges/01-storewhat) for an example of when it goes wrong. +* Your code should be able to handle any combination of funds being passed to it. For instance, you may expect a payment of `10 stake`. In that case, your code needs to accept being paid with two payments, the first of `1 stake` and the other of `9 stake`. And any other condition. To see this concept applied, go to the exercise on [fund handling](./16-fund-handling.html). A rationale for this practice is that another smart contract, the one paying yours, may have poorly assembled a strange but nonetheless-suitable internal message. Another rationale is that, in a possible future, two externally-owned accounts would each pay part of the fee. +* Your code should also be able to return the change on a payment that is too high. The rationale for it is that your fee may decrease, but your users take time to notice. +* If, on the contrary, you make your smart contract only accept an exact unique payment, this decision has to be made explicitly, because of the necessities of your project, and not because it's just easier. +* Your code should be able to handle irrelevant funds being transferred to it. + * Either by atomically returning the change on an expected payment. + * Or by having a privileged account withdraw all unaccounted funds, considering them as donations. + * Or by rejecting the transaction as a whole. +* If you choose to fail when receiving irrelevant funds, this decision has to be made explicitly, because of the necessities of your project, and not because it's just easier. + +## Testing + +## Gas limit + +Even if individual WebAssembly operations are cheap in terms of gas, they are not free. And using the underlying app-chain elements is all metered. + +* The most expensive operations are about access to storage. So avoid storing (and retrieving) a full list to (from) storage when all you need at decision time is a single value. For instance, if you have a whitelist, it is cheaper to use a map than to store the entire list as a single storage item. + + * So instead of `Item>`, + * Use `Map<&Addr, bool>`. + +* Beware of operations whose gas cost increases with usage (`> O(1)`). If for instance, you store a list as a single item, and this list can be enlarged by a user, there will come a time when the list is too large to store or retrieve. At this stage, the transaction will fail for lack of gas. The limit here is not the transaction's gas, which the sender can choose to increase. Instead, the limit is the block's gas limit, which is high, but still finite. What to watch out for includes `for` loops where the number of times it loops through is not known in advance. Remember too that in Rust, there are hidden loops such as array and slice `contains` method. + +Another gas saving trick is to store zipped code. If, even after using [the optimizer](https://github.com/CosmWasm/optimizer), your code is large, you can zip it and pass the [zipped result](https://github.com/CosmWasm/wasmd/blob/v0.53.0/proto/cosmwasm/wasm/v1/tx.proto#L95-L96) as part of the `MsgStoreCode` transaction. + +## Calculations + +When you instruct your smart contract to do calculations, it is using integers. Prevent overflows by using the functions of `Uint128` for instance, and threshold effects + +## Libraries + +If you want your smart contract to send messages to a Cosmos SDK module for which the bindings do not yet exist, you can use the [Anybuf crate](https://docs.rs/anybuf/latest/anybuf/). Protobuf is indeed the serialization method used by the Cosmos SDK messages. See [this example](https://github.com/noislabs/nois-contracts/blob/v0.13.6/contracts/nois-payment/src/contract.rs#L115-L116), where: + +* The Protobuf [`type_url` is declared](https://github.com/noislabs/nois-contracts/blob/v0.13.6/contracts/nois-payment/src/contract.rs#L115) following [its declaration](https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-rc.1/x/distribution/proto/cosmos/distribution/v1beta1/tx.proto#L2). +* The elements of the message [are appended](https://github.com/noislabs/nois-contracts/blob/v0.13.6/contracts/nois-payment/src/contract.rs#L135-L136) according to [the definition](https://github.com/cosmos/cosmos-sdk/blob/v0.52.0-rc.1/x/distribution/proto/cosmos/distribution/v1beta1/tx.proto#L142-L148). + diff --git a/docs/tutorial/platform/_category_.json b/docs/tutorial/platform/_category_.json new file mode 100644 index 0000000..485bbbe --- /dev/null +++ b/docs/tutorial/platform/_category_.json @@ -0,0 +1,4 @@ +{ + "label": "CosmWasm Platform Tutorials", + "position": 3 +} diff --git a/docs/tutorial/platform/index.md b/docs/tutorial/platform/index.md new file mode 100644 index 0000000..02a62b7 --- /dev/null +++ b/docs/tutorial/platform/index.md @@ -0,0 +1,80 @@ +# CosmWasm Platform Tutorials + +This section contains a comprehensive set of tutorials originally from the CosmWasm Developer Platform. These tutorials provide hands-on, step-by-step guidance for learning CosmWasm smart contract development. + +## Overview + +These tutorials follow a progressive learning path from fundamental concepts to advanced topics. Each tutorial builds upon the previous ones, making it easy to follow along and learn at your own pace. + +## Tutorial Structure + +### Introduction and Concepts + +1. **[Introduction](./01-intro)** - Learn what CosmWasm is and why it exists +2. **[Concepts Overview](./02-concepts-overview)** - Deep dive into CosmWasm's architecture and concepts +3. **[Integration into Cosmos](./03-integration)** - Understand how CosmWasm integrates with the Cosmos SDK +4. **[Hello World](./04-hello-world)** - Build your first CosmWasm smart contract + +### Learning By Doing - Inwards + +5. **[First Contract](./05-first-contract)** - Create your first real contract +6. **[First Execute Transaction](./06-first-contract-register)** - Execute transactions with your contract +7. **[First Contract Query](./07-first-contract-query)** - Query your contract's state +8. **[First Integration Test](./08-first-contract-test)** - Write tests for your contract +9. **[First Composed Response](./09-first-response)** - Handle complex responses +10. **[Use the Ownable Library](./10-use-library)** - Leverage existing libraries +11. **[Use the NFT Library](./11-use-large-library)** - Work with larger, more complex libraries + +### Further Doing - Outwards + +12. **[First Contract Integration](./12-cross-contract)** - Call other contracts +13. **[First Contract Query Integration](./13-cross-query)** - Query other contracts +14. **[First Contract Reply Integration](./14-contract-reply)** - Handle asynchronous replies +15. **[First Cross-Module Integration](./15-cross-module)** - Interact with Cosmos SDK modules +16. **[Proper Funds Handling](./16-fund-handling)** - Manage tokens and funds securely + +### Deploy and Maintain + +17. **[First Sudo Message](./17-sudo-msg)** - Implement privileged functions +18. **[First Migration](./18-migration)** - Upgrade your contracts + +### Further Study + +19. **[Best Practices](./19-best-practices)** - Learn recommended patterns and practices + +## Getting Started + +:::caution Version Compatibility +These tutorials reference specific versions of wasmd and CosmWasm: +- The tutorials use **wasmd v0.53.2** with some code links referencing v0.52.0/v0.53.0 +- Current documentation uses **wasmd v0.52.0** as an example version +- Rust version **1.80.1** is used in the tutorials +- Some code references point to older versions of CosmWasm/wasmd + +The core concepts remain valid, but you may need to adjust: +- Version numbers in git clone commands +- CLI command syntax (which has remained relatively stable) +- Some API references + +For the most up-to-date installation and setup instructions, see: +- [Wasmd Setup Guide](../../wasmd/getting-started/setup) +- [Core Installation Guide](../../core/installation) +::: + +:::note Formatting Differences +These tutorials are in their original format from the CosmWasm Developer Platform. Some features may work differently: + +- **Interactive components** like Accordions, TabGroups, and HighlightBoxes may appear as plain text or have different styling +- **Code blocks** should still display correctly +- **Core content** remains intact and valuable + +If you encounter rendering issues with these tutorials, consider referring to our standard [Writing Contracts](../writing-contracts/introduction) tutorials instead. +::: + +Start with the [Introduction](./01-intro) to understand the fundamentals, then follow the numbered sequence for the best learning experience. + +## Additional Resources + +- For our own step-by-step tutorials, check out the [Writing Contracts](../writing-contracts/introduction) section +- For reference documentation, see the [Core documentation](../../core/introduction) +- For other learning resources, visit the [Learning Resources](../learning-resources) page