Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(enumerablemap): add EnumerableBytes32ToBytes32Map #3136

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
44 changes: 43 additions & 1 deletion contracts/mocks/EnumerableMapMock.sol
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ pragma solidity ^0.8.0;

import "../utils/structs/EnumerableMap.sol";

contract EnumerableMapMock {
contract EnumerableUintToAddressMapMock {
using EnumerableMap for EnumerableMap.UintToAddressMap;

event OperationResult(bool result);
Expand Down Expand Up @@ -45,3 +45,45 @@ contract EnumerableMapMock {
return _map.get(key, errorMessage);
}
}

contract EnumerableBytes32ToBytes32MapMock {
using EnumerableMap for EnumerableMap.Bytes32ToBytes32Map;

event OperationResult(bool result);

EnumerableMap.Bytes32ToBytes32Map private _map;

function contains(bytes32 key) public view returns (bool) {
return _map.contains(key);
}

function set(bytes32 key, bytes32 value) public {
bool result = _map.set(key, value);
emit OperationResult(result);
}

function remove(bytes32 key) public {
bool result = _map.remove(key);
emit OperationResult(result);
}

function length() public view returns (uint256) {
return _map.length();
}

function at(uint256 index) public view returns (bytes32 key, bytes32 value) {
return _map.at(index);
}

function tryGet(bytes32 key) public view returns (bool, bytes32) {
return _map.tryGet(key);
}

function get(bytes32 key) public view returns (bytes32) {
return _map.get(key);
}

function getWithMessage(bytes32 key, string calldata errorMessage) public view returns (bytes32) {
return _map.get(key, errorMessage);
}
}
92 changes: 92 additions & 0 deletions contracts/utils/structs/EnumerableMap.sol
Original file line number Diff line number Diff line change
Expand Up @@ -237,4 +237,96 @@ library EnumerableMap {
) internal view returns (address) {
return address(uint160(uint256(_get(map._inner, bytes32(key), errorMessage))));
}

// Bytes32ToBytes32Map

struct Bytes32ToBytes32Map {
Map _inner;
}

/**
* @dev Adds a key-value pair to a map, or updates the value for an existing
* key. O(1).
*
* Returns true if the key was added to the map, that is if it was not
* already present.
*/
function set(
Bytes32ToBytes32Map storage map,
bytes32 key,
bytes32 value
) internal returns (bool) {
return _set(map._inner, key, value);
}

/**
* @dev Removes a value from a set. O(1).
*
* Returns true if the key was removed from the map, that is if it was present.
*/
function remove(Bytes32ToBytes32Map storage map, bytes32 key) internal returns (bool) {
return _remove(map._inner, key);
}

/**
* @dev Returns true if the key is in the map. O(1).
*/
function contains(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool) {
return _contains(map._inner, key);
}

/**
* @dev Returns the number of elements in the map. O(1).
*/
function length(Bytes32ToBytes32Map storage map) internal view returns (uint256) {
return _length(map._inner);
}

/**
* @dev Returns the element stored at position `index` in the set. O(1).
* Note that there are no guarantees on the ordering of values inside the
* array, and it may change when more values are added or removed.
*
* Requirements:
*
* - `index` must be strictly less than {length}.
*/
function at(Bytes32ToBytes32Map storage map, uint256 index) internal view returns (bytes32, bytes32) {
return _at(map._inner, index);
}

/**
* @dev Tries to returns the value associated with `key`. O(1).
* Does not revert if `key` is not in the map.
*
* _Available since v3.4._
*/
function tryGet(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bool, bytes32) {
return _tryGet(map._inner, key);
}

/**
* @dev Returns the value associated with `key`. O(1).
*
* Requirements:
*
* - `key` must be in the map.
*/
function get(Bytes32ToBytes32Map storage map, bytes32 key) internal view returns (bytes32) {
return _get(map._inner, key);
}

/**
* @dev Same as {get}, with a custom error message when `key` is not in the map.
*
* CAUTION: This function is deprecated because it requires allocating memory for the error
* message unnecessarily. For custom revert reasons use {tryGet}.
*/
function get(
Bytes32ToBytes32Map storage map,
bytes32 key,
string memory errorMessage
) internal view returns (bytes32) {
return _get(map._inner, key, errorMessage);
}
}
176 changes: 176 additions & 0 deletions test/utils/structs/EnumerableMap.behavior.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
const { expectEvent, expectRevert } = require('@openzeppelin/test-helpers');
const { expect } = require('chai');

