Skip to content

Latest commit

 

History

History
341 lines (235 loc) · 12.8 KB

send_txs.md

File metadata and controls

341 lines (235 loc) · 12.8 KB

Your First Cyber-js Actions - Send Tokens

Take your first steps with cyber-js. Use it to send some simple transactions.

In this section, you will:

  • Download and install cyber-js.
  • Create a small experiment.
  • Testnet preparation
  • Establish your connection.
  • Inspect a balance.
  • Send transactions.

A basic feature of a Bostrom chain is the ability to send tokens via the bank module. Cyber-js naturally offers functions to cover this facility. You are going to:

  1. Use an existing test network (testnet) with a key of your own.
  2. Run basic Cyber-js commands in a script that you run using the CLI.

Additionally, you can choose to:

  1. Start a local chain that exposes RPCs instead of using a testnet.
  2. Run the same basic Cyber-js commands, but for this local chain.

Along the way, you learn the basic Cyber-js concepts needed to start interacting with the Cosmos Ecosystem.

Script preparation

A small, ready-made repository exists so you can experiment with Cyber-js. Clone it from here. You need NodeJs. If you open the folder in Visual Studio Code, the IDE should give you all the coding help you require. In the cloned folder you need to install the required modules:

$ npm install

Create a new file named experiment.ts. In it, put these lines to confirm it works:

const runAll = async(): Promise<void> => {
    console.log("TODO")
}

runAll()

To execute, this TypeScript file needs to be compiled into JavaScript before being interpreted by NodeJs. Add this as a run target in package.json:

...
    "scripts": {
        ...
        "experiment": "ts-node experiment.ts"
    }
...

Confirm that it does what you want:

$ npm run experiment

This returns:

> ts-node experiment.ts

TODO

You will soon make this script more meaningful. With the basic script ready, you need to prepare some elements.

Testnet preparation

The Bostrom has a number of testnets running. The Bostrom is currently running a public testnet for the Space-pussy-1 upgrade that you are connecting to and running your script on. You need to connect to a public node so that you can query information and broadcast transactions. One of the available nodes is:

RPC: https://rpc.space-pussy-1.cybernode.ai

You need a wallet address on the testnet and you must create a 24-word mnemonic in order to do so. CosmJS can generate one for you. Create a new file generate_mnemonic.ts with the following script:

import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing"

const generateKey = async (): Promise<void> => {
    const wallet: DirectSecp256k1HdWallet = await DirectSecp256k1HdWallet.generate(24)
    process.stdout.write(wallet.mnemonic)
    const accounts = await wallet.getAccounts()
    console.error("Mnemonic with 1st account:", accounts[0].address)
}

generateKey()

Now create a key for our imaginary user Alice:

*Note: You likely need to update Node.js to a later version if this fails. Find a guide here.

$ npx ts-node generate_mnemonic.ts > testnet.alice.mnemonic.key

When done, it should also tell you the address of the first account:

Mnemonic with 1st account: bostrom1sw8xv3mv2n4xfv6rlpzsevusyzzg78r3e78xnp

Temporarily keep this address for convenience, although CosmJS can always recalculate it from the mnemonic. Privately examine the file to confirm it contains your 24 words.

Important considerations:

  1. process.stdout.write was used to avoid any line return. Be careful not to add any empty lines or any other character in your .key file (this occurs with VSCode under certain conditions). If you add any characters, ComsJs may not be able to parse it.

  2. Adjust the .gitignore file to not commit your .key file by mistake:

    node_modules
    *.key
    

Add your imports

You need a small, simple interface to a blockchain, one which could eventually have users. Good practice is to refrain from requesting a user address until necessary (e.g. when a user clicks a relevant button). Therefore, in experiment.ts you first use the read-only client. Import it at the top of the file:

import { CyberClient } from "@cybercongress/cyber-js"

Note that VSCode assists you to auto-complete CyberClient if you type CTRL-Space inside the {} of the import line.

Define your connection

Next, you need to tell the client how to connect to the RPC port of your blockchain:

const rpc = "https://rpc.space-pussy-1.cybernode.ai"

Inside the runAll function you initialize the connection and immediately check you connected to the right place:

const runAll = async(): Promise<void> => {
    const client = await CyberClient.connect(rpc)
    console.log("With client, chain id:", await client.getChainId(), ", height:", await client.getHeight())
}

Run again to check with npm run experiment, and you get:

With client, chain id: space-pussy-1 , height: 9507032

Get a balance

Normally you would not yet have access to your user's address. However, for this exercise you need to know how many tokens Alice has, so add a temporary new command inside runAll:

console.log(
    "Alice balances:",
    await client.getAllBalances("bostrom1sw8xv3mv2n4xfv6rlpzsevusyzzg78r3e78xnp"), // <-- replace with your generated address
)

getAllBalances is used because the default token name is not yet known. When you run it again, you get:

Alice balances: []

