Using CosmJS

This document gives instructions and examples on how to use CosmJS package to broadcast transactions and query the Coreum blockchain using TypeScript.

What Is CosmJs?

CosmJS is a JavaScript library designed for building applications that can interact with blockchains built with the Cosmos SDK. The Cosmos SDK is a framework for building blockchain applications in Go, and it powers a wide range of blockchains in the Cosmos Network, which aims to create an interoperable ecosystem of blockchains.

CosmJS provides developers with a set of tools and functionalities to easily connect to Cosmos SDK-based blockchains, create transactions, query blockchain data, and interact with smart contracts (for blockchains that support them, like those using the CosmWasm standard). It supports various aspects of blockchain interaction, including wallet management, cryptographic functions, and communication with blockchain nodes via REST and RPC endpoints.

Source Code

The complete app source code is located here. You can use the README.md instruction to build and run the application.

Installing dependencies

Install the CosmJS dependency.

npm install @cosmjs/proto-signing @cosmjs/stargate

Preparing test account

Before you may broadcast transactions, you need to have access to a funded account. Normally you would create a private key stored securely in a wallet. Here, for simplicity, we will use mnemonic generated by our faucet. Don't use the mnemonic directly in code and never ever use the key generated by the faucet in mainnet. It might cause complete funds loss!

To get a funded account, go to our faucet website and click on "Generate Funded Wallet" button in "Testnet" section. Assign mnemonic to the constant senderMnemonic in the code snippet below.

Preparing coreum settings

Before we are able to broadcast transaction, we must set up chain specific configuration:

const coreumAccountPrefix = "testcore"; // the address prefix (different for different chains/environments)
const coreumHDPath = "m/44'/990'/0'/0/0"; // coreum HD path (same for all chains/environments)
const coreumDenom = "utestcore"; // core denom (different for different chains/environments)
const coreumRpcEndpoint = "https://full-node.testnet-1.coreum.dev:26657"; // rpc endpoint (different for different chains/environments)
const recipientAddress = "testcore1534s8rz2e36lwycr6gkm9vpfe5yf67wkuca7zs"
const senderMnemonic = "putYourMnemonicHere"; // put mnemonic here

This configuration is for testnet. Parameters of other networks are available at network variables.

Generate TS based on coreum protos

The complete generated files and template are located in coreum-ts-protobuf.

  • Clone the repository where our template is located and change into that directory.
git clone [email protected]:CoreumFoundation/coreum-ts-protobuf.git
cd coreum-ts-protobuf
  • Remove the current generated files so that we can generate them again.
rm -rf ./ts-protos
  • Install the code generator, the runtime library, and the Buf CLI.
npm install @bufbuild/protobuf @bufbuild/protoc-gen-es @bufbuild/buf
  • Clone the coreum and cosmos-sdk branches that you want to generate proto files for.
git clone [email protected]:CoreumFoundation/coreum.git
git clone --branch v0.47.5 [email protected]:cosmos/cosmos-sdk.git
  • Copy all the proto files in the current directory.
cp -r ./cosmos-sdk/proto/ ./coreum/proto/ .
  • Generate the typescript protobuf files.
npx buf generate ./proto/ --template buf.gen.yaml -o ./ts-protos
  • Remove all other directories.
rm -rf ./coreum ./cosmos-sdk ./proto

Prepare RPC/tendermint clients.

The clients will be reused by multiple samples later.

const tendermintClient = await Tendermint34Client.connect(coreumRpcEndpoint);
const queryClient = new QueryClient(tendermintClient);
const rpcClient = createProtobufRpcClient(queryClient);
const feemodelQueryClient = new FeemodelQueryClient(rpcClient)
const stakingExtension = setupStakingExtension(queryClient);

// the custom tx types should be registered in the types registry
const ftTypes: ReadonlyArray<[string, GeneratedType]> = [
    ["/coreum.asset.ft.v1.MsgIssue", MsgIssue],
];
let registryTypes: ReadonlyArray<[string, GeneratedType]> = [
    ...defaultRegistryTypes,
    ...ftTypes,
]
const registry = new Registry(registryTypes)

Prepare sender client

To sign transactions, you need to set up the new account-specific client.

