Use solidity mapper to get value for mapping and nested mapping (balance and allowance) (#48)

* Implement getting value for basic mapping type.

* Add test for basic mapping type.

* Implement getting value for nested mapping type.

Co-authored-by: nikugogoi <95nikass@gmail.com>
This commit is contained in:
Ashwin Phatak 2021-06-09 10:18:19 +05:30 committed by GitHub
parent 84e1927402
commit fc44617db3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 2901 additions and 94 deletions

View File

@ -4,7 +4,9 @@
"workspaces": [ "workspaces": [
"packages/*" "packages/*"
], ],
"devDependencies": {}, "devDependencies": {
"lerna": "^4.0.0"
},
"scripts": { "scripts": {
"test": "lerna run test --stream --no-bail", "test": "lerna run test --stream --no-bail",
"lint": "lerna run lint --stream" "lint": "lerna run lint --stream"

View File

@ -309,4 +309,33 @@ describe('Get value from storage', () => {
({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'uintArray')); ({ value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testFixedArrays.address, 'uintArray'));
expect(value).to.eql(expectedValue.map(el => BigInt(el))); expect(value).to.eql(expectedValue.map(el => BigInt(el)));
}); });
describe('mapping type', () => {
let testMappingTypes: Contract, storageLayout: StorageLayout;
before(async () => {
const TestMappingTypes = await ethers.getContractFactory('TestMappingTypes');
testMappingTypes = await TestMappingTypes.deploy();
await testMappingTypes.deployed();
storageLayout = await getStorageLayout('TestMappingTypes');
});
it('get value for basic mapping type', async () => {
const expectedValue = 123;
const [, signer1] = await ethers.getSigners();
await testMappingTypes.connect(signer1).setAddressUintMap(expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'addressUintMap', signer1.address);
expect(value).to.equal(BigInt(expectedValue));
});
it('get value for nested mapping type', async () => {
const expectedValue = 123;
const [, signer1, signer2] = await ethers.getSigners();
await testMappingTypes.connect(signer1).setNestedAddressUintMap(signer2.address, expectedValue);
const blockHash = await getBlockHash();
const { value } = await getStorageValue(storageLayout, getStorageAt, blockHash, testMappingTypes.address, 'addressUintMap', signer1.address, signer2.address);
expect(value).to.equal(BigInt(expectedValue));
});
});
}); });

View File

