Re-organize event parsing and decoding

This commit is contained in:
Alex Browne
2018-09-25 12:54:10 -07:00
parent 9e9104578c
commit fe523e1f3f
7 changed files with 206 additions and 175 deletions

View File

@@ -1,7 +1,5 @@
import { default as axios } from 'axios';
import { AbiDefinition, BlockParam, BlockParamLiteral } from 'ethereum-types';
import { EventsResponse, ExchangeEventEntity, parseRawEventsResponse } from './events';
import { BlockParam, BlockParamLiteral } from 'ethereum-types';
const ETHERSCAN_URL = 'https://api.etherscan.io/api';
@@ -12,25 +10,43 @@ export class Etherscan {
}
/**
* Gets the decoded events for a specific contract and block range.
* Gets the raw events for a specific contract and block range.
* @param contractAddress The address of the contract to get the events for.
* @param constractAbi The ABI of the contract.
* @param fromBlock The start of the block range to get events for (inclusive).
* @param toBlock The end of the block range to get events for (inclusive).
* @returns A list of decoded events.
*/
public async getContractEventsAsync(
contractAddress: string,
contractAbi: AbiDefinition[],
fromBlock: BlockParam = BlockParamLiteral.Earliest,
toBlock: BlockParam = BlockParamLiteral.Latest,
): Promise<ExchangeEventEntity[]> {
): Promise<EventsResponse> {
const fullURL = `${ETHERSCAN_URL}?module=logs&action=getLogs&address=${contractAddress}&fromBlock=${fromBlock}&toBlock=${toBlock}&apikey=${
this._apiKey
}`;
const resp = await axios.get<EventsResponse>(fullURL);
// TODO(albrow): Check response code.
const decodedEvents = parseRawEventsResponse(contractAbi, resp.data);
return decodedEvents;
return resp.data;
}
}
// Raw events response from etherescan.io
export interface EventsResponse {
status: string;
message: string;
result: EventsResponseResult[];
}
// Events as represented in the response from etherscan.io
export interface EventsResponseResult {
address: string;
topics: string[];
data: string;
blockNumber: string;
timeStamp: string;
gasPrice: string;
gasUsed: string;
logIndex: string;
transactionHash: string;
transactionIndex: string;
}

View File

@@ -0,0 +1,34 @@
import { AbiDefinition, BlockParam, BlockParamLiteral, LogEntry } from 'ethereum-types';
import * as R from 'ramda';
import { BaseEntity } from 'typeorm';
import { Etherscan } from '../../../data_sources/etherscan';
import { convertResponseToLogEntry } from '../event_utils';
export abstract class BaseEventHandler<EntityType extends BaseEntity> {
protected _abi: AbiDefinition[];
protected _address: string;
protected _etherscan: Etherscan;
constructor(abi: AbiDefinition[], address: string, etherscan: Etherscan) {
this._abi = abi;
this._address = address;
this._etherscan = etherscan;
}
public abstract convertLogEntryToEventEntity(logEntry: LogEntry): EntityType;
public async getEventsAsync(
fromBlock: BlockParam = BlockParamLiteral.Earliest,
toBlock: BlockParam = BlockParamLiteral.Latest,
): Promise<EntityType[]> {
const rawEventsResponse = await this._etherscan.getContractEventsAsync(this._address, fromBlock, toBlock);
const logEntries = R.map(convertResponseToLogEntry, rawEventsResponse.result);
// Note(albrow): Imperative for loop is required here because we can't
// bind convertLogEntryToEventEntity without having a specific instance
// of a sub-class.
const result = [];
for (const logEntry of logEntries) {
result.push(this.convertLogEntryToEventEntity(logEntry));
}
return result;
}
}

View File

