Implement eth_call for methods returning struct type (#59)

* Handle tuple return type in ethereum host API

* Update graph-cli version to fix eth_call error

* Handle all types in struct based on abi

Co-authored-by: prathamesh0 <prathamesh.musale0@gmail.com>
This commit is contained in:
2021-12-28 16:08:05 +05:30
committed by nabarun
co-authored by prathamesh
parent f52467f724
commit 31b302c9b5
11 changed files with 341 additions and 247 deletions
+10 -12
View File
@@ -5,8 +5,10 @@
import path from 'path';
import chai, { assert, expect } from 'chai';
import spies from 'chai-spies';
import { utils } from 'ethers';
import { getDummyEventData, getTestDatabase } from '../test/utils';
import abi from '../test/subgraph/example1/build/Example1/abis/Example1.json';
import { instantiate } from './loader';
import { createEvent, createBlock, Block } from './utils';
import { Database } from './database';
@@ -66,18 +68,14 @@ describe('call handler in mapping code', () => {
_start();
// Create event params data.
dummyEventData.eventParams = [
{
name: 'param1',
value: 'abc',
kind: 'string'
},
{
name: 'param2',
value: BigInt(123),
kind: 'uint256'
}
];
const contractInterface = new utils.Interface(abi);
const eventFragment = contractInterface.getEvent('Test(string,uint8)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
param1: 'abc',
param2: BigInt(123)
};
// Dummy contract address string.
const contractAddress = '0xCA6D29232D1435D8198E3E5302495417dD073d61';
+82 -184
View File
@@ -3,7 +3,7 @@
//
import assert from 'assert';
import { ethers } from 'ethers';
import { ethers, utils } from 'ethers';
import path from 'path';
import chai from 'chai';
import spies from 'chai-spies';
@@ -65,6 +65,8 @@ describe('eden wasm loader tests', async () => {
}
};
const contractInterface = new utils.Interface(edenNetworkAbi);
it('should load the subgraph network wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetwork/EdenNetwork.wasm');
({ exports } = await instantiate(db, { event: { block: dummyEventData.block } }, filePath, data));
@@ -78,43 +80,17 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy SlotClaimedEvent params.
dummyEventData.eventParams = [
{
name: 'slot',
kind: 'uint8',
value: 0
},
{
name: 'owner',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'delegate',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'newBidAmount',
kind: 'uint128',
value: BigInt(1)
},
{
name: 'oldBidAmount',
kind: 'uint128',
value: BigInt(1)
},
{
name: 'taxNumerator',
kind: 'uint16',
value: 1
},
{
name: 'taxDenominator',
kind: 'uint16',
value: 1
}
];
const eventFragment = contractInterface.getEvent('SlotClaimed(indexed uint8,indexed address,indexed address,uint128,uint128,uint16,uint16)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
slot: 0,
owner: ZERO_ADDRESS,
delegate: ZERO_ADDRESS,
newBidAmount: BigInt(1),
oldBidAmount: BigInt(1),
taxNumerator: 1,
taxDenominator: 1
};
// Create an ethereum event SlotClaimedEvent to be passed to handler.
const slotClaimedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -128,28 +104,14 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy SlotDelegateUpdatedEvent params.
dummyEventData.eventParams = [
{
name: 'slot',
kind: 'uint8',
value: 0
},
{
name: 'owner',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'newDelegate',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'oldDelegate',
kind: 'address',
value: ZERO_ADDRESS
}
];
const eventFragment = contractInterface.getEvent('SlotDelegateUpdated(indexed uint8,indexed address,indexed address,address)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
slot: 0,
owner: ZERO_ADDRESS,
newDelegate: ZERO_ADDRESS,
oldDelegate: ZERO_ADDRESS
};
// Create an ethereum event SlotDelegateUpdatedEvent to be passed to handler.
const slotClaimedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -163,18 +125,12 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy StakeEvent params.
dummyEventData.eventParams = [
{
name: 'staker',
kind: 'address',
value: '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc'
},
{
name: 'stakeAmount',
kind: 'uint256',
value: BigInt(1)
}
];
const eventFragment = contractInterface.getEvent('Stake(indexed address,uint256)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
staker: '0xDC7d7A8920C8Eecc098da5B7522a5F31509b5Bfc',
stakeAmount: BigInt(1)
};
// Create an ethereum event StakeEvent to be passed to handler.
const stakeEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -188,18 +144,12 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy UnstakeEvent params.
dummyEventData.eventParams = [
{
name: 'staker',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'unstakedAmount',
kind: 'uin256',
value: BigInt(1)
}
];
const eventFragment = contractInterface.getEvent('Unstake(indexed address,uint256)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
staker: ZERO_ADDRESS,
unstakedAmount: BigInt(1)
};
// Create an ethereum event UnstakeEvent to be passed to handler.
const unstakeEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -224,6 +174,8 @@ describe('eden wasm loader tests', async () => {
}
};
const contractInterface = new utils.Interface(merkleDistributorAbi);
it('should load the subgraph network distribution wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkDistribution/EdenNetworkDistribution.wasm');
({ exports } = await instantiate(db, { event: { block: dummyEventData.block } }, filePath, data));
@@ -237,28 +189,14 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy ClaimedEvent params.
dummyEventData.eventParams = [
{
name: 'index',
kind: 'uint256',
value: BigInt(1)
},
{
name: 'totalEarned',
kind: 'uint256',
value: BigInt(1)
},
{
name: 'account',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'claimed',
kind: 'uint256',
value: BigInt(1)
}
];
const eventFragment = contractInterface.getEvent('Claimed(uint256,uint256,indexed address,uint256)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
index: BigInt(1),
totalEarned: BigInt(1),
account: ZERO_ADDRESS,
claimed: BigInt(1)
};
// Create an ethereum event ClaimedEvent to be passed to handler.
const claimedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -272,18 +210,12 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy SlashedEvent params.
dummyEventData.eventParams = [
{
name: 'account',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'slashed',
kind: 'uint256',
value: BigInt(1)
}
];
const eventFragment = contractInterface.getEvent('Slashed(indexed address,uint256)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
account: ZERO_ADDRESS,
slashed: BigInt(1)
};
// Create an ethereum event SlashedEvent to be passed to handler.
const slashedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -297,23 +229,13 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy MerkleRootUpdatedEvent params.
dummyEventData.eventParams = [
{
name: 'merkleRoot',
kind: 'bytes32',
value: ethers.utils.hexlify(ethers.utils.randomBytes(32))
},
{
name: 'distributionNumber',
kind: 'uint256',
value: BigInt(1)
},
{
name: 'metadataURI',
kind: 'string',
value: 'abc'
}
];
const eventFragment = contractInterface.getEvent('MerkleRootUpdated(bytes32,uint256,string)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
merkleRoot: ethers.utils.hexlify(ethers.utils.randomBytes(32)),
distributionNumber: BigInt(1),
metadataURI: 'abc'
};
// Create an ethereum event MerkleRootUpdatedEvent to be passed to handler.
const merkleRootUpdatedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -327,23 +249,13 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy AccountUpdatedEvent params.
dummyEventData.eventParams = [
{
name: 'account',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'totalClaimed',
kind: 'uint256',
value: BigInt(1)
},
{
name: 'totalSlashed',
kind: 'uint256',
value: BigInt(1)
}
];
const eventFragment = contractInterface.getEvent('AccountUpdated(indexed address,uint256,uint256)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
account: ZERO_ADDRESS,
totalClaimed: BigInt(1),
totalSlashed: BigInt(1)
};
// Create an ethereum event AccountUpdatedEvent to be passed to handler.
const accountUpdatedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -368,6 +280,8 @@ describe('eden wasm loader tests', async () => {
}
};
const contractInterface = new utils.Interface(distributorGovernanceAbi);
it('should load the subgraph network governance wasm', async () => {
const filePath = path.resolve(__dirname, '../test/subgraph/eden/EdenNetworkGovernance/EdenNetworkGovernance.wasm');
({ exports } = await instantiate(db, { event: { block: dummyEventData.block } }, filePath, data));
@@ -381,13 +295,9 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy BlockProducerAddedEvent params.
dummyEventData.eventParams = [
{
name: 'produces',
kind: 'address',
value: ZERO_ADDRESS
}
];
const eventFragment = contractInterface.getEvent('BlockProducerAdded(indexed address)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = { produces: ZERO_ADDRESS };
// Create an ethereum event BlockProducerAddedEvent to be passed to handler.
const blockProducerAddedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -401,13 +311,9 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy BlockProducerRemovedEvent params.
dummyEventData.eventParams = [
{
name: 'producer',
kind: 'address',
value: ZERO_ADDRESS
}
];
const eventFragment = contractInterface.getEvent('BlockProducerRemoved(indexed address)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = { producer: ZERO_ADDRESS };
// Create an ethereum event BlockProducerRemovedEvent to be passed to handler.
const blockProducerRemovedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -421,23 +327,13 @@ describe('eden wasm loader tests', async () => {
} = exports;
// Create dummy BlockProducerRewardCollectorChangedEvent params.
dummyEventData.eventParams = [
{
name: 'producer',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'collector',
kind: 'address',
value: ZERO_ADDRESS
},
{
name: 'metadataURI',
kind: 'string',
value: 'abc'
}
];
const eventFragment = contractInterface.getEvent('BlockProducerRewardCollectorChanged(indexed address,indexed address)');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {
producer: ZERO_ADDRESS,
collector: ZERO_ADDRESS,
metadataURI: 'abc'
};
// Create an ethereum event BlockProducerRewardCollectorChangedEvent to be passed to handler.
const blockProducerRewardCollectorChangedEvent = await createEvent(exports, contractAddress, dummyEventData);
@@ -450,7 +346,9 @@ describe('eden wasm loader tests', async () => {
rewardScheduleChanged
} = exports;
dummyEventData.eventParams = [];
const eventFragment = contractInterface.getEvent('RewardScheduleChanged()');
dummyEventData.inputs = eventFragment.inputs;
dummyEventData.event = {};
// Create an ethereum event RewardScheduleChangedEvent to be passed to handler.
const rewardScheduleChangedEvent = await createEvent(exports, contractAddress, dummyEventData);
+18 -9
View File
@@ -138,18 +138,27 @@ export const instantiate = async (database: Database, indexer: IndexerInterface,
// TODO: Check for function overloading.
let result = await contract[functionName](...functionParams);
if (!Array.isArray(result)) {
// TODO: Check for function overloading.
// Using function signature does not work.
const { outputs } = contract.interface.getFunction(functionName);
assert(outputs);
// If method returns a single value, ethers returns it directly compared to returning multiple values in an array.
if (outputs.length === 1) {
// Put result in an array to map with the outputs array from abi.
result = [result];
}
// TODO: Check for function overloading.
// Using function signature does not work.
const outputs = contract.interface.getFunction(functionName).outputs;
const resultPtrArrayPromise = result.map(async (value: any, index: number) => {
assert(outputs);
return toEthereumValue(exports, value, outputs[index].type);
});
const resultPtrArrayPromise = outputs.map(
async (
output: any,
index: number
) => toEthereumValue(
exports,
output,
result[index]
)
);
const resultPtrArray: any[] = await Promise.all(resultPtrArrayPromise);
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
+35 -14
View File
@@ -1,4 +1,4 @@
import { BigNumber } from 'ethers';
import { BigNumber, utils } from 'ethers';
import path from 'path';
import fs from 'fs-extra';
import debug from 'debug';
@@ -11,12 +11,6 @@ import { TypeId, EthereumValueKind, ValueKind } from './types';
const log = debug('vulcanize:utils');
interface EventParam {
name: string;
value: any;
kind: string;
}
interface Transaction {
hash: string;
index: number;
@@ -42,7 +36,8 @@ export interface Block {
export interface EventData {
block: Block;
tx: Transaction;
eventParams: EventParam[];
inputs: utils.ParamType[];
event: { [key: string]: any }
eventIndex: number;
}
@@ -102,16 +97,41 @@ export const fromEthereumValue = async (instanceExports: any, value: any): Promi
* @param type
* @returns
*/
export const toEthereumValue = async (instanceExports: any, value: any, type: string): Promise<any> => {
export const toEthereumValue = async (instanceExports: any, output: utils.ParamType, value: any): Promise<any> => {
const {
__newString,
__newArray,
ByteArray,
Bytes,
Address,
ethereum,
BigInt
BigInt,
id_of_type: getIdOfType
} = instanceExports;
const { type } = output;
// For tuple type.
if (type === 'tuple') {
const arrayEthereumValueId = await getIdOfType(TypeId.ArrayEthereumValue);
// Get values for struct elements.
const ethereumValuePromises = output.components
.map(
async (component: utils.ParamType) => toEthereumValue(
instanceExports,
component,
value[component.name]
)
);
const ethereumValues: any[] = await Promise.all(ethereumValuePromises);
const ethereumValuesArrayPtr = await __newArray(arrayEthereumValueId, ethereumValues);
const ethereumTuple = await ethereum.Tuple.wrap(ethereumValuesArrayPtr);
return ethereum.Value.fromTuple(ethereumTuple);
}
// For boolean type.
if (type === 'bool') {
return ethereum.Value.fromBoolean(value ? 1 : 0);
@@ -163,7 +183,8 @@ export const createEvent = async (instanceExports: any, contractAddress: string,
const {
tx,
eventIndex,
eventParams: eventParamsData,
inputs,
event,
block: blockData
} = eventData;
@@ -214,10 +235,10 @@ export const createEvent = async (instanceExports: any, contractAddress: string,
txinputPtr
);
const eventParamArrayPromise = eventParamsData.map(async data => {
const { name, value, kind } = data;
const eventParamArrayPromise = inputs.map(async input => {
const { name } = input;
const ethValue = await toEthereumValue(instanceExports, value, kind);
const ethValue = await toEthereumValue(instanceExports, input, event[name]);
const namePtr = await __newString(name);
return ethereum.EventParam.__new(
+2 -9
View File
@@ -133,17 +133,10 @@ export class GraphWatcher {
const eventFragment = contractInterface.getEvent(eventSignature);
const eventParams = eventFragment.inputs.map((input) => {
return {
name: input.name,
value: event[input.name],
kind: input.type
};
});
const data = {
eventParams: eventParams,
block: blockData,
inputs: eventFragment.inputs,
event,
tx,
eventIndex
};