Terrain – Terra development environment for better smart contract development experience
Terrain:
- helps you scaffold both the smart contracts and the frontend of your app
- dramatically simplifies the development and deployment process
- a fully-featured Terra CLI. Take a look at terrad.
- an LCD. You will still need an RPC endpoint to deploy your contract. LocalTerra is a good development option for this.
- Terrain
- Setup
- Getting Started
- Migrating CosmWasm contracts on Terra
- Usage
- Commands
- Use main branch in local.
- How to run the project locally
For local developement environment, you need LocalTerra.
note: if you are using m1 chip, you might need to update your Docker Desktop due to qemu bug
git clone --branch v0.5.17 --depth 1 https://github.com/terra-money/localterra
cd localterra
docker-compose up
While WASM smart contracts can theoretically be written in any programming language, we currently only recommend using Rust as it is the only language for which mature libraries and tooling exist for CosmWasm. For this tutorial, you'll need to also install the latest version of Rust by following the instructions here.
Then run the following commands
set stable
as default release channel (used when updating rust)
rustup default stable
add wasm as compilation target
rustup target add wasm32-unknown-unknown
for generating contracts
cargo install cargo-generate --features vendored-openssl
cargo install cargo-run-script
To run Terrain you need to install Node.js and NPM. We recommend the Node.js LTS 16 and Node Package Manager (NPM) 8.5.0 which is the default installed version for Node.js LTS 16.
If you encounter the next error code: error:0308010C:digital envelope routines::unsupported use LTS Node.js 16.
Assumed that you have setup the node env, let's generate our first app
For the first time, you will need to run npm install -g @terra-money/terrain
or npx @terra-money/terrain new my-terra-dapp
since terrain
npm module name is occupied by another module.
npx terrain new my-terra-dapp
cd my-terra-dapp
npm install
The project structure will look like this:
.
├── contracts # contracts' source code
│  ├── counter
│  └── ... # more contract can be added here
├── frontend # frontend application
├── lib # predefined functions for task and console
├── tasks # predefined tasks
├── keys.terrain.js # keys for signing transacitons
├── config.terrain.json # config for connections and contract deployments
└── refs.terrain.json # deployed code and contract referecnes
You will now have counter example contract (no pun intended).
We can right away deploy the contract on LocalTerra. Not specifying network will be defaulted to localterra
.
npx terrain deploy counter --signer validator
npx
will use project'sterrain
binary instead of the global one.
note that signer validator
is one of a pre-configured accounts with balances on LocalTerra.
Deploy command will build and optimize wasm code, store it on the blockchain and instantiate the contract._
note: if you are using m1 chip, you might encounter a docker run error such as
Error: Command failed: docker run --rm -v "$(pwd)":/code --mount
type=volume,source="$(basename "$(pwd)")_cache",target=/code/target
--mount
type=volume,source=registry_cache,target=/usr/local/cargo/registry
cosmwasm/rust-optimizer:0.12.5
in this case, you should run the following:
npx terrain deploy counter --signer validator --arm64
You can deploy to different network defined in the config.terrain.json
(mainnet
and testnet
). But you can not use the pre-configured accounts anymore. So you need to first update your keys.terrain.js
// can use `process.env.SECRET_MNEMONIC` or `process.env.SECRET_PRIV_KEY`
// to populate secret in CI environment instead of hardcoding
module.exports = {
custom_tester_1: {
mnemonic:
"shiver position copy catalog upset verify cheap library enjoy extend second peasant basic kit polar business document shrug pass chuckle lottery blind ecology stand",
},
custom_tester_2: {
privateKey: "fGl1yNoUnnNUqTUXXhxH9vJU0htlz9lWwBt3fQw+ixw=",
},
};
Apparently, there are some example accounts pre-defined in there. But DON'T USE THEM ON MAINNET since it is not a secret anymore.
For demonstration purpose, we are going to use custom_tester_1
.
First, get some Luna from the faucet to pay for gas. Notice that it requires address but you only have mnenomic
or private_key
in keys.terrain.js
.
The easiest way to retrive the address is to use console:
npx terrain console
terrain > wallets.custom_tester_1.key.accAddress
'terra1qd9fwwgnwmwlu2csv49fgtum3rgms64s8tcavp'
Now you can request for Luna on the faucet then check your balance in console.
terrain > (await client.bank.balance(wallets.custom_tester_1.key.accAddress))[0]
client
is an LCDClient (LCD stands for "Light Client Daemon" as opposed to FCD a "Fulll Client Daemon", if you are curious) with some extra utility function. And wallets
contains list of Wallet.
npx terrain deploy counter --signer custom_tester_1 --network testnet
Same goes for mainnet deployment, you might want to store your secrets in enviroment variable instead, which you can populate them through process.env.SOME_SECRET
in keys.terrain.json
.
You can also separate storing code from contract instantiation as using:
After deployment, refs.terrain.json
will get updated. Refs file contains contract references on all network.
{
"localterra": {
"counter": {
"codeId": "1",
"contractAddresses": {
"default": "terra18vd8fpwxzck93qlwghaj6arh4p7c5n896xzem5"
}
}
},
"testnet": {
"counter": {
"codeId": "18160",
"contractAddresses": {
"default": "terra15faphq99pap3fr0dwk46826uqr2usve739l7ms"
}
}
}
}
This information is used by terrain's utility functions and also the frontend template.
But in order to use it with frontend, You sync this in to frontend/src
since it can not import file outside its rootDir (src
). Todo so, run:
npx terrain sync-refs
This is not ideal, ideas and contribution are more than welcome
With this, we can now start frontend:
cd frontend
npm run start
Switching network in your terra wallet extension will result in referencing to different contract address in the respective network.
With the template setup, you can do this
npx terrain console
terrain > await lib.increment()
...
terrain > await lib.getCount()
{ count: 0 }
npx terrain console --network <NETWORK>
is also possible
Where does this lib functions comes from?
The answer is here:
// lib/index.js
module.exports = ({ wallets, refs, config, client }) => ({
getCount: () => client.query("counter", { get_count: {} }),
increment: (signer = wallets.validator) =>
client.execute(signer, "counter", { increment: {} }),
});
With this, you can interact with your contract or the blockchain interactively with your own abstractions.
You can use the lib to create task as well:
// tasks/example-with-lib.js
const { task } = require("@terra-money/terrain");
const lib = require("../lib");
task(async (env) => {
const { getCount, increment } = lib(env);
console.log("count 1 = ", await getCount());
await increment();
console.log("count 2 = ", await getCount());
});
To run this task:
npx terrain task:run example-with-lib
To create new task, run:
npx terrain task:new task-name
You might noticed by now that the env
(wallets
, refs
, config
, client
) in task and lib are the ones that available in the console context.
Also, you can access terrajs
in the console or import it in the lib or task to create custom interaction like in tasks/example-custom-logic.js
or more complex one, if you are willing to do so, please consult terra.js doc.
// tasks/example-custom-logic.js
const { task, terrajs } = require("@terra-money/terrain");
// terrajs is basically re-exported terra.js (https://terra-money.github.io/terra.js/)
task(async ({ wallets, refs, config, client }) => {
console.log("creating new key");
const key = terrajs.MnemonicKey();
console.log("private key", key.privateKey.toString("base64"));
console.log("mnemonic", key.mnemonic);
});
(Thanks to @octalmage)
On Terra it is possible to initilize contracts as migratable. This functionallity allows the adminstrator to upload a new version of the contract, then send a migrate message to move to the new code.
We'll be using Terrain, a Terra development suite to ease the scaffolding, deployment, and migration of our contracts.
This tutorial builds on top of the Terrain Quick Start Guide.
There's two steps required to make a contract migratable:
- Smart contract handles the MigrateMsg transaction.
- Smart contract has an admin set, which is the address that's allowed to perform migrations.
To implement support for MigrateMsg you will need to add the message to msg.rs
:
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct MigrateMsg {}
You can place this anywhere, I usually stick it above the InstantiateMsg struct.
With MigrateMsg defined we need to update contract.rs
. First update the import from crate::msg
to include MigrateMsg
:
use crate::msg::{CountResponse, ExecuteMsg, InstantiateMsg, QueryMsg, MigrateMsg};
Then add the following method above instantiate
:
#[cfg_attr(not(feature = "library"), entry_point)]
pub fn migrate(_deps: DepsMut, _env: Env, _msg: MigrateMsg) -> StdResult<Response> {
Ok(Response::default())
}
In the previous Terrain tutorial we showed you how to deploy the contract, but we did not initilize it as migratable.
After adding MigrateMsg to the smart contract we can redeploy and add the --set-signer-as-admin
flag. This tells Terra that the transaction signer is allowed to migrate the contract in the future.
npx terrain deploy counter --signer validator --set-signer-as-admin
With the new contract deployed you can make some changes, then migrate to the new code with the following command:
npx terrain contract:migrate counter --signer validator
$ npm install -g @terra-money/terrain
$ terrain COMMAND
running command...
$ terrain (-v|--version|version)
@terra-money/terrain/0.2.0 linux-x64 node-v16.14.2
$ terrain --help [COMMAND]
USAGE
$ terrain COMMAND
...
terrain code:new NAME
terrain code:store CONTRACT
terrain console
terrain contract:instantiate CONTRACT
terrain contract:migrate [CONTRACT]
terrain contract:updateAdmin CONTRACT ADMIN
terrain deploy CONTRACT
terrain help [COMMAND]
terrain new NAME
terrain sync-refs [FILE]
terrain task:new [TASK]
terrain task:run [TASK]
terrain test CONTRACT-NAME
terrain wallet:new
Generate new contract.
USAGE
$ terrain code:new [NAME] [--path <value>] [--version <value>]
FLAGS
--path=<value> [default: ./contracts] path to keep the contracts
--version=<value> [default: 0.16]
DESCRIPTION
Generate new contract.
See code: src/commands/code/new.ts
Store code on chain.
USAGE
$ terrain code:store [CONTRACT] --signer <value> [--no-rebuild] [--network <value>] [--config-path <value>]
[--refs-path <value>] [--keys-path <value>] [--code-id <value>]
FLAGS
--code-id=<value>
--config-path=<value> [default: ./config.terrain.json]
--keys-path=<value> [default: ./keys.terrain.js]
--network=<value> [default: localterra]
--no-rebuild
--refs-path=<value> [default: ./refs.terrain.json]
--signer=<value> (required)
DESCRIPTION
Store code on chain.
See code: src/commands/code/store.ts
Start a repl console that provides context and convinient utilities to interact with the blockchain and your contracts.
USAGE
$ terrain console [--network <value>] [--config-path <value>] [--refs-path <value>] [--keys-path <value>]
FLAGS
--config-path=<value> [default: config.terrain.json]
--keys-path=<value> [default: keys.terrain.js]
--network=<value> [default: localterra]
--refs-path=<value> [default: refs.terrain.json]
DESCRIPTION
Start a repl console that provides context and convinient utilities to interact with the blockchain and your
contracts.
See code: src/commands/console.ts
Instantiate the contract.
USAGE
$ terrain contract:instantiate [CONTRACT] --signer <value> [--network <value>] [--config-path <value>] [--refs-path
<value>] [--keys-path <value>] [--instance-id <value>] [--code-id <value>] [--set-signer-as-admin]
FLAGS
--code-id=<value> target code id for migration, can do only once after columbus-5 upgrade
--config-path=<value> [default: ./config.terrain.json]
--instance-id=<value> [default: default]
--keys-path=<value> [default: ./keys.terrain.js]
--network=<value> [default: localterra]
--refs-path=<value> [default: ./refs.terrain.json]
--set-signer-as-admin
--signer=<value> (required)
DESCRIPTION
Instantiate the contract.
See code: src/commands/contract/instantiate.ts
Migrate the contract.
USAGE
$ terrain contract:migrate [CONTRACT] --signer <value> [--no-rebuild] [--network <value>] [--config-path <value>]
[--refs-path <value>] [--keys-path <value>] [--instance-id <value>] [--code-id <value>]
FLAGS
--code-id=<value> target code id for migration
--config-path=<value> [default: ./config.terrain.json]
--instance-id=<value> [default: default]
--keys-path=<value> [default: ./keys.terrain.js]
--network=<value> [default: localterra]
--no-rebuild deploy the wasm bytecode as is.
--refs-path=<value> [default: ./refs.terrain.json]
--signer=<value> (required)
DESCRIPTION
Migrate the contract.
See code: src/commands/contract/migrate.ts
Update the admin of a contract.
USAGE
$ terrain contract:updateAdmin [CONTRACT] [ADMIN] --signer <value> [--network <value>] [--config-path <value>]
[--refs-path <value>] [--keys-path <value>] [--instance-id <value>]
FLAGS
--config-path=<value> [default: ./config.terrain.json]
--instance-id=<value> [default: default]
--keys-path=<value> [default: ./keys.terrain.js]
--network=<value> [default: localterra]
--refs-path=<value> [default: ./refs.terrain.json]
--signer=<value> (required)
DESCRIPTION
Update the admin of a contract.
See code: src/commands/contract/updateAdmin.ts
Build wasm bytecode, store code on chain and instantiate.
USAGE
$ terrain deploy [CONTRACT] --signer <value> [--no-rebuild] [--network <value>] [--config-path <value>]
[--refs-path <value>] [--keys-path <value>] [--instance-id <value>] [--set-signer-as-admin] [--admin-address
<value>] [--frontend-refs-path <value>] [--arm64]
FLAGS
--admin-address=<value> set custom address as contract admin to allow migration.
--arm64 use rust-optimizer-arm64 for optimization. Not recommended for production, but it will
optimize quicker on arm64 hardware during development.
--config-path=<value> [default: ./config.terrain.json]
--frontend-refs-path=<value> [default: ./frontend/src/refs.terrain.json]
--instance-id=<value> [default: default]
--keys-path=<value> [default: ./keys.terrain.js]
--network=<value> [default: localterra]
--no-rebuild deploy the wasm bytecode as is.
--refs-path=<value> [default: ./refs.terrain.json]
--set-signer-as-admin set signer (deployer) as admin to allow migration.
--signer=<value> (required)
DESCRIPTION
Build wasm bytecode, store code on chain and instantiate.
See code: src/commands/deploy.ts
display help for terrain
USAGE
$ terrain help [COMMAND] [--all]
ARGUMENTS
COMMAND command to show help for
FLAGS
--all see all commands in CLI
DESCRIPTION
display help for terrain
See code: @oclif/plugin-help
Create new dapp from a template.
USAGE
$ terrain new [NAME] [--path <value>] [--framework next|vue|vite|lit|svelte|react] [--version <value>]
FLAGS
--framework=<option> [default: react] Choose the frontend framework you want to use. Non-react framework options have
better wallet-provider support but less streamlined contract integration.
<options: next|vue|vite|lit|svelte|react>
--path=<value> path to keep the project
--version=<value> [default: 0.16]
DESCRIPTION
Create new dapp from a template.
EXAMPLES
$ terrain new awesome-dapp
$ terrain new --framework vue awesome-dapp
$ terrain new awesome-dapp --path path/to/dapp
$ terrain new --framework next awesome-dapp --path path/to/dapp
See code: src/commands/new.ts
Sync configuration with frontend app.
USAGE
$ terrain sync-refs [FILE] [--refs-path <value>] [--dest <value>]
FLAGS
--dest=<value> [default: ./frontend/src/refs.terrain.json]
--refs-path=<value> [default: ./refs.terrain.json]
DESCRIPTION
Sync configuration with frontend app.
See code: src/commands/sync-refs.ts
create new task
USAGE
$ terrain task:new [TASK]
DESCRIPTION
create new task
See code: src/commands/task/new.ts
run predefined task
USAGE
$ terrain task:run [TASK] [--network <value>] [--config-path <value>] [--refs-path <value>] [--keys-path
<value>]
FLAGS
--config-path=<value> [default: config.terrain.json]
--keys-path=<value> [default: keys.terrain.js]
--network=<value> [default: localterra]
--refs-path=<value> [default: refs.terrain.json]
DESCRIPTION
run predefined task
See code: src/commands/task/run.ts
Runs unit tests for a contract directory.
USAGE
$ terrain test [CONTRACT-NAME] [--no-fail-fast]
FLAGS
--no-fail-fast Run all tests regardless of failure.
DESCRIPTION
Runs unit tests for a contract directory.
EXAMPLES
$ terrain test counter
$ terrain test counter --no-fail-fast
See code: src/commands/test.ts
Generate a new wallet.
USAGE
$ terrain wallet:new [--outfile <value>] [--index <value>]
FLAGS
--index=<value> key index to use, default value is 0
--outfile=<value> absolute path to store the mnemonic key to. If omitted, output to stdout
DESCRIPTION
Generate a new wallet.
See code: src/commands/wallet/new.ts
Sometimes the most new features or bugfixes are integrated into the main branch but not yet released to npm repository. In exceptional cases you may want to use the latest code of the library even before being released to @terra-money/terrain.
To use main branch in your local machine you must clone the repo and navigate to the project folder:
> git clone --branch main --depth 1 https://github.com/terra-money/terrain
> cd terrain/
Inside the project folder execute install, when it finish you can use link to setup the project as your global terrain instance:
> npm install
> npm link
Take in consideration that the process documented before sometimes will contain fixes and new features but is still being tested.
If you want to contribute to the project just take in consideration that after cloning the repo you need to build and run the built version each time you do a change in the source code because its a CLI tool:
// Clone the repo
> git clone --branch main --depth 1 https://github.com/terra-money/terrain
// Navigate to the project directory
> cd terrain/
// Install the dependencies
> npm install
// Build the project
> npm run prepack
// Execute the build version of the project with one of the available commands
> npm run use [argument]
Another available command is npm run prepack:use [argument] which will build the project and execute the command you like.