Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
Learn the basics of working with the IBC Developer Toolkit
Learn how to use Secret Network's IBC Developer Toolkit to design dApps with confidential computing.
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 to learn how to use Secret Network's Confidential Computing Layer SDK , or dive into the Key value store tutorial π₯
Learn how to use Secret Network as the confidential computation layer of the Cosmos
For any Cosmos chain, you can use encrypted payloads to execute and query confidential messages on Secret Network smart contracts.
Using our Confidential Computation Layer (CCL) SDK, you can seamlessly handle encrypted payloads, as the master gateway contract on Secret Network automatically decrypts the payload and hands the decrypted payload over to the target contract.
The encryption of the payload is done using the ChaCha20-Poly1305, an authenticated encryption with additional data (AEAD) algorithm.
The key for this symmetric encryption is created by using the Elliptic-curve Diffie-Hellman (ECDH) scheme, comprising of two components:
An extra encryption public key provided from the Secret Gateway Contract
A randomly created (ephemeral) encryption private key on the user side (independent of the user wallet's private key)
Combining both of these keys together via the ECDH Scheme yields our encryption key, which we use to encrypt the payload with ChaCha20-Poly1305.
As a first example for this, we have used the CCL SDK to encrypt a string and subsequently store it in a Secret Network contract.
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:
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"
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
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:
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
should implement the following interface for a sudo message:
Secret Network's IBC CCL SDK is available on IBC hooks-enabled chains with an IBC transfer channel enabled for Secret Network.
Noble
SECRET / channel-88
NOBLE / channel-17
Axelar
SECRET / channel-20
AXELAR / channel-12
Juno
SECRET / channel-8
JUNO / channel-48
Evmos
SECRET / channel-18
EVMOS / channel-15
Osmosis
SECRET / channel-1
OSMOSIS / channel-88
Secret Network's IBC CCL SDK is available on IBC hooks-enabled chains with an IBC transfer channel enabled for Secret Network.
Secret Network's IBC Developer Toolkit is available on any Cosmos chain that has established an IBC transfer channel with Secret Network!
memo["wasm"] has exactly two entries, "contract" and "msg"
memo["wasm"]["msg"] is a valid JSON object
receiver == memo["wasm"]["contract"]
{
//... 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"
}
}
}
}
}#[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![],
}),
])),
}
}#[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!(),
}
}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
Data meant to be encrypted and stored in the payload field of EncryptedParams
Your contract must define an endpoint where a user can pass all the required fields of the EncryptedParams. E.g:
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
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:
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:
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:
Your extended type in this case is available under MyGatewayExecuteMsg::Extension variant and you can use it in your contract like this:
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).
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
To verify a message that was was signed through a method cosmos arbitrary (036) message format, you can use the following function:
The method takes in a CosmosCredential struct as an argument which is a a helpful wrapper over essential required fields required for the verification:
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:
The function uses a hardcoded JSON string with all the required keys present and sorted.
/// 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]
#[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,
}pub enum ExecuteMsg {
...
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
...
}trait WithEncryption : Serialize + Clone {
fn encrypted(&self) -> EncryptedParams;
fn is_encrypted(&self) -> bool;
}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
}
}
}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
}
}// Degine your custom message
#[cw_serde]
pub enum MyCustomMessage {
HandleFoo {}
HandleBar {}
}
// Extend the GatewayExecuteMsg
pub type MyGatewayExecuteMsg = GatewayExecuteMsg<MyCustomMessage>;/// MyGatewayExecuteMsg
match msg {
...
ResetEncryptionKey => { ... },
MyGatewayExecuteMsg::Extension{msg} => {
/// MyCustomMessage
match msg {
MyCustomMessage::HandleFoo{} => {
// Do something
}
MyCustomMessage::HandleBar{} => {
// Do something
}
}
}
...
} let decrypted = wallet.decrypt_to_payload(
¶ms.payload,
¶ms.user_key,
¶ms.nonce,
)?;pub fn chacha20poly1305_decrypt(
ciphertext : &impl Deref<Target = [u8]>,
key : &impl Deref<Target = [u8]>,
nonce : &impl Deref<Target = [u8]>,
) -> StdResult<Vec<u8>> {
...
}fn verify_arbitrary<M : Display>(api: &dyn Api, cred: &CosmosCredential<M>) -> StdResult<String>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
}fn preamble_msg_arb_036(signer: &str, data: &str) -> StringCCL 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:
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:
If you are developing in the browser environment or connecting to a public network you might also need
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
@solar-republic/neutrino
@secretjs
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:
For more persistent use-cases you can use secretjs:
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 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 :
Although the API method requires a chainId, it is set to empty string before signing the message
Cosmology
Cosmology 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
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.
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:
The 036 standard requires the message fields to AminoMsg to be:
As for the rest of the fields, they can be set to an empty string or 0. The final example will look like this:
After getting the document we can sign it with the signer:
After getting a public key of a gateway contract you can use it to derive a shared key like this:
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:
After this we are getting all the required fields for creating an EncryptedPayload message or an ExecuteMsg::Encrypted { ... }
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:
Learn how to use Secret Network as the confidential computation layer of the Cosmos with IBC hooks
Secret Network's Confidential Computation SDK uses IBC hooks to seamlessly handle cross-chain encrypted payloads, which means Cosmos developers can now encrypt and decrypt messages with a simple token transfer.
This tutorial explains how to upload your own Key-value store contract on Secret Network, which you can use to encrypt values on Secret Network and transmit them cross-chain from a Cosmos chain of your choice! After this tutorial, you will have the tools you need to encrypt messages on any IBC hooks-enabled Cosmos chain.
To get started, clone the :
Install the dependencies:
Create an env file. Simply update to omit ".testnet" from the file name and then add your wallet's mnemonics:
Now that you have your environment configured, it's time to upload the encryption contract to Secret Network.
First, compile the contract:
This compiles the contract and creates a wasm file in the following directory:
The files that make up the IBC SDK, including the script to upload the contract to Secret Network, are in .
Compile the typescript files so you can execute them with node:
Once you run the above command, the typescript files in ./src will be compiled as javascript files in ./dist.
Upload and instantiate the encryption contract on Secret Network Mainnet:
In your terminal, a codeID, codeHash, and contractAddress will be returned:
Update with your contractAddress and codeHash accordingly:
Now that you have your encryption smart contract uploaded on Secret Network, let's use it to store encrypted messages from Osmosis Mainnet Most of the ECDH cryptography has been abstracted away so there are only a few values you need to change.
The functions in are helper functions that help us configure cross-chain network clients, IBC token transfers, etc. However, there is also an additional function which executes the gateway contract called ! Execute-gateway.js demonstrates sending a token transfer to store an unencrypted as well as an encrypted message.
To encrypt the payload, run execute-gateway.js:
This will initiate a Token transfer:
As well as an IBC Acknowledgement response:
Congrats! You have now used the Secret Network CCL SDK to encrypt a string on Osmosis Mainnet!
Now that you have used Secret Network's CCL SDK to successfully encrypt a string cross-chain, let's examine the you deployed on Secret Network to understand how everything works underneath the hood.
At a high level, you can think of the SDK like so:
There is a gateway contract deployed to Secret Network, which has the ability to encrypt a string, as well as query the encrypted string.
The gateway contract imports helper functions from the , which is where the gateway contract imports encryption and query types, etc.
Now let's examine each of the gateway contract's files to understand how it encrypts a user-inputted string.
is where we define the keymap that holds our encrypted strings. The keymap SECRETS is designed to store a mapping from account user addresses (as strings) to their secrets (also as strings), using the Bincode2 serialization format.
is where we define the functionality of our gateway contract. It has 2 primary functionalities: storing encrypted strings and querying encrypted strings. Note that the types ExecuteMsg and QueryMsg are defined in the SDK .
GatewayExecuteMsg: Defines execution messages that can be sent to the contract, including resetting an encryption key, sending encrypted data, and extending with additional message types.
GatewayQueryMsg: Defines query messages that can be sent to the contract, including querying for an encryption key, querying with authentication data, querying with a permit, and extending with additional query types.
contains the smart contract logic which allows the following:
Execution: Processes messages to reset the encryption key or store a secret, ensuring only authorized access.
Querying: Handles queries to retrieve the encryption key and perform permissioned queries.
The encryption logic, handle_encrypted_wrapper, is imported from the SDK at . This is where the encryption magic happens β.
You can review the function in the SDK . 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:
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.
In this tutorial, you learned how to utilize Secret Network's Confidential Computation SDK to encrypt and decrypt messages across Cosmos chains using IBC hooks. By following the steps outlined, you successfully:
Configured the development environment with the necessary dependencies and environment variables.
Uploaded and instantiated an encryption contract on the Secret Network mainnet.
Encrypted and transmitted a payload from the Osmosis Mainnet to the Secret mainnet using IBC token transfers.
With these tools and knowledge, you are now equipped to handle cross-chain encrypted payloads on any IBC hooks-enabled Cosmos chain, enhancing the security and confidentiality of your blockchain applications.
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).
npm install --save @solar-republic/neutrinonpm install --save @cosmjs/crypto @cosmjs/amino @cosmjs/encodingnpm install --save @cosmjs/stargate
# or
npm install --save @cosmjs/cosmwasm-stargateimport { 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);import { gen_sk, sk_to_pk } from "@solar-republic/neutrino"
const privateKey = gen_sk();
const publicKey = sk_to_pk(privateKey);import { Wallet } from "secretjs";
const { privateKey, publicKey } = new Wallet()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]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
});// window.keplr.signArbi....
signArbitrary(chainId: string, signer: string, data: string | Uint8Array) : Promise<StdSignature>// 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();
// ...function makeSignDoc(msgs: AminoMsg[], fee: StdFee, chainId: string, memo: string | undefined, accountNumber: number | string, sequence: number | string, timeout_height?: bigint): StdSignDoc;type AminoMsg = {
// static type url
type: ;
value: {
// signer address
signer: string;
// plaintext or base64 encoded message
data: string;
}
}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
)const signRes = await signer.signAmino(signerAddress, signDoc);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);
// ...// calling `makeSignDoc` with nullifed fields like described earlier
const signDoc = getArb36SignDoc(signerAddress, ciphertextHash);
// signing the message
const signRes = await signer.signAmino(signerAddress, signDoc);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,
}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])git clone https://github.com/writersblockchain/cosmos-ccl-sdk.gitnpm installSECRET_MNEMONIC=""
SECRET_CHAIN_ENDPOINT="https://lcd.mainnet.secretsaturn.net"
SECRET_CHAIN_ID="secret-4"
SECRET_TOKEN="uscrt"
CONSUMER_MNEMONIC=""
CONSUMER_CHAIN_ENDPOINT="https://rpc.osmosis.zone:443"
CONSUMER_CHAIN_ID="osmosis-1"
CONSUMER_TOKEN="uosmo"
CONSUMER_PREFIX="osmo"
CONSUMER_GAS_PRICE="0.25"
CONSUMER_DECIMALS=6make build-mainnet-reproducible"./optimized-wasm/gateway_simple.wasm.gz"npm run buildnode dist/deploy.js"code_id": 8882,
"code_hash": "f3c2e28cd1574d128ded60ce967cdb46f7515d807be49127bcc9249c5fd97802",
"address": "secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm"{
"gateway": {
"address": "secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm",
"hash": "d4a018804bf63b6cfd5be52b650368e8ad89f57c66841f6b2da7ee143dfc75fb"
}
}node dist/execute-gateway.jsSending IBC token...
receiver: secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm
token: uosmo
amount: 1
source_channel: channel-88
memo: {"wasm":{"contract":"secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm","msg":{"extension":{"msg":{"store_secret":{"text":"new_text_68ss4"}}}}}}Broadcasted IbcTX. Waiting for Ack: Promise { <pending> }
ibc Ack Received!
info ack: {
packet: {
sequence: '252',
source_port: 'transfer',
source_channel: 'channel-88',
destination_port: 'transfer',
destination_channel: 'channel-1',
data: Uint8Array(887) [
123, 34, 97, 109, 111, 117, 110, 116, 34, 58, 34, 49,
34, 44, 34, 100, 101, 110, 111, 109, 34, 58, 34, 117,
97, 120, 108, 34, 44, 34, 109, 101, 109, 111, 34, 58,
34, 123, 92, 34, 119, 97, 115, 109, 92, 34, 58, 123,
92, 34, 99, 111, 110, 116, 114, 97, 99, 116, 92, 34,
58, 92, 34, 115, 101, 99, 114, 101, 116, 49, 113, 48,
109, 121, 99, 99, 108, 117, 57, 50, 55, 117, 53, 109,
48, 116, 110, 53, 48, 122, 103, 108, 53, 97, 102, 52,
117, 116, 114, 108,
... 787 more items
],
timeout_height: { revision_number: '0', revision_height: '0' },
timeout_timestamp: '1722277043000000000'
}, let decrypted = wallet.decrypt_to_payload(
¶ms.payload,
¶ms.user_key,
¶ms.nonce,
)?;Verifiable on-chain random number generator for the entire Cosmos.
Secret VRF is a provably fair and verifiable on-chain random number generator (RNG) that enables smart contracts to access random values without compromising security or usability. Coupled with cross-chain interoperable smart contracts using , Secret Network enables developers and projects across the Cosmos access to state-of-the-art on-chain RNG.
Use Secret VRF to build reliable smart contracts for any application that relies on unpredictable outcomes:
NFT Minting: Utilize randomness for features like unordered minting, trait randomization, and identity numbering, enhancing the authenticity and security of NFT collections.
Web3 Gaming: Apply randomness in gambling, damage calculation, loot boxes, and boss drops to build trust among players by ensuring fairness and preventing any player from having an unfair advantage.
DAO Operations: Employ randomness for wallet initialization, task assigning, unordered voting/liquidations, and order book ordering, facilitating equitable and secure decentralized governance and operations.
To use SecretVRF on any IBC-enabled chain with IBC hooks, all that is required is:
An IBC transfer channel between Secret Network and the consumer chain that receives randomness.
Uploading the RNG Consumer Contract to your chain and directing it to your chain's transfer channel
Git clone the IBC hooks randomness repository:
Update the and the to the channel-id for your IBC-enabled chain:
Once you have updated the transfer channels, compile the contract:
Upload the compiled contract:
Upon successful , a code_id is returned:
Instantiate the contract with the returned code_id:
Upon succesful , a contract_address is returned:
Now that you've instantiated your randomness contract, all that's left is to request randomness! Simply execute request_random:
A transaction hash will be returned:
Navigate to the recently for your contract:
And then view the magic of cross-chain randomness with IBC hooks π:
Congrats! You've just sent a verifiable on-chain random byte with SecretVRF π.
Secret VRF revolutionizes blockchain applications by providing a secure and verifiable source of randomness, critical for fairness in NFT minting, gaming, and DAO operations. Its seamless integration with IBC hooks enables cross-chain interoperability, allowing developers across the Cosmos ecosystem to build more reliable and elegant smart contracts.
If you have any questions, join our and a Secret developer will assist you ASAP.
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 .
IBC Transfer channel between the consumer chain and Secret Network
git clone https://github.com/SecretFoundation/ibchooks-secretVRF.git//Juno
const SECRET_TRANSFER_CHANNEL_ID: &str = "channel-8";
const CHAIN_TRANSFER_CHANNEL_ID: &str = "channel-48";make build-mainnet-reproduciblejunod tx wasm store consumer-side/artifacts/secret_ibc_rng_consumer_side_proxy.wasm --from <your-wallet> --chain-id juno-1 --node https://rpc.juno.chaintools.tech:443 --gas 1200000 --gas-prices 0.075ujuno{"key":"code_id","value":"4210"}]}]}]"junod tx wasm instantiate 4210 '{}' --label RNG-IBC-JUNO --no-admin --from <your-wallet> --chain-id juno-1 --node https://rpc.juno.chaintools.tech:443 --gas 200000 --gas-prices 0.075ujuno[{"key":"_contract_address","value":"juno1srwcjsaslt9ewujg6wcpcwv08lsrsn6rx6ja5mqx4qngqjh8cugqt73c8m"},junod tx wasm execute "juno1srwcjsaslt9ewujg6wcpcwv08lsrsn6rx6ja5mqx4qngqjh8cugqt73c8m" '{"request_random":{"job_id":"1"}}' --from <your-wallet> --chain-id juno-1 --node https://rpc.juno.chaintools.tech:443 --gas 200000 --gas-prices 0.075ujuno --amount 1ujunotxhash: 92D5BDA1344529B58EAD5A0068A807632F46BCCCF05CF10E67211F9CBBD2A74B{"amount":"1","denom":"transfer/channel-8/ujuno","memo":"{\"wasm\": {\"contract\": \"juno1srwcjsaslt9ewujg6wcpcwv08lsrsn6rx6ja5mqx4qngqjh8cugqt73c8m\", \"msg\": {\"receive_random\": {\"job_id\": \"1\", \"randomness\": \"bjFHP7rrLwP4f6fpGpeTt5+N1zPiTO+y7da7RI9kHzk=\", \"signature\": \"y+Kwu0T2gwRwDZGCdDPzGrm6hE2S2UZF1e1jm47pv85pdRdP7HdOfl6T+VKfAE4hPxSWJ5LBcTSNZ+b0KTe0xQ==\"}}}}","receiver":"juno1srwcjsaslt9ewujg6wcpcwv08lsrsn6rx6ja5mqx4qngqjh8cugqt73c8m","sender":"secret1up0mymn4993hgn7zpzu4je34w0n5s7l0mem7rk"}

A fullstack tutorial for cross-chain confidential voting on IBC-connected chains.
This tutorial explains how to upload a confidential voting contract on Secret Network, which you can execute and query for private voting on any IBC-connected chain. π
The SDK abstracts IBC interactions with the Secret Network for applications that use Cosmos wallets. It introduces a secure method for generating confidential messages and reliably authenticating users at the same time using the chacha20poly1305 algorithm.
In this tutorial you will learn:
How to run the fullstack cross-chain Next.js application in your browser.
How to use the core smart contract logic for confidential cross-chain votes and proposals using IBC hooks.
How to deploy your very own voting contract on Secret Network.
Clone the repo:
cd into next-frontend and install the dependencies:
Add in cosmos-ccl-encrypted-payloads-demo/next-frontend:
Start the application:
The fullstack Next.js application should now be running in your browser! You can use it to create confidential votes and proposals. Now that you have the application running in your browser, let's dive into the smart contract logic!
To encrypt proposals and votes using the SDK, we use the enum called , which wraps the possible actions in the voting smart contract, namely, and , with the SDK encryption logic:
InnerMethods leverage the SDK by using the to structure encrypted execution messages for cross-chain proposal management and voting:
Understanding the Encryption SDK
The magic of Secret Network's encryption SDK happens , with handle_encrypted_wrapper:
The Extension variant inExecuteMsg leverages the functionality of handle_encrypted_wrapper because the wrapper decrypts and verifies the entire message payload before the Extension message is processed. Hereβs how it works:
handle_encrypted_wrapper Applies to the Entire Input:
When the execute function is called, the msg and info parameters are initially encrypted.
handle_encrypted_wrapper
Now that you understand how the encryption SDK functions, let's look how it's connected to the frontend. The Next.js encryption logic can be found in :
:
:
Both of these functions access a confidential voting contract already deployed on Secret Network (at the end of this tutorial, you will learn how to deploy your own).
Then, we call the execute_gateway_contract function, which is where all of the cross-chain SDK logic is implemented using IBC hooks:
You can further examine sendIBCToken and gatewayChachaHookMemo in the CCL-SDK ibc.ts file .
There are two types of queries using the CCL SDK:
Unauthenticated queries ie Extension, which are queries that donβt require sensitive or protected data. Example: Retrieving a list of proposals or votes, which is public.
Authenticated queries ie Inner Queries (WithPermit, WithAuthData), which are queries that require auth_data from the caller for permission validation. Example: MyVote { proposal_id } retrieves a user-specific vote.
To query encrypted votes using the CCL SDK, use the enum , which wraps the possible queries in the voting smart contract, namely, MyVote, with the CCL SDK encryption logic:
InnerMethods leverages the SDK by using the types to query encrypted cross-chain votes. You have the choice of using two different types of encrypted queries: query_with_auth_data and query_with_permit. In this tutorial, you we will learn how to implement query_with_permit.
:
:
Now let's take a look at the frontend code to see how query_with_permit is implemented. π
The Next.js query decryption logic can be found in :
:
:
Because votes are encrypted, we must decrypt them in order to query a wallet's votes. The frontend function securely queries the Secret smart contract using query permits, ensuring that only authorized users can access sensitive data. Query permits are cryptographic credentials that:
Prove the userβs ownership of a wallet.
Allow contracts to verify the userβs identity.
Avoid exposing the private key.
You now should have all the tools you need to use the IBC CCL toolkit! Lastly, let's learn how to deploy your own voting contract on Secret Network π
cd into deploy-scripts and install the dependencies:
Add your wallet mnemonic to . Then compile the contract:
Compile the typescript upload script so you can upload the compiled voting contract:
Once you run the above command, the typescript upload file in ./src will be compiled as javascript file in ./dist.
Upload and instantiate the voting contract on Secret Network Mainnet:
In your terminal, a codeID, codeHash, and contractAddress will be returned:
Finally, update with your contract's code_hash and address:
Congratulations on completing this on using Secret Network's IBC SDK to encrypt votes cross-chain using Secret smart contracts! π You've explored the intricacies of encrypted messaging, cross-chain IBC interactions, and secure smart contract execution using the Secret Network CCL SDK. By building and running the fullstack application, youβve gained hands-on experience in contract deployment, frontend integration, and secure querying with query permits. π
This function decrypts and verifies the wrapper (encrypted payload) to produce:
A decrypted msg (of type ExecuteMsg).
A decrypted info (with verified sender details).
Decrypted msg Can Contain ExecuteMsg::Extension:
After decryption, the msg can match any variant of ExecuteMsg, including ExecuteMsg::Extension.
For example:
Here, Extension contains an additional layer of messages (InnerMethods) which define specific functionality.
How handle_encrypted_wrapper Affects Extension:
The msg inside ExecuteMsg::Extension (i.e., InnerMethods) is also encrypted in the original input.
handle_encrypted_wrapper ensures:
The outer msg is decrypted (revealing Extension).
The inner data (e.g., InnerMethods) is now in cleartext and ready for logical execution.
Without this decryption, the contract could not access or process InnerMethods within the Extension.
Validation and Security:
By verifying and decrypting the entire payload at the wrapper level, the contract ensures:
The Extension message is authentic and unaltered.
The sender (info) is authenticated and valid.
Any operations within InnerMethods (like creating proposals or voting) are authorized based on the decrypted info and secure data.
Extended Queries
Inner Queries
Data Access Level
Public or general data
Private or user-specific data
Authorization
No extra authentication required
Requires auth_data or permit
Processing Function
query::query_extended
query::query_with_auth_data
Use Cases
Public info like proposals, votes
Personal data like a userβs vote

A fullstack tutorial for cross-chain confidential sealed bid auctions on IBC-connected chains.
Overview
This tutorial explains how to upload a confidential sealed bid auction contract on Secret Network, which you can execute and query for private auctions on any IBC-connected chain. π
The SDK abstracts IBC interactions with the Secret Network for applications that use Cosmos wallets. It introduces a secure method for generating confidential messages and reliably authenticating users at the same time using the chacha20poly1305 algorithm.
git clone https://github.com/writersblockchain/cosmos-ccl-encrypted-payloads-democd next-frontend && npm installNEXT_PUBLIC_SECRET_CHAIN_ENDPOINT="https://lcd.mainnet.secretsaturn.net"
NEXT_PUBLIC_SECRET_CHAIN_ID="secret-4"
NEXT_PUBLIC_CONSUMER_CHAIN_ID="osmosis-1"npm run dev#[cw_serde]
pub enum InnerMethods {
CreateProposal {
name: String,
description: String,
end_time: Uint64
},
Vote {
proposal_id: Uint64,
vote: VoteOption
},
}
pub type ExecuteMsg = GatewayExecuteMsg<InnerMethods>;#[cw_serde]
pub enum ExecuteMsg {
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
ResetEncryptionKey { },
Extension {
msg: {
CreateProposal { ... } / Vote { ... },
}
}
}let (
msg,
info
) = sdk::handle_encrypted_wrapper(
deps.api, deps.storage, info, msg
)?; const create_proposal = async (name: string, description: string, end_time: string = "60") => {
const contract = contractConfig.votes;
const msg = { create_proposal: { name, description, end_time } }
return await execute_gateway_contract(contract, msg);
} const vote_proposal = async (proposal_id: string, vote: string) => {
const contract = contractConfig.votes;
const msg = { vote: { proposal_id, vote } }
return await execute_gateway_contract(contract, msg);
}const execute_gateway_contract = async (contract: Contract, msg: object) => {
const ibcConfig = loadIbcConfig(chainId);
const keplrOfflineSigner = (window as any).getOfflineSigner(chainId);
const response = await sendIBCToken(
cosmosjs!,
keplrAddress!,
contract.address,
token!,
"1",
ibcConfig.consumer_channel_id,
await gatewayChachaHookMemo(
keplrOfflineSigner,
{ extension: { msg } },
chainId!,
contract,
)
);
return response;
};#[cw_serde]
pub enum ExtendedQueries {
Proposals {},
Proposal { proposal_id: u64 },
AllVotes { proposal_id: u64 },
}
#[cw_serde]
pub enum InnerQueries {
MyVote { proposal_id: u64 },
}
pub type QueryMsg = GatewayQueryMsg<InnerQueries, sdk::CosmosAuthData, ExtendedQueries>;pub fn query_with_auth_data(
deps : Deps,
env : Env,
auth_data : CosmosAuthData,
query : InnerQueries
) -> StdResult<Binary> {
auth_data.verify(deps.api)?;
auth_data.check_data(deps.storage, &env)?;
let address = auth_data.primary_address()?;
query_inner(deps, env,address, query)
}pub fn query_with_permit(
deps : Deps,
env : Env,
permit : Permit,
hrp : Option<String>,
query : InnerQueries
) -> StdResult<Binary> {
let address = secret_toolkit::permit::validate(
deps,
PERMIT_PREFIX,
&permit,
env.contract.address.to_string(),
hrp.as_deref()
)?;
query_inner(deps, env, address, query)
}const query_proposals = (): Promise<[number, Proposal][]> => {
return query_contract_public(contractConfig.votes, { extension: { query: { proposals: { } } } });
}const query_my_vote = (proposal_id: number, message?: string) => {
return query_contract_auth(contractConfig.votes, { my_vote: { proposal_id } }, message);
}const query_contract_auth = async (
contract: Contract,
query: object,
data: string = "Query Permit"
): Promise<any> => {
const storageKey = `${keplrAddress}:${contract.address}:queryPermit}`;
const queryPermitStored = localStorage.getItem(storageKey);
let credential: CosmosCredential;
if (queryPermitStored) {
credential = JSON.parse(queryPermitStored) as CosmosCredential;
} else {
const toSign: DataToSign = {
chain_id: "secret-4",
contract_address: contract.address,
nonce: toBase64(Random.getBytes(32)),
data: btoa(data)
}
const message = toUtf8(JSON.stringify(toSign));
const signRes = await (window as any).keplr.signArbitrary(chainId, keplrAddress!, JSON.stringify(toSign))
credential = {
message: toBase64(message),
signature: signRes.signature,
pubkey: signRes.pub_key.value,
hrp: keplrAddress!.split("1")[0]
}
localStorage.setItem(storageKey, JSON.stringify(credential));
}
const res = await queryGatewayAuth(contract, query, [credential]);
console.log("query:", query, " res:", res);
return res;
}cd deploy-scripts && npm installmake build-mainnet-reproduciblenpm run buildnode dist/deploy_voting.js"code_id": 8882,
"code_hash": "f3c2e28cd1574d128ded60ce967cdb46f7515d807be49127bcc9249c5fd97802",
"address": "secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm"const contractMultiConfig: ContractMultiConfig = {
votes: {
address: "secret1jtc7f8cj5hhc2mg9v5uknd84knvythvsjhd66a",
hash: "ff8443878e8a339637c45c13abc4385c4f0c5668b992afc912e5f59e5d098654"
},
}; let (msg, info) = sdk::handle_encrypted_wrapper(deps.api, deps.storage, info, msg)?;ExecuteMsg::Extension { msg } => {
match msg {
InnerMethods::CreateProposal { name, description, end_time } => { /* logic */ },
InnerMethods::Vote { proposal_id, vote } => { /* logic */ },
}
},View the typescript SDK here, which we will learn how to implement shortly π
In this tutorial you will learn:
How to run the fullstack cross-chain Next.js application in your browser.
How to use the core smart contract logic for confidential cross-chain auctions using IBC hooks.
How to deploy your very own auction contract on Secret Network.
Clone the repo:
cd into next-frontend and install the dependencies:
Add environment variables in cosmos-ccl-encrypted-payloads-demo/next-frontend:
Start the application:
The fullstack Next.js application should now be running in your browser! You can use it to create confidential auctions and bids. Now that you have the application running in your browser, let's dive into the smart contract logic!
To encrypt auction bids using the SDK, we use the enum called InnerMethods, which wraps the possible actions in the auction smart contract, namely, CreateAuctionItem and Bid, with the SDK encryption logic:
InnerMethods leverage the SDK by using the GatewayExecuteMsg to structure encrypted execution messages for cross-chain auction management and bidding:
Understanding the Encryption SDK
The magic of Secret Network's encryption SDK happens here, with handle_encrypted_wrapper:
The Extension variant inExecuteMsg leverages the functionality of handle_encrypted_wrapper because the wrapper decrypts and verifies the entire message payload before the Extension message is processed. Hereβs how it works:
handle_encrypted_wrapper Applies to the Entire Input:
When the execute function is called, the msg and info parameters are initially encrypted.
handle_encrypted_wrapper is invoked with these parameters:
This function decrypts and verifies the wrapper (encrypted payload) to produce:
A decrypted msg (of type ExecuteMsg).
A decrypted info (with verified sender details).
Decrypted msg Can Contain ExecuteMsg::Extension:
After decryption, the msg can match any variant of ExecuteMsg, including ExecuteMsg::Extension.
How handle_encrypted_wrapper Affects Extension:
The msg inside ExecuteMsg::Extension (i.e., InnerMethods) is also encrypted in the original input.
Validation and Security:
By verifying and decrypting the entire payload at the wrapper level, the contract ensures:
The Extension message is authentic and unaltered.
Now that you understand how the encryption SDK functions, let's look how it's connected to the frontend. The Next.js encryption logic can be found in Gateway.ts:
Both of these functions access a confidential auction contract already deployed on Secret Network (at the end of this tutorial, you will learn how to deploy your own).
Then, we call the execute_gateway_contract function, which is where all of the cross-chain SDK logic is implemented using IBC hooks:
You can further examine sendIBCToken and gatewayChachaHookMemo in the CCL-SDK ibc.ts file here.
There are two types of queries using the CCL SDK:
Unauthenticated queries ie Extension, which are queries that donβt require sensitive or protected data. Example: Retrieving a list of auctions, which is public.
Authenticated queries ie Inner Queries (WithPermit, WithAuthData), which are queries that require auth_data from the caller for permission validation. Example: MyBid { auction_id: u64 } retrieves a user-specific bid.
Extended Queries
Inner Queries
Data Access Level
Public or general data
Private or user-specific data
Authorization
No extra authentication required
Requires auth_data or permit
Processing Function
query::query_extended
query::query_with_auth_data
Use Cases
Public info like auctions
Personal data like a userβs bids
To query encrypted bids using the CCL SDK, use the enum InnerQueries, which wraps the possible queries in the auction smart contract, namely, MyBid, with the CCL SDK encryption logic:
InnerMethods leverages the SDK by using the GatewayQueryMsg types to query encrypted cross-chain bids. You have the choice of using two different types of encrypted queries: query_with_auth_data and query_with_permit. In this tutorial, you we will learn how to implement query_with_permit.
Now let's take a look at the frontend code to see how query_with_permit is implemented. π
The Next.js query decryption logic can be found in Gateway.ts:
Because bids are encrypted, we must decrypt them in order to query a wallet's bids. The frontend function query_contract_auth securely queries the Secret smart contract using query permits, ensuring that only authorized users can access sensitive data. Query permits are cryptographic credentials that:
Prove the userβs ownership of a wallet.
Allow contracts to verify the userβs identity.
Avoid exposing the private key.
You now should have all the tools you need to use the IBC CCL toolkit! Lastly, let's learn how to deploy your own auction contract on Secret Network π
cd into deploy-scripts and install the dependencies:
Add your wallet mnemonic to .env. Then compile the contract:
Compile the typescript upload script so you can upload the compiled vauction contract:
Once you run the above command, the typescript upload file in ./src will be compiled as javascript file in ./dist.
Upload and instantiate the voting contract on Secret Network Mainnet:
In your terminal, a codeID, codeHash, and contractAddress will be returned:
Finally, update config.ts with your contract's code_hash and address:
Congratulations on completing this on using Secret Network's IBC SDK to encrypt auction bids cross-chain using Secret Network smart contracts! π You've explored the intricacies of encrypted messaging, cross-chain IBC interactions, and secure smart contract execution using the Secret Network CCL SDK. By building and running the fullstack IBC application, youβve gained hands-on experience in contract deployment, frontend integration, and secure querying with Secret Network query permits. π
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 :
Install Go:
Set Go path:
Build the Go relayer:
To check that the installation was successful, run:
Which returns:
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:
And check the config with:
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.
The rly chains add command fetches chain metadata from the and adds it to your config file:
Create new keys for the relayer to use when signing and relaying transactions:
Query your key balances:
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 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:
Create a relayer path:
Before starting to relay and after making some changes to the config, you can check the status of the chains in the config:
Which returns this output when healthy:
And you can check the status of the paths in the config:
Finally, start the relayer on the desired path. The relayer will periodically update the clients and listen for IBC messages to relay:
Congrats! You are now relaying between Secret Network testnet and Neutron testnet! π
git clone https://github.com/writersblockchain/cosmos-ccl-encrypted-payloads-democd next-frontend && npm installNEXT_PUBLIC_SECRET_CHAIN_ENDPOINT="https://lcd.mainnet.secretsaturn.net"
NEXT_PUBLIC_SECRET_CHAIN_ID="secret-4"
NEXT_PUBLIC_CONSUMER_CHAIN_ID="osmosis-1"npm run dev#[cw_serde]
pub enum InnerMethods {
CreateAuctionItem {
name: String,
description: String,
end_time: Uint64
},
Bid {
auction_id: Uint64,
amount: Uint128
},
}
pub type ExecuteMsg = GatewayExecuteMsg<InnerMethods>;#[cw_serde]
pub enum ExecuteMsg {
Encrypted {
payload : Binary,
payload_signature : Binary,
payload_hash : Binary,
user_key : Binary,
nonce : Binary,
}
ResetEncryptionKey { },
Extension {
msg: {
CreateAuctionItem { ... } / Bid { ... },
}
}
}let (
msg,
info
) = sdk::handle_encrypted_wrapper(
deps.api, deps.storage, info, msg
)?; const create_auction = async (name: string, description: string, end_time: string = "60") => {
const contract = contractConfig.auctions;
const msg = { create_auction_item: { name, description, end_time } }
return await execute_gateway_contract(contract, msg);
} const bid_auction = async (auction_id: string, amount: string) => {
const contract = contractConfig.auctions;
const msg = { bid: { auction_id, amount } }
return await execute_gateway_contract(contract, msg);
}const execute_gateway_contract = async (contract: Contract, msg: object) => {
const ibcConfig = loadIbcConfig(chainId);
const keplrOfflineSigner = (window as any).getOfflineSigner(chainId);
const response = await sendIBCToken(
cosmosjs!,
keplrAddress!,
contract.address,
token!,
"1",
ibcConfig.consumer_channel_id,
await gatewayChachaHookMemo(
keplrOfflineSigner,
{ extension: { msg } },
chainId!,
contract,
)
);
return response;
};#[cw_serde]
pub enum ExtendedQueries {
Auctions {},
Auction { auction_id: u64 },
AllBids { auction_id: u64 },
Result { auction_id: u64 },
}
#[cw_serde]
pub enum InnerQueries {
MyBid { auction_id: u64 },
}
pub type ExecuteMsg = GatewayExecuteMsg<InnerMethods>;
pub type QueryMsg = GatewayQueryMsg<InnerQueries, CosmosAuthData, ExtendedQueries>pub fn query_with_auth_data(
deps : Deps,
env : Env,
auth_data : CosmosAuthData,
query : InnerQueries
) -> StdResult<Binary> {
auth_data.verify(deps.api)?;
auth_data.check_data(deps.storage, &env)?;
let address = auth_data.primary_address()?;
query_inner(deps, env,address, query)
}pub fn query_with_permit(
deps : Deps,
env : Env,
permit : Permit,
hrp : Option<String>,
query : InnerQueries
) -> StdResult<Binary> {
let address = secret_toolkit::permit::validate(
deps,
PERMIT_PREFIX,
&permit,
env.contract.address.to_string(),
hrp.as_deref()
)?;
query_inner(deps, env, address, query)
} const query_auctions = () => {
return query_contract_public(contractConfig.auctions, { extension: { query: { auctions: { } } } });
} const query_my_bid = (auction_id: number, message?: string) => {
return query_contract_auth(contractConfig.auctions, { my_bid: { auction_id } }, message);
}const query_contract_auth = async (
contract: Contract,
query: object,
data: string = "Query Permit"
): Promise<any> => {
const storageKey = `${keplrAddress}:${contract.address}:queryPermit}`;
const queryPermitStored = localStorage.getItem(storageKey);
let credential: CosmosCredential;
if (queryPermitStored) {
credential = JSON.parse(queryPermitStored) as CosmosCredential;
} else {
const toSign: DataToSign = {
chain_id: "secret-4",
contract_address: contract.address,
nonce: toBase64(Random.getBytes(32)),
data: btoa(data)
}
const message = toUtf8(JSON.stringify(toSign));
const signRes = await (window as any).keplr.signArbitrary(chainId, keplrAddress!, JSON.stringify(toSign))
credential = {
message: toBase64(message),
signature: signRes.signature,
pubkey: signRes.pub_key.value,
hrp: keplrAddress!.split("1")[0]
}
localStorage.setItem(storageKey, JSON.stringify(credential));
}
const res = await queryGatewayAuth(contract, query, [credential]);
console.log("query:", query, " res:", res);
return res;
}cd deploy-scripts && npm installmake build-mainnet-reproduciblenpm run buildnode dist/deploy_auction.js"code_id": 8882,
"code_hash": "f3c2e28cd1574d128ded60ce967cdb46f7515d807be49127bcc9249c5fd97802",
"address": "secret1q0mycclu927u5m0tn50zgl5af4utrlkzz706lm"const contractMultiConfig: ContractMultiConfig = {
auctions: {
address: "secret1jtc7f8cj5hhc2mg9v5uknd84knvythvsjhd66a",
hash: "ff8443878e8a339637c45c13abc4385c4f0c5668b992afc912e5f59e5d098654"
},
};Here, Extension contains an additional layer of messages (InnerMethods) which define specific functionality.
handle_encrypted_wrapper ensures:The outer msg is decrypted (revealing Extension).
The inner data (e.g., InnerMethods) is now in cleartext and ready for logical execution.
Without this decryption, the contract could not access or process InnerMethods within the Extension.
infoAny operations within InnerMethods (like bidding) are authorized based on the decrypted info and secure data.

git clone https://github.com/cosmos/relayer.gitbrew install goexport GOPATH=$HOME/go
export PATH=$PATH:$GOPATH/bincd 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.gorly versionversion: 2.6.0-rc.1
commit: 3b9ec008999973469aeab4bbdbcb44ff4886b8b8
cosmos-sdk: v0.50.5
go: go1.23.4 darwin/arm64rly config initrly config showrly chains add testnets/secretnetworktestnet
rly chains add testnets/neutrontestnetrly keys add secretnetworktestnet secret-test #this is the name of your key
rly keys add neutrontestnet neutron-test #this is the name of your keyrly query balance secretnetworktestnet secret-test
rly query balance neutrontestnet neutron-testchains:
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:443global:
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: nullrly paths new pulsar-3 pion-1 my-path #this is the name of your path rly chains list0: pulsar-3 -> type(cosmos) key(β) bal(β) path(β)
1: pion-1 -> type(cosmos) key(β) bal(β) path(β)rly paths list0: secretnetworktestnet-nuetrontestnet -> chns(β) clnts(β) conn(β) (pulsar-3<>pion-1)rly start my-path 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} let (msg, info) = sdk::handle_encrypted_wrapper(deps.api, deps.storage, info, msg)?;ExecuteMsg::Extension { msg } => {
match msg {
InnerMethods::CreateAuctionItem { name, description, end_time } => { /* logic */ },
InnerMethods::Bid { auction_id,
amount } => { /* logic */ },
}
},