console.log("preparing sender wallet");
const senderWallet = await DirectSecp256k1HdWallet.fromMnemonic(senderMnemonic, {
    prefix: coreumAccountPrefix,
    hdPaths: [stringToPath(coreumHDPath)],
});
const [sender] = await senderWallet.getAccounts();
console.log(`sender address: ${sender.address}`);

const senderClient = await SigningStargateClient.connectWithSigner(
    coreumRpcEndpoint,
    senderWallet,
    { registry }
);
const senderCoreBalance = await senderClient.getBalance(sender.address, coreumDenom);
console.log(`sender balance: ${senderCoreBalance.amount}`);

Send coins

Now we are ready to broadcast transaction. As an example we send 100000utestcore tokens from sender wallet to recipient:

 console.log("preparing recipient wallet");
const recipientWallet = await DirectSecp256k1HdWallet.generate(12, {
    prefix: coreumAccountPrefix,
    hdPaths: [stringToPath(coreumHDPath)],
});
const [recipient] = await recipientWallet.getAccounts();
console.log(`recipient address: ${recipient.address}`);

const msgBankSend: MsgSendEncodeObject = {
    typeUrl: "/cosmos.bank.v1beta1.MsgSend",
    value: MsgSend.fromPartial({
        fromAddress: sender.address,
        toAddress: recipient.address,
        amount: [{
            denom: coreumDenom,
            amount: "100000",
        }],
    }),
};
console.log(
    `sending ${msgBankSend.value.amount?.[0].amount}${msgBankSend.value.amount?.[0].denom} from ${msgBankSend.value.fromAddress} to ${msgBankSend.value.toAddress}`
);

let gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
const bankSendGas = await senderClient.simulate(sender.address, [msgBankSend], "")
console.log(`estimated gas: ${bankSendGas}, gasPrice: ${gasPrice.toString()}`);
const bankSendFee: StdFee = calculateFee(bankSendGas, gasPrice);
const bankSendResult = await senderClient.signAndBroadcast(
    sender.address,
    [msgBankSend],
    bankSendFee
);
isDeliverTxSuccess(bankSendResult);
console.log(`successfully sent, tx hash: ${bankSendResult.transactionHash}`);

const recipientCoreBalance = await senderClient.getBalance(recipient.address, coreumDenom);
console.log(`recipient balance: ${recipientCoreBalance.amount}`);

After executing this code, you will see output like this:

successfully sent, tx hash: DC77A19C73D463CA5365F115C900CDD435DE6616B49A93573D64628D25699941

Please copy transaction hash and paste it in the search box of our block explorer to confirm the transaction execution and check its properties.

GasPrice

In the example above we have already used the correct computation of the gas price.

export async function getGasPriceWithMultiplier(feemodelQueryClient: FeemodelQueryClient) {
    const gasPriceMultiplier = 1.1
    const minGasPriceRes = await feemodelQueryClient.MinGasPrice({})
    const minGasPrice = decodeCosmosSdkDecFromProto(minGasPriceRes.minGasPrice?.amount || "")
    let gasPrice = minGasPrice.toFloatApproximation() * gasPriceMultiplier
    return GasPrice.fromString(`${gasPrice}${minGasPriceRes.minGasPrice?.denom || ""}`);
}

To understand the function read about the coreum gas price.

Staking Delegation

Once the send tx is executed we can use the recipient to Delegate 100utestcore to a first bonded validator.

const recipientClient = await SigningStargateClient.connectWithSigner(
    coreumRpcEndpoint,
    recipientWallet,
    { registry }
);