@@ -5,70 +5,28 @@ import { AbiDecoder, BigNumber } from '@0xproject/utils';
import { AbiDefinition, LogEntry, LogWithDecodedArgs } from 'ethereum-types';
import * as R from 'ramda';
import { ExchangeFillEvent } from '../../entities/ExchangeFillEvent';
import { ExchangeFillEvent } from '../../../entities/ExchangeFillEvent';
import { decodeLogEntry } from '../event_utils';
import { BaseEventHandler } from './base_event_handler';
// TODO(albrow): Union with other exchange event entity types
export type ExchangeEventEntity = ExchangeFillEvent;
// Raw events response from etherescan.io
export interface EventsResponse {
status: string;
message: string;
result: EventsResponseResult[];
export class ExchangeEventHandler extends BaseEventHandler<ExchangeEventEntity> {
public convertLogEntryToEventEntity(logEntry: LogEntry): ExchangeEventEntity {
const decodedLogEntry = decodeLogEntry<ExchangeEventArgs>(this._abi, logEntry);
return _convertToEntity(decodedLogEntry);
}
}
// Events as represented in the response from etherscan.io
export interface EventsResponseResult {
address: string;
topics: string[];
data: string;
blockNumber: string;
timeStamp: string;
gasPrice: string;
gasUsed: string;
logIndex: string;
transactionHash: string;
transactionIndex: string;
}
const hexRadix = 16;
function hexToInt(hex: string): number {
return parseInt(hex.replace('0x', ''), hexRadix);
}
// Converts a raw event response to a LogEntry
// tslint:disable-next-line:completed-docs
export function _convertResponseToLogEntry(result: EventsResponseResult): LogEntry {
return {
logIndex: hexToInt(result.logIndex),
transactionIndex: hexToInt(result.transactionIndex),
transactionHash: result.transactionHash,
blockHash: '',
blockNumber: hexToInt(result.blockNumber),
address: result.address,
data: result.data,
topics: result.topics,
};
}
// Decodes a LogEntry into a LogWithDecodedArgs
// tslint:disable-next-line:completed-docs
export const _decodeLogEntry = R.curry((contractAbi: AbiDefinition[], log: LogEntry): LogWithDecodedArgs<
ExchangeEventArgs
> => {
const abiDecoder = new AbiDecoder([contractAbi]);
const logWithDecodedArgs = abiDecoder.tryToDecodeLogOrNoop(log);
// tslint:disable-next-line:no-unnecessary-type-assertion
return logWithDecodedArgs as LogWithDecodedArgs<ExchangeEventArgs>;
});
export function _convertToEntity(eventLog: LogWithDecodedArgs<ExchangeEventArgs>): ExchangeEventEntity {
switch (eventLog.event) {
case 'Fill':
return _convertToExchangeFillEvent(eventLog as LogWithDecodedArgs<ExchangeFillEventArgs>);
default:
throw new Error('unexpected eventLog.event type: ' + eventLog.event);
return new ExchangeFillEvent();
// throw new Error('unexpected eventLog.event type: ' + eventLog.event);
}
}
@@ -116,21 +74,3 @@ function filterEventLogs(
): Array<LogWithDecodedArgs<ExchangeEventArgs>> {
return R.filter(eventLog => eventLog.event === 'Fill', eventLogs);
}
/**
* Parses and abi-decodes the raw events response from etherscan.io.
* @param contractAbi The ABI for the contract that the events where emited from.
* @param rawEventsResponse The raw events response from etherescan.io.
* @returns Parsed and decoded event entities, ready to be saved to database.
*/
export function parseRawEventsResponse(
contractAbi: AbiDefinition[],
rawEventsResponse: EventsResponse,
): ExchangeEventEntity[] {
return R.pipe(
R.map(_convertResponseToLogEntry),
R.map(_decodeLogEntry(contractAbi)),
filterEventLogs,
R.map(_convertToEntity),
)(rawEventsResponse.result);
}

View File

@@ -0,0 +1,35 @@
import { AbiDecoder } from '@0xproject/utils';
import { AbiDefinition, LogEntry, LogWithDecodedArgs } from 'ethereum-types';
import { EventsResponseResult } from '../../data_sources/etherscan';
const hexRadix = 16;
function hexToInt(hex: string): number {
return parseInt(hex.replace('0x', ''), hexRadix);
}
// Converts a raw event response to a LogEntry
export function convertResponseToLogEntry(result: EventsResponseResult): LogEntry {
return {
logIndex: hexToInt(result.logIndex),
transactionIndex: hexToInt(result.transactionIndex),
transactionHash: result.transactionHash,
blockHash: '',
blockNumber: hexToInt(result.blockNumber),
address: result.address,
data: result.data,
topics: result.topics,
};
}
// Decodes a LogEntry into a LogWithDecodedArgs
export function decodeLogEntry<EventArgsType>(
contractAbi: AbiDefinition[],
log: LogEntry,
): LogWithDecodedArgs<EventArgsType> {
const abiDecoder = new AbiDecoder([contractAbi]);
const logWithDecodedArgs = abiDecoder.tryToDecodeLogOrNoop<EventArgsType>(log);
// tslint:disable-next-line:no-unnecessary-type-assertion
return logWithDecodedArgs as LogWithDecodedArgs<EventArgsType>;
}

View File

@@ -1,9 +1,9 @@
import { Column, Entity, PrimaryColumn } from 'typeorm';
import { BaseEntity, Column, Entity, PrimaryColumn } from 'typeorm';
export type ExchangeFillEventAssetType = 'erc20' | 'erc721';
@Entity()
export class ExchangeFillEvent {
export class ExchangeFillEvent extends BaseEntity {
@PrimaryColumn() public logIndex!: number;
@Column() public address!: string;

View File

@@ -1,26 +1,32 @@
import { ExchangeFillEventArgs } from '@0xproject/contract-wrappers';
import { assetDataUtils } from '@0xproject/order-utils';
import { LogWithDecodedArgs } from 'ethereum-types';
import 'reflect-metadata';
import { createConnection } from 'typeorm';
import { artifacts } from './artifacts';
import { Etherscan } from './data-sources/etherscan';
import { Etherscan } from './data_sources/etherscan';
import { ExchangeFillEvent } from './entities/ExchangeFillEvent';
import { config } from './ormconfig';
import { ExchangeEventHandler } from './data_types/events/event_handlers/exchange_event_handler';
const etherscan = new Etherscan(process.env.ETHERSCAN_API_KEY as string);
const EXCHANGE_ADDRESS = '0x4f833a24e1f95d70f028921e27040ca56e09ab0b';
(async () => {
const connection = await createConnection(config);
const repository = connection.getRepository(ExchangeFillEvent);
console.log(`found ${await repository.count()} existing fill events`);
const events = await etherscan.getContractEventsAsync(
'0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
const exchangeEventHandler = new ExchangeEventHandler(
artifacts.Exchange.compilerOutput.abi,
EXCHANGE_ADDRESS,
etherscan,
);
const events = await exchangeEventHandler.getEventsAsync();
console.log(JSON.stringify(events, null, 2));
for (const event of events) {
await repository.save(event);
// TODO(albrow): remove this check once we can parse all Exchange events
if (event.address != null) {
await event.save();
}
}
console.log(`now ${await repository.count()} total fill events`);
})();

View File

@@ -1,90 +1,90 @@
import { BigNumber } from '@0xproject/utils';
import * as chai from 'chai';
import { DecodedLogArgs, LogEntry, LogWithDecodedArgs } from 'ethereum-types';
import 'mocha';
// import { BigNumber } from '@0xproject/utils';
// import * as chai from 'chai';
// import { DecodedLogArgs, LogEntry, LogWithDecodedArgs } from 'ethereum-types';
// import 'mocha';
import { artifacts } from '../../../src/artifacts';
// import { artifacts } from '../../../src/artifacts';
import {
_convertResponseToLogEntry,
_decodeLogEntry,
EventsResponseResult,
} from '../../../src/data-sources/etherscan/events';
import { chaiSetup } from '../../utils/chai_setup';
// import {
// _convertResponseToLogEntry,
// _decodeLogEntry,
// EventsResponseResult,
// } from '../../../src/data-sources/etherscan/events';
// import { chaiSetup } from '../../utils/chai_setup';
chaiSetup.configure();
const expect = chai.expect;
// chaiSetup.configure();
// const expect = chai.expect;
describe('etherscan#events', () => {
describe('_convertResponseToLogEntry', () => {
it('converts EventsResponseResult to LogEntry', () => {
const input: EventsResponseResult = {
address: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
topics: [
'0x82af639571738f4ebd4268fb0363d8957ebe1bbb9e78dba5ebd69eed39b154f0',
'0x00000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e930',
'0x0000000000000000000000000000000000000000000000000000000000000000',
],
data: '0x00000000000000000000000000000000000000000000000000000165f2d3f94d',
blockNumber: '0x61127b',
timeStamp: '0x5ba2878e',
gasPrice: '0x1a13b8600',
gasUsed: '0xd9dc',
logIndex: '0x63',
transactionHash: '0xa3f71931ddab6e758b9d1755b2715b376759f49f23fff60755f7e073367d61b5',
transactionIndex: '0x35',
};
const expected: LogEntry = {
logIndex: 99,
transactionIndex: 53,
transactionHash: input.transactionHash,
blockHash: '',
blockNumber: 6361723,
address: input.address,
data: input.data,
topics: input.topics,
};
const actual = _convertResponseToLogEntry(input);
expect(actual).deep.equal(expected);
});
});
describe('_decodeLogEntry', () => {
it('decodes LogEntry into LogWithDecodedArgs', () => {
const input: LogEntry = {
logIndex: 96,
transactionIndex: 52,
transactionHash: '0x02b59043e9b38b430c8c66abe67ab4a9e5509def8f8552b54231e88db1839831',
blockHash: '',
blockNumber: 6361723,
address: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
data:
'0x00000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e93000000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e930000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000000013ab668000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000',
topics: [
'0x0bcc4c97732e47d9946f229edb95f5b6323f601300e4690de719993f3c371129',
'0x0000000000000000000000003f7f832abb3be28442c0e48b7222e02b322c78f3',
'0x000000000000000000000000a258b39954cef5cb142fd567a46cddb31a670124',
'0x523404b4e6f847d9aefcf5be024be396449b4635590291fd7a28a8c940843858',
],
};
const expected: LogWithDecodedArgs<DecodedLogArgs> = {
...input,
event: 'Fill',
args: {
makerAddress: '0x3f7f832abb3be28442c0e48b7222e02b322c78f3',
feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
takerAddress: '0x67032ef7be8fa07c4335d0134099db0f3875e930',
senderAddress: '0x67032ef7be8fa07c4335d0134099db0f3875e930',
makerAssetFilledAmount: new BigNumber('100000000000'),
takerAssetFilledAmount: new BigNumber('330000000'),
makerFeePaid: new BigNumber('0'),
takerFeePaid: new BigNumber('0'),
orderHash: '0x523404b4e6f847d9aefcf5be024be396449b4635590291fd7a28a8c940843858',
makerAssetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
},
};
const actual = _decodeLogEntry(artifacts.Exchange.compilerOutput.abi, input);
expect(actual).deep.equal(expected);
});
});
});
// describe('etherscan#events', () => {
// describe('_convertResponseToLogEntry', () => {
// it('converts EventsResponseResult to LogEntry', () => {
// const input: EventsResponseResult = {
// address: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
// topics: [
// '0x82af639571738f4ebd4268fb0363d8957ebe1bbb9e78dba5ebd69eed39b154f0',
// '0x00000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e930',
// '0x0000000000000000000000000000000000000000000000000000000000000000',
// ],
// data: '0x00000000000000000000000000000000000000000000000000000165f2d3f94d',
// blockNumber: '0x61127b',
// timeStamp: '0x5ba2878e',
// gasPrice: '0x1a13b8600',
// gasUsed: '0xd9dc',
// logIndex: '0x63',
// transactionHash: '0xa3f71931ddab6e758b9d1755b2715b376759f49f23fff60755f7e073367d61b5',
// transactionIndex: '0x35',
// };
// const expected: LogEntry = {
// logIndex: 99,
// transactionIndex: 53,
// transactionHash: input.transactionHash,
// blockHash: '',
// blockNumber: 6361723,
// address: input.address,
// data: input.data,
// topics: input.topics,
// };
// const actual = _convertResponseToLogEntry(input);
// expect(actual).deep.equal(expected);
// });
// });
// describe('_decodeLogEntry', () => {
// it('decodes LogEntry into LogWithDecodedArgs', () => {
// const input: LogEntry = {
// logIndex: 96,
// transactionIndex: 52,
// transactionHash: '0x02b59043e9b38b430c8c66abe67ab4a9e5509def8f8552b54231e88db1839831',
// blockHash: '',
// blockNumber: 6361723,
// address: '0x4f833a24e1f95d70f028921e27040ca56e09ab0b',
// data:
// '0x00000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e93000000000000000000000000067032ef7be8fa07c4335d0134099db0f3875e930000000000000000000000000000000000000000000000000000000174876e8000000000000000000000000000000000000000000000000000000000013ab668000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000001600000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000024f47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000000000000000000000000000000000000',
// topics: [
// '0x0bcc4c97732e47d9946f229edb95f5b6323f601300e4690de719993f3c371129',
// '0x0000000000000000000000003f7f832abb3be28442c0e48b7222e02b322c78f3',
// '0x000000000000000000000000a258b39954cef5cb142fd567a46cddb31a670124',
// '0x523404b4e6f847d9aefcf5be024be396449b4635590291fd7a28a8c940843858',
// ],
// };
// const expected: LogWithDecodedArgs<DecodedLogArgs> = {
// ...input,
// event: 'Fill',
// args: {
// makerAddress: '0x3f7f832abb3be28442c0e48b7222e02b322c78f3',
// feeRecipientAddress: '0xa258b39954cef5cb142fd567a46cddb31a670124',
// takerAddress: '0x67032ef7be8fa07c4335d0134099db0f3875e930',
// senderAddress: '0x67032ef7be8fa07c4335d0134099db0f3875e930',
// makerAssetFilledAmount: new BigNumber('100000000000'),
// takerAssetFilledAmount: new BigNumber('330000000'),
// makerFeePaid: new BigNumber('0'),
// takerFeePaid: new BigNumber('0'),
// orderHash: '0x523404b4e6f847d9aefcf5be024be396449b4635590291fd7a28a8c940843858',
// makerAssetData: '0xf47261b0000000000000000000000000e41d2489571d322189246dafa5ebde1f4699f498',
// takerAssetData: '0xf47261b0000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc2',
// },
// };
// const actual = _decodeLogEntry(artifacts.Exchange.compilerOutput.abi, input);
// expect(actual).deep.equal(expected);
// });
// });
// });