const zip = require('lodash.zip');

function shouldBehaveLikeMap (keys, values, zeroValue) {
const [keyA, keyB, keyC] = keys;
const [valueA, valueB, valueC] = values;

async function expectMembersMatch (map, keys, values) {
expect(keys.length).to.equal(values.length);

await Promise.all(keys.map(async key =>
expect(await map.contains(key)).to.equal(true),
));

expect(await map.length()).to.bignumber.equal(keys.length.toString());

expect(await Promise.all(keys.map(key =>
map.get(key),
))).to.have.same.members(values);

// To compare key-value pairs, we zip keys and values, and convert BNs to
// strings to workaround Chai limitations when dealing with nested arrays
expect(await Promise.all([...Array(keys.length).keys()].map(async (index) => {
const entry = await map.at(index);
return [entry.key.toString(), entry.value];
}))).to.have.same.deep.members(
zip(keys.map(k => k.toString()), values),
);
}

it('starts empty', async function () {
expect(await this.map.contains(keyA)).to.equal(false);

await expectMembersMatch(this.map, [], []);
});

describe('set', function () {
it('adds a key', async function () {
const receipt = await this.map.set(keyA, valueA);
expectEvent(receipt, 'OperationResult', { result: true });

await expectMembersMatch(this.map, [keyA], [valueA]);
});

it('adds several keys', async function () {
await this.map.set(keyA, valueA);
await this.map.set(keyB, valueB);

await expectMembersMatch(this.map, [keyA, keyB], [valueA, valueB]);
expect(await this.map.contains(keyC)).to.equal(false);
});

it('returns false when adding keys already in the set', async function () {
await this.map.set(keyA, valueA);

const receipt = (await this.map.set(keyA, valueA));
expectEvent(receipt, 'OperationResult', { result: false });

await expectMembersMatch(this.map, [keyA], [valueA]);
});

it('updates values for keys already in the set', async function () {
await this.map.set(keyA, valueA);

await this.map.set(keyA, valueB);

await expectMembersMatch(this.map, [keyA], [valueB]);
});
});

describe('remove', function () {
it('removes added keys', async function () {
await this.map.set(keyA, valueA);

const receipt = await this.map.remove(keyA);
expectEvent(receipt, 'OperationResult', { result: true });

expect(await this.map.contains(keyA)).to.equal(false);
await expectMembersMatch(this.map, [], []);
});

it('returns false when removing keys not in the set', async function () {
const receipt = await this.map.remove(keyA);
expectEvent(receipt, 'OperationResult', { result: false });

expect(await this.map.contains(keyA)).to.equal(false);
});

it('adds and removes multiple keys', async function () {
// []

await this.map.set(keyA, valueA);
await this.map.set(keyC, valueC);

// [A, C]

await this.map.remove(keyA);
await this.map.remove(keyB);

// [C]

await this.map.set(keyB, valueB);

// [C, B]

await this.map.set(keyA, valueA);
await this.map.remove(keyC);

// [A, B]

await this.map.set(keyA, valueA);
await this.map.set(keyB, valueB);

// [A, B]

await this.map.set(keyC, valueC);
await this.map.remove(keyA);

// [B, C]

await this.map.set(keyA, valueA);
await this.map.remove(keyB);

// [A, C]

await expectMembersMatch(this.map, [keyA, keyC], [valueA, valueC]);

expect(await this.map.contains(keyB)).to.equal(false);
});
});

describe('read', function () {
beforeEach(async function () {
await this.map.set(keyA, valueA);
});

describe('get', function () {
it('existing value', async function () {
expect(await this.map.get(keyA)).to.be.equal(valueA);
});
it('missing value', async function () {
await expectRevert(this.map.get(keyB), 'EnumerableMap: nonexistent key');
});
});

describe('get with message', function () {
it('existing value', async function () {
expect(await this.map.getWithMessage(keyA, 'custom error string')).to.be.equal(valueA);
});
it('missing value', async function () {
await expectRevert(this.map.getWithMessage(keyB, 'custom error string'), 'custom error string');
});
});

describe('tryGet', function () {
it('existing value', async function () {
expect(await this.map.tryGet(keyA)).to.be.deep.equal({
0: true,
1: valueA,
});
});
it('missing value', async function () {
expect(await this.map.tryGet(keyB)).to.be.deep.equal({
0: false,
1: zeroValue,
});
});
});
});
}

module.exports = {
shouldBehaveLikeMap,
};