Learn the basics of working with the IBC Developer Toolkit
Secret Network's CCL (Confidential Computing Layer) IBC Toolkit is a cross-chain messaging and confidential computation SDK for Cosmos developers.
The toolkit allows Cosmos developers to build novel cross-chain applications such as:
private DAO voting mechanisms
secure random number generation (Secret VRF)
confidential data access control via Secret NFTs
encrypted DeFi order books
and more!
Continue to Cross-Chain messaging to learn how to use Secret Network's Confidential Computing Layer SDK , or dive into the Key value store tutorial here🔥
IBC-Hooks is an IBC middleware that uses incoming ICS-20 token transfers to initiate smart contract calls. Secret Network created a Cosmos Developer SDK that uses IBC hooks to execute Secret gateway contracts, which allows Cosmos developers on other chains to use Secret as a privacy layer for your chain.
The SDK abstracts the complexities involved in interacting with the Secret Network for applications that deal with Cosmos wallets. It introduces a secure method for generating confidential messages and reliably authenticating users at the same time using the chacha20poly1305 algorithm.
The SDK can be used be for developing major gateways that forward incoming messages to Secret Network, as well as built-in support for confidential messages directly in other contracts. To learn by doing, start with the key value store developer tutorial here.
IBC Transfer channel between the consumer chain and Secret Network
Initiate a contract call with an incoming IBC token transfer using IBC hooks
IBC-Hooks is an IBC middleware that uses incoming ICS-20 token transfers to initiate smart contract calls. This allows for arbitrary data to be passed in along with token transfers. This is useful for a variety of use cases such as cross-chain token swaps, auto-wrapping of SNIP-20 tokens, General Message Passing (GMP) between Secret Network and EVM chains, and much more! The mechanism enabling this is a memo field on every ICS20 transfer packet as of IBC v3.4.0. Wasm hooks is an IBC middleware that parses an ICS20 transfer, and if the memo field is of a particular form, it executes a Wasm contract call.
ICS20 is JSON native, so JSON is used for the memo format:
{
//... other ibc fields omitted for example
"data": {
"denom": "denom on counterparty chain (e.g. uatom)", // will be transformed to the local denom (ibc/...)
"amount": "1000",
"sender": "addr on counterparty chain", // will be ignored and shown to the contract as a null sender (cannot be verifed over IBC)
"receiver": "secret1contractAddr",
"memo": {
"wasm": {
"contract": "secret1contractAddr",
"msg": {
"raw_message_fields": "raw_message_data"
}
}
}
}
}An ICS20 packet is formatted correctly for Wasm hooks if the following all hold:
memo is not blank
memo is valid JSON
memo has at least one key, with value "wasm"
memo["wasm"] has exactly two entries, "contract" and "msg"
memo["wasm"]["msg"] is a valid JSON object
receiver == memo["wasm"]["contract"]
Before Wasm hooks:
Ensure the incoming IBC packet is cryptographically valid
Ensure the incoming IBC packet has not timed out
In Wasm hooks, before packet execution:
Ensure the packet is correctly formatted (as defined above)
Edit the receiver to be the hardcoded IBC module account
In Wasm hooks, after packet execution:
Construct wasm message as defined above
Execute wasm message
If wasm message has error, return ErrAck
Otherwise continue through middleware
The following contract receives funds from IBC, wraps them as SNIP-20 tokens, and then transfers them to the recipient that is specified in the ibc-hooks message:
#[entry_point]
pub fn execute(_deps: DepsMut, env: Env, info: MessageInfo, msg: Msg) -> StdResult<Response> {
match msg {
Msg::WrapDeposit {
snip20_address,
snip20_code_hash,
recipient_address,
} => Ok(Response::default().add_messages(vec![
CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
contract_addr: snip20_address.clone(),
code_hash: snip20_code_hash.clone(),
msg: to_binary(&secret_toolkit::snip20::HandleMsg::Deposit { padding: None })
.unwrap(),
funds: info.funds.clone(),
}),
CosmosMsg::Wasm(cosmwasm_std::WasmMsg::Execute {
contract_addr: snip20_address,
code_hash: snip20_code_hash,
msg: to_binary(&secret_toolkit::snip20::HandleMsg::Transfer {
recipient: recipient_address,
amount: info.funds[0].amount,
memo: None,
padding: None,
})
.unwrap(),
funds: vec![],
}),
])),
}
}A contract that sends an IBC transfer may need to listen for the acknowledgment (ack) of that packet. To allow contracts to listen to ack of specific packets, we provide Ack callbacks. The sender of an IBC transfer packet may specify a callback in the memo field of the transfer packet when the ack of that packet is received.
For the callback to be processed, the transfer packet's memo should contain the following in its JSON:
{"ibc_callback": "secret1contractAddr"}
The WASM hooks will keep the mapping from the packet's channel and sequence to the contract in storage. When an ack is received, it will notify the specified contract via an execute message.
Interface for receiving the Acks and Timeouts
The contract that awaits the callback should implement the following interface for a sudo message:
#[cw_serde]
pub enum IBCLifecycleComplete {
#[serde(rename = "ibc_ack")]
IBCAck {
/// The source channel (secret side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
/// String encoded version of the ack as seen by OnAcknowledgementPacket(..)
ack: String,
/// Weather an ack is a success of failure according to the transfer spec
success: bool,
},
#[serde(rename = "ibc_timeout")]
IBCTimeout {
/// The source channel (secret side) of the IBC packet
channel: String,
/// The sequence number that the packet was sent with
sequence: u64,
},
}
/// Message type for `sudo` entry_point
#[cw_serde]
pub enum SudoMsg {
#[serde(rename = "ibc_lifecycle_complete")]
IBCLifecycleComplete(IBCLifecycleComplete),
}
#[entry_point]
pub fn sudo(_deps: DepsMut, _env: Env, msg: SudoMsg) -> StdResult<Response> {
match msg {
SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCAck {
channel,
sequence,
ack,
success,
}) => todo!(),
SudoMsg::IBCLifecycleComplete(IBCLifecycleComplete::IBCTimeout {
channel,
sequence,
}) => todo!(),
}
}Learn how to run the Go relayer to create a transfer channel between any Cosmos chain and Secret Network.
The Go relayer is a relayer implementation written in Golang. It can create clients, connections, and channels, as well as relay packets and update and upgrade clients.
In order to use Secret Network's IBC Developer Toolkit, you need an IBC transfer channel established between Secret Network and your Cosmos chain.
In this section, you will learn:
How to get started with the Go relayer.
Basic Go relayer commands.
How to create a transfer channel between Secret Network testnet and Neutron testnet.
Let's get started! 🚀
Clone the Go relayer repository:
git clone https://github.com/cosmos/relayer.gitInstall Go:
brew install goSet Go path:
export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/binBuild the Go relayer:
cd relayer
make installexport GOBIN=$HOME/go/bin
mkdir -p $GOBIN
go clean -cache
go build -ldflags "-X github.com/cosmos/relayer/v2/cmd.Version=$(git describe --tags | sed 's/^v//') \
-X github.com/cosmos/relayer/v2/cmd.Commit=$(git log -1 --format='%H') \
-X github.com/cosmos/relayer/v2/cmd.Dirty=$(git status --porcelain | wc -l | xargs)" \
-o $GOBIN/rly main.goTo check that the installation was successful, run:
rly versionWhich returns:
version: 2.6.0-rc.1
commit: 3b9ec008999973469aeab4bbdbcb44ff4886b8b8
cosmos-sdk: v0.50.5
go: go1.23.4 darwin/arm64The configuration data is added to the config file, stored at $HOME/.relayer/config/config.yaml by default.
If this is the first time you run the relayer, first initialize the config with the following command:
rly config initAnd check the config with:
rly config showNow you are all set to add the chains and paths you want to relay on, add your keys and start relaying. You will set up two testnet chains: Neutron's pion-1 and Secret Network's pulsar-3.
The rly chains add command fetches chain metadata from the chain registry and adds it to your config file:
rly chains add testnets/secretnetworktestnet
rly chains add testnets/neutrontestnetCreate new keys for the relayer to use when signing and relaying transactions:
rly keys add secretnetworktestnet secret-test #this is the name of your key
rly keys add neutrontestnet neutron-test #this is the name of your keyQuery your key balances:
rly query balance secretnetworktestnet secret-test
rly query balance neutrontestnet neutron-testThen, edit the relayer's key values in the config file to match the key-names chosen above.
The configuration data is added to the config file, stored at $HOME/.relayer/config/config.yaml:
chains:
neutrontestnet:
type: cosmos
value:
key-directory: /Users/yourname/.relayer/keys/pion-1
key: neutron-test
chain-id: pion-1
rpc-addr: https://rpc-lb-pion.ntrn.tech:443
secretnetworktestnet:
type: cosmos
value:
key-directory: /Users/yourname/.relayer/keys/pulsar-3
key: secret-test
chain-id: pulsar-3
rpc-addr: https://pulsar.rpc.secretnodes.com:443You configured the chain metadata, now you need path metadata.
Update your config file like so to use a configuration path that has been tested in production:
global:
debug-listen-addr: 127.0.0.1:5183
metrics-listen-addr: 127.0.0.1:5184
timeout: 10s
memo: ""
light-cache-size: 20
log-level: info
ics20-memo-limit: 0
max-receiver-size: 150
chains:
neutrontestnet:
type: cosmos
value:
key-directory: /Users/<your-user-name>/.relayer/keys/pion-1
key: neutron-test
chain-id: pion-1
rpc-addr: https://rpc-lb-pion.ntrn.tech:443
backup-rpc-addrs: []
account-prefix: neutron
keyring-backend: test
dynamic-gas-price: true
gas-adjustment: 2
gas-prices: 0.043untrn
min-gas-amount: 400000
max-gas-amount: 500000
debug: false
timeout: 20s
block-timeout: ""
output-format: json
sign-mode: direct
extra-codecs: []
coin-type: 118
signing-algorithm: ""
broadcast-mode: batch
min-loop-duration: 0s
extension-options: []
feegrants: null
secretnetworktestnet:
type: cosmos
value:
key-directory: /Users/<your-user-name>/.relayer/keys/pulsar-3
key: secret-test
chain-id: pulsar-3
rpc-addr: https://pulsar.rpc.secretnodes.com:443
backup-rpc-addrs:
- https://pulsar.rpc.secretnodes.com:443
account-prefix: secret
keyring-backend: test
dynamic-gas-price: false
gas-adjustment: 1.2
gas-prices: 0.1uscrt
min-gas-amount: 400000
max-gas-amount: 500000
debug: false
timeout: 20s
block-timeout: ""
output-format: json
sign-mode: direct
extra-codecs: []
coin-type: 529
signing-algorithm: ""
broadcast-mode: batch
min-loop-duration: 0s
extension-options: []
feegrants: nullCreate a relayer path:
rly paths new pulsar-3 pion-1 my-path #this is the name of your path Before starting to relay and after making some changes to the config, you can check the status of the chains in the config:
rly chains listWhich returns this output when healthy:
0: pulsar-3 -> type(cosmos) key(✔) bal(✔) path(✔)
1: pion-1 -> type(cosmos) key(✔) bal(✔) path(✔)And you can check the status of the paths in the config:
rly paths list0: secretnetworktestnet-nuetrontestnet -> chns(✔) clnts(✔) conn(✔) (pulsar-3<>pion-1)Finally, start the relayer on the desired path. The relayer will periodically update the clients and listen for IBC messages to relay:
rly start my-path Congrats! You are now relaying between Secret Network testnet and Neutron testnet! 🎉
2024-12-13T17:40:56.021378Z info Chain is in sync {"chain_name": "neutrontestnet", "chain_id": "pion-1"}
2024-12-13T17:41:01.999823Z info Client update threshold condition met {"path_name": "my_demo_path", "chain_id": "pion-1", "client_id": "07-tendermint-543", "trusting_period": 72000000, "time_since_client_update": 85721551, "client_threshold_time": 0}The essential parameters required for chacha20poly1305flow are defined in the following data structure:
EncryptedParams
A data structure that is visible to all network participants and can be transmitted over non-secure channels
/// A data structure that is safe to be visible by all network participants and can be transmited over non-secure channels
struct EncryptedParams {
/// Encrypted payload containging hidden message
pub payload : Binary,
/// Sha256 hash of the payload
pub payload_hash : Binary,
/// Signed base64 digest of the payload_hash being wrapped
/// in an cosmos arbitrary (036) object and rehashed again with sha256
pub payload_signature : Binary,
/// Public key of wallet used for deriving a shared key for chacha20_poly1305
/// Not necessary the same as user's public key
pub user_key : Binary,
/// One-time nonce used for chacha20_poly1305 encryption
pub nonce : Binary,
}Data meant to be encrypted and stored in the payload field of EncryptedParams
/// Data meant to be encrypted and stored in the payload field of [EncryptedParams]
#[cw_serde]
pub struct EncryptedPayload {
/// bech32 prefix address of a wallet used for signing hash of the payload
pub user_address : String,
/// Public key of a wallet used for signing hash of the payload
pub user_pubkey : Binary,
/// Human readable prefix for the bech32 address on the remote cosmos chain
pub hrp : String,
/// Plaintext message e.g. normal `ExecuteMsg` of your contract
pub msg : Binary,
}Your contract must define an endpoint where a user can pass all the required fields of the EncryptedParams. E.g:
pub enum ExecuteMsg {
...
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
...
}If you want to define a custom message, rename the fields, or add additional fields, there is a helpful trait WithEncryption that you can implement. It simply tells the compiler how to extract the essential parameters from your custom message and turn it into EncryptedParams
trait WithEncryption : Serialize + Clone {
fn encrypted(&self) -> EncryptedParams;
fn is_encrypted(&self) -> bool;
}Implementing the trait for your message will allow you to use other useful methods of the SDK (like handle_encrypted_wrapper) that significantly simplify the development experience.
Example of the implementation for the ExecuteMsg is as follows:
impl WithEncryption for ExecuteMsg {
fn encrypted(&self) -> EncryptedParams {
match self.clone() {
ExecuteMsg::Encrypted {
payload,
payload_signature,
payload_hash,
user_key,
nonce,
} => EncryptedParams {
payload,
payload_signature,
payload_hash,
user_key,
nonce
},
_ => panic!("Not encrypted")
}
}
fn is_encrypted(&self) -> bool {
if ExecuteMsg::Encrypted{..} = self {
true
} else {
false
}
}
}The SDK has multiple data structures that already implement WithEncryption trait and also use the template engine of Rust to make them easily extendable. Take for example the following message:
pub enum GatewayExecuteMsg<E = Option<Empty>>
where E: JsonSchema
{
ResetEncryptionKey {} ,
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
},
Extension {
msg : E
}
}You can define a new message that extends the GatewayExecuteMsg by simply providing a new type for the Extension instead of the default Option<Empty> like this:
// Degine your custom message
#[cw_serde]
pub enum MyCustomMessage {
HandleFoo {}
HandleBar {}
}
// Extend the GatewayExecuteMsg
pub type MyGatewayExecuteMsg = GatewayExecuteMsg<MyCustomMessage>;Your extended type in this case is available under MyGatewayExecuteMsg::Extension variant and you can use it in your contract like this:
/// MyGatewayExecuteMsg
match msg {
...
ResetEncryptionKey => { ... },
MyGatewayExecuteMsg::Extension{msg} => {
/// MyCustomMessage
match msg {
MyCustomMessage::HandleFoo{} => {
// Do something
}
MyCustomMessage::HandleBar{} => {
// Do something
}
}
}
...
}handle_encrypted_wrapper
The encryption logic, handle_encrypted_wrapper, is where the encryption magic happens ⭐
You can review the function in the SDK here. It has the following functionality:
Check if Message is Encrypted:
If the message is encrypted (msg.is_encrypted()), it proceeds with decryption.
Extract Encryption Parameters:
Retrieves the encryption parameters from the message (msg.encrypted()).
Check Nonce:
Ensures the nonce has not been used before to prevent replay attacks.
Load Encryption Wallet:
Loads the encryption wallet from storage.
Decrypt Payload:
Decrypts the payload using the wallet and the provided parameters (payload, user_key, and nonce).
let decrypted = wallet.decrypt_to_payload(
¶ms.payload,
¶ms.user_key,
¶ms.nonce,
)?;decrypt_to_payload uses chacha20poly1305 algorithm
Verify Credentials:
Constructs a CosmosCredential from the decrypted data.
Inserts the nonce into storage to mark it as used.
Verifies the sender using the verify_arbitrary function with the credential.
Deserialize Inner Message:
Converts the decrypted payload into the original message type E.
Ensures the decrypted message is not encrypted (nested encryption is not allowed).
Return Decrypted Message and Updated Info:
Returns the decrypted message and updated MessageInfo with the verified sender.
chacha20poly1305_decrypt
The following function uses the following types for as the input parameters:
cosmwasm_std::Binary,
std::vec::Vec.
[u8]
and others that implement Deref<Target = [u8]> trait
pub fn chacha20poly1305_decrypt(
ciphertext : &impl Deref<Target = [u8]>,
key : &impl Deref<Target = [u8]>,
nonce : &impl Deref<Target = [u8]>,
) -> StdResult<Vec<u8>> {
...
}To verify a message that was was signed through a method cosmos arbitrary (036) message format, you can use the following function:
fn verify_arbitrary<M : Display>(api: &dyn Api, cred: &CosmosCredential<M>) -> StdResult<String>The method takes in a CosmosCredential struct as an argument which is a a helpful wrapper over essential required fields required for the verification:
pub struct CosmosCredential<M = String>
where M: Display
{
/// public key matching the respective secret that was used to sign message
pub pubkey : Binary,
/// signed sha256 digest of a message wrapped in arbitary data (036) object
pub signature : Binary,
/// signed inner message before being wrapped with 036
pub message : M,
/// prefix for the bech32 address on remote cosmos chain
pub hrp : String
}Both CosmosCredential and EncryptedParams can be used with String or base64 encoded Binary types
To generate a preamble message for the cosmos arbitrary (036) message format, you can use the following utility function:
fn preamble_msg_arb_036(signer: &str, data: &str) -> StringThe function uses a hardcoded JSON string with all the required keys present and sorted.
CCL IBC SDK for typescript developers
For encryption, we recommend using @solar-republic/neutrino which has useful chacha20poly1305related functionalities and additional primitives for generating ephemereal lightweight wallets. Installation:
npm install --save @solar-republic/neutrinoFor signing, encoding and other cryptographic needs in the Cosmos ecosystem, it is common to use the suite of @cosmjs packages. You can install the following:
npm install --save @cosmjs/crypto @cosmjs/amino @cosmjs/encodingIf you are developing in the browser environment or connecting to a public network you might also need
npm install --save @cosmjs/stargate
# or
npm install --save @cosmjs/cosmwasm-stargateFor chacha20poly1305, we need to use a crypthographic keypair and it's advised to use one that isn't the same as the user's wallet. The SDK provides a method for generating a new wallet that can be used for encryption purposes. For our purposes, we just need a private / public keys of Secp256k1 type and there are various ways to generate them.
@cosmjs/crypto
import { Slip10Curve, Random, Bip39, Slip10, stringToPath, Secp256k1 } from "@cosmjs/crypto"
const seed = await Bip39.mnemonicToSeed(Bip39.encode(Random.getBytes(16)));
const { privateKey } = Slip10.derivePath(Slip10Curve.Secp256k1, seed, stringToPath("m/44'/1'/0'/0"));
const pair = await Secp256k1.makeKeypair(privateKey);
// must be compressed to 33 bytes from 65
const publicKey = Secp256k1.compressPubkey(pair.pubkey);@solar-republic/neutrino
import { gen_sk, sk_to_pk } from "@solar-republic/neutrino"
const privateKey = gen_sk();
const publicKey = sk_to_pk(privateKey);@secretjs
import { Wallet } from "secretjs";
const { privateKey, publicKey } = new Wallet()Before proceeding to encryption, you might want to create a quering client that will be used for querying the state and contract of the Secret Network. At the very least, it is requited for fetching the public key of a gateway contract for deriving a shared key used later for encryption.
To perform a simple query on a secret contract we can use methods from @solar-republic/neutrino:
import { SecretContract } from "@solar-republic/neutrino"
// create a contract instantse
const contract = await SecretContract(
secretNodeEndpoint,
secretContractAddress
)
// query example:
// get encryption key from a gateway contract
const queryRes = await contract.query({ encryption_key: {} })
// extract res value
const gatewayPublicKey = queryRes[2]For more persistent use-cases you can use secretjs:
import { SecretNetworkClient } from "secretjs"
// create a client:
const client = new SecretNetworkClient({
chainId,
url // endpoint URL of the node
});
// query the contact and get the value directly
const gatewayPublicKey = await client.query.compute.queryContract({
contract_address
code_hash, // optionally
{ encryption_key: {} } // query msg
});To make sure that malicious applications aren't tricking the user into signing an actual blockchain transaction, it is discouraged to sign arbitrary blobs of data. To address the situation, there are various standards that inject additional data to the message before signing it. The most used in the Cosmos ecosystem is defined in ADR 036 which is also used in the SDK.
Most of the Cosmos wallets provide a method for signing arbitrary messages following the mentioned specification.
Here is a definition taken from documentation of Keplr wallet:
// window.keplr.signArbi....
signArbitrary(chainId: string, signer: string, data: string | Uint8Array) : Promise<StdSignature>Although the API method requires a chainId, it is set to empty string before signing the message
Cosmology
Cosmology defines signArbitrary method as part of the interface for their wallet client and provides implementation / integration for every popular Cosmos wallet out there
CosmJS
The logic of the method has already been implemented and proposed as an addition to the library however it has been hanging in a unmerged PR for a while. You can find the full implementation with examples and tests [PR] Here
Manually
Firstly we need to get an amino signer that will be used for generating the signature. @cosmjs has a defined interface OfflineAminoSigner with signAmino and getAccounts methods and any other signer that implements it can be used for the purpose.
// In browser environment we can get it from a wallet extension. E.g with Keplr:
const signer = window.keplr.getOfflineSigner(chainId);
// ...
// In Node environment we can use `Secp256k1Wallet` class that also implements `OfflineAminoSigner` interface
import { Secp256k1Wallet } from "@cosmjs/amino"
// see examples of generating a random above but in this case you will probably be using a persistent one from a .env file
// you can also pass extra options like prefix as the second argument
const signer = await Secp256k1HdWallet.fromMnemonic(userMnemonic)
// ...
// Here we are getting the first accounts from the signer
// accessing the address and renaming it to `signerAddress`
const [{ address : signerAddress }] = await signer.getAccounts();
// ...StdSignDocTo use signAmino, we need to generate a StdSignDoc object that will be used for signing the message to pass as an argument.
CosmJS provides a function for this:
function makeSignDoc(msgs: AminoMsg[], fee: StdFee, chainId: string, memo: string | undefined, accountNumber: number | string, sequence: number | string, timeout_height?: bigint): StdSignDoc;The 036 standard requires the message fields to AminoMsg to be:
type AminoMsg = {
// static type url
type: ;
value: {
// signer address
signer: string;
// plaintext or base64 encoded message
data: string;
}
}As for the rest of the fields, they can be set to an empty string or 0. The final example will look like this:
const data = "my message";
const signDoc = makeSignDoc(
[{ // list of amino messages
type: "sign/MsgSignData",
value: {
signer: signerAddress,
data: data
}
}],
{ gas: "0", amount: [] }, // StdFee
"", // chainId
"", // memo
0, // accountNumber
0 // sequence
// timeout_height
)After getting the document we can sign it with the signer:
const signRes = await signer.signAmino(signerAddress, signDoc);After getting a public key of a gateway contract you can use it to derive a shared key like this:
import { sha256, Random } from "@cosmjs/crypto"
import { fromBase64, toBase64, toAscii } from "@cosmjs/encoding";
import { chacha20_poly1305_seal, ecdh } from "@solar-republic/neutrino"
// this is a dependency of `@solar-republic/neutrino` so consider importing these methos
import { concat, json_to_bytes } from "@blake.regalia/belt";
// ...
// define
//
// `clientPrivateKey`
// `gatewayPublicKey`
// `signerAddress`
//
// like described above
// ...
const sharedKey = sha256(ecdh(clientPrivateKey, gatewayPublicKey))
// We also need to generate a one-time nonce, which can be done like this:
const nonce = Random.getBytes(12)
// Prepare a message for the action you want to take on the contract
const msg = ExecuteMsg { ...}
/// Defining payload structure idential to the Rust data structure
const payload : EncryptedPayload = {
// e.g, cosmos1...
user_address: signerAddress,
// uint8array -> base64 (-> Binary)
user_pubkey: toBase64(signerPubkey),
// e.g. "cosmos" from "cosmos1..."
hrp: signerAddress.split("1")[0],
// or to toBinary(msg) from `@cosmjs/cosmwasm-stargate`
msg: toBase64(json_to_bytes(msg))
}
/// getting the payload ciphertext
const ciphertext = concat(chacha20_poly1305_seal(
sharedKey,
nonce,
// or toUtf8( JSON.stringify(payload) )
json_to_bytes( payload )
));
// finally the payload_hash is sha256 of the ciphertext
const ciphertextHash = sha256(ciphertext);
// ...Produced digest of hashing the ciphertext can be used as our message that we want to sign according to the 036 standard. The final message will look like this:
// calling `makeSignDoc` with nullifed fields like described earlier
const signDoc = getArb36SignDoc(signerAddress, ciphertextHash);
// signing the message
const signRes = await signer.signAmino(signerAddress, signDoc);After this we are getting all the required fields for creating an EncryptedPayload message or an ExecuteMsg::Encrypted { ... }
const encrypted = {
// uint8array -> base64 (-> Binary)
nonce : toBase64(nonce),
// public key of a pair that was used in deriving a shread key
user_key : toBase64(clientPublicKey),
// ciphertext of with the user data and actual message
payload : toBase64(ciphertext),
// sha256 hash of the ciphertext
payload_hash : toBase64(ciphertextHash),
// signatire over sha256( getArb36SignDoc( payload_hash ) ) already in base64
payload_signature : signRes.signature.signature,
}The encrypted message is safe to broadcast over public blockchain and other infrastructure. A common use-case in context of Cosmos account might be broadcasting it over IBC originating from a chain other than the Secret Network.
The potential use-case might involve broadcasting the message by initiating an IBC message directly and attaching the message as a payload (IBC-Hook) or passing the message to a smart contract on a remote chain to process and bridge it to the Secret Network.
Since Cosmwasm is quite flexible with defining messages due to supporting JSON serialization, it is possible the process is very similar in both cases so we only going to cover IBC-hooks for simplicity:
import { MsgTransfer } from "cosmjs-types/ibc/applications/transfer/v1/tx";
import { SigningStargateClient, MsgTransferEncodeObject } from "@cosmjs/stargate";
/// creating a client
const client = await SigningStargateClient(
"https://rpc.cosmoshub.io" // endpoint of the remote network
signer, // offlineSigner
)
// defining the IBC transfer message
const msg : MsgTransferEncodeObject = {
typeUrl: "/ibc.applications.transfer.v1.MsgTransfer",
value: MsgTransfer.fromPartial({
sender: signerAddress,
receiver: secretGatewayContractAddress,
sourceChannel: "channel-0",
sourcePort: "transfer",
timeoutTimestamp: BigInt(
// 5 minutes from now | ms -> ns
Math.floor(Date.now() + 300_000) * 1_000_000
),
// IBC Hook memo msg
memo: JSON.stringify({
wasm: {
// must be same as receiver
contract: secretGatewayContractAddress,
// encrypted message defined above
msg: encrypted
}
})
})
}
// signing and broadcasting the message
const res = await client.signAndBroadcast(signerAddress, [msg])