hardhat-scilla-plugin
Version:
Hardhat TypeScript plugin for scilla testing
353 lines (251 loc) • 10.7 kB
Markdown
# hardhat-scilla-plugin
[Hardhat](https://hardhat.org) 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
```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";
```
## 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](https://github.com/Zilliqa/scilla/) 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:
```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);
```
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:
```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`.
### 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 accepted
* `timeout` - 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:
```typescript
await contract.Set(12);
```
### Call a transition with a custom nonce
```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)});
```
### 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.
```typescript
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()`
```typescript
const msg = await contract.msg();
```
### Expect a result
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](https://github.com/Zilliqa/Zilliqa/tree/master/tests/EvmAcceptanceTests/test/scilla).
### 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](./contracts/) 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
```
### TODO
- Add `scilla-fmt` task
# 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.
```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](https://www.npmjs.com/package/hardhat-scilla-plugin), follow these steps:
1. Increase the plugin version in [package.json](./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.