mirror of
https://github.com/cerc-io/watcher-ts
synced 2024-11-19 20:36:19 +00:00
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:
parent
84e1927402
commit
fc44617db3
@ -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"
|
||||||
|
@ -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));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@ -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
|
||||||
|
21
packages/solidity-mapper/test/contracts/TestMappingTypes.sol
Normal file
21
packages/solidity-mapper/test/contracts/TestMappingTypes.sol
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -10,5 +10,5 @@ export class Contract {
|
|||||||
address!: string;
|
address!: string;
|
||||||
|
|
||||||
@Column('numeric')
|
@Column('numeric')
|
||||||
startingBlock!: BigInt;
|
startingBlock!: bigint;
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user