Documentation

Overview

Get Started Immediately

$ npx redspot-new erc20

npx is a package runner tool that comes with npm 5.2+ and higher, it ensures that you always install the latest version)

Redspot will create a directory called flipper inside the current folder.

Once the installation is done, you can open your project folder:

$ cd erc20

Inside the newly created project, you can run some built-in commands:

npx redspot compile

Compile your contract into wasm

npx redspot test

Test your contract

npx redspot console

Open the interactive javascript console

npx redspot help

Get help information

Install from template

Redspot provides several contract templates: flipper, delegator , dns, erc20, erc721 , incrementer, multisig_plain. You can intall them like this:

$ npx redspot-new <app-name> --template <template-name>

For instance: npx redspot-new flipper --template flipper

The default contract template is erc20.

Directory tree

app-name/
    |-.vscode/
        |-launch.json
    |-artifacts/
        |- <would store compile contract artifacts, e.g. contracts abi(json) and wasm files>
    |-contracts/
    		|-lib.rs
        |-Cargo.toml
    |-scripts
    		|-deploy.ts
    |-node_modules/
        |- ...
    |-tests/
        |-<template-name>.test.ts
    |-package.json
    |-redspot.config.js
    |-tsconfig.json
  • The files generated by npx redspot compile are in artifacts folder by default. There are compile artifacts for contracts, like contract abi and wasm files.
  • test keeps test files. There are used for test contracts.

Compile contract

