✨Works out of the box guarantee. If you face any issue at all, hit us up on Telegram and we will write the integration for you.

Cosmos
Neutron-Terra(IBC)

Publish on chain using CosmWasm

Pre-requisite

At this stage, we assume that you followed the steps at ReactJs.

We will be using

You can access the code of this walkthrough on Gitlab:

Contract deployment

Clone the client contract repo.

The Neutron-Client (opens in a new tab) contract interacts with Terra-host contract for sending verification requsts. The Terra-host (opens in a new tab) contract receives the request, relays it to the Core Verification contract deployed on the same network and records the result in itself.

git clone https://gitlab.reclaimprotocol.org/starterpacks/reclaim-neutron-terra-client

Add your wallet information.

  1. Neutron-Client

Add your wallet credentials to your wasmkit.config.js as specified in wasmkit.config.js.example.

const neutron_mainnet_accounts = [
  {
    name: 'account_0',
    address: '', // your neutron address
    mnemonic: ''// your mnemonic key
  },
];
  1. Terra-Host

Make an .env file in the scripts folder and add your wallet MNEMONIC there.

MNEMONIC="..."

Deploy and verify a proof.

  1. Neutron-Client

Take a look at wasmkit.config.js, make sure that you have the correct RPCs and contract addresses.

By running the following commands, you can compile, upload the contract and instantiate it.

wasmkit compile
wasmkit run scripts/deploy.ts

Run the script and take a note of your contract address, we will be using them later. To verify a Reclaim proof on it, run wasmkit run scripts/verifyProof.ts.

npm i
wasmkit run scripts/verifyProof.ts

Here is an example of how your output should look like:

storeCode instantiate&verify

  1. Terra-host

Run the following commands to upload the contract and instantiate it.

wasmkit compile
cd Terra
npm i
node deploy.js

Take a note of the deployed contract address. We're going to need it.

IBC Relayer Configuration

Adding a private key

Prepare two wallets(Neutron and Terra) with enough funds in them. They will be used for executing IBC messages on both chains.

Run the following commands for setting up accounts.

echo word1 ... word12or24 > mnemonic_file_neutron
hermes keys add --key-name keyneutron --chain neutron-1 --mnemonic-file mnemonic_file_neutron
rm mnemonic_file_neutron
 
echo word1 ... word12or24 > mnemonic_file_terra
hermes keys add --key-name keyterra --chain phoenix-1 --mnemonic-file mnemonic_file_terra
rm mnemonic_file_terra
 

Warning: If you look at the keyterra.json file in the $HOME/.hermes/keys/phoenix-1/keyring-test, you may see a different account address from your wallet address.

That is because Terra's address generation algorithm is different from other cosmos chains and hermes as well.

You should send funds to the account adddress in the json file, as that is the actual address that executes the messages.

Configuration file

The command hermes config auto provides a way to automatically generate a configuration file for the chains:

hermes config auto --output $HOME/.hermes/config.toml --chain neutron:keyneutron terra2:keyphoenix --chain 

If the command runs successfully, it should output something like:

2024-05-04T01:06:45.546898Z  INFO ThreadId(01) running Hermes v1.8.2+06dfbaf
2024-05-04T01:06:45.550373Z  INFO ThreadId(11) neutron: Fetching asset list...
2024-05-04T01:06:45.550395Z  INFO ThreadId(10) terra2: Fetching asset list...
2024-05-04T01:06:45.550406Z  INFO ThreadId(13) terra2: Fetching chain data...
2024-05-04T01:06:45.550409Z  INFO ThreadId(12) neutron: Fetching chain data...
2024-05-04T01:06:49.118997Z  INFO ThreadId(01) phoenix-1: uses key 'keyphoenix'
2024-05-04T01:06:49.140496Z  INFO ThreadId(01) neutron-1: uses key 'keyneutron'
SUCCESS "Config file written successfully : $HOME/.hermes/config.toml."

(Make sure that each chain uses the correct corresponding key.)

And generate the following configuration:

[global]
log_level = "info"
 
[mode.clients]
enabled = true
refresh = true
misbehaviour = true
 
[mode.connections]
enabled = false
 
[mode.channels]
enabled = false
 
[mode.packets]
enabled = true
clear_interval = 100
clear_on_start = true
tx_confirmation = false
auto_register_counterparty_payee = false
 
[mode.packets.ics20_max_memo_size]
enabled = true
size = 32768
 
[mode.packets.ics20_max_receiver_size]
enabled = true
size = 2048
 
[rest]
enabled = false
host = "127.0.0.1"
port = 3000
 
[telemetry]
enabled = false
host = "127.0.0.1"
port = 3001
 