If you just created this account, Alice's balance is zero. Alice needs tokens to be able to send transactions and participate in the network. A common practice with testnets is to expose faucets (services that send you test tokens for free, within limits).

Request tokens for Alice by entering this command in command line:

curl --header "Content-Type: application/json" --request POST --data '{"denom":"boot","address":"bostrom1sw8xv3mv2n4xfv6rlpzsevusyzzg78r3e78xnp"}' https://space-pussy-1.cybernode.ai/credit

Check that Alice received the tokens with npm run experiment.

Prepare a signing client

If you go through the methods inside CyberClient, you see that it only contains query-type methods and none for sending transactions.

Now, for Alice to send transactions, she needs to be able to sign them. And to be able to sign transactions, she needs access to her private keys or mnemonics. Or rather she needs a client that has access to those. That is where SigningCyberClient comes in. Conveniently, SigningCyberClient inherits from CyberClient.

Update your import line:

import { SigningCyberClient, CyberClient } from "@cybercongress/cyber-js"

Look at its declaration by right-clicking on the SigningCyberClient in your imports and choosing Go to Definition.

When you instantiate SigningCyberClient by using the connectWithSigner method, you need to pass it a signer. In this case, use the OfflineDirectSigner interface.

The recommended way to encode messages is by using OfflineDirectSigner, which uses Protobuf. However, hardware wallets such as Ledger do not support this and still require the legacy Amino encoder. If your app requires Amino support, you have to use the OfflineAminoSigner.

Read more about encoding here.

The signer needs access to Alice's private key, and there are several ways to accomplish this. In this example, use Alice's saved mnemonic. To load the mnemonic as text in your code you need this import:

import { readFile } from "fs/promises"

There are several implementations of OfflineDirectSigner available. The DirectSecp256k1HdWallet implementation is most relevant to us due to its fromMnemonic method. Add the import:

import { DirectSecp256k1HdWallet, OfflineDirectSigner } from "@cosmjs/proto-signing"

The fromMnemonic factory function needs a string with the mnemonic. You read this string from the mnemonic file. Create a new top-level function that returns an OfflineDirectSigner:

const getAliceSignerFromMnemonic = async (): Promise<OfflineDirectSigner> => {
    return DirectSecp256k1HdWallet.fromMnemonic((await readFile("./testnet.alice.mnemonic.key")).toString(), {
        prefix: "bostrom",
    })
}

The Bostrom Testnet uses the bostrom address prefix. This is the default used by DirectSecp256k1HdWallet, but you are encouraged to explicitly define it as you might be working with different prefixes on different blockchains. In your runAll function, add:

const aliceSigner: OfflineDirectSigner = await getAliceSignerFromMnemonic()

As a first step, confirm that it recovers Alice's address as expected:

const alice = (await aliceSigner.getAccounts())[0].address
console.log("Alice's address from signer", alice)

Now add the line that finally creates the signing client:

const signingClient = await SigningCyberClient.connectWithSigner(rpc, aliceSigner)

Check that it works like the read-only client that you used earlier, and from which it inherits, by adding:

console.log(
    "With signing client, chain id:",
    await signingClient.getChainId(),
    ", height:",
    await signingClient.getHeight()
)

Send tokens

Alice can now send some tokens to Bob, but to do so she also needs to pay the network's gas fee. How much gas should she use, and at what price?

She can copy this:

Gas fee: [ { denom: 'boot', amount: '0' } ]
Gas limit: 200000

With the gas information now decided, how does Alice structure her command so that she sends tokens to Bob ? SigningCyberClient's sendTokens function takes a Coin[] as input. Coin is simply defined as:

export interface Coin {
    denom: string;
    amount: string;
}

Alice can pick any denom and any amount as long as she owns them, the signing client signs the transaction and broadcasts it. In this case it is:

{ denom: "boot", amount: "1" }

With this gas and coin information, add the command:

// Check the balance of Alice and the Faucet
console.log("Alice balance before:", await client.getAllBalances(alice))
console.log("Bob balance before:", await client.getAllBalances(bobAddress))
// Execute the sendTokens Tx and store the result
const result = await signingClient.sendTokens(
    alice,
    bobAddress,
    [{ denom: "boot", amount: "1" }],
    {
        amount: [{ denom: "boot", amount: "0" }],
        gas: "200000",
    },
)
// Output the result of the Tx
console.log("Transfer result:", result)

Run this with npm run experiment and you should get:

...
Transfer result: {
  code: 0,
  height: 0,
  rawLog: '[]',
  transactionHash: '104BFAB101D8EB88C14D7EFCF50F96F29DA02C7DD85C70FAAB1D96D41C2FA04E',
  gasUsed: 0,
  gasWanted: 0
}

Check that Alice sended the tokens with getTx

const result = await client.getTx("104BFAB101D8EB88C14D7EFCF50F96F29DA02C7DD85C70FAAB1D96D41C2FA04E")

This concludes your first use of cyber-js to send tokens.