mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 20:36:19 +00:00
Tests for getStorageValue on mapping and nested mappings with different key types (#52)
* Tests for mapping with value type keys. * Add test for mapping with string type keys. * Add test for mapping with dynamically-sized byte array as keys. * Add tests for nested mapping. Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
parent
c916e61a9b
commit
df025433ec
0
packages/cache/src/cache.spec.ts
vendored
Normal file
0
packages/cache/src/cache.spec.ts
vendored
Normal file
0
packages/ipld-eth-client/src/eth-client.spec.ts
Normal file
0
packages/ipld-eth-client/src/eth-client.spec.ts
Normal file
@ -56,10 +56,11 @@ $ yarn test
|
||||
* [ ] Value Types
|
||||
* [ ] Reference Types
|
||||
* [ ] Mapping Types
|
||||
* [ ] Value Type keys
|
||||
* [ ] Dynamically-sized byte array keys
|
||||
* [x] Value Type keys
|
||||
* [ ] Fixed-size byte array keys
|
||||
* [x] Dynamically-sized byte array keys
|
||||
* [ ] Reference Type Mapping values
|
||||
* [ ] Nested Mapping
|
||||
* [x] Nested Mapping
|
||||
|
||||
## Observations
|
||||
|
||||
|
@ -310,17 +310,17 @@ describe('Get value from storage', () => {
|
||||
expect(value).to.eql(expectedValue.map(el => BigInt(el)));
|
||||
});
|
||||
|
||||
describe('mapping type', () => {
|
||||
describe('basic mapping type', () => {
|
||||
let testMappingTypes: Contract, storageLayout: StorageLayout;
|
||||
|
||||
before(async () => {
|
||||
const TestMappingTypes = await ethers.getContractFactory('TestMappingTypes');
|
||||
const TestMappingTypes = await ethers.getContractFactory('TestBasicMapping');
|
||||
testMappingTypes = await TestMappingTypes.deploy();
|
||||
await testMappingTypes.deployed();
|
||||
storageLayout = await getStorageLayout('TestMappingTypes');
|
||||
storageLayout = await getStorageLayout('TestBasicMapping');
|
||||
});
|
||||
|
||||
it('get value for basic mapping type', async () => {
|
||||
it('get value for mapping with address type keys', async () => {
|
||||
const expectedValue = 123;
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testMappingTypes.connect(signer1).setAddressUintMap(expectedValue);
|
||||
@ -329,12 +329,126 @@ describe('Get value from storage', () => {
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for nested mapping type', async () => {
|
||||
it('get value for mapping with boolean type keys', async () => {
|
||||
const expectedValue = 123;
|
||||
const mapKey = true;
|
||||
await testMappingTypes.setBoolIntMap(mapKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'boolIntMap', mapKey);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for mapping with signed integer type keys', async () => {
|
||||
const mapKey = 123;
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testMappingTypes.setIntAddressMap(mapKey, signer1.address);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'intAddressMap', mapKey);
|
||||
expect(value).to.equal(signer1.address.toLowerCase());
|
||||
});
|
||||
|
||||
it('get value for mapping with unsigned integer type keys', async () => {
|
||||
const expectedValue = ethers.utils.hexlify(ethers.utils.randomBytes(16));
|
||||
const mapKey = 123;
|
||||
await testMappingTypes.setUintBytesMap(mapKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'uintBytesMap', mapKey);
|
||||
expect(value).to.equal(expectedValue);
|
||||
});
|
||||
|
||||
it.skip('get value for mapping with fixed-size byte array keys', async () => {
|
||||
const mapKey = ethers.utils.hexlify(ethers.utils.randomBytes(8));
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testMappingTypes.setBytesAddressMap(mapKey, signer1.address);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'bytesAddressMap', mapKey);
|
||||
expect(value).to.equal(signer1.address);
|
||||
});
|
||||
|
||||
it('get value for mapping with enum type keys', async () => {
|
||||
const mapKey = 1;
|
||||
const expectedValue = 123;
|
||||
await testMappingTypes.setEnumIntMap(mapKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'enumIntMap', mapKey);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for mapping with string type keys', async () => {
|
||||
const mapKey = 'abc';
|
||||
const expectedValue = 123;
|
||||
await testMappingTypes.setStringIntMap(mapKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'stringIntMap', mapKey);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for mapping with dynamically-sized byte array as keys', async () => {
|
||||
const mapKey = ethers.utils.hexlify(ethers.utils.randomBytes(64));
|
||||
const expectedValue = 123;
|
||||
await testMappingTypes.setBytesUintMap(mapKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'bytesUintMap', mapKey);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
});
|
||||
|
||||
describe('nested mapping type', () => {
|
||||
let testNestedMapping: Contract, storageLayout: StorageLayout;
|
||||
|
||||
before(async () => {
|
||||
const TestNestedMapping = await ethers.getContractFactory('TestNestedMapping');
|
||||
testNestedMapping = await TestNestedMapping.deploy();
|
||||
await testNestedMapping.deployed();
|
||||
storageLayout = await getStorageLayout('TestNestedMapping');
|
||||
});
|
||||
|
||||
it('get value for nested mapping with address type keys', async () => {
|
||||
const expectedValue = 123;
|
||||
const [, signer1, signer2] = await ethers.getSigners();
|
||||
await testMappingTypes.connect(signer1).setNestedAddressUintMap(signer2.address, expectedValue);
|
||||
await testNestedMapping.connect(signer1).setNestedAddressUintMap(signer2.address, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'addressUintMap', signer1.address, signer2.address);
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'nestedAddressUintMap', signer1.address, signer2.address);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for nested mapping with signed integer type keys', async () => {
|
||||
const expectedValue = false;
|
||||
const key = 123;
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testNestedMapping.setIntAddressBoolMap(key, signer1.address, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'intAddressBoolMap', key, signer1.address);
|
||||
expect(value).to.equal(expectedValue);
|
||||
});
|
||||
|
||||
it('get value for nested mapping with unsigned integer type keys', async () => {
|
||||
const expectedValue = 123;
|
||||
const key = 456;
|
||||
const nestedKey = 'abc';
|
||||
await testNestedMapping.setUintStringIntMap(key, nestedKey, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'uintStringIntMap', key, nestedKey);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
|
||||
it('get value for nested mapping with dynamically-sized byte array as keys', async () => {
|
||||
const key = ethers.utils.hexlify(ethers.utils.randomBytes(64));
|
||||
const nestedKey = 123;
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testNestedMapping.setBytesIntAddressMap(key, nestedKey, signer1.address);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'bytesIntAddressMap', key, nestedKey);
|
||||
expect(value).to.equal(signer1.address.toLowerCase());
|
||||
});
|
||||
|
||||
it('get value for nested mapping with string type keys', async () => {
|
||||
const key = 'abc';
|
||||
const expectedValue = 123;
|
||||
const [, signer1] = await ethers.getSigners();
|
||||
await testNestedMapping.setStringAddressIntMap(key, signer1.address, expectedValue);
|
||||
const blockHash = await getBlockHash();
|
||||
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testNestedMapping.address, 'stringAddressIntMap', key, signer1.address);
|
||||
expect(value).to.equal(BigInt(expectedValue));
|
||||
});
|
||||
});
|
||||
|
@ -14,9 +14,12 @@ interface Types {
|
||||
label: string;
|
||||
base?: string;
|
||||
value?: string;
|
||||
key?: string;
|
||||
};
|
||||
}
|
||||
|
||||
type MappingKey = string | boolean | number;
|
||||
|
||||
export interface StorageLayout {
|
||||
storage: Storage[];
|
||||
types: Types
|
||||
@ -58,7 +61,7 @@ export const getStorageInfo = (storageLayout: StorageLayout, variableName: strin
|
||||
* @param variableName
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string, ...mappingKeys: Array<string>): Promise<{ value: any, proof: { data: string } }> => {
|
||||
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string, ...mappingKeys: Array<MappingKey>): Promise<{ value: any, proof: { data: string } }> => {
|
||||
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
|
||||
|
||||
return getDecodedValue(getStorageAt, blockHash, address, types, { slot, offset, type }, mappingKeys);
|
||||
@ -97,9 +100,9 @@ export const getValueByType = (storageValue: string, typeLabel: string): bigint
|
||||
* @param storageInfo
|
||||
*/
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, types: Types, storageInfo: { slot: string, offset: number, type: string }, mappingKeys: Array<string>): Promise<{ value: any, proof: { data: string } }> => {
|
||||
const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, address: string, types: Types, storageInfo: { slot: string, offset: number, type: string }, mappingKeys: Array<MappingKey>): Promise<{ value: any, proof: { data: string } }> => {
|
||||
const { slot, offset, type } = storageInfo;
|
||||
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType } = types[type];
|
||||
const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType, key: mappingKeyType } = types[type];
|
||||
|
||||
const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false];
|
||||
let value: string, proof: { data: string };
|
||||
@ -142,10 +145,10 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
|
||||
break;
|
||||
|
||||
case 'mapping': {
|
||||
const mappingSlot = await getMappingSlot(slot, mappingKeys[0]);
|
||||
if (mappingValueType && mappingKeyType) {
|
||||
const mappingSlot = await getMappingSlot(slot, types, mappingKeyType, mappingKeys[0]);
|
||||
|
||||
if (mappingValueType) {
|
||||
({ value, proof } = await getDecodedValue(getStorageAt, blockHash, address, types, { slot: mappingSlot, offset: 0, type: mappingValueType }, mappingKeys.slice(1)));
|
||||
return getDecodedValue(getStorageAt, blockHash, address, types, { slot: mappingSlot, offset: 0, type: mappingValueType }, mappingKeys.slice(1));
|
||||
} else {
|
||||
throw new Error(`Mapping value type not specified for ${mappingKeys[0]}`);
|
||||
}
|
||||
@ -168,11 +171,32 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
|
||||
* @param mappingSlot
|
||||
* @param key
|
||||
*/
|
||||
export const getMappingSlot = (mappingSlot: string, key: string): string => {
|
||||
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
|
||||
const mappingSlotPadded = utils.hexZeroPad(BigNumber.from(mappingSlot).toHexString(), 32);
|
||||
const keyPadded = utils.hexZeroPad(key, 32);
|
||||
export const getMappingSlot = (mappingSlot: string, types: Types, keyType: string, key: MappingKey): string => {
|
||||
const { encoding, label: typeLabel } = types[keyType];
|
||||
|
||||
// If key is boolean type convert to 1 or 0 which is the way value is stored in memory.
|
||||
if (typeLabel === 'bool') {
|
||||
key = key ? 1 : 0;
|
||||
}
|
||||
|
||||
// If key is string convert to hex string representation.
|
||||
if (typeLabel.includes('string') && typeof key === 'string') {
|
||||
key = utils.hexlify(utils.toUtf8Bytes(key));
|
||||
}
|
||||
|
||||
// If key is still boolean type the argument passed as key is invalid.
|
||||
if (typeof key === 'boolean') {
|
||||
throw new Error('Invalid key.');
|
||||
}
|
||||
|
||||
// https://github.com/ethers-io/ethers.js/issues/1079#issuecomment-703056242
|
||||
const mappingSlotPadded = utils.hexZeroPad(mappingSlot, 32);
|
||||
|
||||
const keyPadded = encoding === 'bytes'
|
||||
? utils.hexlify(key)
|
||||
: utils.hexZeroPad(utils.hexlify(key), 32);
|
||||
|
||||
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
|
||||
const fullKey = utils.concat([
|
||||
keyPadded,
|
||||
mappingSlotPadded
|
||||
|
72
packages/solidity-mapper/test/contracts/TestBasicMapping.sol
Normal file
72
packages/solidity-mapper/test/contracts/TestBasicMapping.sol
Normal file
@ -0,0 +1,72 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
contract TestBasicMapping {
|
||||
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
|
||||
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
|
||||
mapping(address => uint) public addressUintMap;
|
||||
|
||||
// Mapping type variable occupies the next single slot.
|
||||
mapping(bool => int) public boolIntMap;
|
||||
|
||||
// Mapping with int128 keys and contract type values.
|
||||
mapping(int128 => address) public intAddressMap;
|
||||
|
||||
// Mapping with uint32 keys and fixed-size byte array values.
|
||||
mapping(uint32 => bytes16) public uintBytesMap;
|
||||
|
||||
// Mapping with fixed-size byte array keys and address type values.
|
||||
mapping(bytes8 => address) public bytesAddressMap;
|
||||
|
||||
// Enum declaration.
|
||||
enum Choices { Choice0, Choice1, Choice2, Choice3 }
|
||||
|
||||
// Mapping with enum type keys and integer type values.
|
||||
mapping(Choices => int) public enumIntMap;
|
||||
|
||||
// Mapping with string type keys and integer type values.
|
||||
mapping(string => int) public stringIntMap;
|
||||
|
||||
// Mapping with dynamically-sized byte array as keys and unsigned integer type values.
|
||||
mapping(bytes => uint) public bytesUintMap;
|
||||
|
||||
// Set variable addressUintMap.
|
||||
function setAddressUintMap(uint value) external {
|
||||
addressUintMap[msg.sender] = value;
|
||||
}
|
||||
|
||||
// Set variable boolIntMap.
|
||||
function setBoolIntMap(bool key, int value) external {
|
||||
boolIntMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable intAddressMap.
|
||||
function setIntAddressMap(int128 key, address value) external {
|
||||
intAddressMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable uintBytesMap.
|
||||
function setUintBytesMap(uint32 key, bytes16 value) external {
|
||||
uintBytesMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable bytesAddressMap.
|
||||
function setBytesAddressMap(bytes8 key, address value) external {
|
||||
bytesAddressMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable enumIntMap.
|
||||
function setEnumIntMap(Choices key, int value) external {
|
||||
enumIntMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable stringIntMap.
|
||||
function setStringIntMap(string calldata key, int value) external {
|
||||
stringIntMap[key] = value;
|
||||
}
|
||||
|
||||
// Set variable bytesUintMap.
|
||||
function setBytesUintMap(bytes calldata key, uint value) external {
|
||||
bytesUintMap[key] = value;
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
contract TestMappingTypes {
|
||||
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
|
||||
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
|
||||
mapping (address => mapping (address => uint)) private nestedAddressUintMap;
|
||||
|
||||
// Mapping type variable occupies the next single slot.
|
||||
mapping(address => uint) public addressUintMap;
|
||||
|
||||
// Set variable addressUintMap.
|
||||
function setAddressUintMap(uint value) external {
|
||||
addressUintMap[msg.sender] = value;
|
||||
}
|
||||
|
||||
// Set variable nestedAddressUintMap.
|
||||
function setNestedAddressUintMap(address addressValue, uint uintValue) external {
|
||||
nestedAddressUintMap[msg.sender][addressValue] = uintValue;
|
||||
}
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: MIT
|
||||
pragma solidity ^0.7.0;
|
||||
|
||||
contract TestNestedMapping {
|
||||
// Mapping type variable occupies one single slot but the actual elements are stored at a different storage slot that is computed using a Keccak-256 hash.
|
||||
// https://docs.soliditylang.org/en/v0.8.4/internals/layout_in_storage.html#mappings-and-dynamic-arrays
|
||||
mapping (address => mapping (address => uint)) private nestedAddressUintMap;
|
||||
|
||||
mapping (int => mapping (address => bool)) private intAddressBoolMap;
|
||||
|
||||
mapping (uint => mapping (string => int)) private uintStringIntMap;
|
||||
|
||||
mapping (bytes => mapping (int => address)) private bytesIntAddressMap;
|
||||
|
||||
mapping (string => mapping (address => int)) private stringAddressIntMap;
|
||||
|
||||
// Set variable nestedAddressUintMap.
|
||||
function setNestedAddressUintMap(address nestedKey, uint value) external {
|
||||
nestedAddressUintMap[msg.sender][nestedKey] = value;
|
||||
}
|
||||
|
||||
// Set variable intAddressBoolMap.
|
||||
function setIntAddressBoolMap(int key, address nestedKey, bool value) external {
|
||||
intAddressBoolMap[key][nestedKey] = value;
|
||||
}
|
||||
|
||||
// Set variable uintStringIntMap.
|
||||
function setUintStringIntMap(uint key, string calldata nestedKey, int value) external {
|
||||
uintStringIntMap[key][nestedKey] = value;
|
||||
}
|
||||
|
||||
// Set variable bytesIntAddressMap.
|
||||
function setBytesIntAddressMap(bytes calldata key, int nestedKey, address value) external {
|
||||
bytesIntAddressMap[key][nestedKey] = value;
|
||||
}
|
||||
|
||||
// Set variable stringAddressIntMap.
|
||||
function setStringAddressIntMap(string calldata key, address nestedKey, int value) external {
|
||||
stringAddressIntMap[key][nestedKey] = value;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user