I'm trying to test a factory contract using hardhat and waffle. I have a contract called Domain
:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "hardhat/console.sol";
contract Domain {
string private publicKey;
address[] public children;
constructor(string memory _publicKey) {
console.log("Deploying a domain using public key: ", _publicKey);
publicKey = _publicKey;
}
function getChildren() public view returns (address[] memory){
return children;
}
}
And a factory for deploying this contract:
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "./Domain.sol";
import "hardhat/console.sol";
contract DomainFactory {
Domain[] private _domains;
function createDomain(
string memory _publicKey
) public returns (address){
Domain domain = new Domain(
_publicKey
);
_domains.push(domain);
return address(domain);
}
function allDomains(uint256 limit, uint256 offset)
public
view
returns (Domain[] memory coll)
{
return coll;
}
}
I have the following tests defined, where this
refers to a context object defined in a "world" file (using cucumber.js
.
When('the holder of this public key creates a domain', async function () {
this.domain = await this.factory.createDomain('<public_key>');
});
Then('a child of this domain has the name name', async function () {
const children = this.domain.getChildren();
const childrenWithName = children.find((child:any) => {
return child.getNames().some((childName:any) => {
return childName === 'name';
})
})
expect(childrenWithName).to.be.an('array').that.is.not.empty;
});
Ideally in the when
step, I could define this.domain
as the result of deploying a contract, and thereafter test the methods of the contract I deploy:
// world.ts
import { setWorldConstructor, setDefaultTimeout } from '@cucumber/cucumber'
import {deployContract, MockProvider, solidity} from 'ethereum-waffle';
import {use} from "chai";
import DomainContract from "../../../artifacts/contracts/Domain.sol/Domain.json";
import DomainFactoryContract from "../../../artifacts/contracts/DomainFactory.sol/DomainFactory.json";
import { Domain, DomainFactory } from "../../../typechain-types";
import {Wallet} from "ethers";
use(solidity);
setDefaultTimeout(20 * 1000);
class DomainWorld {
public owner: string
public wallets: Wallet[]
public factory: DomainFactory | undefined
public domain: Domain | undefined
public ready: boolean = false
private _initialized: Promise<boolean>
async deployContractByAddress(address, ...args){
return await deployContract(this.wallets[0], address, ...args);
}
constructor() {
this.wallets = new MockProvider().getWallets();
this.owner = this.wallets[0].address
const that = this
this._initialized = new Promise(async (resolve, reject) => {
try {
that.factory = (await deployContract(that.wallets[0], DomainFactoryContract, [])) as DomainFactory;
that.ready = true
resolve(true)
}catch (err) {
reject(err)
}
})
}
}
setWorldConstructor(DomainWorld);
My problem is, hardhat's deployContract
function isn't expecting a contract address, which is what is returned by my DomainFactory
's create method. How can I test contracts deployed via my factory if the return value is an address?
I've made a quick hardhat
project for you to test it.
Here are the highlights:
The easiest way I find to get this returned valued of the contract offchain (and by offchain in this case, I mean inside your test environment) is by emitting an Event. So, I made the following change to your code.
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;
import "./Domain.sol";
import "hardhat/console.sol";
contract DomainFactory {
Domain[] private _domains;
event CreatedDomain(address domainAddress);
function createDomain(string memory _publicKey) public returns (address) {
Domain domain = new Domain(_publicKey);
_domains.push(domain);
emit CreatedDomain(address(domain));
return address(domain);
}
function allDomains(uint256 limit, uint256 offset)
public
view
returns (Domain[] memory coll)
{
return coll;
}
}
I just simply emit a CreatedDomain()
event containing the deployed Domain
address.
One more thing: if I remember correctly, you can only retrieve values direct from function returns offchain, if your function is of type view
. Otherwise, you will need to emit the event and then find it later.
In order to test the Domain
deployed by the DomainFactory
take a look at this test script:
import { expect } from "chai";
import { ethers } from "hardhat";
import { Domain, DomainFactory } from "../typechain";
describe("Domain", function () {
let domainFactory: DomainFactory;
let domain: Domain;
let domainAddress: string;
it("Should deploy a DomainFactory ", async () => {
const DomainFactory = await ethers.getContractFactory("DomainFactory");
domainFactory = await DomainFactory.deploy();
await domainFactory.deployed();
});
it("deploy a Domain using DomainFactory ", async () => {
const tx = await domainFactory.createDomain("public string here");
const rc = await tx.wait();
const event = rc.events?.find((event) => event.event === "CreatedDomain");
const args = event?.args;
if (args) domainAddress = args[0];
});
it("attach an abi interface to the deployed domain", async () => {
const Domain = await ethers.getContractFactory("Domain");
domain = await Domain.attach(domainAddress);
});
it("get data from Domain deployed by DomainFactory ", async () => {
const res = await domain.getChildren();
console.log(res);
});
});
It deploys a DomainFactory
then uses the createDomain()
method, fetches the deployed address from the functino events, then use it to attach the ABI to the deployed Domain
.
Full code here: https://github.com/pedrohba1/stackoverflow/tree/main/Domain
Anything else related to running it I will be adding in the comments.