[telemetry.buckets.latency_submitted]
start = 500
end = 20000
buckets = 10
 
[telemetry.buckets.latency_confirmed]
start = 1000
end = 30000
buckets = 10
 
[[chains]]
type = "CosmosSdk"
id = "phoenix-1"
rpc_addr = "https://terra2.tdrsys.com:2053/"
grpc_addr = "https://terra2.tdrsys.com:2083/"
rpc_timeout = "10s"
trusted_node = false
account_prefix = "terra"
key_name = "keyphoenix"
key_store_type = "Test"
store_prefix = "ibc"
default_gas = 1000000
max_gas = 4000000
gas_multiplier = 1.1
max_msg_num = 30
max_tx_size = 180000
max_grpc_decoding_size = 33554432
query_packets_chunk_size = 50
clock_drift = "5s"
max_block_time = "30s"
client_refresh_rate = "1/3"
ccv_consumer_chain = false
memo_prefix = ""
sequential_batch_tx = false
 
[chains.event_source]
mode = "push"
url = "wss://terra2.tdrsys.com:2053/websocket"
batch_delay = "500ms"
 
[chains.trust_threshold]
numerator = 2
denominator = 3
 
[chains.gas_price]
price = 0.015
denom = "uluna"
 
[chains.packet_filter]
policy = "allow"
list = [
    [
    "transfer",
    "channel-229",
],
    [
    "wasm.terra1jhfjnm39y3nn9l4520mdn4k5mw23nz0674c4gsvyrcr90z9tqcvst22fce",
    "channel-167",
],
    [
    "wasm.terra1k9j8rcyk87v5jvfla2m9wp200azegjz0eshl7n2pwv852a7ssceqsnn7pq",
    "channel-396",
],
]
 
[chains.packet_filter.min_fees]
 
[chains.dynamic_gas_price]
enabled = true
multiplier = 1.2
max = 0.6
 
[chains.address_type]
derivation = "cosmos"
 
[chains.excluded_sequences]
 
[[chains]]
type = "CosmosSdk"
id = "neutron-1"
rpc_addr = "https://rpc-neutron.whispernode.com/"
grpc_addr = "https://neutron-grpc.publicnode.com/"
rpc_timeout = "10s"
trusted_node = false
account_prefix = "neutron"
key_name = "keyneutron"
key_store_type = "Test"
store_prefix = "ibc"
default_gas = 1000000
max_gas = 4000000
gas_multiplier = 1.2
max_msg_num = 30
max_tx_size = 180000
max_grpc_decoding_size = 33554432
query_packets_chunk_size = 50
clock_drift = "5s"
max_block_time = "30s"
client_refresh_rate = "1/3"
ccv_consumer_chain = false
memo_prefix = ""
sequential_batch_tx = false
 
[chains.event_source]
mode = "push"
url = "wss://rpc-neutron.cosmos-spaces.cloud/websocket"
batch_delay = "500ms"
 
[chains.trust_threshold]
numerator = 2
denominator = 3
 
[chains.gas_price]
price = 0.0053
denom = "untrn"
 
[chains.packet_filter]
policy = "allow"
list = [
    [
    "transfer",
    "channel-25",
],
    [
    "transfer",
    "channel-5",
],
    [
    "wasm.neutron1ffus553eet978k024lmssw0czsxwr97mggyv85lpcsdkft8v9ufsz3sa07",
    "channel-2112",
],
]
 
[chains.packet_filter.min_fees]
 
[chains.dynamic_gas_price]
enabled = true
multiplier = 1.2
max = 0.6
 
[chains.address_type]
derivation = "cosmos"
 
[chains.excluded_sequences]
 
[tracing_server]
enabled = false
port = 5555
 

Take special care to the highlighted lines. Your setup transaction may fail if the gas settings are not properly configured. Refer them and make your own adjustion.

Creating a channel

Run the following command for creating a channel between the two contracts you have already deployed. Replace the contract addresses with your own.

The channel version should be identical to the one set in your contract.

hermes create channel --a-chain phoenix-1 --a-connection connection-192 
--a-port wasm.terra1gmn5mmqjssvtscr3ce7n6ztd5dgacg0kwws6dw3ugu20j7nny0vqj5zw4f 
--b-port wasm.neutron1zcytw9p07ytdskz4wk2txyqwlggpenly5d0adxsr4qlm6hfc2r8seqaezl 
--channel-version ibc-cosmwasm

Now your two contracts can interact with each other.

Perform a health-check to verify that your setup is correct with:

hermes health-check

Save the channel id in the result and pass them as a parameter in the verifyProof query.

