Hardhat TypeScript plugin for scilla testing
npm install hardhat-scilla-pluginHardhat plugin to test Scilla contracts.
This plugin is used to test scilla contracts in hardhat. It tries to be like ethers.js:
* You can deploy contracts using their names.
* You can call transitions like a normal function call.
* You can get field easily.
* You can use custom chai matchers to expect scilla events.
``bash`
pnpm install hardhat-scilla-plugin
Import the plugin in your hardhat.config.js:
`js`
require("hardhat-scilla-plugin");
Or if you are using TypeScript, in your hardhat.config.ts:
`ts`
import "hardhat-scilla-plugin";
In order to check, and extract data from, Scilla contracts, we use binaries from the Scilla distribution itself.
By default, we pull these from the zilliqa/scilla container in docker hub, using Scilla v0.13.3, but if you want to run them from your local machine, you can set the USE_NATIVE_SCILLA environment variable to run them from your PATH. If want to run scilla-checker with USE_NATIVE_SCILLA set, you will need to give the -libDir argument to tell it where to find the Scilla standard library.
If you want to set USE_NATIVE_SCILLA, you need to have scilla-fmt and scilla-checker binaries from the Scilla project on your PATH. You can build them by following the instructions in the scilla project repository.
This plugin adds the _scilla-check_ task to Hardhat:
`
Hardhat version 2.16.0
Usage: hardhat [GLOBAL OPTIONS] scilla-check --libdir
OPTIONS:
--libdir Path to Scilla stdlib
POSITIONAL ARGUMENTS:
contracts An optional list of files to check (default: [])
scilla-check: Parsing scilla contracts and performing a number of static checks including typechecking.
For global options help run: hardhat help
`
This plugin extends the Hardhat Runtime Environment by adding an scillaContracts fieldScillaContracts
whose type is .
Scilla testing can be done in the same way ethers.js is used for solidity. It's possible to deploy a scilla contract by its name and call its transitions just like a normal function call. It's also possible to get a field value through a function call. In the below sections, all of these topics are covered in detail.
To deploy a contract all you need to know is its name:
`typescript
import {ScillaContract, initZilliqa} from "hardhat-scilla-plugin";
const privateKeys = ["254d9924fc1dcdca44ce92d80255c6a0bb690f867abde80e626fbfef4d357004"];
const network_url = "http://localhost:5555";
const chain_id = 1;
initZilliqa(network_url, chain_id, privateKeys);
let contract: ScillaContract = await hre.deployScillaContract("SetGet");
let contract: ScillaContract = await hre.deployScillaContract("HelloWorld", "Hello World"); // Contract with initial parameters.
`
You can override the following parameters while deploying a contract:
`typescript`
TxParams {
version: number;
toAddr: string;
amount: BN;
gasPrice: BN;
gasLimit: Long;
code?: string;
data?: string;
receipt?: TxReceipt;
nonce?: number;
pubKey?: string;
signature?: string;
}`typescript`
let contract: ScillaContract = await hre.deployScillaContract("HelloWorld", "Hello World", {gasLimit: 8000}); // Override a parameter
Alternatively, you can deploy them using the contractDeployer object injected to hre:`typescript
const contract = await hre.contractDeployer
.withName("Codehash")
.deploy();
const contract = await this.hre.contractDeployer
.withName("HelloWorld")
.withContractParams("Hello world!")
.deploy();
const contract = await this.hre.contractDeployer
.withName("HelloWorld")
.withContractParams("sss")
.withContractCompression() // To enable contract compression.
.deploy();
`
In the same way, you can deploy your libraries with their names:
`typescript`
let library: ScillaContract = await hre.deployScillaLibrary("MyLibrary", false);true
Pass as the second parameter if you want your library's contract gets compressed before deployment.
and finally, here is how you can deploy a contract importing a user-defined library:
`typescript`
contract2 = await hre.deployScillaWithLib("TestContract2",
[{name: "MutualLib", address: mutualLibAddress}]`
Or:typescript`
const contract = await this.hre.contractDeployer
.withName("TestContract2")
.withUserDefinedLibraries(
[{name: "MutualLib", address: mutualLibAddress}]
)
.deploy();
To change the deployer of the contract, you can send an instance of Account class to hre.setActiveAccount.
You can call
``
hre.setScillaDefaults( obj )
to set the defaults used when deploying a Scilla contract. Parameters supported are:
* gasPrice - a string denoting the gas price in Li (to match the initZilliqa use).gasLimit
* - a string denoting the gas limit (in Qa, to match initZilliqa use)attempts
* - a number denoting the number of attempts to make to check whether a transaction has been acceptedtimeout
* - the space between attempts, in milliseconds.
Call
``
hre.interactWithScillaContract(address)
To:
* Retrieve the code for a contract from the configured chain.
* Parse it.
* Construct a proxy contract object for it.
* Return that object, or undefined if we failed.
address should be a string, and the function returns ScillaContract | undefined.
It's not harder than calling a normal function in typescript.
Let's assume we have a transition named Set which accepts a number as its parameter. Here is how to call it:
`typescript`
await contract.Set(12);
typescript
await contract.Set(12, {nonce: 12});
`
It's possible to override the following properties:`typescript
export interface TxParams {
version: number;
toAddr: string;
amount: BN;
gasPrice: BN;
gasLimit: Long;
code?: string;
data?: string;
receipt?: TxReceipt;
nonce?: number;
pubKey?: string;
signature?: string;
}
``typescript
await contract.Set(12, {nonce: 12, amount: new BN(1000)});
`$3
You can call
connect on a contract to change its default account which is used to execute transitions.`typescript
await contract.connect(newAccount).Set(123);
`$3
If a given contract has a filed named
msg is possible to get its current value using a function call to msg()`typescript
const msg = await contract.msg();
`$3
Chai matchers can be used to expect a value:
`typescript
it("Should set state correctly", async function () {
const VALUE = 12;
await contract.Set(VALUE);
expect(await contract.value()).to.be.eq(VALUE);
});
`There are two custom chai matchers specially developed to
expect scilla events. eventLog and eventLogWithParams.
Use eventLog if you just need to expect event name:`typescript
import chai from "chai";
import {scillaChaiEventMatcher} from "hardhat-scilla-plugin";chai.use(scillaChaiEventMatcher);
it("Should contain event data if emit function is called", async function () {
const tx = await contract.emit();
expect(tx).to.have.eventLog("Emit");
});
`Otherwise, if you need to deeply expect an event, you should use
eventLogWithParams. The first parameter is again the event name. The rest are parameters of the expected event. If you expect to have an event like getHello sending a parameter named msg with a "hello world" value:`typescript
import chai from "chai";
import {scillaChaiEventMatcher} from "hardhat-scilla-plugin";chai.use(scillaChaiEventMatcher);
it("Should send getHello() event when getHello() transition is called", async function () {
const tx = await contract.getHello();
expect(tx).to.have.eventLogWithParams("getHello()", {value: "hello world", vname: "msg"});
});
`You can even expect data type of the parameter(s):
`typescript
expect(tx).to.have.eventLogWithParams("getHello()", {value: "hello world", vname: "msg", type: "String"});
`Type should be a valid Scilla type.
But if you just want to expect on the value of a event parameter do this:
`typescript
expect(tx).to.have.eventLogWithParams("getHello()", {value: "hello world"});
`For easier value matching, some value conversions are done under the hood.
* 32/64 bit integer values are converted to
Number
* 128/256 bit integer values are converted to BigNumber
* Option is converted to its inner value if exists any, or null otherwise.
* Bool is converted to underlying boolean value.for more tests please take look at scilla tests.
$3
- Support formatting complex data types such as
Map and List$3
To run
scilla-checker on all of the scilla contracts in the contracts directory run:`bash
npx hardhat scilla-check --libdir path_to_stdlib
`alternatively, you can check a specific file(s):
`bash
npx hardhat scilla-check --libdir path_to_stdlib contracts/scilla/helloWorld.scilla
`$3
- Add
scilla-fmt taskPlugin development
Running internal tests
If you want to monitor your requests:
`
mitmweb --mode reverse:https://dev-api.zilliqa.com --modify-headers /~q/Host/dev-api.zilliqa.com --no-web-open-browser --listen-port 5600 --web-port 8600
export ZILLIQA_API_URL=http://localhost:5600/
`Set
ZILLIQA_API_URL to the URL of a network to test - or to eg. http://localhost:5600 if you're proxying as above.
Set ZILLIQA_NETWORK to the name of the network to test against - see test/fixture-projects/hardhat-proxy/hardhat.config.ts for details.`sh
pnpm test
`Will run all tests that don't require an external network (so that test passes will be deterministic).
`sh
pnpm test-live
`Will run just the tests that do require an external network.
`sh
pnpm test-all
`Will run both sets of tests.
Publishing the plugin
In order to publish the plugin to npmjs.com, follow these steps:
1. Increase the plugin version in package.json
2. Run
npm login and enter your credentials.
3. Run pnpm install
3. Run pnpm publish. This command will run pnpm build && pnpm test` beforehand.