diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..b691586a --- /dev/null +++ b/.env.example @@ -0,0 +1,6 @@ +CHAINLINK_SUBSCRIPTION_ID= +CHAINLINK_DON_ID= +CHAINLINK_ROUTER_ADDRESS= +PRIVATE_KEY= +BASE_SEPOLIA_RPC_URL= +SECONDARY_PRIVATE_KEY= \ No newline at end of file diff --git a/networks.js b/networks.js index 5311e1d3..8e49a3e0 100644 --- a/networks.js +++ b/networks.js @@ -6,6 +6,7 @@ // Loads environment variables from .env.enc file (if it exists) require("@chainlink/env-enc").config() +require("dotenv").config() const DEFAULT_VERIFICATION_BLOCK_CONFIRMATIONS = 2 diff --git a/package-lock.json b/package-lock.json index 9d5527f7..0c0d57bc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "@typechain/hardhat": "^6.1.3", "axios": "^1.1.3", "chai": "^4.3.6", + "dotenv": "^16.5.0", "eth-crypto": "^2.4.0", "ethers": "^5.7.2", "hardhat": "^2.17.3", @@ -3886,6 +3887,18 @@ "node": ">=8" } }, + "node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", diff --git a/package.json b/package.json index 80b10972..5be1d628 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "@typechain/hardhat": "^6.1.3", "axios": "^1.1.3", "chai": "^4.3.6", + "dotenv": "^16.5.0", "eth-crypto": "^2.4.0", "ethers": "^5.7.2", "hardhat": "^2.17.3", diff --git a/test/unit/FunctionsConsumer.spec.js b/test/unit/FunctionsConsumer.spec.js index 10bd1174..587b6eff 100755 --- a/test/unit/FunctionsConsumer.spec.js +++ b/test/unit/FunctionsConsumer.spec.js @@ -1,13 +1,172 @@ -// const { assert } = require("chai") -// const { loadFixture } = require("@nomicfoundation/hardhat-network-helpers") -// const { network } = require("hardhat") +const { expect } = require("chai") +const { ethers } = require("hardhat") +require("dotenv").config() -describe("Functions Consumer Unit Tests", async function () { - // We define a fixture to reuse the same setup in every test. - // We use loadFixture to run this setup once, snapshot that state, - // and reset Hardhat Network to that snapshot in every test. +describe("FunctionsConsumer Unit Tests", function () { + let owner, nonOwner, consumer, router, donId, subscriptionId - it("empty test", async () => { - // TODO + const dummySource = ` + if (!args || args.length !== 2) throw Error("Need two args"); + return Functions.encodeUint256(parseInt(args[0]) + parseInt(args[1])); + ` + + beforeEach(async function () { + expect(process.env.PRIVATE_KEY, "Missing PRIVATE_KEY in .env").to.not.be.undefined + owner = new ethers.Wallet(process.env.PRIVATE_KEY, ethers.provider) + nonOwner = new ethers.Wallet(process.env.SECONDARY_PRIVATE_KEY, ethers.provider) + expect(owner.address).to.not.equal(nonOwner.address, "Owner and non-owner should be different") + expect(owner.address).to.not.equal(ethers.constants.AddressZero, "Owner address should not be zero") + expect(nonOwner.address).to.not.equal(ethers.constants.AddressZero, "Non-owner address should not be zero") + + router = process.env.CHAINLINK_ROUTER_ADDRESS + donId = process.env.CHAINLINK_DON_ID + subscriptionId = process.env.CHAINLINK_SUBSCRIPTION_ID + + console.log("Owner address:", owner.address) + console.log("Non-owner address:", nonOwner.address) + console.log("Router:", router) + console.log("DON ID:", donId) + console.log("Subscription ID:", subscriptionId) + + if (!router || !donId || !subscriptionId) { + console.warn("Missing Chainlink config. Skipping unit tests.") + this.skip() // Properly skip tests + } + + const Consumer = await ethers.getContractFactory("FunctionsConsumer") + consumer = await Consumer.deploy(router, donId) + await consumer.deployed() + + console.log("Consumer deployed at:", consumer.address) + }) + + describe("Deployment", function () { + it("sets DON ID and owner correctly", async function () { + expect(consumer).to.not.be.undefined + expect(await consumer.donId()).to.equal(donId) + expect(await consumer.owner()).to.equal(owner.address) + }) + }) + + describe("Ownership", function () { + it("only owner can set DON ID", async function () { + const newDonId = ethers.utils.formatBytes32String("nonowner-don") + + let errorCaught = false + try { + await consumer.connect(nonOwner).setDonId(newDonId) + } catch (error) { + errorCaught = true + console.log("Error caught for setDonId:", error.reason || error.message) + + // Check multiple levels of error nesting based on actual error structure + const errorString = JSON.stringify(error) + const errorMessage = error.message || error.toString() + const errorReason = error.reason || "" + + const hasExpectedError = + errorString.includes("Only callable by owner") || + errorMessage.includes("Only callable by owner") || + errorReason.includes("Only callable by owner") || + (error.error && error.error.reason && error.error.reason.includes("Only callable by owner")) || + (error.error && error.error.message && error.error.message.includes("Only callable by owner")) || + (error.error && + error.error.error && + error.error.error.reason && + error.error.error.reason.includes("Only callable by owner")) + + expect(hasExpectedError, `Expected "Only callable by owner" error not found. Full error: ${errorString}`).to.be + .true + } + + expect(errorCaught, "Expected transaction to revert").to.be.true + + // The DON ID should remain unchanged + expect(await consumer.donId()).to.equal(donId) + }) + + it("owner can set DON ID", async function () { + const newDonId = ethers.utils.formatBytes32String("new-don-id") + + const tx = await consumer.connect(owner).setDonId(newDonId) + await tx.wait() + + console.log("DON ID set transaction hash:", tx.hash) + + await new Promise((resolve) => setTimeout(resolve, 200)) + + expect(await consumer.donId()).to.equal(newDonId) + }) + + it("only owner can transfer ownership", async function () { + // Verify current owner first + expect(await consumer.owner()).to.equal(owner.address) + + let errorCaught = false + try { + await consumer.connect(nonOwner).transferOwnership(nonOwner.address) + } catch (error) { + errorCaught = true + console.log("Error caught for transferOwnership:", error.reason || error.message) + + // Check multiple levels of error nesting based on actual error structure + const errorString = JSON.stringify(error) + const errorMessage = error.message || error.toString() + const errorReason = error.reason || "" + + const hasExpectedError = + errorString.includes("Only callable by owner") || + errorMessage.includes("Only callable by owner") || + errorReason.includes("Only callable by owner") || + (error.error && error.error.reason && error.error.reason.includes("Only callable by owner")) || + (error.error && error.error.message && error.error.message.includes("Only callable by owner")) || + (error.error && + error.error.error && + error.error.error.reason && + error.error.error.reason.includes("Only callable by owner")) + + expect(hasExpectedError, `Expected "Only callable by owner" error not found. Full error: ${errorString}`).to.be + .true + } + + expect(errorCaught, "Expected transaction to revert").to.be.true + + // Owner should remain unchanged + expect(await consumer.owner()).to.equal(owner.address) + }) + }) + + describe("sendRequest", function () { + it("blocks non-owner from sending request", async function () { + let errorCaught = false + try { + await consumer.connect(nonOwner).sendRequest(dummySource, 0, "0x", [], [], subscriptionId, 100000) + } catch (error) { + errorCaught = true + console.log("Full error structure:", JSON.stringify(error, null, 2)) + + // Check multiple levels of error nesting based on the actual error structure + const errorString = JSON.stringify(error) + const errorMessage = error.message || error.toString() + const errorReason = error.reason || "" + + // Check the specific nested structure from the actual error + const hasExpectedError = + errorString.includes("Only callable by owner") || + errorMessage.includes("Only callable by owner") || + errorReason.includes("Only callable by owner") || + (error.error && error.error.reason && error.error.reason.includes("Only callable by owner")) || + (error.error && error.error.message && error.error.message.includes("Only callable by owner")) || + (error.error && + error.error.error && + error.error.error.reason && + error.error.error.reason.includes("Only callable by owner")) + + expect(hasExpectedError, `Expected "Only callable by owner" error not found. Full error: ${errorString}`).to.be + .true + } + + expect(errorCaught, "Expected transaction to revert with gas estimation error").to.be.true + }) }) })