React client development

Bootstrap project and install packages.

Add the following to project dependency(packages.json) and install modules.

"dependencies": {
    "@cosmjs/stargate": "^0.32.0",
    "@cosmjs/proto-signing": "^0.32.0",
    "@cosmjs/crypto": "^0.32.0",
    "@cosmjs/encoding": "^0.32.0",
    "@cosmjs/math": "^0.32.0",
    "@cosmjs/cosmwasm-stargate": "^0.32.0",
    "@terra-money/terra.js": "^3.1.10",
    "@reclaimprotocol/js-sdk": "^0.1.5",
    "react-qr-code": "^2.0.12",
    ...
}
npm i

Setup your React codebase.

We will continue building on the reclaim react client (opens in a new tab), new lines are highlighted below.

import "./App.css";
import { Reclaim } from "@reclaimprotocol/js-sdk";
import { useState } from "react";
import QRCode from "react-qr-code";
 
function App() {
  const [url, setUrl] = useState("");
  const [ready, setReady] = useState(false);
  const [proof, setProof] = useState({});
 
  const reclaimClient = new Reclaim.ProofRequest("YOUR_APPLICATION_ID_HERE"); //TODO: replace with your applicationId
 
  async function generateVerificationRequest() {
    const providerId = "PROVIDER_ID"; //TODO: replace with your provider ids you had selected while creating the application
 
    reclaimClient.addContext(
      `user's address`,
      "for acmecorp.com on 1st january"
    );
 
    await reclaimClient.buildProofRequest(providerId);
 
    reclaimClient.setSignature(
      await reclaimClient.generateSignature(
        APP_SECRET //TODO : replace with your APP_SECRET
      )
    );
 
    const { requestUrl, statusUrl } =
      await reclaimClient.createVerificationRequest();
 
    setUrl(requestUrl);
 
    await reclaimClient.startSession({
      onSuccessCallback: (proofs) => {
        console.log("Verification success", proofs);
        setReady(true);
        setProof(proofs[0]);
        // Your business logic here
      },
      onFailureCallback: (error) => {
        console.error("Verification failed", error);
        // Your business logic here to handle the error
      },
    });
  }
 
  return (
    <div className="App">
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          height: "50vh",
        }}
      >
        {!url && (
          <button onClick={generateVerificationRequest}>
            Create Claim QrCode
          </button>
        )}
        {url && <QRCode value={url} />}
      </div>
    </div>
  );
}
 
export default App;

Create a new folder (utilities).

Structure the folder as per the following, these are configs to call Reclaim on Neutron.

src/
 |- utilities/NeutronContext.js
 |- utilities/NeutronFunctions.js
 |- App.js

Copy this to NeutronContext.js.

By default, the values below are for NeutronContext's testnet, uncomment for mainnet.

