32 releases (5 stable)
1.0.4 | Oct 14, 2024 |
---|---|
1.0.0 | Jul 22, 2024 |
0.4.0 | Jun 18, 2024 |
0.3.2 | Mar 8, 2024 |
0.1.11 | Nov 16, 2022 |
#90 in #chain
2,725 downloads per month
34KB
498 lines
Router-wasm-bindings
The wasm bindings for building CosmWasm smart contracts that can run on the Router chain.
Prerequisites
Before starting, make sure you have rustup along with a
recent rustc
and cargo
version installed. Currently, we are testing on 1.62.1+.
And you need to have the wasm32-unknown-unknown
target installed as well.
You can check that via:
rustc --version
cargo --version
rustup target list --installed
# if wasm32 is not listed above, run this
rustup target add wasm32-unknown-unknown
Context
On the Router chain, We can build two types of contracts.
- Contracts that are not interacting with cross-chain contracts.
- Contracts that are back and forth sending the data request to cross-chain contracts.
To build second type of contracts that are interacting with other chains, the user/ applications needs to implement router-wasm-binding crate.
# add the following line in the cargo.toml [dependencies] section
router-wasm-bindings = "1.0.4"
How to use the Router-Wasm-Bindings
To implement cross-chain interoperability, the contract needs to implement the following functionality
- HandleIReceive for handling incoming requests from the other chains
- HandleIAck to send a request to the other chains.
The Contract can write the intermediate business logic in-between the incoming request and outbound request. While writing the intermediate business logic, the developer can convert single or multiple incoming requests into single or multiple outbound requests.
Also, while creating requests to other chains, the contract can be developed in such a way that multiple requests can be generated to different chains.
You can find examples of different scenarios in the cw-bridge-contracts repository.
[SudoMsg]
The SudoMsg is an enum and it has two different message types.
- HandleIReceive
- HandleIAck
In the following code snippet, we added the details at the field level of the SudoMsg. This will helps us in building an understanding of the data that will be coming either in the inbound request or in the outbound acknowledgment request.
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum SudoMsg {
// Sudo msg to handle incoming requests from other chains
HandleIReceive {
// the inbound initiator application contract address
request_sender: String,
// inbound request src chain id
source_chain_id: String,
// inbound request event nonce
request_identifier: u64,
// the inbound request instructions in base64 format
payload: Binary,
},
// Sudo msg to handle outbound message acknowledgment
HandleIAck {
// cross-chain request nonce
request_identifier: u64,
// cross-chain request contract call execution status
exec_flag: u64,
// cross-chain request contract call execution
exec_data: Binary,
// excess fee refunded amount
refund_amount: Coin,
},
}
The sudo function is one of the entry-point in a cosmwasm contract. It can be called internally by the chain only. In Router Chain, the developer needs to implement this sudo function to receive an incoming request. Here, in the following code snippet, we have shown the sample sudo function implementation.
Developers can have any sort of business logic inside the handle_sudo_request and handle_sudo_ack functions.
// import router binding message
use router_wasm_bindings::{RouterMsg, SudoMsg};
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn sudo(deps: DepsMut, _env: Env, msg: SudoMsg) -> StdResult<Response<RouterMsg>> {
match msg {
// Sudo msg to handle incoming requests from other chains
SudoMsg::HandleIReceive {
request_sender,
src_chain_id,
request_identifier,
payload,
} => handle_sudo_request(
deps,
env,
request_sender,
src_chain_id,
request_identifier,
payload,
),
// Sudo msg to handle outbound message acknowledgment
SudoMsg::HandleIAck {
request_identifier,
exec_flag,
exec_data,
refund_amount,
} => handle_sudo_ack(
deps,
env,
request_identifier,
exec_flag,
exec_data,
refund_amount,
),
}
}
The sudo message HandleIReceive
contains 4 arguments. This sudo function gets called when an inbound request comes for your middleware contract. We can handle this sudo request in any possible way or even skip it. As you can see in the code snippet, a function handle_sudo_request
has been created to handle the incoming inbound request in the cosmwasm contact. Within this function, you can apply any logic to the payload from the incoming request before creating the request for the destination chain. Each field has its own purpose and meaning in the HandleIReceive
request.
- request_sender: The application contract address on the source chain from which the request to the Router chain was sent.
- source_chain_id: The chain ID of the chain from which the inbound request to the Router chain has been initiated.
- request_identifier: The request identifier is a unique identifier of the request that is added by the source chain's gateway contract.
- payload: The payload comes from the source chain contract.
The sudo message HandleIAck
has 4 arguments. This sudo function gets called when the acknowledgment is received by the middleware contract on the Router chain post-execution of the contract call on the destination chain. We can handle this sudo request in any possible way or even skip it. As you can see in the code snippet, the function handle_sudo_ack
has been created to handle the incoming acknowledgment request in the cosmwasm contact. Each field has its own purpose and meaning in the HandleIAck
request.
- request_identifier: The unique and incremented integer value for the outbound request.
- exec_flag: The execution status flag for the contract call which was made on the destination chain.
- exec_data: The execution data for all the requests executed on the destination chain.
- refund_amount: The refunded fee amount is the extra fee that we have passed for the destination side contract execution.
[RouterMsg]
The RouterMsg is an enum type inside the router-wasm-bindings. It contains one custom message type.
- CrosschainCall
In the following code snippet, we have added one implementation of CrosschainCall. This message is used to create an outbound request. In the outbound request, we can specify the destination chain id & type, the contract addresses & instructions, the request expiry timestamp, the atomicity flag, etc.
// import router binding message
use router_wasm_bindings::{RouterMsg, SudoMsg};
use router_wasm_bindings::types::{
AckType, RequestMetaData,
};
use cosmwasm_std::{SubMsg, SubMsgResult, Uint128};
let request_packet: Bytes = encode(&[
Token::String(destination_address.clone()),
Token::Bytes(payload),
]);
let request_metadata: RequestMetaData = RequestMetaData {
dest_gas_limit: gas_limit,
dest_gas_price: gas_price,
ack_gas_limit: 300_000,
ack_gas_price: 10_000_000,
relayer_fee: Uint128::zero(),
ack_type: AckType::AckOnBoth,
is_read_call: false,
asm_address: String::default(),
};
let i_send_request: RouterMsg = RouterMsg::CrosschainCall {
version: 1,
route_amount,
route_recipient,
dest_chain_id: destination_chain_id,
request_metadata: request_metadata.get_abi_encoded_bytes(),
request_packet,
};
let cross_chain_sub_msg: SubMsg<RouterMsg> = SubMsg {
id: CREATE_OUTBOUND_REPLY_ID,
msg: i_send_request.into(),
gas_limit: None,
reply_on: ReplyOn::Success,
};
let res = Response::new()
.add_submessage(cross_chain_sub_msg.into())
Ok(res)
The CrosschainCall
is a data_type that helps the end user to create an cross-chain request to any destination chain. It has 6 arguments.
- version: The chain type of the chain for which the outbound request from the Router chain has been created.
- route_amount: The route token amount that needs to be burned on the router chain and minted/unlocked on the destination chain.
- route_recipient: The recipient address of the route token on the destination chain.
- destination_chain_id: The chain ID of the chain for which the outbound request from the Router chain has been created.
- request_metadata: The request metadata is encodedPacked information that contains information destination gas limit & price, ack gas limit & price, relayer fee, ack_type, is_read_call and asm_address.
- request_packet: The request packet is encoded information of destination address and payload. In example we can see how are we encoding this information.
Since the application developer is writing the application middleware contracts, they will have complete control over what kind of data is received in the payload. They can define the encoding and decoding of the data accordingly and perform any operation on the data.
Compiling and running tests
Now that you created your custom contract, make sure you can compile and run it before making any changes. Go into the repository and do:
# this will produce a wasm build in ./target/wasm32-unknown-unknown/release/YOUR_NAME_HERE.wasm
cargo wasm
# this runs unit tests with helpful backtraces
RUST_BACKTRACE=1 cargo unit-test
# auto-generate json schema
cargo schema
Understanding the tests
The main code is in src/contract.rs
and the unit tests there run in pure rust,
which makes them very quick to execute and give nice output on failures, especially
if you do RUST_BACKTRACE=1 cargo unit-test
.
We consider testing critical for anything on a blockchain, and recommend to always keep the tests up to date.
Generating JSON Schema
While the Wasm calls (instantiate
, execute
, query
) accept JSON, this is not enough
information to use it. We need to expose the schema for the expected messages to the
clients. You can generate this schema by calling cargo schema
, which will output
3 files in ./schema
, corresponding to the 3 message types the contract accepts.
These files are in standard json-schema format, which should be usable by various client side tools, either to auto-generate codecs, or just to validate incoming json wrt. the defined schema.
Preparing the Wasm bytecode for production
Before we upload it to a chain, we need to ensure the smallest output size possible, as this will be included in the body of a transaction. We also want to have a reproducible build process, so third parties can verify that the uploaded Wasm code did indeed come from the claimed rust code.
To solve both these issues, we have produced rust-optimizer
, a docker image to
produce an extremely small build output consistently. The suggested way
to run it is this:
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer:0.12.6
Or, If you're on an arm64 machine, you should use a docker image built with arm64.
docker run --rm -v "$(pwd)":/code \
--mount type=volume,source="$(basename "$(pwd)")_cache",target=/code/target \
--mount type=volume,source=registry_cache,target=/usr/local/cargo/registry \
cosmwasm/rust-optimizer-arm64:0.12.6
We must mount the contract code to /code
. You can use an absolute path instead
of $(pwd)
if you don't want to cd
to the directory first. The other two
volumes are nice for speedup. Mounting /code/target
in particular is useful
to avoid docker overwriting your local dev files with root permissions.
Note the /code/target
cache is unique for each contract being compiled to limit
interference, while the registry cache is global.
This is rather slow compared to local compilations, especially the first compilation of a given contract. The use of the two volume caches is very useful to speed up following compiles of the same contract.
This produces an artifacts
directory with a PROJECT_NAME.wasm
, as well as
checksums.txt
, containing the Sha256 hash of the wasm file.
The wasm file is compiled deterministically (anyone else running the same
docker on the same git commit should get the identical file with the same Sha256 hash).
It is also stripped and minimized for upload to a blockchain (we will also
gzip it in the uploading process to make it even smaller).
Dependencies
~4.5–6.5MB
~138K SLoC