All pages
Powered by GitBook
1 of 7

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Loading...

Basics

Learn the basics of working with the IBC Developer Toolkit

Overview

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🔥

Cross-chain Messaging with IBC Hooks

IBC Hooks for Cross-Chain Messaging

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.

SDK Requirements

  • IBC Transfer channel between the consumer chain and Secret Network

See Mintscan for a list of existing transfer channels between Cosmos chains and Secret Network

IBC-Hooks

Initiate a contract call with an incoming IBC token transfer using IBC hooks

Overview

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.

Note that the metadata in the memo field is not used within ICS-20 itself, but instead, a middleware or custom CosmWasm contract can wrap around the transfer protocol to parse the metadata and execute custom logic based off of it. See more here.

ICS20 Packet Structure

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"]

If an ICS20 packet is not directed towards wasmhooks, wasmhooks doesn't do anything. If an ICS20 packet is directed towards wasmhooks, and is formatted incorrectly, then wasmhooks returns an error.

ICS20 Packet Execution Flow

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

Auto-wrapping of SNIP-20 Example

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![],
            }),
        ])),
        }
    }

Ack callbacks

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.

Only the IBC packet sender can set the callback

Ack callback implementation

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!(),
    }
}

IBC Relaying with Go Relayer

Learn how to run the Go relayer to create a transfer channel between any Cosmos chain and Secret Network.

Overview

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! 🚀

Installing Go Relayer

Clone the Go relayer repository:

git clone https://github.com/cosmos/relayer.git

Install Go:

brew install go

Set Go path:

export GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bin

Build the Go relayer:

cd relayer
make install

If you run into any errors during installation, you can install without make like so:

export 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.go

To check that the installation was successful, run:

rly version

Which returns:

version: 2.6.0-rc.1
commit: 3b9ec008999973469aeab4bbdbcb44ff4886b8b8
cosmos-sdk: v0.50.5
go: go1.23.4 darwin/arm64

Configuring Go Relayer

The 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 init

And check the config with:

rly config show

Now 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.

Add chain configs

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/neutrontestnet

rly chains add will check the liveliness of the available RPC endpoints for that chain in the chain registry. The command may fail if none of these RPC endpoints are available. In this case, you will want to manually add the chain config.

Create wallet keys

Create 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 key

Query your key balances:

rly query balance secretnetworktestnet secret-test
rly query balance neutrontestnet neutron-test

You can fund your Secret Network testnet wallet here and your Neutron testnet wallet here 🎉

Then, 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:

You can unveil hidden folders on mac (ie ./relayer) with keyboard shortcut : Command + Shift + .

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:443

Configure path metadata in the config file

You 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: null

Create a relayer path:

rly paths new pulsar-3 pion-1 my-path #this is the name of your path 

Check Configuration Status

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 list

Which 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 list
0: secretnetworktestnet-nuetrontestnet -> chns(✔) clnts(✔) conn(✔) (pulsar-3<>pion-1)

In case one of the checks receives a ✘ instead of ✔, you will need to check if you completed all the previous steps correctly.

Starting the Relayer

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}

Further reading:

  • IBC Go Relayer docs

  • Cosmos Go Relayer docs

  • Creating paths across chains

Functions, Methods, and Data Structures

CCL SDK

The Secret Network CCL SDK can be forked here.

Data Structures

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,
}

EncryptedPayload

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,
}

Custom Contract Message

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
       }
   }
}

Extending existing data structures

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
            }
        }
    }
    
    ...
}

Functions and methods

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:

  1. Check if Message is Encrypted:

    • If the message is encrypted (msg.is_encrypted()), it proceeds with decryption.

  2. Extract Encryption Parameters:

    • Retrieves the encryption parameters from the message (msg.encrypted()).

  3. Check Nonce:

    • Ensures the nonce has not been used before to prevent replay attacks.

  4. Load Encryption Wallet:

    • Loads the encryption wallet from storage.

  5. Decrypt Payload:

    • Decrypts the payload using the wallet and the provided parameters (payload, user_key, and nonce).

      let decrypted  = wallet.decrypt_to_payload(
            &params.payload,
            &params.user_key,
            &params.nonce,
        )?;

decrypt_to_payload uses chacha20poly1305 algorithm

  1. 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.

  1. 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).

  1. 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>> {
    ...
}

Various authentication utilities

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) -> String

The function uses a hardcoded JSON string with all the required keys present and sorted.

Typescript SDK

CCL IBC SDK for typescript developers

Typescript Demo

See a fullstack Next.js typescript demo here (using Osmosis Mainnet). Code available here.

Dependencies

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/neutrino

For 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/encoding

If 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-stargate

Note: You can also use any Typescript / Javascript package managers and runtimes e,g, bun, yarn, pnpm etc.

Generating Wallets

For 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()

Query Client

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
});

Signatures

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.

Browser Wallets

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

Getting Signer and Signer Address

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();

//  ...

Generating the message and StdSignDoc

To 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);

Encryption

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);


//    ...

Encrypting + Signing

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,
}

Broadcasting the message

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])