diff --git a/contracts/PoIVerificationManager.sol b/contracts/PoIVerificationManager.sol new file mode 100644 index 0000000..55df03e --- /dev/null +++ b/contracts/PoIVerificationManager.sol @@ -0,0 +1,155 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {VerifierRegistry} from "./VerifierRegistry.sol"; + +/** + * @title PoIVerificationManager + * @dev Manages verification results for Proof of Impact (PoI) submissions + * Integrates with VerifierRegistry to ensure only authorized verifiers can submit verifications + */ +contract PoIVerificationManager is Ownable, ReentrancyGuard { + // Custom errors + error POIVERIFICATION__InvalidVerifierRegistry(address invalidAddress); + error POIVERIFICATION__InvalidPoiId(uint256 poiId); + error POIVERIFICATION__NotAuthorizedVerifier(address verifier); + error POIVERIFICATION__AlreadyVerified(uint256 poiId, address verifier); + error POIVERIFICATION__InvalidStatus(VerificationStatus status); + + + // State variables + enum VerificationStatus { + Pending, + Approved, + Rejected + } + + struct VerificationResult { + address verifier; // Address of the verifier + VerificationStatus status; // Status of the verification + uint256 timestamp; // When the verification was submitted + string reason; // (This is optional) reason for rejection + } + + VerifierRegistry public verifierRegistry; + + // Mapping from PoI ID to verification results + mapping(uint256 => VerificationResult[]) public verificationResults; + + // Mapping to track if a verifier has already verified a specific PoI + mapping(uint256 => mapping(address => bool)) public hasVerified; + + // Events + event PoIVerified( + uint256 indexed poiId, + address indexed verifier, + VerificationStatus status, + uint256 timestamp, + string reason + ); + event VerifierRegistryUpdated(address indexed oldRegistry, address indexed newRegistry); + + + constructor(address _verifierRegistry) Ownable(msg.sender) { + if (_verifierRegistry == address(0)) revert POIVERIFICATION__InvalidVerifierRegistry(_verifierRegistry); + verifierRegistry = VerifierRegistry(_verifierRegistry); + } + + /** + * @dev Submit a verification result for a PoI + * @param poiId The ID of the PoI being verified + * @param status The verification status (Approved/Rejected) + * @param reason Optional reason string (mainly for rejections) + */ + function submitVerification( + uint256 poiId, + VerificationStatus status, + string calldata reason + ) external nonReentrant { + // Input validation + if (poiId == 0) revert POIVERIFICATION__InvalidPoiId(poiId); + if (status == VerificationStatus.Pending) revert POIVERIFICATION__InvalidStatus(status); + if (!verifierRegistry.isVerifier(msg.sender)) revert POIVERIFICATION__NotAuthorizedVerifier(msg.sender); + if (hasVerified[poiId][msg.sender]) revert POIVERIFICATION__AlreadyVerified(poiId, msg.sender); + + // Record verification + verificationResults[poiId].push( + VerificationResult({ + verifier: msg.sender, + status: status, + timestamp: block.timestamp, + reason: reason + }) + ); + + // Mark verifier as having verified this PoI + hasVerified[poiId][msg.sender] = true; + + // Emit event + emit PoIVerified( + poiId, + msg.sender, + status, + block.timestamp, + reason + ); + } + + /** + * @dev Get all verification results for a specific PoI + * @param poiId The ID of the PoI + * @return VerificationResult[] Array of verification results + */ + function getVerificationResults(uint256 poiId) external view returns (VerificationResult[] memory) { + return verificationResults[poiId]; + } + + /** + * @dev Get the latest verification result for a specific PoI + * @param poiId The ID of the PoI + * @return VerificationResult Latest verification result (reverts if no verifications) + */ + function getLatestVerification(uint256 poiId) external view returns (VerificationResult memory) { + VerificationResult[] storage results = verificationResults[poiId]; + if (results.length == 0) revert POIVERIFICATION__InvalidPoiId(poiId); + return results[results.length - 1]; + } + + /** + * @dev Check if a PoI has been approved by any verifier + * @param poiId The ID of the PoI + * @return bool True if the PoI has been approved + */ + function isPoIApproved(uint256 poiId) external view returns (bool) { + VerificationResult[] storage results = verificationResults[poiId]; + for (uint256 i = 0; i < results.length; i++) { + if (results[i].status == VerificationStatus.Approved) { + return true; + } + } + return false; + } + + /** + * @dev Get the number of verifications for a specific PoI + * @param poiId The ID of the PoI + * @return uint256 Number of verifications + */ + function getVerificationCount(uint256 poiId) external view returns (uint256) { + return verificationResults[poiId].length; + } + + /** + * @dev Updates the VerifierRegistry contract address + * @param _newRegistry New VerifierRegistry contract address + */ + function updateVerifierRegistry(address _newRegistry) external onlyOwner { + if (_newRegistry == address(0)) revert POIVERIFICATION__InvalidVerifierRegistry(_newRegistry); + + address oldRegistry = address(verifierRegistry); + verifierRegistry = VerifierRegistry(_newRegistry); + emit VerifierRegistryUpdated(oldRegistry, _newRegistry); + } +} \ No newline at end of file diff --git a/contracts/VerifierRegistry.sol b/contracts/VerifierRegistry.sol new file mode 100644 index 0000000..5eda4ff --- /dev/null +++ b/contracts/VerifierRegistry.sol @@ -0,0 +1,99 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import "@openzeppelin/contracts/access/Ownable.sol"; +import "./VerifierStaking.sol"; + +/** + * @title VerifierRegistry + * @dev Manages the registry of authorized verifiers for PoI verification + * Integrates with VerifierStaking to check staking status + */ +contract VerifierRegistry is Ownable { + // State variables + VerifierStaking public verifierStaking; + mapping(address => bool) private additionalVerifiers; + + // Events + event VerifierAdded(address indexed verifier); + event VerifierRemoved(address indexed verifier); + event VerifierStakingUpdated(address indexed oldContract, address indexed newContract); + + // Custom errors + error REGISTRY__InvalidAddress(); + error REGISTRY__InvalidVerifierStaking(); + error REGISTRY__AlreadyVerifier(); + error REGISTRY__NotVerifier(); + + /** + * @dev Constructor to initialize the registry with the VerifierStaking contract + * @param _verifierStaking Address of the VerifierStaking contract + */ + constructor(address _verifierStaking) Ownable(msg.sender) { + if (_verifierStaking == address(0)) revert REGISTRY__InvalidVerifierStaking(); + verifierStaking = VerifierStaking(_verifierStaking); + } + + /** + * @dev Modifier to restrict function access to only verified verifiers + */ + modifier onlyVerifier() { + if (!isVerifier(msg.sender)) revert REGISTRY__NotVerifier(); + _; + } + + /** + * @dev Checks if an address is an authorized verifier + * @param user Address to check + * @return bool True if the address is an authorized verifier + */ + function isVerifier(address user) public view returns (bool) { + if (user == address(0)) revert REGISTRY__InvalidAddress(); + return verifierStaking.isVerifier(user) || additionalVerifiers[user]; + } + + /** + * @dev Adds an address as an additional verifier (for internal/trusted verifiers) + * @param verifier Address to add as a verifier + */ + function addVerifier(address verifier) external onlyOwner { + if (verifier == address(0)) revert REGISTRY__InvalidAddress(); + if (additionalVerifiers[verifier]) revert REGISTRY__AlreadyVerifier(); + + additionalVerifiers[verifier] = true; + emit VerifierAdded(verifier); + } + + /** + * @dev Removes an address from additional verifiers + * @param verifier Address to remove + */ + function removeVerifier(address verifier) external onlyOwner { + if (verifier == address(0)) revert REGISTRY__InvalidAddress(); + if (!additionalVerifiers[verifier]) revert REGISTRY__NotVerifier(); + + additionalVerifiers[verifier] = false; + emit VerifierRemoved(verifier); + } + + /** + * @dev Updates the VerifierStaking contract address + * @param _newVerifierStaking New VerifierStaking contract address + */ + function updateVerifierStaking(address _newVerifierStaking) external onlyOwner { + if (_newVerifierStaking == address(0)) revert REGISTRY__InvalidVerifierStaking(); + + address oldContract = address(verifierStaking); + verifierStaking = VerifierStaking(_newVerifierStaking); + emit VerifierStakingUpdated(oldContract, _newVerifierStaking); + } + + /** + * @dev Checks if an address is an additional verifier (not through staking) + * @param verifier Address to check + * @return bool True if the address is an additional verifier + */ + function isAdditionalVerifier(address verifier) external view returns (bool) { + return additionalVerifiers[verifier]; + } +} \ No newline at end of file diff --git a/contracts/VerifierStaking.sol b/contracts/VerifierStaking.sol new file mode 100644 index 0000000..8dc3bd2 --- /dev/null +++ b/contracts/VerifierStaking.sol @@ -0,0 +1,187 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.28; + +import {Ownable} from "@openzeppelin/contracts/access/Ownable.sol"; +import {ReentrancyGuard} from "@openzeppelin/contracts/utils/ReentrancyGuard.sol"; +import {IERC20} from "@openzeppelin/contracts/token/ERC20/IERC20.sol"; +import {SafeERC20} from "@openzeppelin/contracts/token/ERC20/utils/SafeERC20.sol"; +import {IDCUToken} from "./interfaces/IDCUToken.sol"; + +/** + * @title VerifierStaking + * @dev Manages staking of DCU tokens for verifier qualification + * Implements stake tracking, minimum thresholds, and unstaking mechanics + */ +contract VerifierStaking is Ownable, ReentrancyGuard { + using SafeERC20 for IERC20; + + // Custom errors + error STAKE__InsufficientStakeAmount(uint256 provided, uint256 required); + error STAKE__NoStakeFound(address staker); + error STAKE__StakeAlreadyExists(address staker); + error STAKE__UnstakingLocked(uint256 remainingTime); + error STAKE__InvalidDCUToken(address token); + error STAKE__InvalidAmount(uint256 amount); + error STAKE__FailedToTransfer(); + + // Constants + uint256 public constant MINIMUM_STAKE = 1000 * 10**18; // 1,000 DCU with 18 decimals + uint256 public constant UNSTAKING_DELAY = 7 days; + + // State variables + IDCUToken public dcuToken; + + struct StakeInfo { + uint256 amount; + uint256 timestamp; + bool isActive; + } + + // Staker info mapping + mapping(address => StakeInfo) public stakes; + + // Events + event VerifierRegistered( + address indexed staker, + uint256 amount, + uint256 timestamp + ); + + event VerifierUnstaked( + address indexed staker, + uint256 amount, + uint256 timestamp + ); + + event StakeAmountUpdated( + address indexed staker, + uint256 oldAmount, + uint256 newAmount, + uint256 timestamp + ); + + /** + * @dev Constructor sets the DCU token contract + * @param _dcuToken Address of the DCU token contract + */ + constructor(address _dcuToken) Ownable(msg.sender) { + if (_dcuToken == address(0)) revert STAKE__InvalidDCUToken(_dcuToken); + dcuToken = IDCUToken(_dcuToken); + } + + /** + * @dev Allows a user to stake DCU tokens to become a verifier + * @param amount Amount of DCU to stake + */ + function stakeDCU(uint256 amount) external nonReentrant { + // Validate amount + if (amount == 0) revert STAKE__InvalidAmount(amount); + if (amount < MINIMUM_STAKE) { + revert STAKE__InsufficientStakeAmount(amount, MINIMUM_STAKE); + } + + // Check if staker already exists + if (stakes[msg.sender].isActive) { + revert STAKE__StakeAlreadyExists(msg.sender); + } + + // Transfer tokens to this contract using SafeERC20 + IERC20(address(dcuToken)).safeTransferFrom(msg.sender, address(this), amount); + + // Record stake + stakes[msg.sender] = StakeInfo({ + amount: amount, + timestamp: block.timestamp, + isActive: true + }); + + emit VerifierRegistered(msg.sender, amount, block.timestamp); + } + + /** + * @dev Allows a verifier to unstake their DCU tokens after delay period + */ + function unstakeDCU() external nonReentrant { + StakeInfo storage stakeInfo = stakes[msg.sender]; + + // Validate stake exists + if (!stakeInfo.isActive) { + revert STAKE__NoStakeFound(msg.sender); + } + + // Check unstaking delay + if (block.timestamp < stakeInfo.timestamp + UNSTAKING_DELAY) { + revert STAKE__UnstakingLocked( + (stakeInfo.timestamp + UNSTAKING_DELAY) - block.timestamp + ); + } + + uint256 amount = stakeInfo.amount; + + // Clear stake before transfer + delete stakes[msg.sender]; + + // Transfer tokens back to staker using SafeERC20 + IERC20(address(dcuToken)).safeTransfer(msg.sender, amount); + + emit VerifierUnstaked(msg.sender, amount, block.timestamp); + } + + /** + * @dev Allows increasing stake amount + * @param additionalAmount Amount to add to existing stake + */ + function increaseStake(uint256 additionalAmount) external nonReentrant { + if (additionalAmount == 0) revert STAKE__InvalidAmount(additionalAmount); + + StakeInfo storage stakeInfo = stakes[msg.sender]; + if (!stakeInfo.isActive) { + revert STAKE__NoStakeFound(msg.sender); + } + + uint256 oldAmount = stakeInfo.amount; + uint256 newAmount = oldAmount + additionalAmount; + + // Transfer additional tokens using SafeERC20 + IERC20(address(dcuToken)).safeTransferFrom(msg.sender, address(this), additionalAmount); + + // Update stake amount + stakeInfo.amount = newAmount; + + emit StakeAmountUpdated(msg.sender, oldAmount, newAmount, block.timestamp); + } + + /** + * @dev Check if an address is a verifier + * @param account Address to check + * @return bool True if address is a verifier + */ + function isVerifier(address account) external view returns (bool) { + return stakes[account].isActive; + } + + /** + * @dev Get stake information for an address + * @param account Address to query + * @return amount Staked amount + * @return timestamp Stake timestamp + * @return isActive Whether stake is active + */ + function getStakeInfo(address account) external view returns ( + uint256 amount, + uint256 timestamp, + bool isActive + ) { + StakeInfo memory stakeInfo = stakes[account]; + return (stakeInfo.amount, stakeInfo.timestamp, stakeInfo.isActive); + } + + /** + * @dev Emergency function to update DCU token address + * @param _newDcuToken New DCU token address + */ + function updateDCUToken(address _newDcuToken) external onlyOwner { + if (_newDcuToken == address(0)) revert STAKE__InvalidDCUToken(_newDcuToken); + dcuToken = IDCUToken(_newDcuToken); + } +} \ No newline at end of file diff --git a/package.json b/package.json index 10a0e1d..59574cd 100644 --- a/package.json +++ b/package.json @@ -22,18 +22,20 @@ "ethers": "^5.7.2" }, "devDependencies": { - "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomicfoundation/hardhat-ignition": "^0.15.10", "@nomicfoundation/hardhat-ignition-viem": "^0.15.11", "@nomicfoundation/hardhat-network-helpers": "^1.0.12", "@nomicfoundation/hardhat-toolbox-viem": "^3.0.0", "@nomicfoundation/hardhat-verify": "^2.0.13", "@nomicfoundation/hardhat-viem": "^2.0.6", + "@nomiclabs/hardhat-ethers": "^2.2.3", "@typechain/ethers-v5": "^10.2.0", "@typechain/hardhat": "^7.0.0", + "@types/chai": "^5.2.2", "@types/mocha": "^10.0.10", "chai": "^4.5.0", "dotenv": "^16.4.7", + "ethereum-waffle": "^4.0.10", "hardhat": "^2.22.18", "hardhat-gas-reporter": "^1.0.10", "solidity-coverage": "^0.8.15", diff --git a/test/PoIVerificationManager.test.ts b/test/PoIVerificationManager.test.ts new file mode 100644 index 0000000..9dfa6a9 --- /dev/null +++ b/test/PoIVerificationManager.test.ts @@ -0,0 +1,472 @@ +import { expect } from "./helpers/setup"; +import hre from "hardhat"; +import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; +import { getAddress } from "viem"; +import { PoIVerificationManagerErrorMap } from "./helpers/errorMessages"; + +describe("PoIVerificationManager", function () { + async function deployContractsFixture() { + const [owner, user1, user2] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + + // Deploy DCU token with owner as temporary reward logic + const dcuToken = await hre.viem.deployContract("DCUToken", [ + owner.account.address, // Use owner as temporary reward logic + ]); + + // Deploy VerifierStaking + const verifierStaking = await hre.viem.deployContract("VerifierStaking", [ + dcuToken.address, + ]); + + // Deploy VerifierRegistry + const verifierRegistry = await hre.viem.deployContract("VerifierRegistry", [ + verifierStaking.address, + ]); + + // Deploy PoIVerificationManager + const poiManager = await hre.viem.deployContract("PoIVerificationManager", [ + verifierRegistry.address, + ]); + + // Mint some tokens to users for testing + await dcuToken.write.mint([getAddress(user1.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + await dcuToken.write.mint([getAddress(user2.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + + // Approve staking contract + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user1.account } + ); + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user2.account } + ); + + return { + poiManager, + verifierRegistry, + verifierStaking, + dcuToken, + owner, + user1, + user2, + publicClient, + }; + } + + describe("Deployment", function () { + it("Should set the correct VerifierRegistry address", async function () { + const { poiManager, verifierRegistry } = await loadFixture(deployContractsFixture); + expect(await poiManager.read.verifierRegistry()).to.equal(getAddress(verifierRegistry.address)); + }); + + it("Should revert if VerifierRegistry address is zero", async function () { + await expect( + hre.viem.deployContract("PoIVerificationManager", [ + "0x0000000000000000000000000000000000000000", + ]) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__InvalidVerifierRegistry); + }); + }); + + describe("Verification Submission", function () { + it("Should allow authorized verifier to submit verification", async function () { + const { poiManager, verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + // Make user1 a verifier through staking + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + const poiId = 1n; + const status = 1; // Approved + const reason = "Valid submission"; + + await poiManager.write.submitVerification( + [poiId, status, reason], + { account: user1.account } + ); + + const result = await poiManager.read.getLatestVerification([poiId]); + expect(result.verifier).to.equal(getAddress(user1.account.address)); + expect(result.status).to.equal(1); + expect(result.reason).to.equal(reason); + }); + + it("Should allow additional verifier to submit verification", async function () { + const { poiManager, verifierRegistry, user2, owner } = await loadFixture(deployContractsFixture); + + // Add user2 as additional verifier + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + + const poiId = 1n; + const status = 1; // Approved + const reason = "Valid submission"; + + await poiManager.write.submitVerification( + [poiId, status, reason], + { account: user2.account } + ); + + const result = await poiManager.read.getLatestVerification([poiId]); + expect(result.verifier).to.equal(getAddress(user2.account.address)); + expect(result.status).to.equal(1); + expect(result.reason).to.equal(reason); + }); + + it("Should revert when non-verifier tries to submit verification", async function () { + const { poiManager, user1 } = await loadFixture(deployContractsFixture); + + await expect( + poiManager.write.submitVerification( + [1n, 1, "Valid submission"], + { account: user1.account } + ) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__NotAuthorizedVerifier); + }); + + it("Should revert when submitting verification with invalid PoI ID", async function () { + const { poiManager, verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + // Add user1 as verifier + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + await expect( + poiManager.write.submitVerification( + [0n, 1, "Valid submission"], + { account: user1.account } + ) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__InvalidPoiId); + }); + + it("Should revert when submitting verification with invalid status", async function () { + const { poiManager, verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + // Add user1 as verifier + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + await expect( + poiManager.write.submitVerification( + [1n, 0, "Valid submission"], // 0 = Pending + { account: user1.account } + ) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__InvalidStatus); + }); + + it("Should revert when verifier tries to verify same PoI twice", async function () { + const { poiManager, verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + // Add user1 as verifier + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + // First verification + await poiManager.write.submitVerification( + [1n, 1, "Valid submission"], + { account: user1.account } + ); + + // Second verification attempt + await expect( + poiManager.write.submitVerification( + [1n, 1, "Valid submission"], + { account: user1.account } + ) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__AlreadyVerified); + }); + }); + + describe("Verification Queries", function () { + it("Should return all verification results for a PoI", async function () { + const { poiManager, verifierRegistry, user1, user2, owner } = await loadFixture(deployContractsFixture); + + // Add both users as verifiers + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + + await poiManager.write.submitVerification( + [1n, 1, "Approved by user1"], + { account: user1.account } + ); + await poiManager.write.submitVerification( + [1n, 2, "Rejected by user2"], + { account: user2.account } + ); + + const results = await poiManager.read.getVerificationResults([1n]); + expect(results.length).to.equal(2); + expect(results[0].verifier).to.equal(getAddress(user1.account.address)); + expect(results[0].status).to.equal(1); + expect(results[0].reason).to.equal("Approved by user1"); + expect(results[1].verifier).to.equal(getAddress(user2.account.address)); + expect(results[1].status).to.equal(2); + expect(results[1].reason).to.equal("Rejected by user2"); + }); + + it("Should correctly report PoI approval status", async function () { + const { poiManager, verifierRegistry, user1, user2, owner } = await loadFixture(deployContractsFixture); + + // Add both users as verifiers + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + + // Submit verifications + await poiManager.write.submitVerification( + [1n, 1, "First verification"], + { account: user1.account } + ); + expect(await poiManager.read.getVerificationCount([1n])).to.equal(1n); + + await poiManager.write.submitVerification( + [1n, 2, "Second verification"], + { account: user2.account } + ); + expect(await poiManager.read.getVerificationCount([1n])).to.equal(2n); + }); + + it("Should revert when getting latest verification for non-existent PoI", async function () { + const { poiManager } = await loadFixture(deployContractsFixture); + + await expect( + poiManager.read.getLatestVerification([1n]) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__InvalidPoiId); + }); + + it("Should correctly report PoI approval status with multiple verifications", async function () { + const { poiManager, verifierRegistry, user1, user2, owner } = await loadFixture(deployContractsFixture); + + // Add both users as verifiers + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + + // Submit verifications for PoI #1 + await poiManager.write.submitVerification( + [1n, 2, "Rejected first"], // Rejected + { account: user1.account } + ); + expect(await poiManager.read.isPoIApproved([1n])).to.equal(false); + + await poiManager.write.submitVerification( + [1n, 1, "Approved later"], // Approved + { account: user2.account } + ); + expect(await poiManager.read.isPoIApproved([1n])).to.equal(true); + + // Submit verifications for PoI #2 + await poiManager.write.submitVerification( + [2n, 2, "Rejected only"], // Rejected + { account: user1.account } + ); + expect(await poiManager.read.isPoIApproved([2n])).to.equal(false); + }); + + it("Should return empty array for PoI with no verifications", async function () { + const { poiManager } = await loadFixture(deployContractsFixture); + const results = await poiManager.read.getVerificationResults([1n]); + expect(results.length).to.equal(0); + }); + + it("Should handle multiple PoIs with multiple verifications", async function () { + const { poiManager, verifierRegistry, user1, user2, owner } = await loadFixture(deployContractsFixture); + + // Add both users as verifiers + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + + // Submit verifications for multiple PoIs + await poiManager.write.submitVerification( + [1n, 1, "PoI 1 - First verification"], + { account: user1.account } + ); + await poiManager.write.submitVerification( + [2n, 1, "PoI 2 - First verification"], + { account: user1.account } + ); + await poiManager.write.submitVerification( + [1n, 2, "PoI 1 - Second verification"], + { account: user2.account } + ); + + // Check PoI 1 verifications + const results1 = await poiManager.read.getVerificationResults([1n]); + expect(results1.length).to.equal(2); + expect(results1[0].status).to.equal(1); + expect(results1[1].status).to.equal(2); + + // Check PoI 2 verifications + const results2 = await poiManager.read.getVerificationResults([2n]); + expect(results2.length).to.equal(1); + expect(results2[0].status).to.equal(1); + }); + }); + + describe("Contract Updates", function () { + it("Should allow owner to update VerifierRegistry", async function () { + const { poiManager, verifierRegistry, owner } = await loadFixture(deployContractsFixture); + const newAddress = verifierRegistry.address; // Using same address for simplicity + + await poiManager.write.updateVerifierRegistry([getAddress(newAddress)], { + account: owner.account, + }); + + expect(await poiManager.read.verifierRegistry()).to.equal(getAddress(newAddress)); + }); + + it("Should revert when updating to zero address", async function () { + const { poiManager, owner } = await loadFixture(deployContractsFixture); + + await expect( + poiManager.write.updateVerifierRegistry(["0x0000000000000000000000000000000000000000"], { + account: owner.account, + }) + ).to.be.rejectedWith(PoIVerificationManagerErrorMap.POIVERIFICATION__InvalidVerifierRegistry); + }); + + it("Should revert when non-owner updates registry", async function () { + const { poiManager, verifierRegistry, user1 } = await loadFixture(deployContractsFixture); + + await expect( + poiManager.write.updateVerifierRegistry([getAddress(verifierRegistry.address)], { + account: user1.account, + }) + ).to.be.rejectedWith("OwnableUnauthorizedAccount"); + }); + }); + + describe("Event Emission", function () { + it("Should emit PoIVerified event on verification submission", async function () { + const { poiManager, verifierRegistry, user1, owner, publicClient } = await loadFixture(deployContractsFixture); + + // Add user1 as verifier + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + const poiId = 1n; + const status = 1; // Approved + const reason = "Event test verification"; + + // Watch for the event + const unwatch = await publicClient.watchEvent({ + address: poiManager.address, + event: { + type: 'event', + name: 'PoIVerified', + inputs: [ + { type: 'uint256', name: 'poiId', indexed: true }, + { type: 'address', name: 'verifier', indexed: true }, + { type: 'uint8', name: 'status' }, + { type: 'uint256', name: 'timestamp' }, + { type: 'string', name: 'reason' } + ] + }, + onLogs: logs => { + const log = logs[0]; + expect(log.args.poiId).to.equal(poiId); + expect(log.args.verifier).to.equal(getAddress(user1.account.address)); + expect(log.args.status).to.equal(status); + expect(log.args.reason).to.equal(reason); + unwatch(); + } + }); + + await poiManager.write.submitVerification( + [poiId, status, reason], + { account: user1.account } + ); + }); + + it("Should emit VerifierRegistryUpdated event on registry update", async function () { + const { poiManager, verifierRegistry, owner, publicClient } = await loadFixture(deployContractsFixture); + const oldRegistry = await poiManager.read.verifierRegistry(); + + // Watch for the event + const unwatch = await publicClient.watchEvent({ + address: poiManager.address, + event: { + type: 'event', + name: 'VerifierRegistryUpdated', + inputs: [ + { type: 'address', name: 'oldRegistry', indexed: true }, + { type: 'address', name: 'newRegistry', indexed: true } + ] + }, + onLogs: logs => { + const log = logs[0]; + expect(log.args.oldRegistry).to.equal(oldRegistry); + expect(log.args.newRegistry).to.equal(getAddress(verifierRegistry.address)); + unwatch(); + } + }); + + await poiManager.write.updateVerifierRegistry( + [getAddress(verifierRegistry.address)], + { account: owner.account } + ); + }); + }); + + describe("Edge Cases", function () { + it("Should handle empty reason string in verification", async function () { + const { poiManager, verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + await poiManager.write.submitVerification( + [1n, 1, ""], // Empty reason + { account: user1.account } + ); + + const result = await poiManager.read.getLatestVerification([1n]); + expect(result.reason).to.equal(""); + }); + + it("Should handle maximum uint256 PoI ID", async function () { + const { poiManager, verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + const maxUint256 = 2n ** 256n - 1n; + await poiManager.write.submitVerification( + [maxUint256, 1, "Max PoI ID test"], + { account: user1.account } + ); + + const result = await poiManager.read.getLatestVerification([maxUint256]); + expect(result.verifier).to.equal(getAddress(user1.account.address)); + }); + }); +}); \ No newline at end of file diff --git a/test/VerifierRegistry.test.ts b/test/VerifierRegistry.test.ts new file mode 100644 index 0000000..8eb621f --- /dev/null +++ b/test/VerifierRegistry.test.ts @@ -0,0 +1,222 @@ +import { expect } from "./helpers/setup"; +import hre from "hardhat"; +import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; +import { getAddress } from "viem"; + +describe("VerifierRegistry", function () { + async function deployContractsFixture() { + const [owner, user1, user2] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + + // Deploy DCU token with owner as temporary reward logic + const dcuToken = await hre.viem.deployContract("DCUToken", [ + owner.account.address, // Use owner as temporary reward logic + ]); + + // Deploy VerifierStaking + const verifierStaking = await hre.viem.deployContract("VerifierStaking", [ + dcuToken.address, + ]); + + // Deploy VerifierRegistry + const verifierRegistry = await hre.viem.deployContract("VerifierRegistry", [ + verifierStaking.address, + ]); + + // Mint some tokens to users for testing + await dcuToken.write.mint([getAddress(user1.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + await dcuToken.write.mint([getAddress(user2.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + + // Approve staking contract + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user1.account } + ); + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user2.account } + ); + + return { + verifierRegistry, + verifierStaking, + dcuToken, + owner, + user1, + user2, + publicClient, + }; + } + + describe("Deployment", function () { + it("Should set the correct VerifierStaking address", async function () { + const { verifierRegistry, verifierStaking } = await loadFixture(deployContractsFixture); + expect(await verifierRegistry.read.verifierStaking()).to.equal(getAddress(verifierStaking.address)); + }); + + it("Should revert if VerifierStaking address is zero", async function () { + await expect( + hre.viem.deployContract("VerifierRegistry", [ + "0x0000000000000000000000000000000000000000", + ]) + ).to.be.rejectedWith("REGISTRY__InvalidVerifierStaking"); + }); + }); + + describe("Verifier Status", function () { + it("Should recognize staked verifiers", async function () { + const { verifierRegistry, verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + // Initially not a verifier + expect(await verifierRegistry.read.isVerifier([getAddress(user1.account.address)])).to.equal(false); + + // Stake and become a verifier + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + expect(await verifierRegistry.read.isVerifier([getAddress(user1.account.address)])).to.equal(true); + }); + + it("Should recognize additional verifiers", async function () { + const { verifierRegistry, user2, owner } = await loadFixture(deployContractsFixture); + + // Initially not a verifier + expect(await verifierRegistry.read.isVerifier([getAddress(user2.account.address)])).to.equal(false); + + // Add as additional verifier + await verifierRegistry.write.addVerifier([getAddress(user2.account.address)], { + account: owner.account, + }); + expect(await verifierRegistry.read.isVerifier([getAddress(user2.account.address)])).to.equal(true); + }); + + it("Should revert isVerifier check for zero address", async function () { + const { verifierRegistry } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.read.isVerifier(["0x0000000000000000000000000000000000000000"]) + ).to.be.rejectedWith("REGISTRY__InvalidAddress"); + }); + }); + + describe("Additional Verifiers Management", function () { + it("Should allow owner to add verifier", async function () { + const { verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + expect(await verifierRegistry.read.isAdditionalVerifier([getAddress(user1.account.address)])).to.equal(true); + }); + + it("Should revert when adding zero address as verifier", async function () { + const { verifierRegistry, owner } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.write.addVerifier(["0x0000000000000000000000000000000000000000"], { + account: owner.account, + }) + ).to.be.rejectedWith("REGISTRY__InvalidAddress"); + }); + + it("Should revert when adding existing verifier", async function () { + const { verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + await expect( + verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }) + ).to.be.rejectedWith("REGISTRY__AlreadyVerifier"); + }); + + it("Should allow owner to remove verifier", async function () { + const { verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + await verifierRegistry.write.removeVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + expect(await verifierRegistry.read.isAdditionalVerifier([getAddress(user1.account.address)])).to.equal(false); + }); + + it("Should revert when removing non-existent verifier", async function () { + const { verifierRegistry, user1, owner } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.write.removeVerifier([getAddress(user1.account.address)], { + account: owner.account, + }) + ).to.be.rejectedWith("REGISTRY__NotVerifier"); + }); + + it("Should revert when non-owner adds verifier", async function () { + const { verifierRegistry, user1, user2 } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: user2.account, + }) + ).to.be.rejectedWith("OwnableUnauthorizedAccount"); + }); + + it("Should revert when non-owner removes verifier", async function () { + const { verifierRegistry, user1, user2, owner } = await loadFixture(deployContractsFixture); + + await verifierRegistry.write.addVerifier([getAddress(user1.account.address)], { + account: owner.account, + }); + + await expect( + verifierRegistry.write.removeVerifier([getAddress(user1.account.address)], { + account: user2.account, + }) + ).to.be.rejectedWith("OwnableUnauthorizedAccount"); + }); + }); + + describe("Contract Updates", function () { + it("Should allow owner to update VerifierStaking contract", async function () { + const { verifierRegistry, verifierStaking, owner } = await loadFixture(deployContractsFixture); + const newAddress = verifierStaking.address; // Using same address for simplicity + + await verifierRegistry.write.updateVerifierStaking([getAddress(newAddress)], { + account: owner.account, + }); + + expect(await verifierRegistry.read.verifierStaking()).to.equal(getAddress(newAddress)); + }); + + it("Should revert when updating to zero address", async function () { + const { verifierRegistry, owner } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.write.updateVerifierStaking(["0x0000000000000000000000000000000000000000"], { + account: owner.account, + }) + ).to.be.rejectedWith("REGISTRY__InvalidVerifierStaking"); + }); + + it("Should revert when non-owner updates contract", async function () { + const { verifierRegistry, verifierStaking, user1 } = await loadFixture(deployContractsFixture); + + await expect( + verifierRegistry.write.updateVerifierStaking([getAddress(verifierStaking.address)], { + account: user1.account, + }) + ).to.be.rejectedWith("OwnableUnauthorizedAccount"); + }); + }); +}); \ No newline at end of file diff --git a/test/VerifierStaking.test.ts b/test/VerifierStaking.test.ts new file mode 100644 index 0000000..facfcd8 --- /dev/null +++ b/test/VerifierStaking.test.ts @@ -0,0 +1,259 @@ +import { expect } from "chai"; +import hre from "hardhat"; +import { loadFixture } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; +import { getAddress } from "viem"; +import { time } from "@nomicfoundation/hardhat-toolbox-viem/network-helpers"; + +describe("VerifierStaking", function () { + async function deployContractsFixture() { + const [owner, user1, user2] = await hre.viem.getWalletClients(); + const publicClient = await hre.viem.getPublicClient(); + + // Deploy DCU token with owner as temporary reward logic + const dcuToken = await hre.viem.deployContract("DCUToken", [ + owner.account.address, // Use owner as temporary reward logic + ]); + + // Deploy VerifierStaking + const verifierStaking = await hre.viem.deployContract("VerifierStaking", [ + dcuToken.address, + ]); + + // Mint some tokens to users for testing + await dcuToken.write.mint([getAddress(user1.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + await dcuToken.write.mint([getAddress(user2.account.address), 2000n * 10n ** 18n], { + account: owner.account, + }); + + // Approve staking contract + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user1.account } + ); + await dcuToken.write.approve( + [getAddress(verifierStaking.address), 2000n * 10n ** 18n], + { account: user2.account } + ); + + return { + verifierStaking, + dcuToken, + owner, + user1, + user2, + publicClient, + }; + } + + describe("Deployment", function () { + it("Should set the correct DCU token address", async function () { + const { verifierStaking, dcuToken } = await loadFixture(deployContractsFixture); + expect(await verifierStaking.read.dcuToken()).to.equal(getAddress(dcuToken.address)); + }); + + it("Should revert if DCU token address is zero", async function () { + await expect( + hre.viem.deployContract("VerifierStaking", [ + "0x0000000000000000000000000000000000000000", + ]) + ).to.be.rejectedWith("STAKE__InvalidDCUToken"); + }); + }); + + describe("Staking", function () { + it("Should allow staking minimum amount", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + const stakeInfo = await verifierStaking.read.getStakeInfo([getAddress(user1.account.address)]); + expect(stakeInfo[0]).to.equal(amount); // amount + expect(stakeInfo[2]).to.equal(true); // isActive + }); + + it("Should revert when staking below minimum", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const belowMinimum = 999n * 10n ** 18n; // 999 DCU + + await expect( + verifierStaking.write.stakeDCU([belowMinimum], { + account: user1.account, + }) + ).to.be.rejectedWith("STAKE__InsufficientStakeAmount"); + }); + + it("Should revert when staking zero amount", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + + await expect( + verifierStaking.write.stakeDCU([0n], { + account: user1.account, + }) + ).to.be.rejectedWith("STAKE__InvalidAmount"); + }); + + it("Should revert when staking twice", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + await expect( + verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }) + ).to.be.rejectedWith("STAKE__StakeAlreadyExists"); + }); + }); + + describe("Unstaking", function () { + it("Should revert when unstaking before delay", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + await expect( + verifierStaking.write.unstakeDCU({ + account: user1.account, + }) + ).to.be.rejectedWith("STAKE__UnstakingLocked"); + }); + + it("Should allow unstaking after delay", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + await time.increase(7 * 24 * 60 * 60); // 7 days + + await verifierStaking.write.unstakeDCU({ + account: user1.account, + }); + + const stakeInfo = await verifierStaking.read.getStakeInfo([getAddress(user1.account.address)]); + expect(stakeInfo[2]).to.equal(false); // isActive + expect(stakeInfo[0]).to.equal(0n); // amount + }); + + it("Should revert when unstaking with no stake", async function () { + const { verifierStaking, user2 } = await loadFixture(deployContractsFixture); + + await expect( + verifierStaking.write.unstakeDCU({ + account: user2.account, + }) + ).to.be.rejectedWith("STAKE__NoStakeFound"); + }); + }); + + describe("Increasing Stake", function () { + it("Should allow increasing stake", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const initialAmount = 1000n * 10n ** 18n; // 1,000 DCU + const additionalAmount = 500n * 10n ** 18n; // 500 DCU + + await verifierStaking.write.stakeDCU([initialAmount], { + account: user1.account, + }); + + await verifierStaking.write.increaseStake([additionalAmount], { + account: user1.account, + }); + + const stakeInfo = await verifierStaking.read.getStakeInfo([getAddress(user1.account.address)]); + expect(stakeInfo[0]).to.equal(initialAmount + additionalAmount); + }); + + it("Should revert when increasing stake with zero", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + + await expect( + verifierStaking.write.increaseStake([0n], { + account: user1.account, + }) + ).to.be.rejectedWith("STAKE__InvalidAmount"); + }); + + it("Should revert when increasing non-existent stake", async function () { + const { verifierStaking, user2 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + await expect( + verifierStaking.write.increaseStake([amount], { + account: user2.account, + }) + ).to.be.rejectedWith("STAKE__NoStakeFound"); + }); + }); + + describe("Verifier Status", function () { + it("Should correctly report verifier status", async function () { + const { verifierStaking, user1 } = await loadFixture(deployContractsFixture); + const amount = 1000n * 10n ** 18n; // 1,000 DCU + + expect(await verifierStaking.read.isVerifier([getAddress(user1.account.address)])).to.equal(false); + + await verifierStaking.write.stakeDCU([amount], { + account: user1.account, + }); + expect(await verifierStaking.read.isVerifier([getAddress(user1.account.address)])).to.equal(true); + + await time.increase(7 * 24 * 60 * 60); // 7 days + await verifierStaking.write.unstakeDCU({ + account: user1.account, + }); + expect(await verifierStaking.read.isVerifier([getAddress(user1.account.address)])).to.equal(false); + }); + }); + + describe("Admin Functions", function () { + it("Should allow owner to update DCU token", async function () { + const { verifierStaking, dcuToken, owner } = await loadFixture(deployContractsFixture); + const newToken = dcuToken.address; // Using same token for simplicity + + await verifierStaking.write.updateDCUToken([getAddress(newToken)], { + account: owner.account, + }); + + expect(await verifierStaking.read.dcuToken()).to.equal(getAddress(newToken)); + }); + + it("Should revert when non-owner updates DCU token", async function () { + const { verifierStaking, dcuToken, user1 } = await loadFixture(deployContractsFixture); + + await expect( + verifierStaking.write.updateDCUToken([getAddress(dcuToken.address)], { + account: user1.account, + }) + ).to.be.rejectedWith("OwnableUnauthorizedAccount"); + }); + + it("Should revert when updating to zero address", async function () { + const { verifierStaking, owner } = await loadFixture(deployContractsFixture); + + await expect( + verifierStaking.write.updateDCUToken(["0x0000000000000000000000000000000000000000"], { + account: owner.account, + }) + ).to.be.rejectedWith("STAKE__InvalidDCUToken"); + }); + }); +}); \ No newline at end of file diff --git a/test/helpers/errorMessages.ts b/test/helpers/errorMessages.ts index 9ca74bd..30d8895 100644 --- a/test/helpers/errorMessages.ts +++ b/test/helpers/errorMessages.ts @@ -72,6 +72,32 @@ export const LockErrorMap: ErrorMessageMap = { LOCK__NotOwner: "You aren't the owner", }; +export const StakeErrorMap: ErrorMessageMap = { + STAKE__InsufficientStakeAmount: "Stake amount below minimum required", + STAKE__NoStakeFound: "No active stake found for this address", + STAKE__StakeAlreadyExists: "Address already has an active stake", + STAKE__UnstakingLocked: "Unstaking is locked until delay period passes", + STAKE__InvalidDCUToken: "Invalid DCU token address", + STAKE__InvalidAmount: "Invalid stake amount", + STAKE__FailedToTransfer: "Token transfer failed", +}; + +export const RegistryErrorMap: ErrorMessageMap = { + REGISTRY__InvalidAddress: "Invalid address provided", + REGISTRY__InvalidVerifierStaking: "Invalid VerifierStaking contract address", + REGISTRY__AlreadyVerifier: "Address is already a verifier", + REGISTRY__NotVerifier: "Address is not a verifier", +}; + +// PoIVerificationManager errors +export const PoIVerificationManagerErrorMap: ErrorMessageMap = { + POIVERIFICATION__InvalidVerifierRegistry: "Invalid VerifierRegistry address", + POIVERIFICATION__InvalidPoiId: "Invalid PoI ID", + POIVERIFICATION__NotAuthorizedVerifier: "Not authorized verifier", + POIVERIFICATION__AlreadyVerified: "Already verified", + POIVERIFICATION__InvalidStatus: "Invalid status" +}; + /** * Normalizes an error message by extracting the custom error type if present * @param error The error message to normalize @@ -102,6 +128,9 @@ export function normalizeErrorMessage(error: Error | string): string { AccountingErrorMap, StorageErrorMap, LockErrorMap, + StakeErrorMap, + RegistryErrorMap, + PoIVerificationManagerErrorMap, ]; for (const map of errorMaps) { @@ -115,6 +144,11 @@ export function normalizeErrorMessage(error: Error | string): string { } } + // Special handling for Ownable errors + if (errorMsg.includes("OwnableUnauthorizedAccount")) { + return "OwnableUnauthorizedAccount"; + } + return errorMsg; } diff --git a/test/helpers/setup.ts b/test/helpers/setup.ts index fe62722..2f3a78a 100644 --- a/test/helpers/setup.ts +++ b/test/helpers/setup.ts @@ -2,7 +2,22 @@ import chai from "chai"; import { setupErrorMatchers } from "./errorMessages"; // Setup custom error matchers for tests -setupErrorMatchers(chai); +setupErrorMatchers(chai as any); export { chai }; export const { expect } = chai; + +declare global { + namespace Chai { + interface Assertion { + rejectedWith(expected: string): Promise; + } + } +} + +declare module "chai" { + interface ChaiStatic { + AssertionError: any; + version: string; + } +} diff --git a/tsconfig.json b/tsconfig.json index 574e785..7ca1c28 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,6 +6,8 @@ "forceConsistentCasingInFileNames": true, "strict": true, "skipLibCheck": true, - "resolveJsonModule": true + "resolveJsonModule": true, + "lib": ["es2020", "dom"], + "types": ["node", "chai", "mocha"] } }