import _ from 'lodash'; import BigNumber from 'bignumber.js'; import Web3 from 'web3'; import { Log, EventLog } from 'web3-core'; import { Contract } from 'web3-eth-contract'; import { AbiInput, AbiItem } from 'web3-utils'; import { Contracts } from '../modules/Contracts'; import { Balance, BaseValue, Fee, Index, LoggedFundingRate, Price, TxResult, PerpetualMarket, } from '../lib/types'; import { ORDER_FLAGS } from '../lib/Constants'; import { addressesAreEqual } from '../lib/BytesHelper'; type IContractsByAddress = { [address: string]: Contract }; const TUPLE_MAP = { 'struct P1Orders.Fill': ['amount', 'price', 'fee', 'isNegativeFee'], 'struct P1InverseOrders.Fill': ['amount', 'price', 'fee', 'isNegativeFee'], }; // Old contracts used by PBTC-USDC. const OLD_LIQUIDATION_ADDRESSES = [ '0x1F8b4f89a5b8CA0BAa0eDbd0d928DD68B3357280', '0x18Ba3F12f9d3699dE7D451cA97ED55Cd33DD0f80', ].map(a => a.toLowerCase()); const OLD_LIQUIDATOR_PROXY_ADDRESSES = [ '0x51C72bEfAe54D365A9D0C08C486aee4F99285e08', ].map(a => a.toLowerCase()); export class Logs { private contracts: Contracts; private _contractsByAddress?: IContractsByAddress; private web3: Web3; constructor( contracts: Contracts, web3: Web3, ) { this.contracts = contracts; this.web3 = web3; } private get contractsByAddress(): IContractsByAddress { if (!this._contractsByAddress) { this._contractsByAddress = {}; for (const { contract, isTest } of this.contracts.contractsList) { if (isTest) { continue; // Ignore test contracts. } if (!contract.options.address) { continue; // Ignore contracts which aren't deployed for this market pair and network ID. } this._contractsByAddress[contract.options.address.toLowerCase()] = contract; } } return this._contractsByAddress; } public parseLogs(receipt: TxResult): any[] { let events: any[]; if (receipt.logs) { events = JSON.parse(JSON.stringify(receipt.logs)); return events.map(e => this.parseLog(e)).filter(l => !!l); } if (receipt.events) { const tempEvents = JSON.parse(JSON.stringify(receipt.events)); events = []; Object.values(tempEvents).forEach((e: any) => { if (Array.isArray(e)) { e.forEach(ev => events.push(ev)); } else { events.push(e); } }); events.sort((a, b) => a.logIndex - b.logIndex); return events.map(e => this.parseEvent(e)).filter(l => !!l); } throw new Error('Receipt has no logs'); } private parseEvent(event: EventLog): any { return this.parseLog({ address: event.address, data: event.raw.data, topics: event.raw.topics, logIndex: event.logIndex, transactionHash: event.transactionHash, transactionIndex: event.transactionIndex, blockHash: event.blockHash, blockNumber: event.blockNumber, }); } private parseLog(log: Log): any { const logAddress = log.address.toLowerCase(); // Check if the logs are coming from the proxy ABI. if (addressesAreEqual(logAddress, this.contracts.perpetualProxy.options.address)) { const parsedLog = this.parseLogWithContract(this.contracts.perpetualProxy, log); if (parsedLog) { return parsedLog; } } // PBTC-USDC: Check if the logs are coming from old contracts. if (this.contracts.market === PerpetualMarket.PBTC_USDC) { if (OLD_LIQUIDATION_ADDRESSES.includes(logAddress.toLowerCase())) { const parsedLog = this.parseLogWithContract(this.contracts.p1Liquidation, log); if (parsedLog) { return parsedLog; } } if (OLD_LIQUIDATOR_PROXY_ADDRESSES.includes(logAddress.toLowerCase())) { const parsedLog = this.parseLogWithContract(this.contracts.p1LiquidatorProxy, log); if (parsedLog) { return parsedLog; } } } if (logAddress in this.contractsByAddress) { return this.parseLogWithContract(this.contractsByAddress[logAddress], log); } return null; } private parseLogWithContract(contract: Contract, log: Log): any { const events = contract.options.jsonInterface.filter( (e: AbiItem) => e.type === 'event', ); const eventJson = events.find( (e: any) => e.signature.toLowerCase() === log.topics[0].toLowerCase(), ); if (!eventJson) { return null; } const eventArgs = this.web3.eth.abi.decodeLog( eventJson.inputs, log.data, log.topics.slice(1), ); return { ...log, name: eventJson.name, args: this.parseArgs(eventJson.inputs, eventArgs), }; } private parseArgs(inputs: AbiInput[], eventArgs: any): any { const parsedObject: any = {}; for (const input of inputs) { const { name } = input; parsedObject[name] = this.parseValue(input, eventArgs[name]); } return parsedObject; } private parseValue(input: AbiInput, argValue: any): any { if (input.type === 'bytes32') { switch (input.name) { case 'balance': case 'makerBalance': case 'takerBalance': return this.parseBalance(argValue); case 'index': return this.parseIndex(argValue); case 'flags': return this.parseOrderFlags(argValue); case 'fundingRate': return this.parseFundingRate(argValue); } } if (input.type === 'uint256') { switch (input.name) { case 'fee': return Fee.fromSolidity(argValue); case 'price': case 'oraclePrice': case 'settlementPrice': case 'triggerPrice': return Price.fromSolidity(argValue); } } if (input.type === 'address') { return argValue; } if (input.type === 'bool') { return argValue; } if (input.type.match(/^bytes[0-9]*$/)) { return argValue; } if (input.type.match(/^uint[0-9]*$/)) { return new BigNumber(argValue); } if (input.type === 'tuple') { return this.parseTuple(input, argValue); } throw new Error(`Unknown event arg type ${input.type}`); } private parseTuple(input: any, argValue: any): any { const { internalType } = input; if (!(internalType in TUPLE_MAP)) { throw new Error(`Unknown tuple type '${internalType}' in event`); } const expectedTupleArgs = TUPLE_MAP[internalType]; const actualTupleArgs = _.map(input.components, 'name'); if (!_.isEqual(expectedTupleArgs, actualTupleArgs)) { throw new Error(`Arg name mismatch for tuple ${internalType}`); } return this.parseArgs(input.components, argValue); } private parseBalance(balance: string): Balance { const margin = new BigNumber(balance.substr(4, 30), 16); const position = new BigNumber(balance.substr(36, 30), 16); const marginIsPositive = !new BigNumber(balance.substr(2, 2), 16).isZero(); const positionIsPositive = !new BigNumber(balance.substr(34, 2), 16).isZero(); const result = new Balance( marginIsPositive ? margin : margin.negated(), positionIsPositive ? position : position.negated(), ); (result as any).rawValue = balance; return result; } private parseFundingRate(fundingRate: string): LoggedFundingRate { return this.parseIndex(fundingRate) as LoggedFundingRate; } private parseIndex(index: string): Index { const timestamp = new BigNumber(index.substr(2, 30), 16); const isPositive = !new BigNumber(index.substr(32, 2), 16).isZero(); const value = new BigNumber(index.substr(34, 32), 16); return { timestamp, rawValue: index, baseValue: BaseValue.fromSolidity(value, isPositive), } as Index; } private parseOrderFlags(flags: string): any { const flagsNumber = new BigNumber(flags, 16).mod(8).toNumber(); return { rawValue: flags, isBuy: (flagsNumber & ORDER_FLAGS.IS_BUY) !== 0, isDecreaseOnly: (flagsNumber & ORDER_FLAGS.IS_DECREASE_ONLY) !== 0, isNegativeLimitFee: (flagsNumber & ORDER_FLAGS.IS_NEGATIVE_LIMIT_FEE) !== 0, }; } }