Use command npx redspot compile to compile the contracts and abi files used for generating contracts in project. This command can compile both main contract and sub-contract, but please notice that it can only produce the abi files of the main contract. (Will be resolved in the future https://github.com/paritytech/cargo-contract/issues/60)

Prerequisites:

You can specify the contract name to compile, by default it compiles all contracts in the workspace.

$ npx redspot compile --package <ContractName>

You can also specify the toolchain needed by compilation. The default one is nightly:

$ npx redspot compile --toolchain stable

The files generated by compilation will be in artifacts directory by default.

Testing contract

The sample project comes with these tests that use @redspot/patract and @redspot/chai.

import BN from 'bn.js';
import { expect } from 'chai';
import { patract, network, artifacts } from 'redspot';

const { getContractFactory, getRandomSigner } = patract;

const { api, getSigners } = network;

describe('ERC20', () => {
after(() => {
return api.disconnect();
});

async function setup() {
const one = new BN(10).pow(new BN(api.registry.chainDecimals));
const signers = await getSigners();
const Alice = signers[0];
const sender = await getRandomSigner(Alice, one.muln(100));
const contractFactory = await getContractFactory('erc20', sender);
const contract = await contractFactory.deploy('new', '1000');
const abi = artifacts.readAbi('erc20');
const receiver = await getRandomSigner();

return { sender, contractFactory, contract, abi, receiver, Alice, one };
}

it('Assigns initial balance', async () => {
const { contract, sender } = await setup();
const result = await contract.query.balanceOf(sender.address);
expect(result.output).to.equal(1000);
});

it('Transfer adds amount to destination account', async () => {
const { contract, receiver } = await setup();

await expect(() =>
contract.tx.transfer(receiver.address, 7)
).to.changeTokenBalance(contract, receiver, 7);

await expect(() =>
contract.tx.transfer(receiver.address, 7)
).to.changeTokenBalances(contract, [contract.signer, receiver], [-7, 7]);
});

it('Transfer emits event', async () => {
const { contract, sender, receiver } = await setup();

await expect(contract.tx.transfer(receiver.address, 7))
.to.emit(contract, 'Transfer')
.withArgs(sender.address, receiver.address, 7);
});

it('Can not transfer above the amount', async () => {
const { contract, receiver } = await setup();

await expect(contract.tx.transfer(receiver.address, 1007)).to.not.emit(
contract,
'Transfer'
);
});

it('Can not transfer from empty account', async () => {
const { contract, Alice, one, sender } = await setup();

const emptyAccount = await getRandomSigner(Alice, one.muln(10));

await expect(
contract.tx.transfer(sender.address, 7, {
signer: emptyAccount
})
).to.not.emit(contract, 'Transfer');
});
});

You can run your tests with npx redspot test

$ npx redspot test

===== Instantiate erc20 =====
Endowment: 20003200000
GasLimit: 400000000000
CodeHash: 0x372f716ec52a6662ff84b9c7670d087fe413ee717efd4c5ff40a9bb213452408
InputData: 0x50d183512be8030000000000000000000000000000
✔ Transaction fees: 2101427891
✔ The gas consumption of erc20 instantiate: 26766000000
✔ erc20 address: 5EZN12RXKxUWdESK9kSBfFt6516FVnZW45TuMF5oXkj4egEP
ℹ Generate random signer: 5G6iEXWUpZGdoogetkau1URB1t3N6i9ZQL84CisuEFtGn7c4
ℹ Mnemonic: dash host stuff tortoise alcohol warm awful artefact expand spoon gloom hover
ℹ Generate random signer: 5G14hV8a3NLKUxt5tczGoPEfGY3ADm7Nt6PBFWuhSyNqYpYj
ℹ Mnemonic: wrist toast layer erosion involve hedgehog rhythm illegal parent copy miracle tent
ℹ Transfer 100000000000 from 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY to 5G14hV8a3NLKUxt5tczGoPEfGY3ADm7Nt6PBFWuhSyNqYpYj

===== Exec transfer =====
dest: 0x6e53be1dfa20af0403088fa1c94748bf1864e60016a910a9005b0e520ac8a145
value: 0
gasLimit: 400000000000
inputData: 0xd0fae3a09da4d7bfa17ea2d60835ca3c5fac7ea6ca6e18f32f842562d69adf7b7e5742f51307000000000000000000000000000000
✔ Execute successfully
✔ https://polkadot.js.org/apps/#/explorer/query/0x62b09ff094c9259df70832e35845a609b5b5b7a8217ba8c8263eb7ab24e39182
✓ Can not transfer from empty account (4007ms)


5 passing (16s)

Deploying contract

To deploy the contract we will use a Redspot script. Inside scripts/ you will find deploy.ts with the following code:

import { patract, network } from 'redspot';

const { getContractFactory } = patract;
const { createSigner, keyring, api } = network;

const uri =
'bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice';

async function run() {
await api.isReady;

const signer = createSigner(keyring.createFromUri(uri));
const contractFactory = await getContractFactory('erc20', signer);

const balance = await api.query.system.account(signer.address);

console.log('Balance: ', balance.toHuman());

const contract = await contractFactory.deployed('new', '1000000', {
gasLimit: '200000000000',
value: '10000000000000000',
salt: '12312'
});

console.log('');
console.log(
'Deploy successfully. The contract address: ',
contract.address.toString()
);

api.disconnect();
}

run().catch((err) => {
console.log(err);
});

Run it with npx redspot run scripts/deploy.ts:

===== Instantiate erc20 =====
Endowment: 10000000000000000
GasLimit: 200000000000
CodeHash: 0x372f716ec52a6662ff84b9c7670d087fe413ee717efd4c5ff40a9bb213452408
InputData: 0x50d183512b40420f00000000000000000000000000
✔ Transaction fees: 2319536570
✔ The gas consumption of erc20 instantiate: 26766000000
✔ erc20 address: 5FX3AbZFE1jedXSMT3C8uXbDLDjp6AyAer56TzwewtWrahHQ

Deploy successfully. The contract address: 5FX3AbZFE1jedXSMT3C8uXbDLDjp6AyAer56TzwewtWrahHQ

ERROR Disconnected from ws://192.168.1.165:9944: 1000:: Normal connection closure

Configuration

When Redspot is run, it searches for the closest redspot.config.js file starting from the Current Working Directory. This file normally lives in the root of your project. An empty redspot.config.js is enough for Redspot to work.

The entirety of your Redspot setup (i.e. your config, plugins and custom tasks) is contained in this file

Available config options

To set up your config, you have to export an object from redspot.config.js.

This object can have the following entries: defaultNetwork, networks, and paths. For example:

module.exports = {
defaultNetwork: "development",
rust: {
toolchain: "nightly",
},
networks: {
development: {
endpoint: "ws://127.0.0.1:9944",
types: {
Address: "AccountId",
LookupSource: "AccountId",
},
gasLimit: "400000000000",
explorerUrl: "https://polkadot.js.org/apps/#/explorer/query/",
},
substrate: {
endpoint: "ws://127.0.0.1:9944",
gasLimit: "400000000000",
accounts: ["//Alice"],
types: {},
},
},
mocha: {
timeout: 60000,
},
};

You can get your configuration details via env.config.

defaultNetwork

You can customize which network is used by default when running Redspot by setting the config's defaultNetwork field. If you omit this config, its default value is "localhost".

networks

The networks config field is an optional object where network names map to their configuration.

The default localhost network configuration is:

{
localhost: {
gasLimit: "400000000000",
accounts: ["//Alice", "//Bob", "//Charlie", "//Dave", "//Eve", "//Ferdie"],
endpoint: ["ws://127.0.0.1:9944"],
types: {},
httpHeaders: {},
explorerUrl: "https://polkadot.js.org/apps/#/explorer/query/"
}
}

[network].gasLimit

This value is used by default when instantiating a contract and invoking a contract transaction.

If this value is too small, you will get a contracts.OutOfGas error. The maximum value of gaslimit is System.MaximumBlockWeight(In Sustrate, it is 2000000000000).

[network].accounts

The accounts should be an array of suri or KeyringPair.

At run time, you can obtain accounts in the form keyringPair by await env.network.provider.getKeyringpairs(). For details, see network.provider

[network].endpoint

The endpoint can specify the address of the node to which you want to connect, either string or string[]

At this time, only WebSockets versions of RPC are supported.

[network].types

the types are the concepts defined in polkadotjs. If you have any questions about this, you can see it here types.extend. You can also set [network].typesbundle, [network].typesSpec and so on. In general, if you encounter an error that is similar to 'No such variant in enum MultiSignature', ,maybe you should think about adding or removing types { Address: "AccountId", LookupSource: "AccountId"}, see impact-on-extrinsics.

[network].httpHeaders

This setting will be added to the webSocket connection headers

[network].explorerUrl

This setting is currently used with @redspot/patract. After the transaction is inblock, the console will print the link: explorerUrl + bloackhash. The default value is https://polkadot.js.org/apps/#/explorer/query/.

Runtime Environment

The RedSpot runtime environment(RSE) contains all the functionality that Redspot exposes when running a task, test or script.

When you require Redspot (const redspot = require("redspot")) you're getting an instance of the RSE.

During initialization, the Redspot configuration file essentially constructs a list of things to be added to the RSE. This includes tasks, configs and plugins. Then when tasks, tests or scripts run, the RSE is always present and available to access anything that is contained in it.

The RSE has a role of centralizing coordination across all Redspot components. This architecture allows for plugins to inject functionality that becomes available everywhere the RedSpot runtime environment is accessible.

Using the RSE

By default, the RSE gives you programmatic access to the task runner and the config system, and exports a Provider that is compatible with Polkadot WsProvider

Plugins can extend the RSE. For example, @redspot/patract adds a api instance to it, making it available to tasks, tests and scripts.

When writing tests or scripts, you can use require("redspot") to import the RSE. You can read more about this in Accessing the RSE from outside a task.

Accessing the RSE from outside a task

The RSE can be used from any JavaScript or TypeScript file. To do so, you only have to import it with require("redspot"). You can do this to keep more control over your development workflow, create your own tools, or to use Redspot with other dev tools from the node.js ecosystem.

Running test directly with Mocha instead of npx redspot test can be done by explicitly importing the RSE in them like this:

const rse = require("redspot");
const assert = require("assert");

describe("Redspot Runtime Environment", function () {
it("should have a config field", function () {
assert.notEqual(rse.config, undefined);
});
});

This way, tests written for Redspot are just normal Mocha tests. This enables you to run them any way you like without the need of any Redspot-specific plugin.

Extending the RSE

Redspot lets you hook into the RSE construction, and extend it with new functionality. This way, you only have to initialize everything once, and your new features or libraries will be available everywhere the RSE is used.

You can do this by adding a RSE extender into a queue. This extender is just a synchronous function that receives the RSE, and adds fields to it with your new functionality. These new fields will also get injected into the global scope during runtime.

For example, adding an instance of Polkadot.js to the RSE can be done in this way:

const { extendEnvironment } = require("redspot/config");

extendEnvironment((rse) => {
const { ApiPromise } = require("@polkadot/api");
rse.api = api;

rse.api = new ApiPromise();
});

rse.config

Config contains configuration options for the user config file and default options for RedSpot. For example, if you want to get the current default network:

const rse = require("redspot");

console.log(rse.config.defaultNetwork);

rse.network

Network Contains information about the network you are currently running.

rse.network.name

The name of the network being used.

rse.network.config

A configuration item for the network being used. It is equivalent to rse.config.networks.[rse.network.name]

rse.network.provider

The Provider for polkadot network. It derived from Polkadot.js WsProvider.

rse.network.provider.registry

TypeRegistry of Polkadot.js, which has registered the configured types

rse.network.provider.getKeyringPairs() ⇒ Promise<KeyringPair[]>

Gets the accounts that you configure for the network. These accounts will be resolved to the KeyringPair

Console

Redspot comes built-in with an interactive JavaScript console. You can use it by running npx redspot console:

Welcome to Node.js v14.15.1.
Type ".help" for more information.
> 

The compile task will be called before opening the console prompt, but you can skip this with the --no-compile parameter.

The execution environment for the console is the same as for tasks. This means the configuration has been processed, and the Redspot Runtime Environment initialized and injected into the global scope. For example, that you'll have access in the global scope to the config object:

> config
{ 
  defaultNetwork: 'development',
  rust: { toolchain: 'nightly-2020-10-07' },
  
  ...
}
>

And the initialized patract object if you're using the @redspot/patract plugin:

> patract
{ provider:
       
  ...

  },
  ...
>

Anything that has been injected into the Redspot Runtime Environment will be available in the global scope. you can also require the RSE explicitly and get autocomplete:

> config.i
config.isPrototypeOf

config.ink

Redspot's console supports await top-level await (i.e. console.log(await patract.ready())

Plugins

@redspot/chai

It provides a set of matchers for contract testing

Installation

yarn add @redspot/chai

And add the following statement to your redspot.config.ts:

import "@redspot/chai";

Tasks

This plugin creates no additional tasks.

Environment extensions

This plugin does not extend the environment.

Usage

Comparison of Equivalence. This matcher will call the internal eq function for comparison.

expect(new BN(1000)).to.equal(1000) // true

Check Balance

await expect(() =>
contract.tx.transfer(receiver.address, 7)
).to.changeTokenBalance(contract, receiver, 7);

await expect(() =>
contract.tx.transfer(receiver.address, 7)
).to.changeTokenBalances(contract, [contract.signer, receiver], [-7, 7]);

Contract Events

await expect(contract.tx.transfer(receiver.address, 7))
.to.emit(contract, 'Transfer')

await expect(contract.tx.transfer(receiver.address, 7))
.to.emit(contract, 'Transfer')
.withArgs(sender.address, receiver.address, 7);

await expect(
contract.tx.transfer(sender.address, 7, {
signer: emptyAccount
})
).to.not.emit(contract, 'Transfer');

@redspot/gas-reporter

A Mocha reporter for Redspot test suites. It can print gas usage per unit test.

image-20201208000110560

Installation

yarn add @redspot/gas-reporter

And add the following statement to your redspot.config.ts:

import "@redspot/gas-reporter";

Tasks

This plugin creates no additional tasks.

Environment extensions

This plugin does not extend the environment.

@redspot/jupiter

Adaptation to jupiter chains.

When calling encodeSalt, if no salt is passed, salt will default to account nonce.

Installation

yarn add @redspot/jupiter

And add the following statement to your redspot.config.ts:

import "@redspot/jupiter";

Tasks

This plugin creates no additional tasks.

Environment extensions

This plugin does not extend the environment.

@redspot/patract

For deploying and invoking contracts

Installation

yarn add @redspot/patract

And add the following statement to your redspot.config.ts:

import "@redspot/patract";

Tasks

This plugin creates no additional tasks.

Environment extensions

This plugin adds a patract object to the Redspot Runtime Environment.

The patract object has these properties:

  • Contract
  • ContractFactory
  • getContractAt
  • getContractFactory
  • getRandomSigner

Usage

Deployment Contract

const [sender] = await getSigners();
const contractFactory = await patract.getContractFactory('erc20', sender);
const contract = await contractFactory.deploy('new', '1000');

Calling Contract

const result = await contract.query.balanceOf(sender.address);

Executive Contract

const result = await contract.tx.transfer(sender.address, 7)

Get Contract Events

contract.events

Estimated gas

const gas = contract.estimateGas.transfer(sender.address, 7)

Changing the default gaslimit and value

const contract = await contractFactory.deploy('new', '1000', {
gasLimit: ...
value: ...
signer: ...
salt: ...
});

const result = await contract.tx.transfer(sender.address, 7, {
gasLimit: ...
value: ...
signer: ...
})