Skip to main content

Report for Redspot v0.3

5 weeks ago, Patract Hub launched a Treasury Proposal #23 for Redspot v0.3, and now we have finished all the development works on time. For a quick review, you can visit our website at

Redspot v0.1 has the basic functions of compiling contracts, testing contracts and deploying contracts. The v0.2 version refactored the bottom layer, allowing Redspot to have better scalability, adding TypeScript support, a flexible plug-in system, and simpler and more intuitive logic. Redspot v0.2 already has a very complete basic framework, so v0.3 mainly upgrades the plug-in system on the basis of v0.2, adds chai matcher, sample plug-ins, and supplements Redspot's official website and documentation tutorials.

Summary of Redspot’s Future Plan:#

  • v0.1: Build core functions based on Truffle framework
  • v0.2: Migrate to Hardhat framework to enhance the extensibility of plugins and add some features to provide a smoother development workflow
  • v0.3: Promote Redspot and allow more contract developers to participate. Combine the features of Substrate to add various plug-ins, such as Waffle, Jupiter, Gas report.
  • v0.4: Provide Typescript type support for contracts (similar to Typechain), integrate multi-language SDK, etc.

New features in Redspot v0.3#

Now let us show the design and implementation of v0.3, and how to run and verify it. Like v0.2, you can install a template project locally via npx redspot-new erc20. For specific usage methods, please refer to Treasury Report #156 of v0.2

TypeScript upgrade#

The templates provided by Redspot by default are based on TypeScript, so developers who are not familiar with TypeScript can easily use TypeScript's code hints and type system. In v0.2, if a new plug-in is introduced, and this plug-in extends the type of Redspot Runtime Environment, we need to manually import the type file of the plug-in in the tsconfig.json file so that TypeScript can recognize it correctly. like this:

"files": [  "./redspot.config.ts"  "./node_modules/@redspot/patract/type-extensions.d.ts"]

We improved this in v0.3. Now the plug-in type file does not need to be set up specially, the type will be automatically imported.

Now the types of Redspot are all provided by redspot/types, and the plug-in can describe the types of new attributes and interfaces by inheriting RuntimeEnvironment. like this:

// type-extensions.tsimport 'redspot/types';
declare module 'redspot/types' {  interface RuntimeEnvironment {    [pluginName]: {      ... // type    };  }}

Then import this file import "type-extensions.ts" inside the index.js of the plugin. In this way, when a plug-in is imported, TypeScript will automatically recognize the type file.

We always recommend users to use TypeScript. Redspot uses ts-node to run TypeScript code internally. When running scripts or tests, if the value of TS_NODE_TRANSPILE_ONLY is not explicitly set, Redspot will defaultly set TS_NODE_TRANSPILE_ONLY to true. This can speed up the compilation speed of ts-node and avoid type errors. In Redspot, you can write TypeScript in the same way as JavaScript. Users will not have additional learning burden, but they can directly enjoy the benefits of code hints brought by TypeScript.

Optimization of plug-in system#

In v0.2 we need to use usePlugin to introduce a plugin. In v0.3, the introduction of plugins has become more natural. Only the plugins you need in require or import in redspot.config.ts.

import '@redspot/patract'// orrequire('@redspot/patract')

Now the plug-in does not need to export a function, but directly calls extendEnvironment to extend the Runtime Environment:

import { extendEnvironment } from 'redspot/config';
extendEnvironment((env) => {  env.patract = ...});

We have abandoned The plug-in internalTask and changed to subtask. subtask supports complex types and no longer needs stringified.

Add artifacts object#

In v0.2, you need to import the module redspot/plugins to get the readAbi and readWasm functions:

const { readAbi, readWasm } = require("redspot/plugins")
readAbi(env.paths.artifacts, contractName) // get abireadWasm(env.paths.artifacts, contractName) // get wasm

Now the Redspot Runtime Environment will export an artifacts object, which contains artifact-related methods.

const { artifacts } = env
artifacts.readAbi(contractName)  // get abiartifacts.readWasm(contractName)  // get wasmartifacts.saveArtifact(path) // save artifact 

get the abi file at the same time:

const { artifacts } = env
artifacts.readAbiSync(contractName)  // get abiartifacts.readWasmSync(contractName)  // get wasm

Refactor the network object#

In v0.2, will import a provider object compatible with Polkadot.js's ProviderInterface type. Now we have extended the network object and added more methods and properties.

export interface Network {  name: string;  config: NetworkConfig;  provider: WsProvider;  api: ApiPromise;  registry: Registry;  keyring: Keyring;  getSigners(): Promise<Signer[]>;  createSigner(pair: KeyringPair): Signer;  gasLimit?: BN;  explorerUrl?: string;  utils: {    encodeSalt(      salt?: Uint8Array | string | null,      signer?: Signer    ): Promise<Uint8Array>;  };}

Now we are more compatible with polkadot.js. api, registry, keyring and provider are all from polkadot.js, Redspot will automatically handle the initialization and setting of these objects.

The signer object returned by getSigners is compatible with the signer in polkadot.js and can be directly used for transaction signing. If you want to create a signer, it is also very simple by calling the createSigner method:

const uri =  'bottom drive obey lake curtain smoke basket hold race lonely fit walk//Alice';
const signer = createSigner(network.keyring.createFromUri(uri));

Add @redspot/chai plug-in#

This plug-in is used to simplify the writing of test cases. The values returned by all APIs in polkadot.js are not basic types, so it is impossible to simply judge whether they are equal. Many times you need to write: expect(account.toString()).to.equal('5GHs.....'). If you write like this: expect(account.eq('5GHs....')).to.true, you will lose the detailed error message provided by the test framework. @redspot/chai solves this problem, it covers the eq and equal matchers of chai internally. example:

expect(Uint8Array.from([0x11,0x22])).to.equal('0x1122'); // trueexpect(Uint8Array.from([0x11,0x22])).to.equal('0x2122'); // falseexpect(new BN('10000000000000000')).to.equal(10000000000000000n); // trueexpect(new BN('10000000000000000')).to.equal('10000000000000000'); // trueexpect(AccountId).to.equal('5GHs....'); // true

For contracts, if you use the @redspot/patract plugin, you can easily perform contract event matching, simple and easy to read:

// Detects if a transfer event is sent for the transactionawait expect(contract.tx.transfer(receiver.address, 7))    .to.emit(contract, 'Transfer')     // Detects if the transaction sends a transfer event with the specified parametersawait expect(contract.tx.transfer(receiver.address, 7))  .to.emit(contract, 'Transfer')  .withArgs(sender.address, receiver.address, 7);

You can also detect the change in the balance of the erc20 contract:

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]);

There are also some tool adapters:


If you want to import the @redspot/chai plugin, you only need to add import "@redspot/chai" in redspot.config.ts. When the plugin is imported, it will automatically add the matcher to chai.

Now the complete test case of erc20 is as follows:

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();    sender.pair.publicKey    const result = await contract.query.balanceOf(sender.address);    expect(result.output).to.equal(1000);  });
  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');  });});