// query all bonded validators to find first bonded to delegate to
const bondedValidators = await stakingExtension.staking.validators("BOND_STATUS_BONDED");
const validatorOperatorAddress = bondedValidators.validators[0].operatorAddress;
const msgStakingDelegate: MsgDelegateEncodeObject = {
    typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
    value: MsgDelegate.fromPartial({
        delegatorAddress: recipient.address,
        validatorAddress: validatorOperatorAddress,
        amount: {
            denom: coreumDenom,
            amount: "100",
        },
    }),
};
console.log(
    `delegating ${msgStakingDelegate.value.amount?.amount}${msgStakingDelegate.value.amount?.denom} from ${recipient.address} to ${validatorOperatorAddress}`
);
// the gas price can be changed at that time, we need to re-fetch it
gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
const stakingDelegateGas = await recipientClient.simulate(recipient.address, [msgStakingDelegate], "")
console.log(`estimated gas: ${stakingDelegateGas}, gasPrice: ${gasPrice.toString()}`);
const stakingDelegateFee: StdFee = calculateFee(stakingDelegateGas, gasPrice);
const stakingDelegateResult = await recipientClient.signAndBroadcast(
    recipient.address,
    [msgStakingDelegate],
    stakingDelegateFee
);
isDeliverTxSuccess(stakingDelegateResult);
console.log(`successfully delegated, tx hash: ${stakingDelegateResult.transactionHash}`);

After executing this code, you will see output like this:

successfully delegated, tx hash: 8B44E82051A2D45C08C7A8A6EB2C444C048AD8BFBE77E98DC4ED6A57666CE88B

Please copy transaction hash and paste it in the search box of our block explorer to confirm the transaction execution and check its properties.

Coreum custom message (FT)

In the getGasPriceWithMultiplier we have already used the coreum query to get feemodel params and MinGasPrice. In this section, we interact with the custom-generated transaction. The sample shows how to create and broadcast FT issuance transaction.

const msgIssueFT: MsgIssueEncodeObject = {
    typeUrl: "/coreum.asset.ft.v1.MsgIssue",
    value: MsgIssue.fromPartial({
        issuer: sender.address,
        subunit: `mysubunit`,
        symbol: `mysymbol`,
        precision: 18,
        initialAmount: "1000000",
        features: [Feature.minting, Feature.burning],
        sendCommissionRate: `${Decimal.fromUserInput("0.5", 18).atomics}` // 50%
    }),
};
const ftDenom = `${msgIssueFT.value.subunit}-${sender.address}`
console.log(
    `issuing ${ftDenom} FT`
);

gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
const issueFTGas = await senderClient.simulate(sender.address, [msgIssueFT], "")
console.log(`estimated gas: ${issueFTGas}, gasPrice: ${gasPrice.toString()}`);
const issueFTFee: StdFee = calculateFee(issueFTGas, gasPrice);
// pay attention that additionally to the gas the `issue_fee` will be burned.
const issueFTResult = await senderClient.signAndBroadcast(
    sender.address,
    [msgIssueFT],
    issueFTFee
);
isDeliverTxSuccess(issueFTResult);
console.log(`successfully issued, tx hash: ${issueFTResult.transactionHash}`);

const senderFTBalance = await senderClient.getBalance(sender.address, ftDenom);
console.log(`sender ft balance: ${senderFTBalance.amount}${ftDenom}`);

After executing this code, you will see output like this:

successfully issued, tx hash: A1FD3D02BA51FE65EB593442FF9A979A99EC40CA70A2E55FFEF1FAA8A36BAC3F

Please copy transaction hash and paste it in the search box of our block explorer to confirm the transaction execution and check its properties.

Docs and additional examples

Additional examples and docs can be found in the CosmJS repository.

Complete code

Here is the complete code listing with all the features implemented above:

import { StdFee } from "@cosmjs/amino";
import { stringToPath } from "@cosmjs/crypto";
import { DirectSecp256k1HdWallet, EncodeObject, GeneratedType, Registry } from "@cosmjs/proto-signing";
import { Decimal } from "@cosmjs/math";
import {
    calculateFee,
    createProtobufRpcClient, decodeCosmosSdkDecFromProto,
    GasPrice,
    SigningStargateClient,
} from "@cosmjs/stargate";
import { MsgDelegateEncodeObject, setupStakingExtension } from "@cosmjs/stargate/build/modules";
import { MsgSendEncodeObject } from "@cosmjs/stargate/build/modules";
import { QueryClient } from "@cosmjs/stargate/build/queryclient/queryclient";
import { isDeliverTxSuccess } from "@cosmjs/stargate/build/stargateclient";
import { Tendermint34Client } from "@cosmjs/tendermint-rpc";
import { defaultRegistryTypes } from "@cosmjs/stargate"
import { MsgDelegate } from "cosmjs-types/cosmos/staking/v1beta1/tx";
import { MsgSend } from "cosmjs-types/cosmos/bank/v1beta1/tx";
import { QueryClientImpl as FeemodelQueryClient } from "../coreum-ts/coreum/feemodel/v1/query";
import { MsgIssue } from "../coreum-ts/coreum/asset/ft/v1/tx";
import { Feature } from "../coreum-ts/coreum/asset/ft/v1/token";

