Skip to content

Zilliqa/hardhat-scilla-plugin

Repository files navigation

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"); // Contract with initial parameters.

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}); // Override a parameter

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()  // To enable contract compression.
    .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 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:

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

  • 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.

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:

  1. Increase the plugin version in package.json
  2. Run npm login and enter your credentials.
  3. Run pnpm install
  4. Run pnpm publish. This command will run pnpm build && pnpm test beforehand.