Add @redspot/gas-reporter plug-in#

@redspot/gas-reporter will print the gas consumption at the end of each test. The plugin covers the Test task. When calling test, it adds a reporter to mocha.

subtask(TASK_TEST_RUN_MOCHA_TESTS).setAction(  async (args: any, rse, runSuper) => {    const options = getOptions(rse);
    if (options.enabled) {      mochaConfig = rse.config.mocha || {};      mochaConfig.reporter = GasReporter;      mochaConfig.reporterOptions = options;      rse.config.mocha = mochaConfig;    }
    return runSuper();  });

Input npx redspot test,and will get results:


The gas-reporter will monitor the block at the beginning of the test, and process the data of all blocks at the end of the test, and get the contract transaction. Then use the transaction data of the contract to call to get the estimated gas consumption value. Due to the estimated gas value, it is not very accurate. So gas-reporter will also analyze the events in the block to get the weight value of the contract transaction. Both gas and weight will be displayed after the test case is completed.

Add @redspot/jupiter plug-in#

This is a sample plug-in that shows how Redspot adapts some chains. This plugin modifies the contract address calculation method, changing the random salt to account nonce. And it only takes effect for the jupiter chain.

extendEnvironment((env) => { = async function encodeSalt(    salt?: string | Uint8Array | null,    signer?: Signer  ): Promise<Uint8Array> {    if (!signer) throw new Error('Need Signer');
    const accountInfo = await signer.api.query.system.account(signer.address);
    const runtimeVersion = signer.api.runtimeVersion;
    const isJupiter = runtimeVersion.specName      .toString()      .toLowerCase()      .includes('jupiter');
    if (!isJupiter) return, signer);
    const nonce = accountInfo.nonce.toNumber();
    return salt instanceof Bytes ? salt : compactAddLength(numberToU8a(nonce));  };});

We get the specname through the runtimeversion of the chain. Then judge whether the specname is Jupiter, and if not, return to the original encodeSalt method. Our subsequent judgment process is integrated into the Redspot framework.

We will add more chain plugins in the future, such as europa. It has some special RPCs to facilitate contract testing. Such as the function of resetting the state of the block.

Redspot's official website and tutorials#

We designed and developed the Redspot homepage, filled with some documents and tutorials. You can view it here

Our homepage: img

Our Toturial page: img

Our Documentation page: img

Other optimizations#

  • We have adjusted the Redspot release process, and Redspot will now be released automatically through CI. Now, like polkadot.js, x.y.1 will be the stable version of Redspot, and x.y.2-z will be the test version of Redspot. We also added the vers parameter to Redspot-template. If you want to install a historical version of Redspot, you can do by this:
npx redspot-new erc20 --vers ^0.4.2-15
  • We have sorted out the types of Redspot's framework, and now all types can be found in redspot/types.
  • We have moved some exported functions and properties of @redspot/patract to network to facilitate other plugins to extend.

Recap of v0.3 verification#

  • M1: Homepage and API docs page development

View Redspot's homepage

  • M2: Write detailed tutorials and documents

View Redspot's tutorials and . Create a new Redspot project and view the typescript code hints in the project.

  • M3: Upgrade Redspot's plugin system

Optimizations and upgrades of redspot have been demonstrated above.

  • M4: Develop Jupiter plugin and Gas Report v0.2 plugin

The gas-reports plug-in has been added to the default template. Just run npx redspot test to get the gas-reporter results. You can use the @redspot/jupiter plugin by importing the jupiter plugin import ‘@redspot/jupiter’. If you are accessing the jupiter network, the contract address generated at this time is calculated through account nonce.

  • M5: Develop assertion library

Check the test case for sample erc20 template