@ -13,6 +13,7 @@ interface Types {
numberOfBytes: string; numberOfBytes: string;
label: string; label: string;
base?: string; base?: string;
value?: string;
}; };
} }
@ -57,10 +58,10 @@ export const getStorageInfo = (storageLayout: StorageLayout, variableName: strin
* @param variableName * @param variableName
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* eslint-disable @typescript-eslint/no-explicit-any */
export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string): Promise<{ value: any, proof: { data: string } }> => { export const getStorageValue = async (storageLayout: StorageLayout, getStorageAt: GetStorageAt, blockHash: string, address: string, variableName: string, ...mappingKeys: Array<string>): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName); const { slot, offset, type, types } = getStorageInfo(storageLayout, variableName);
return getDecodedValue(getStorageAt, blockHash, address, types, { slot, offset, type }); return getDecodedValue(getStorageAt, blockHash, address, types, { slot, offset, type }, mappingKeys);
}; };
/** /**
@ -96,9 +97,9 @@ export const getValueByType = (storageValue: string, typeLabel: string): bigint
* @param storageInfo * @param storageInfo
*/ */
/* eslint-disable @typescript-eslint/no-explicit-any */ /* 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 }): 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<string>): Promise<{ value: any, proof: { data: string } }> => {
const { slot, offset, type } = storageInfo; const { slot, offset, type } = storageInfo;
const { encoding, numberOfBytes, label: typeLabel, base } = types[type]; const { encoding, numberOfBytes, label: typeLabel, base, value: mappingValueType } = types[type];
const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false]; const [isArray, arraySize] = typeLabel.match(/\[([0-9]*)\]/) || [false];
let value: string, proof: { data: string }; let value: string, proof: { data: string };
@ -114,7 +115,7 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
for (let i = 0; i < Number(baseNumberOfBytes) * Number(arraySize); i = i + Number(baseNumberOfBytes)) { for (let i = 0; i < Number(baseNumberOfBytes) * Number(arraySize); i = i + Number(baseNumberOfBytes)) {
const arraySlot = BigNumber.from(slot).add(Math.floor(i / 32)).toHexString(); const arraySlot = BigNumber.from(slot).add(Math.floor(i / 32)).toHexString();
const slotOffset = i % 32; const slotOffset = i % 32;
const { value, proof } = await getDecodedValue(getStorageAt, blockHash, address, types, { slot: arraySlot, offset: slotOffset, type: base }); ({ value, proof } = await getDecodedValue(getStorageAt, blockHash, address, types, { slot: arraySlot, offset: slotOffset, type: base }, []));
resultArray.push(value); resultArray.push(value);
proofs.push(JSON.parse(proof.data)); proofs.push(JSON.parse(proof.data));
} }
@ -140,6 +141,18 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
({ value, proof } = await getBytesValue(blockHash, address, slot, getStorageAt)); ({ value, proof } = await getBytesValue(blockHash, address, slot, getStorageAt));
break; break;
case 'mapping': {
const mappingSlot = await getMappingSlot(slot, mappingKeys[0]);
if (mappingValueType) {
({ value, proof } = await 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]}`);
}
break;
}
default: default:
throw new Error(`Encoding ${encoding} not implemented.`); throw new Error(`Encoding ${encoding} not implemented.`);
} }
@ -150,6 +163,25 @@ const getDecodedValue = async (getStorageAt: GetStorageAt, blockHash: string, ad
}; };
}; };
/**
* Function to get slot for mapping types.
* @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);
const fullKey = utils.concat([
keyPadded,
mappingSlotPadded
]);
const slot = utils.keccak256(fullKey);
return slot;
};
/** /**
* Function to get value for inplace encoding. * Function to get value for inplace encoding.
* @param address * @param address

View File

@ -0,0 +1,21 @@
// 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;
}
}

View File

@ -150,7 +150,7 @@ export class Database {
.getCount(); .getCount();
if (numRows === 0) { if (numRows === 0) {
const entity = repo.create({ address, startingBlock }); const entity = repo.create({ address, startingBlock: BigInt(startingBlock) });
await repo.save(entity); await repo.save(entity);
} }
}); });

View File

@ -19,7 +19,7 @@ export class Allowance {
spender!: string; spender!: string;
@Column('numeric') @Column('numeric')
value!: BigInt; value!: bigint;
@Column('text') @Column('text')
proof!: string; proof!: string;

View File

@ -16,7 +16,7 @@ export class Balance {
owner!: string; owner!: string;
@Column('numeric') @Column('numeric')
value!: BigInt; value!: bigint;
@Column('text') @Column('text')
proof!: string; proof!: string;

View File

@ -10,5 +10,5 @@ export class Contract {
address!: string; address!: string;
@Column('numeric') @Column('numeric')
startingBlock!: BigInt; startingBlock!: bigint;
} }

View File

@ -31,7 +31,7 @@ export class Event {
transferTo!: string; transferTo!: string;
@Column('numeric', { nullable: true }) @Column('numeric', { nullable: true })
transferValue!: BigInt; transferValue!: bigint;
// Approval event columns. // Approval event columns.
@Column('varchar', { length: 42, nullable: true }) @Column('varchar', { length: 42, nullable: true })
@ -41,5 +41,5 @@ export class Event {
approvalSpender!: string; approvalSpender!: string;
@Column('numeric', { nullable: true }) @Column('numeric', { nullable: true })
approvalValue!: BigInt; approvalValue!: bigint;
} }

View File

@ -6,8 +6,8 @@ import { DeepPartial } from 'typeorm';
import JSONbig from 'json-bigint'; import JSONbig from 'json-bigint';
import { ethers } from 'ethers'; import { ethers } from 'ethers';
import { EthClient, getMappingSlot, topictoAddress } from '@vulcanize/ipld-eth-client'; import { EthClient, topictoAddress } from '@vulcanize/ipld-eth-client';
import { getStorageInfo, getEventNameTopics, getStorageValue, GetStorageAt, StorageLayout } from '@vulcanize/solidity-mapper'; import { getEventNameTopics, getStorageValue, GetStorageAt, StorageLayout } from '@vulcanize/solidity-mapper';
import { Database } from './database'; import { Database } from './database';
import { Event } from './entity/Event'; import { Event } from './entity/Event';
@ -20,7 +20,7 @@ interface Artifacts {
} }
export interface ValueResult { export interface ValueResult {
value: string | BigInt; value: string | bigint;
proof: { proof: {
data: string; data: string;
} }
@ -85,17 +85,8 @@ export class Indexer {
}; };
} }
// TODO: Use getStorageValue when it supports mappings. const result = await this._getStorageValue(blockHash, token, '_balances', owner);
const { slot: balancesSlot } = getStorageInfo(this._storageLayout, '_balances');
const slot = getMappingSlot(balancesSlot, owner);
const vars = {
blockHash,
contract: token,
slot
};
const result = await this._getStorageAt(vars);
log(JSONbig.stringify(result, null, 2)); log(JSONbig.stringify(result, null, 2));
const { value, proof } = result; const { value, proof } = result;
@ -113,17 +104,8 @@ export class Indexer {
}; };
} }
// TODO: Use getStorageValue when it supports nested mappings. const result = await this._getStorageValue(blockHash, token, '_allowances', owner, spender);
const { slot: allowancesSlot } = getStorageInfo(this._storageLayout, '_allowances');
const slot = getMappingSlot(getMappingSlot(allowancesSlot, owner), spender);
const vars = {
blockHash,
contract: token,
slot
};
const result = await this._getStorageAt(vars);
log(JSONbig.stringify(result, null, 2)); log(JSONbig.stringify(result, null, 2));
const { value, proof } = result; const { value, proof } = result;
@ -250,13 +232,14 @@ export class Indexer {
} }
// TODO: Move into base/class or framework package. // TODO: Move into base/class or framework package.
async _getStorageValue (blockHash: string, token: string, variable: string): Promise<ValueResult> { async _getStorageValue (blockHash: string, token: string, variable: string, ...mappingKeys: string[]): Promise<ValueResult> {
return getStorageValue( return getStorageValue(
this._storageLayout, this._storageLayout,
this._getStorageAt, this._getStorageAt,
blockHash, blockHash,
token, token,
variable variable,
...mappingKeys
); );
} }

2854
yarn.lock

File diff suppressed because it is too large Load Diff