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:
- Use an existing test network (testnet) with a key of your own.
- Run basic Cyber-js commands in a script that you run using the CLI.
Additionally, you can choose to:
- Start a local chain that exposes RPCs instead of using a testnet.
- 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.
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.
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:
-
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. -
Adjust the
.gitignore
file to not commit your.key
file by mistake:node_modules *.key
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.
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
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
.
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()
)
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.