Skip to main content

Report for Redspot v0.2

  • Patract Hub's treasury report for Redspot v0.2, a WASM contract development workflow and scaffold

Patract Hub (https://patract.io) develops local open source toolkits and one-stop cloud smart IDE, committed to provide free development toolkits and infrastructure services for the entire smart contract ecosystem. Six weeks ago, we applied a treasury proposal for Redspot v0.2 (https://polkadot.polkassembly.io/treasury/13) , and now we have finished the development (https://github.com/patractlabs/redspot) on time and recorded a YouTube demo video (https://youtu.be/nVhLW_XEhk).

Redspot v0.1 includes the basic functions of compiling contracts, testing contracts, and deploying contracts. When developing v0.2, we took a closer look at Ethereum's contract development tools, such as Truffle, Waffle, Ethers.js, Hardhat (aka Buidler when we applied v0.2), etc. We believe that the architecture of Hardhat (https://hardhat.org/) is the most suitable for the Polkadot contract ecosystem. Hardhat has a very high degree of flexibility, scalability and a complete set of plugin ecology. These plugins can add additional functions and integrate into your workflow. Redspot v0.2 refactored Redspot v0.1 based on Hardhat's code, and finally Redspot v0.2 has the same plugin system as Hardhat. This allows us to easily support contract development on different parachains through plugins in the future.

We have completed all the required features in the proposal for v0.2 and also provided the support for TypeScript. Now the templates installed through npx redspot-new erc20 will use TypeScript by default, and no complicated settings are required. In addition, we have developed two plugins: @redspot/patract and @redspot/polkadot. @redspot/polkadot encapsulates and integrates the functions of @polkadot/api-contract. @redspot/patract and @polkadot/api-contract also provide SDKs for interacting with contracts, but @redspot/patract has more convenient and intuitive api and log prompts. In the process of developing @redspot/patract, we referred to the API design of Ethers.js.

## New features#

Like v0.1, you can install the template we provided through npx redspot-new ERC20. You can also easily integrate Redspot into your existing contract project by following the demo video we provided earlier.

Considering that some developers are not familiar with the configuration of TypeScript, we provide a template based on TypeScript by default. We also turn off TypeScript's runtime type checking by default, so that developers can easily use TypeScript's code hinting function, and they don't need to be troubled by TypeScript's type errors. If you want to use JavaScript for development, you only need to set allowjs in the tsconfig file.

After the installation is complete, you will get a directory structure similar to this:

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

In the template, we provide a test file, a deploy script, and an ERC20 contract. Unlike v0.1, we moved cargo.toml and lib.rs to the contracts directory and the structure became clearer. In v0.2, we provide configuration options for artifacts, contracts, and tests in redspot.config.js, you can change it to your favorite name.

Contract Compiling#

On the basis of v0.1, we have added toolchain configuration items. You can set the value of rust.toolchain in the redspot.config.js file. The default is nightly. We have also integrated this step of compiling the contract into the test and run commands. When you run npx redspot test or npx redspot run, the contract will be compiled in advance. You can also cancel this behavior by passing the --no-compile parameter.

Contract testing#

When we developed v0.1, because the ERC20 contract was not yet available, our test code was relatively simple, and we could only test reading values and sending transactions. Now we can write more complete test cases. Below is the unit test code for the ERC20 contract:

import BN from "bn.js";import { patract } from "redspot";import { expect } from "chai";
const {  disconnect,  getContractFactory,  getRandomSigner,  getSigners,  getAbi,  api,} = patract!;
describe("ERC20", () => {  after(() => {    return 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(10));    const contractFactory = await getContractFactory("ERC20", sender);    const contract = await contractFactory.deploy("new", "1000");    const abi = getAbi("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?.toString()).to.equal("1000");  });
  it("Transfer adds amount to destination account", async () => {    const { contract, sender, receiver } = await setup();
    await contract.tx.transfer(receiver.address, 7);
    const result = await contract.query.balanceOf(receiver.address);
    expect(result.output?.toString()).to.equal("7");  });
  it("Transfer emits event", async () => {    const { contract, sender, receiver } = await setup();
    const result = await contract.tx.transfer(receiver.address, 7);
    const event = result?.events?.find((e) => e.name === "Transfer");
    const [from, to, value] = event?.args as any;
    expect(from.unwrap().toString()).to.equal(sender.address);    expect(to.unwrap().toString()).to.equal(receiver.address);    expect(value.toNumber()).to.equal(7);  });
  it("Can not transfer above the amount", async () => {    const { contract, receiver } = await setup();
    const result = await contract.tx.transfer(receiver.address, 1007);
    const event = result?.events?.find((e) => e.name === "Transfer");
    expect(event).to.be.undefined;  });
  it("Can not transfer from empty account", async () => {    const { contract, Alice, one, sender } = await setup();
    const emptyAccount = await getRandomSigner(Alice, one.muln(10));
    const result = await contract.tx.transfer(sender.address, 7, {      signer: emptyAccount,    });
    const event = result?.events?.find((e) => e.name === "Transfer");
    expect(event).to.be.undefined;  });});

In this test case, we used @redspot/patract to perform basic tests on events that read data, transactions, and contracts. In v0.1, we use the jest test framework. During the development process, we found that the scalability of jest is not good, and the print log is very confusing. So in v0.2 we switched to mocha and chai. They are more scalable and the logs are more friendly. But we do not force developers to use mocha. If you still want to continue using jest, you can also install the dependency of jest, yarn add jest ts-jest @types/jest, and then use the command npx jest starts the test. In v0.1, you cannot choose the test framework you want, but it is different in v0.2. We hope to provide a framework that allows users to freely combine various functions.

In v0.1, we found it difficult to repeat unit tests. Because Substate does not support the deployment of multiple identical contracts for the same account. So in v0.2, we call a setup function before running each test. It will initialize the contract and provide a random account, and then we will use this random account to redeploy the contract. In this way, the influence of the external environment can be avoided and unit testing can be carried out more stably. Now you can run npx redspot test to test, and you will get a result like this:

image

As you can see, we provide a very detailed log of contract calls. Including contract call parameters, consumed handling fees, consumed GAS, etc. If the user wants to know the details of the transaction, he can also view it on the browser by clicking the successful transaction link. Of course, the log level can be configured through loglevel.

JavaScript Interactive Console#

In v0.2, we added the function of JavaScript Interactive Console. It supports injecting the runtime environment of redspot in the repl of nodejs. Similar to Truffle's develop command. Console is a supplement to unit testing. Through the Console, we can easily perform temporary tests on the contract. Users can directly call the contract in the Console and get the result. You can open the console through the npx redspot console command.

image

Running custom script#

The essence of Redspot v0.2 is to provide a contract-developed JavaScirpt runtime environment and plugin ecology. In v0.1, you must use our built-in commands to run the code, and you cannot freely run your own scripts. But this is not the case in v0.2, we just provide a runtime environment and a package of some commonly used commands. You can include the Redspot runtime environment in any JavaScript code. And run the code the way you like.

const { patract, network } = require("redspot");
const { getContractFactory, disconnect } = patract;
const { connect, api } = patract!;
async function run() {  await connect();
  const contractFactory = await getContractFactory("ERC20", signer);
  const contract = await contractFactory.deployed("new", "1000000", {    gasLimit: "200000000000",    value: "10000000000000000",  });
  console.log("");  console.log(    "Deploy successfully. The contract address: ",    contract.address.toString()  );
  disconnect();}
run().catch((err) => {  console.log(err);});

In the above code, we use the @redspot/patract plugin to deploy our contract. You can use the npx run [filename] command to run this code, or use node [filename] to call this code. Unlike Truffle, we did not do excessive encapsulation. All functions can be called through the Redspot runtime environment. However, introducing it is very simple, you only need to do const env = require('redspot').

Configuration File#

This is the code from the configuration file for Redspot v0.2:

import { usePlugin } from "redspot/config";import { RedspotConfig } from "redspot/types";
usePlugin("@redspot/patract");
export default {  defaultNetwork: "development",  rust: {    toolchain: "nightly",  },  networks: {    development: {      endpoint: "ws://192.168.1.165: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,  },} as RedspotConfig;

In this file, you can import the plug-ins you need to use. On the basis of v0.1, we also added gasLimit, mocha and other configuration items. In the v0.1 version, accounts can only be imported via private keys, while in v0.2, we support accounts in Substrate URI and keypair format. Users can import the json file of their private key in the form of keypair. This can make the private key more secure and avoid leakage.

Plugins#

In the current version, we provide two plugins, @redspot/polkadot and @redspot/patract. Their role is to provide the SDK for calling the contract, but their api design is different. @redspot/polkadot uses @polkadot/api-contract internally, so the interface it provides is basically the same as ʻapi-contract. In @redspot/patract`, we have made many optimizations and improvements to the api. The interfaces provided by Patract are as follows:

env.patract = {    api: ApiPromise;  Contract: typeof Contract;  ContractFactory: typeof ContractFactory;  connect: () => Promise<ApiPromise>;  disconnect: () => Promise<void>;  getContractAt(contractName: string, address: AccountId | string, signer?: AccountSigner): Promise<Contract>;  getContractFactory(contractName: string, signer?: AccountSigner): Promise<ContractFactory>;  getAbi(contractName: string): Abi;  getWasm(contractName: string): string;  getSigners: () => Promise<AccountSigner[]>;  getRandomSigner(from?: AccountSigner, amount?: BN | string | number): Promise<AccountSigner>;}

You can get the ContractFactory object by getContractFactory(contractName). This function will automatically discover the wasm and abi in the project through the passed in contractName, and then instantiate the ContractFactory. Then call the deploy function of ContractFactory, which will send two transactions, putcode and ʻinstantiate` to the chain, and instantiate the contract after obtaining the address of the contract.

The role of the Contract object is to call the contract, which is similar to api-contract. However, we redesigned the interface based on actual needs and referred to the contract module of Ethers.js. Compared with api-contract, there are several differences:

  • The parameters passed when calling the contract are different. In api-contract, you need to call a transaction like this:
await new Promise((resolve, reject) => {    contract.tx.transfer(value, gaslimit, ...params).signAndSend(Alice, (result) => {    ...  })})

In @redspot/patract, we encapsulate the cumbersome template code, so you only need to do this to call the contract transaction:

await contract.tx.transfer(...params)

You can also set gaslimit, value and signer

await contract.tx.transfer(...params, {gaslimit, value, signer: Alice})
  • In api-contract, you need to handle transaction errors and find contract events yourself. In fact, many people don't know how to get detailed information about errors in metadata. But in @redspot/patract, the user does not need to do additional processing. We will return such an object:
{    from: string;  txHash?: string;  blockHash?: string;  error?: {    message?: any;    data?: any;  };  result: SubmittableResult;  gasConsumed: u64;  events?: DecodedEvent[];}

Error will contain the parsed error message, events will contain the parsed contract events, gasConsumed is the gas consumed by this call. After the transaction is on the chain, we will provide a URL link to polkadot apps, The user can view the detailed information of the transaction on the browser.

  • We provide a more convenient way to call contracts. You can check the balance through contract.query.balanceOf(address) or contract.balanceOf(address). We also provide a function for estimating gas, just call contract.estimateGas(...params).
  • When instantiating the contract, we provide the default endowment value, which is probably (existentialDeposit + tombstoneDeposit)*2. The default gaslimit will be set to MaximumBlockWeight * 0.2.
  • We provide more detailed contract call logs and error prompts so that users can find their errors more easily through the logs printed on the console.

Writing a Plugin#

Plugins are the core part of Redspot, and they are also the focus of our next work. Most of our future functions will be integrated in the form of plug-ins. We hope that Redspot can become a progressive, scalable contract development framework, rather than being bloated like Truffle. Currently, plugins can be integrated into Redspot by extending the runtime environment, adding Tasks, and extending Tasks.

Extend the runtime environment#

The @redspot/patract plugin was introduced in this way. Let me demonstrate how to add a new plug-in.

We can add the previous setup function to the redspot runtime environment to simplify the contract deployment process in the console. code show as below:

import { extendEnvironment } from "redspot/config";import type { RedspotRuntimeEnvironment } from "redspot/types";
extendEnvironment((env: RedspotRuntimeEnvironment) => {  (env as any).setup = async function setup() {    const {      getContractFactory,      getRandomSigner,      getSigners,      getAbi,      api,    } = env.patract!;    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(10));    const contractFactory = await getContractFactory("ERC20", sender);    const contract = await contractFactory.deploy("new", "1000");    const abi = getAbi("ERC20");    const receiver = await getRandomSigner();
    return { sender, contractFactory, contract, abi, receiver, Alice, one };  }})

The ʻextendEnvironmentfunction can help us inherit and extend env. In the callback function of extendEnvironment, we can access the existing env, and we hang thesetup` function under env. Now try the console again:

image

We successfully injected the setup function into the Redspot runtime environment. In the console, setup is a global variable. Now only two lines of code can complete the contract initialization and transfer test.

Writing a Task#

Task is a JavaScript asynchronous function with some related metadata. Redspot uses this metadata to automatically perform some operations for you, such as parameter parsing, verification and help information.

Everything you do in redspot can be defined as a Task. The test, compile, run commands provided by Redspot are essentially Tasks. And the api they use is exactly the same as the api provided to the user. So the user can write the function plug-ins he wants without restrictions, even compile Solidity, and call EVM contracts.

Next I will create a task that prints the balance.

const {task} = require("redspot/config");
task("balance", "Prints an account's balance")  .addParam("account", "The account's address")  .setAction(async (taskArgs, { patract }) => {    const api = patract.api;    await patract.connect()    const accountInfo = await api.query.system.account(taskArgs.account);    console.log(accountInfo.toHuman());  });
module.exports = {};

Then we can run it and get the result:

$ npx redspot balance {  nonce: '0',  refcount: '0',  data: { free: '0', reserved: '0', miscFrozen: '0', feeFrozen: '0' }}

Extend a Task#

Redspot has many built-in Tasks and Subtasks. For example, to compile a contract, there will be these Tasks:

export const TASK_COMPILE = "compile";export const TASK_COMPILE_COMPILE = "compile:compile";export const TASK_COMPILE_GET_RESOLVED_WORKSPACE = "compile:get-resolved-workspace";export const TASK_BUILD_ARTIFACTS = "compile:build-artifacts";export const TASK_COMPILE_GET_COMPILER_INPUT = "compile:get-compile-input";export const TASK_COMPILE_RUN_COMPILER = "compile:run-compiler";export const TASK_COMPILE_RUN_GENERATE_METADATA = "compile:run-generate-metadata";

We can achieve the desired function by overriding these Tasks. For example, the compile task can be overridden to compile the solidity contract.

You can also extend a task. We provide the runSuper function in the task, which allows you to run the original task. for example:

const { exec } = require("child_process");
task('test', async (_args, env, runSuper) => {  exec("[start node]")  await runSuper()  exec("[close node]")});

We have extended the test command in this task to start your node before running the test command, and shut down your node when the test runs. You can run npx redspot test to test as before.

Through this plug-in system, we can achieve more complex and changeable requirements. We expect to support more contract development languages, AssemblyScript, C++, Go or C# in the future. We will also realize the compatibility of contracts on different parachains in the future. Redspot v0.2 has been upgraded on the more mature contract development tools of Ethereum. We believe that standing on the shoulders of giants, we can go further. I also hope that more developers can come to contribute code, develop plug-ins, and help us improve the contract development ecology on Substrate.

New features about v0.3#