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 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 frameworkv0.2: Migrate to Hardhat framework to enhance the extensibility of plugins and add some features to provide a smoother development workflowv0.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.3Now 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 upgradeThe 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 systemIn 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 objectIn 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 objectIn 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-inThis 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:
expect(...).to.properAddressexpect(...).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:
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-inThis 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.
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 tutorialsWe designed and developed the Redspot homepage, filled with some documents and tutorials. You can view it here https://redspot.patract.io
#
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 verificationM1: 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 pluginimport ‘@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