import { createContext, useState } from "react";
import { SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate";
 
const NeutronContext = createContext(null);
const NEUTRON_CHAIN_ID = "neutron-1";
const NEUTRON_LCD = "https://rpc-kralum.neutron-1.neutron.org";
 
const NeutronContextProvider = ({ children }) => {
  const [neutronClient, setNeutronClient] = useState(null);
  const [neutronAddress, setNeutronAddress] = useState("");
 
  async function setupKeplr(setNeutronClient, setNeutronAddress) {
    const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
    while (
      !window.keplr ||
      !window.getEnigmaUtils ||
      !window.getOfflineSignerOnlyAmino
    ) {
      await sleep(50);
    }
 
    await window.keplr.enable(NEUTRON_CHAIN_ID);
    window.keplr.defaultOptions = {
      sign: {
        preferNoSetFee: false,
        disableBalanceCheck: true,
      },
    };
 
    const keplrOfflineSigner =
      window.getOfflineSignerOnlyAmino(NEUTRON_CHAIN_ID);
    const accounts = await keplrOfflineSigner.getAccounts();
 
    console.log(accounts[0].address);
    const neutronAddress = accounts[0].address;
 
    const neutronClient = await SigningCosmWasmClient.connectWithSigner(NEUTRON_LCD, keplrOfflineSigner);
 
    setNeutronAddress(neutronAddress);
    setNeutronClient(neutronClient);
  }
 
  async function connectWallet() {
    try {
      if (!window.keplr) {
        console.log("intall keplr!");
      } else {
        await setupKeplr(setNeutronClient, setNeutronAddress);
        localStorage.setItem("keplrAutoConnect", "true");
        console.log(neutronAddress);
      }
    } catch (error) {
      alert(
        "An error occurred while connecting to the wallet. Please try again."
      );
    }
  }
 
  function disconnectWallet() {
    // reset neutronClient and neutronAddress
    setNeutronAddress("");
    setNeutronClient(null);
 
    // disable auto connect
    localStorage.setItem("keplrAutoConnect", "false");
 
    // console.log for success
    console.log("Wallet disconnected!");
  }
 
  async function addNeutronToKeplr () {
        try {
          if (!window.keplr) {
            alert("Intall keplr!");
          } else {
            const chainConfig = {
              "rpc": "https://rpc-neutron.keplr.app",
              "rest": "https://lcd-neutron.keplr.app",
              "chainId": "neutron-1",
              "chainName": "Neutron",
              "chainSymbolImageUrl": "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/neutron/chain.png",
              "bip44": {
                "coinType": 118
              },
              "bech32Config": {
                "bech32PrefixAccAddr": "neutron",
                "bech32PrefixAccPub": "neutronpub",
                "bech32PrefixValAddr": "neutronvaloper",
                "bech32PrefixValPub": "neutronvaloperpub",
                "bech32PrefixConsAddr": "neutronvalcons",
                "bech32PrefixConsPub": "neutronvalconspub"
              },
              "currencies": [
                {
                  "coinDenom": "NTRN",
                  "coinMinimalDenom": "untrn",
                  "coinDecimals": 6,
                  "coinGeckoId": "neutron-3",
                  "coinImageUrl": "https://raw.githubusercontent.com/chainapsis/keplr-chain-registry/main/images/neutron/untrn.png"
                }
              ],
              "feeCurrencies": [
                {
                  "coinDenom": "NTRN",
                  "coinMinimalDenom": "untrn",
                  "coinDecimals": 6,
                  "coinGeckoId": "neutron-3",
                  "gasPriceStep": {
                    "low": 0.0053,
                    "average": 0.0053,
                    "high": 0.0053
                  }
                }
              ],
              "features": ["cosmwasm"]
            }
          await window.keplr.experimentalSuggestChain(chainConfig);
      }
    } catch (error) {
      alert(
        "An error occurred while connecting to the wallet. Please try again."
      );
    }
  }
 
  return (
    <NeutronContext.Provider
      value={{
        neutronClient,
        setNeutronClient,
        neutronAddress,
        setNeutronAddress,
        connectWallet,
        disconnectWallet,
        addNeutronToKeplr
      }}
    >
      {children}
    </NeutronContext.Provider>
  );
};
 
export { NeutronContext, NeutronContextProvider };

Copy this to NeutronFunctions.js.

Replace with your contract address. You can leave values as are if you did not do the deployment step, make sure to uncomment values for mainnet.

import { useContext } from "react";
import { NeutronContext } from "./NeutronContext";
import { LCDClient } from '@terra-money/terra.js';
import { calculateFee, GasPrice } from "@cosmjs/stargate";
 
const contractAddress = "neutron1zcytw9p07ytdskz4wk2txyqwlggpenly5d0adxsr4qlm6hfc2r8seqaezl";
const channel_id = "channel-4282";
const terra2Address = "terra13z58nvwxf5hjx0l7pwd72tt4dd2ycvj3zp3n5m5vtr3wnfw2jdzss35maw";
const terra2URL = "https://luna.nownodes.io/8e88b7de-791c-4ec1-9892-535929724e87";
const terra2ChainId = "phoenix-1";
 
const NeutronFunctions = () => {
  const { neutronClient, neutronAddress } = useContext(NeutronContext);
 
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
 
  let verify_proof = async (proof) => {
    const defaultGasPrice = GasPrice.fromString("0.025untrn");
    const defaultExecuteFee = calculateFee(200_000, defaultGasPrice);
 
    console.log(proof);
 
    const terra = new LCDClient({
      URL: terra2URL, 
      chainID: terra2ChainId, 
    });
 
    const result = await neutronClient.execute(
      neutronAddress, 
      contractAddress,
      {
        verify_proof: {
          proof: {
            proof: proof
          },
          channel: channel_id
        }
      },
      defaultExecuteFee
    );
    console.log(result);
    
    // Wait till the packet from neutron arrives at terra
    // (This mechanism will be soon deprecated in the next version.)
    await sleep(5000);
    const queryResult = await terra.wasm.contractQuery(
      terra2Address,
      { query_result: { identifier: proof.signedClaim.claim.identifier} } // query msg
    );
    return queryResult.proof_result === 'Success';
  }
 
  return {
    verify_proof
  };
};
 
export { NeutronFunctions };

Wrap the App with the specified Neutron context (index.js).

import { NeutronContextProvider } from "./utilities/NeutronContext";
 
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <NeutronContextProvider>
    <App />
  </NeutronContextProvider>
);