export interface MsgIssueEncodeObject extends EncodeObject {
    readonly typeUrl: "/coreum.asset.ft.v1.MsgIssue";
    readonly value: Partial<MsgIssue>;
}

const main = (async function() {
    const coreumAccountPrefix = "testcore"; // the address prefix (different for different chains/environments)
    const coreumHDPath = "m/44'/990'/0'/0/0"; // coreum HD path (same for all chains/environments)
    const coreumDenom = "utestcore"; // core denom (different for different chains/environments)
    const coreumRpcEndpoint = "https://full-node-pluto.testnet-1.coreum.dev:26657"; // rpc endpoint (different for different chains/environments)
    const senderMnemonic =
        "emerge cake river crush explain long else rebuild author duty bulb mind pelican sun alcohol add sample purity two crop wish oven engage tone";

    // ******************** Initialize clients ********************

    const tendermintClient = await Tendermint34Client.connect(coreumRpcEndpoint);
    const queryClient = new QueryClient(tendermintClient);
    const rpcClient = createProtobufRpcClient(queryClient);
    const feemodelQueryClient = new FeemodelQueryClient(rpcClient)
    const stakingExtension = setupStakingExtension(queryClient);

    // the custom tx types should be registered in the types registry
    const ftTypes: ReadonlyArray<[string, GeneratedType]> = [
        ["/coreum.asset.ft.v1.MsgIssue", MsgIssue],
    ];
    let registryTypes: ReadonlyArray<[string, GeneratedType]> = [
        ...defaultRegistryTypes,
        ...ftTypes,
    ]
    const registry = new Registry(registryTypes)

    // ******************** Bank MsgSend example ********************

    console.log("preparing sender wallet");
    const senderWallet = await DirectSecp256k1HdWallet.fromMnemonic(senderMnemonic, {
        prefix: coreumAccountPrefix,
        hdPaths: [stringToPath(coreumHDPath)],
    });
    const [sender] = await senderWallet.getAccounts();
    console.log(`sender address: ${sender.address}`);

    const senderClient = await SigningStargateClient.connectWithSigner(
        coreumRpcEndpoint,
        senderWallet,
        { registry }
    );
    const senderCoreBalance = await senderClient.getBalance(sender.address, coreumDenom);
    console.log(`sender balance: ${senderCoreBalance.amount}`);

    console.log("preparing recipient wallet");
    const recipientWallet = await DirectSecp256k1HdWallet.generate(12, {
        prefix: coreumAccountPrefix,
        hdPaths: [stringToPath(coreumHDPath)],
    });
    const [recipient] = await recipientWallet.getAccounts();
    console.log(`recipient address: ${recipient.address}`);

    const msgBankSend: MsgSendEncodeObject = {
        typeUrl: "/cosmos.bank.v1beta1.MsgSend",
        value: MsgSend.fromPartial({
            fromAddress: sender.address,
            toAddress: recipient.address,
            amount: [{
                denom: coreumDenom,
                amount: "100000",
            }],
        }),
    };
    console.log(
        `sending ${msgBankSend.value.amount?.[0].amount}${msgBankSend.value.amount?.[0].denom} from ${msgBankSend.value.fromAddress} to ${msgBankSend.value.toAddress}`
    );

    let gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
    const bankSendGas = await senderClient.simulate(sender.address, [msgBankSend], "")
    console.log(`estimated gas: ${bankSendGas}, gasPrice: ${gasPrice.toString()}`);
    const bankSendFee: StdFee = calculateFee(bankSendGas, gasPrice);
    const bankSendResult = await senderClient.signAndBroadcast(
        sender.address,
        [msgBankSend],
        bankSendFee
    );
    isDeliverTxSuccess(bankSendResult);
    console.log(`successfully sent, tx hash: ${bankSendResult.transactionHash}`);

    const recipientCoreBalance = await senderClient.getBalance(recipient.address, coreumDenom);
    console.log(`recipient balance: ${recipientCoreBalance.amount}`);

    // ******************** Staking MsgDelegate example ********************

    const recipientClient = await SigningStargateClient.connectWithSigner(
        coreumRpcEndpoint,
        recipientWallet,
        { registry }
    );

    // query all bonded validators to find first bonded to delegate to
    const bondedValidators = await stakingExtension.staking.validators("BOND_STATUS_BONDED");
    const validatorOperatorAddress = bondedValidators.validators[0].operatorAddress;
    const msgStakingDelegate: MsgDelegateEncodeObject = {
        typeUrl: "/cosmos.staking.v1beta1.MsgDelegate",
        value: MsgDelegate.fromPartial({
            delegatorAddress: recipient.address,
            validatorAddress: validatorOperatorAddress,
            amount: {
                denom: coreumDenom,
                amount: "100",
            },
        }),
    };
    console.log(
        `delegating ${msgStakingDelegate.value.amount?.amount}${msgStakingDelegate.value.amount?.denom} from ${recipient.address} to ${validatorOperatorAddress}`
    );
    // the gas price can be changed at that time, we need to re-fetch it
    gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
    const stakingDelegateGas = await recipientClient.simulate(recipient.address, [msgStakingDelegate], "")
    console.log(`estimated gas: ${stakingDelegateGas}, gasPrice: ${gasPrice.toString()}`);
    const stakingDelegateFee: StdFee = calculateFee(stakingDelegateGas, gasPrice);
    const stakingDelegateResult = await recipientClient.signAndBroadcast(
        recipient.address,
        [msgStakingDelegate],
        stakingDelegateFee
    );
    isDeliverTxSuccess(stakingDelegateResult);
    console.log(`successfully delegated, tx hash: ${stakingDelegateResult.transactionHash}`);

    // ******************** Coreum custom message example, FT ********************

    const msgIssueFT: MsgIssueEncodeObject = {
        typeUrl: "/coreum.asset.ft.v1.MsgIssue",
        value: MsgIssue.fromPartial({
            issuer: sender.address,
            subunit: `mysubunit`,
            symbol: `mysymbol`,
            precision: 18,
            initialAmount: "1000000",
            features: [Feature.minting, Feature.burning],
            sendCommissionRate: `${Decimal.fromUserInput("0.5", 18).atomics}` // 50%
        }),
    };
    const ftDenom = `${msgIssueFT.value.subunit}-${sender.address}`
    console.log(
        `issuing ${ftDenom} FT`
    );

    gasPrice = await getGasPriceWithMultiplier(feemodelQueryClient)
    const issueFTGas = await senderClient.simulate(sender.address, [msgIssueFT], "")
    console.log(`estimated gas: ${issueFTGas}, gasPrice: ${gasPrice.toString()}`);
    const issueFTFee: StdFee = calculateFee(issueFTGas, gasPrice);
    // pay attention that additionally to the gas the `issue_fee` will be burned.
    const issueFTResult = await senderClient.signAndBroadcast(
        sender.address,
        [msgIssueFT],
        issueFTFee
    );
    isDeliverTxSuccess(issueFTResult);
    console.log(`successfully issued, tx hash: ${issueFTResult.transactionHash}`);

    const senderFTBalance = await senderClient.getBalance(sender.address, ftDenom);
    console.log(`sender ft balance: ${senderFTBalance.amount}${ftDenom}`);
})();

export async function getGasPriceWithMultiplier(feemodelQueryClient: FeemodelQueryClient) {
    const gasPriceMultiplier = 1.1
    const minGasPriceRes = await feemodelQueryClient.MinGasPrice({})
    const minGasPrice = decodeCosmosSdkDecFromProto(minGasPriceRes.minGasPrice?.amount || "")
    let gasPrice = minGasPrice.toFloatApproximation() * gasPriceMultiplier
    return GasPrice.fromString(`${gasPrice}${minGasPriceRes.minGasPrice?.denom || ""}`);
}

export default main;