hardhat-scilla-plugin
Hardhat plugin to test Scilla contracts.
What
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.
Installation
pnpm install hardhat-scilla-plugin
Import the plugin in your hardhat.config.js
:
require("hardhat-scilla-plugin");
Or if you are using TypeScript, in your hardhat.config.ts
:
import "hardhat-scilla-plugin";
Running Scilla
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.
Tasks
This plugin adds the scilla-check task to Hardhat:
Hardhat version 2.16.0
Usage: hardhat [GLOBAL OPTIONS] scilla-check --libdir <STRING> [...contracts]
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
Environment extensions
This plugin extends the Hardhat Runtime Environment by adding an scillaContracts
field
whose type is ScillaContracts
.
Usage
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.
Deploy a contract
To deploy a contract all you need to know is its name:
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");
You can override the following parameters while deploying a contract:
TxParams {
version: number;
toAddr: string;
amount: BN;
gasPrice: BN;
gasLimit: Long;
code?: string;
data?: string;
receipt?: TxReceipt;
nonce?: number;
pubKey?: string;
signature?: string;
}
let contract: ScillaContract = await hre.deployScillaContract("HelloWorld", "Hello World", {gasLimit: 8000});
Alternatively, you can deploy them using the contractDeployer
object injected to hre
:
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()
.deploy();
In the same way, you can deploy your libraries with their names:
let library: ScillaContract = await hre.deployScillaLibrary("MyLibrary", false);
Pass true
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:
contract2 = await hre.deployScillaWithLib("TestContract2",
[{name: "MutualLib", address: mutualLibAddress}]
Or:
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
.
Change the default parameters when deploying a contract
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.
Connect to an existing Scilla contract
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
.
Call a transition
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:
await contract.Set(12);
Call a transition with a custom nonce
await contract.Set(12, {nonce: 12});
It's possible to override the following properties:
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;
}
await contract.Set(12, {nonce: 12, amount: new BN(1000)});
call a transition with a new account
You can call connect
on a contract to change its default account which is used to execute transitions.
await contract.connect(newAccount).Set(123);
Get field value
If a given contract has a filed named msg
is possible to get its current value using a function call to msg()
const msg = await contract.msg();
Expect a result
Chai matchers can be used to expect a value:
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:
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:
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):
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:
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.
TODO
- Support formatting complex data types such as
Map
and List
Scilla checker task
To run scilla-checker
on all of the scilla contracts in the contracts directory run:
npx hardhat scilla-check --libdir path_to_stdlib
alternatively, you can check a specific file(s):
npx hardhat scilla-check --libdir path_to_stdlib contracts/scilla/helloWorld.scilla
TODO
Plugin 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.
pnpm test
Will run all tests that don't require an external network (so that test passes will be deterministic).
pnpm test-live
Will run just the tests that do require an external network.
pnpm test-all
Will run both sets of tests.
Publishing the plugin
In order to publish the plugin to npmjs.com, follow these steps:
- Increase the plugin version in package.json
- Run
npm login
and enter your credentials. - Run
pnpm install
- Run
pnpm publish
. This command will run pnpm build
&& pnpm test
beforehand.