Create a VerifyProof component.

src/
 |- utilities/NeutronContext.js
 |- utilities/NeutronFunctions.js
 |- VerifyProof.jsx
 |- App.js

Include the VerifyProof() function.

import { useState, useEffect } from "react";
import { Reclaim } from "@reclaimprotocol/js-sdk";
import { NeutronFunctions } from "./utilities/NeutronFunctions";
 
export default function VerifyProof(props) {
  const [proof, setProof] = useState({});
  const [verified, setVerified] = useState(false);
  const { verify_proof } = NeutronFunctions();
 
  useEffect(() => {
    const newProof = Reclaim.transformForOnchain(props.proof);
    setProof(newProof);
  }, []);
 
  return (
    <div>
      <button
        className="button"
        onClick={async () => {
          try {
            if (await verify_proof(proof) === true) {
              setVerified(true);
            } else {
              setVerified(false);
            }
          } catch (e) {
            console.error(e);
          }
        }}
      >
        Verify Proof
      </button>
      {verified && <p> Proof verified </p>}
      <style jsx="true">{`
        .container {
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
        }
        .button {
          border: solid 1px #ccc;
          margin: 0 0 20px;
          border-radius: 3px;
        }
      `}</style>
    </div>
  );
}

Create a ConnectButton component.

|- utilities/NeutronContext.js
|- utilities/NeutronFunctions.js
|- VerifyProof.jsx
|- ConnectButton.jsx
|- App.js

Add a button and placeholder for connecting.

import { useContext } from "react";
import { NeutronContext } from "./utilities/NeutronContext";
 
export default function ConnectButton () {
    const { neutronAddress, connectWallet, addNeutronToKeplr } = useContext(NeutronContext);
  
    return (
        <div>Neutron
        <div>
          <button className="button" onClick={addNeutronToKeplr}>
            Add Neutron to Keplr
          </button>
          <hr/>
          <button className="button" onClick={connectWallet}>
            Connect Keplr
          </button>
        </div>
        <h2>
          {neutronAddress
            ? neutronAddress.slice(0, 10) + "...." + neutronAddress.slice(41, 45)
            : "Please connect wallet"}
        </h2>
      </div>
    )
}

Bringing it all together (App.js).

We will submit the proof on chain once we get the success callback. New lines are highlighted.

import "./App.css";
import { Reclaim } from "@reclaimprotocol/js-sdk";
 import { useState } from "react";
import QRCode from "react-qr-code";
import VerifyProof from "./VerifyProof";
import ConnectButton from "./ConnectButton";
 
function App() {
  const [url, setUrl] = useState("");
  const [ready, setReady] = useState(false);
  const [proof, setProof] = useState({});
 
 
  const reclaimClient = new Reclaim.ProofRequest("YOUR_APPLICATION_ID_HERE"); //TODO: replace with your applicationId
 
  async function generateVerificationRequest() {
    const providerId = "PROVIDER_ID"; //TODO: replace with your provider ids you had selected while creating the application
 
    reclaimClient.addContext(
      `user's address`,
      "for acmecorp.com on 1st january"
    );
 
    await reclaimClient.buildProofRequest(providerId);
 
    reclaimClient.setSignature(
      await reclaimClient.generateSignature(
        APP_SECRET //TODO : replace with your APP_SECRET
      )
    );
 
    const { requestUrl, statusUrl } =
      await reclaimClient.createVerificationRequest();
 
    setUrl(requestUrl);
 
    await reclaimClient.startSession({
      onSuccessCallback: (proofs) => {
        console.log("Verification success", proofs);
        setReady(true);
        setProof(proofs[0]);
        // Your business logic here
      },
      onFailureCallback: (error) => {
        console.error("Verification failed", error);
        // Your business logic here to handle the error
      },
    });
  }
 
  return (
    <div className="App">
      <ConnectButton />
      <div
        style={{
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
          height: "50vh",
        }}
      >
        {!url && (
          <button onClick={generateVerificationRequest}>
            Create Claim QrCode
          </button>
        )}
        {url && <QRCode value={url} />}
      </div>
      {ready && <VerifyProof proof={proof}></VerifyProof>}
    </div>
  );
}
 
export default App;

Submitting the proof

npm run build
 
npm run start

After requesting a proof from Reclaim and performing the verification on your end, a verify proof button will appear on the screen. Make sure your Keplr is connected, click the button, a wallet pop-up will show prompting you to submit.

screen1

keplr1

Now your proof will get approved on-chain, here is the sample transaction (opens in a new tab) from the screenshot above.