Patract Hub's treasury report for Redspot v0.3 (contract dev scaffold)

3yrs ago
2 Comments

5 weeks ago, Patract Hub applied 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 https://redspot.patract.io/.

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.ts
import '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'
// or
require('@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 abi
readWasm(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 abi
artifacts.readWasm(contractName)  // get wasm
artifacts.saveArtifact(path) // save artifact 

get the abi file at the same time:

const { artifacts } = env

artifacts.readAbiSync(contractName)  // get abi
artifacts.readWasmSync(contractName)  // get wasm

Refactor the network object

In v0.2, env.network 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'); // true
expect(Uint8Array.from([0x11,0x22])).to.equal('0x2122'); // false
expect(new BN('10000000000000000')).to.equal(10000000000000000n); // true
expect(new BN('10000000000000000')).to.equal('10000000000000000'); // true
expect(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 transaction
await expect(contract.tx.transfer(receiver.address, 7))
    .to.emit(contract, 'Transfer') 
    
// Detects if the transaction sends a transfer event with the specified parameters
await 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:

expect(...).to.properAddress
expect(...).to.properHex

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:

img

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 contract.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) => {
  env.network.utils.encodeSalt = 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 env.network.utils.encodeSalt(salt, 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.

If you need to add unique types to the current chain, you can do this in the plugin:

extendEnvironment((rse) => {
  rse.network.api.registry.setKnownTypes({
     types: {
        Address: 'AccountId',
            LookupSource: 'AccountId'
    }
  })
});

In this way, users do not need to manually specify types in redspot.config.ts. Only need to introduce the plug-in of the chain.

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 https://redspot.patract.io

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 https://redspot.patract.io

  • M2: Write detailed tutorials and documents

View Redspot's tutorials https://redspot.patract.io/en/tutorial/ and https://redspot.patract.io/en/plugins/ .
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

For v0.4 and the future

By v0.3, most of the functions of the design target has finished and Redspot has been in production level. Developers can use Redspot as a powerful tool to facilitate their development for WASM contracts. We will need more time to build Patract Hub's other tools and services, like Himalia, Leda and Carpo, and integrate them with Redspot v0.4 or future versions, so stay tuned.

Up
Comments
No comments here