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.
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
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.
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
.
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
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));
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');
});
});
@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.
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.
We designed and developed the Redspot homepage, filled with some documents and tutorials. You can view it here https://redspot.patract.io
Our homepage:
Our Toturial page:
Our Documentation page:
npx redspot-new erc20 --vers ^0.4.2-15
redspot/types
.@redspot/patract
to network to facilitate other plugins to extend.View Redspot's homepage https://redspot.patract.io
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.
Optimizations and upgrades of redspot have been demonstrated above.
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.
Check the test case for sample erc20 template
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.