diff --git a/contracts/data/BinaryHeap.sol b/contracts/data/BinaryHeap.sol new file mode 100644 index 00000000..674330fb --- /dev/null +++ b/contracts/data/BinaryHeap.sol @@ -0,0 +1,296 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.20; + +import { Array } from '../utils/Array.sol'; + +/** + * @title Binary Heap implementation + * @dev The data structure is configured as a max-heap + */ +library BinaryHeap { + using Array for bytes32[]; + + struct Heap { + bytes32[] _values; + // 1-indexed to allow 0 to signify nonexistence + mapping(bytes32 => uint256) _indexes; + } + + struct Bytes32Heap { + Heap _inner; + } + + struct AddressHeap { + Heap _inner; + } + + struct UintHeap { + Heap _inner; + } + + function at( + Bytes32Heap storage heap, + uint256 index + ) internal view returns (bytes32) { + return _at(heap._inner, index); + } + + function at( + AddressHeap storage heap, + uint256 index + ) internal view returns (address) { + return address(uint160(uint256(_at(heap._inner, index)))); + } + + function at( + UintHeap storage heap, + uint256 index + ) internal view returns (uint256) { + return uint256(_at(heap._inner, index)); + } + + function contains( + Bytes32Heap storage heap, + bytes32 value + ) internal view returns (bool) { + return _contains(heap._inner, value); + } + + function contains( + AddressHeap storage heap, + address value + ) internal view returns (bool) { + return _contains(heap._inner, bytes32(uint256(uint160(value)))); + } + + function contains( + UintHeap storage heap, + uint256 value + ) internal view returns (bool) { + return _contains(heap._inner, bytes32(value)); + } + + function indexOf( + Bytes32Heap storage heap, + bytes32 value + ) internal view returns (uint256) { + return _indexOf(heap._inner, value); + } + + function indexOf( + AddressHeap storage heap, + address value + ) internal view returns (uint256) { + return _indexOf(heap._inner, bytes32(uint256(uint160(value)))); + } + + function indexOf( + UintHeap storage heap, + uint256 value + ) internal view returns (uint256) { + return _indexOf(heap._inner, bytes32(value)); + } + + function length(Bytes32Heap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function length(AddressHeap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function length(UintHeap storage heap) internal view returns (uint256) { + return _length(heap._inner); + } + + function root(Bytes32Heap storage heap) internal view returns (bytes32) { + return _root(heap._inner); + } + + function root(AddressHeap storage heap) internal view returns (address) { + return address(uint160(uint256(_root(heap._inner)))); + } + + function root(UintHeap storage heap) internal view returns (uint256) { + return uint256(_root(heap._inner)); + } + + function add( + Bytes32Heap storage heap, + bytes32 value + ) internal returns (bool) { + return _add(heap._inner, value); + } + + function add( + AddressHeap storage heap, + address value + ) internal returns (bool) { + return _add(heap._inner, bytes32(uint256(uint160(value)))); + } + + function add(UintHeap storage heap, uint256 value) internal returns (bool) { + return _add(heap._inner, bytes32(value)); + } + + function remove( + Bytes32Heap storage heap, + bytes32 value + ) internal returns (bool) { + return _remove(heap._inner, value); + } + + function remove( + AddressHeap storage heap, + address value + ) internal returns (bool) { + return _remove(heap._inner, bytes32(uint256(uint160(value)))); + } + + function remove( + UintHeap storage heap, + uint256 value + ) internal returns (bool) { + return _remove(heap._inner, bytes32(value)); + } + + function toArray( + Bytes32Heap storage heap + ) internal view returns (bytes32[] memory array) { + array = _toArray(heap._inner); + } + + function toArray( + AddressHeap storage heap + ) internal view returns (address[] memory array) { + array = _toArray(heap._inner).toAddressArray(); + } + + function toArray( + UintHeap storage heap + ) internal view returns (uint256[] memory array) { + array = _toArray(heap._inner).toUint256Array(); + } + + function _at( + Heap storage heap, + uint256 index + ) private view returns (bytes32) { + return heap._values[index]; + } + + function _contains( + Heap storage heap, + bytes32 value + ) private view returns (bool) { + return heap._indexes[value] != 0; + } + + function _indexOf( + Heap storage heap, + bytes32 value + ) private view returns (uint256) { + unchecked { + return heap._indexes[value] - 1; + } + } + + function _length(Heap storage heap) private view returns (uint256) { + return heap._values.length; + } + + function _root(Heap storage heap) private view returns (bytes32) { + return _at(heap, 0); + } + + function _add( + Heap storage heap, + bytes32 value + ) private returns (bool update) { + if (!_contains(heap, value)) { + heap._values.push(value); + heap._indexes[value] = _length(heap); + _heapify(heap); + + update = true; + } + } + + function _remove( + Heap storage heap, + bytes32 value + ) private returns (bool update) { + if (_contains(heap, value)) { + uint256 index = _indexOf(heap, value); + + unchecked { + // move node with last element in the tree, then remove it + _swap(heap, index, _length(heap) - 1); + } + + heap._values.pop(); + delete heap._indexes[value]; + + _heapify(heap); + + update = true; + } + } + + function _toArray( + Heap storage heap + ) private view returns (bytes32[] storage array) { + array = heap._values; + } + + function _heapify(Heap storage heap) private { + uint256 len = _length(heap); + unchecked { + uint256 index = len / 2; + while (index > 0) { + _maxHeapify(heap, len, --index); + } + } + } + + function _maxHeapify( + Heap storage heap, + uint256 len, + uint256 index + ) private { + uint256 largest = index; + bytes32[] storage values = heap._values; + + unchecked { + uint256 left = (index << 1) | 1; + uint256 right = left + 1; + + if (left < len && values[largest] < values[left]) { + largest = left; + } + + if (right < len && values[largest] < values[right]) { + largest = right; + } + } + + if (largest != index) { + // swap until the largest node is the root node + _swap(heap, index, largest); + _maxHeapify(heap, len, largest); + } + } + + function _swap(Heap storage heap, uint256 a, uint256 b) private { + bytes32[] storage values = heap._values; + + bytes32 aValue = values[a]; + bytes32 bValue = values[b]; + + (values[a], values[b]) = (bValue, aValue); + + mapping(bytes32 => uint256) storage indexes = heap._indexes; + (indexes[aValue], indexes[bValue]) = (indexes[bValue], indexes[aValue]); + } +} diff --git a/lib/mock_contract.ts b/lib/mock_contract.ts index 91dded52..827fb340 100644 --- a/lib/mock_contract.ts +++ b/lib/mock_contract.ts @@ -1,16 +1,16 @@ // MIT-licensed code derived from https://github.com/TrueFiEng/Waffle import DoppelgangerContract from './Doppelganger.json'; -import type { JsonFragment } from '@ethersproject/abi'; import type { JsonRpcProvider } from '@ethersproject/providers'; +import { Signer } from 'ethers'; import { BaseContract, Contract, ContractFactory, - Signer, + ContractInterface, + Signer as EthersV5Signer, utils, } from 'ethers5'; - -type ABI = string | Array; +import { Interface } from 'ethers5/lib/utils'; interface StubInterface { returns(...args: any): StubInterface; @@ -151,7 +151,7 @@ type DeployOptions = { override?: boolean; }; -async function deploy(signer: Signer, options?: DeployOptions) { +async function deploy(signer: EthersV5Signer, options?: DeployOptions) { if (options) { const { address, override } = options; const provider = signer.provider as JsonRpcProvider; @@ -190,7 +190,7 @@ async function deploy(signer: Signer, options?: DeployOptions) { } function createMock( - abi: ABI, + abi: Exclude, mockContractInstance: Contract, ): MockContract['mock'] { const { functions } = new utils.Interface(abi); @@ -229,7 +229,11 @@ function createMock( async function deployEthersV5MockContract< T extends BaseContract = BaseContract, ->(signer: Signer, abi: ABI, options?: DeployOptions): Promise> { +>( + signer: EthersV5Signer, + abi: Exclude, + options?: DeployOptions, +): Promise> { const mockContractInstance = await deploy(signer, options); const mock = createMock(abi, mockContractInstance); @@ -288,11 +292,11 @@ async function deployEthersV5MockContract< } export async function deployMockContract( - ethersV6Signer: any, - abi: any, + ethersV6Signer: Signer, + abi: Exclude, options?: DeployOptions, ) { - const ethersV5Signer = ethersV6Signer; + const ethersV5Signer = ethersV6Signer as any; ethersV5Signer._isSigner = true; diff --git a/test/data/BinaryHeap.ts b/test/data/BinaryHeap.ts new file mode 100644 index 00000000..9ba6f64d --- /dev/null +++ b/test/data/BinaryHeap.ts @@ -0,0 +1,931 @@ +import { PANIC_CODES } from '@nomicfoundation/hardhat-chai-matchers/panic'; +import { bigintToBytes32, bigintToAddress } from '@solidstate/library'; +import { $BinaryHeap, $BinaryHeap__factory } from '@solidstate/typechain-types'; +import { expect } from 'chai'; +import { ethers } from 'hardhat'; + +// data structures can be defined at any storage slot +// it doesn't matter which slot is used as long as it's consistent +const STORAGE_SLOT = 0n; + +const numbers = [0, 1, 2].map((n) => n); + +const constants = { + bytes32: numbers.map((n) => bigintToBytes32(n)), + address: numbers.map((n) => bigintToAddress(n)), + uint256: numbers, +}; + +const randomBytes32 = () => ethers.hexlify(ethers.randomBytes(32)); + +const randomAddress = () => + ethers.getAddress(ethers.zeroPadValue(ethers.randomBytes(20), 20)); + +const randomUint256 = () => BigInt(ethers.toQuantity(ethers.randomBytes(32))); + +// checks that the parent node is greater than or equal to the children nodes +function checkNodes(nodes: any[]) { + nodes.forEach((node, index) => { + const left = 2 * index + 1; + const right = 2 * index + 2; + + if (left < nodes.length && nodes[left] != null) { + expect(BigInt(node)).to.be.gte(BigInt(nodes[left])); + } + + if (right < nodes.length && nodes[right] != null) { + expect(BigInt(node)).to.be.gte(BigInt(nodes[right])); + } + }); +} + +describe('BinaryHeap', async () => { + describe('Bytes32Heap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(bytes32)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_Bytes32Heap(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(bytes32)', () => { + it('returns true if the value has been added', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(bytes32)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + await instance['$add(uint256,bytes32)']( + STORAGE_SLOT, + randomBytes32(), + ); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,bytes32)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomBytes32(), randomBytes32(), randomBytes32()]; + + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(3); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.bytes32; + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, min); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, mid); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(max); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(mid); + + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_Bytes32Heap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(bytes32)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomBytes32(); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(bytes32)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: string[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomBytes32(); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,bytes32)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomBytes32(); + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,bytes32)'].staticCall( + STORAGE_SLOT, + randomBytes32(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.bytes32; + + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, min); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, mid); + await instance['$add(uint256,bytes32)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_Bytes32Heap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); + + describe('AddressHeap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(address)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_AddressHeap.staticCall(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(address)', () => { + it('returns true if the value has been added', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(address)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + await instance['$add(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,address)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,address)']( + STORAGE_SLOT, + randomAddress(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomAddress(), randomAddress(), randomAddress()]; + + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$add(uint256,address)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(3); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(2); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(1); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.address; + + await instance['$add(uint256,address)'](STORAGE_SLOT, min); + await instance['$add(uint256,address)'](STORAGE_SLOT, mid); + await instance['$add(uint256,address)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(max); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(mid); + + await instance['$remove(uint256,address)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_AddressHeap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(address)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomAddress(); + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(address)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: string[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomAddress(); + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,address)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomAddress(); + + await instance['$add(uint256,address)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,address)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,address)'].staticCall( + STORAGE_SLOT, + randomAddress(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.address; + + await instance['$add(uint256,address)'](STORAGE_SLOT, min); + await instance['$add(uint256,address)'](STORAGE_SLOT, mid); + await instance['$add(uint256,address)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_AddressHeap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); + + describe('UintHeap', async () => { + let instance: $BinaryHeap; + + beforeEach(async () => { + const [deployer] = await ethers.getSigners(); + instance = await new $BinaryHeap__factory(deployer).deploy(); + }); + + describe('__internal', () => { + describe('#at(uint256)', () => { + it('returns the value corresponding to index provided', async () => { + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = await instance.$at_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + key, + ); + expect(value).to.equal(array[key]); + } + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$at_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT, 0n), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#contains(uint256)', () => { + it('returns true if the value has been added', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$contains(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if the value has not been added', async () => { + expect( + await instance['$contains(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.false; + }); + }); + + describe('#indexOf(uint256)', () => { + it('returns index of the value', async () => { + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + await instance['$add(uint256,uint256)']( + STORAGE_SLOT, + randomUint256(), + ); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + for (const key in array) { + const value = array[key]; + const index = await instance[ + '$indexOf(uint256,uint256)' + ].staticCall(STORAGE_SLOT, value); + expect(index).to.equal(key); + } + }); + + it('returns max uint256 if value does not exist', async () => { + expect( + await instance['$indexOf(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.equal(ethers.MaxUint256); + }); + }); + + describe('#length()', () => { + it('returns length of binary heap', async () => { + const values = [randomUint256(), randomUint256(), randomUint256()]; + + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(0); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(1); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(2); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(3); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[0]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(2); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[1]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(1); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, values[2]); + expect( + await instance.$length_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(0); + }); + }); + + describe('#root()', () => { + it('returns the highest value in the heap', async () => { + const [min, mid, max] = constants.uint256; + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, min); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, mid); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(max); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, max); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(mid); + + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, mid); + + expect( + await instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.equal(min); + }); + + describe('reverts if', () => { + it('index out of bounds', async () => { + await expect( + instance.$root_BinaryHeap_UintHeap.staticCall(STORAGE_SLOT), + ).to.be.revertedWithPanic(PANIC_CODES.ARRAY_ACCESS_OUT_OF_BOUNDS); + }); + }); + }); + + describe('#add(uint256)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is added', async () => { + for (let index = 0; index < 10; index++) { + const value = randomUint256(); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + const nodes = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + checkNodes(Array.from(nodes)); + } + }); + + it('returns true if value is added', async () => { + expect( + await instance['$add(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.true; + }); + + it('returns false if value has already been added', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$add(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.false; + }); + }); + + describe('#remove(uint256)', () => { + it('sets the parent node such that it is greater than or equal to the values of its children when a node is removed', async () => { + const values: bigint[] = []; + + for (let index = 0; index < 10; index++) { + const value = randomUint256(); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + values.push(value); + } + + for (const value of values) { + await instance['$remove(uint256,uint256)'](STORAGE_SLOT, value); + checkNodes( + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ), + ); + } + }); + + it('returns true if value is removed', async () => { + const value = randomUint256(); + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, value); + + expect( + await instance['$remove(uint256,uint256)'].staticCall( + STORAGE_SLOT, + value, + ), + ).to.be.true; + }); + + it('returns false if value does not exist', async () => { + expect( + await instance['$remove(uint256,uint256)'].staticCall( + STORAGE_SLOT, + randomUint256(), + ), + ).to.be.false; + }); + }); + + describe('#toArray()', () => { + it('returns the max heap as an array', async () => { + const [min, mid, max] = constants.uint256; + + await instance['$add(uint256,uint256)'](STORAGE_SLOT, min); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, mid); + await instance['$add(uint256,uint256)'](STORAGE_SLOT, max); + + const array = + await instance.$toArray_BinaryHeap_UintHeap.staticCall( + STORAGE_SLOT, + ); + + expect(array.length).to.equal(3); + expect(array).to.deep.equal([max, min, mid]); + }); + }); + }); + }); +}); diff --git a/test/inheritance.ts b/test/inheritance.ts index 1d91ec4c..07f5e79f 100644 --- a/test/inheritance.ts +++ b/test/inheritance.ts @@ -323,6 +323,7 @@ describe('Inheritance Graph', () => { 'ECDSA', 'EIP712', 'MerkleProof', + 'BinaryHeap', 'DoublyLinkedList', 'PackedDoublyLinkedList', 'EnumerableMap',