"use strict"; import { getAddress } from "@ethersproject/address"; import { BigNumber } from "@ethersproject/bignumber"; import { arrayify, concat, hexDataSlice, hexlify, hexZeroPad, isHexString } from "@ethersproject/bytes"; import { id } from "@ethersproject/hash"; import { keccak256 } from "@ethersproject/keccak256"; import { defineReadOnly, Description, getStatic } from "@ethersproject/properties"; import { defaultAbiCoder } from "./abi-coder"; import { ConstructorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, ParamType } from "./fragments"; import { Logger } from "@ethersproject/logger"; import { version } from "./_version"; const logger = new Logger(version); export class LogDescription extends Description { } export class TransactionDescription extends Description { } export class Indexed extends Description { static isIndexed(value) { return !!(value && value._isIndexed); } } export class Interface { constructor(fragments) { logger.checkNew(new.target, Interface); let abi = []; if (typeof (fragments) === "string") { abi = JSON.parse(fragments); } else { abi = fragments; } defineReadOnly(this, "fragments", abi.map((fragment) => { return Fragment.from(fragment); }).filter((fragment) => (fragment != null))); defineReadOnly(this, "_abiCoder", getStatic((new.target), "getAbiCoder")()); defineReadOnly(this, "functions", {}); defineReadOnly(this, "errors", {}); defineReadOnly(this, "events", {}); defineReadOnly(this, "structs", {}); // Add all fragments by their signature this.fragments.forEach((fragment) => { let bucket = null; switch (fragment.type) { case "constructor": if (this.deploy) { logger.warn("duplicate definition - constructor"); return; } defineReadOnly(this, "deploy", fragment); return; case "function": bucket = this.functions; break; case "event": bucket = this.events; break; default: return; } let signature = fragment.format(); if (bucket[signature]) { logger.warn("duplicate definition - " + signature); return; } bucket[signature] = fragment; }); // If we do not have a constructor use the default "constructor() payable" if (!this.deploy) { defineReadOnly(this, "deploy", ConstructorFragment.from({ type: "constructor" })); } defineReadOnly(this, "_isInterface", true); } format(format) { if (!format) { format = FormatTypes.full; } if (format === FormatTypes.sighash) { logger.throwArgumentError("interface does not support formating sighash", "format", format); } const abi = this.fragments.map((fragment) => fragment.format(format)); // We need to re-bundle the JSON fragments a bit if (format === FormatTypes.json) { return JSON.stringify(abi.map((j) => JSON.parse(j))); } return abi; } // Sub-classes can override these to handle other blockchains static getAbiCoder() { return defaultAbiCoder; } static getAddress(address) { return getAddress(address); } static getSighash(functionFragment) { return hexDataSlice(id(functionFragment.format()), 0, 4); } static getTopic(eventFragment) { return id(eventFragment.format()); } // Find a function definition by any means necessary (unless it is ambiguous) getFunction(nameOrSignatureOrSighash) { if (isHexString(nameOrSignatureOrSighash)) { for (const name in this.functions) { if (nameOrSignatureOrSighash === this.getSighash(name)) { return this.functions[name]; } } logger.throwArgumentError("no matching function", "sighash", nameOrSignatureOrSighash); } // It is a bare name, look up the function (will return null if ambiguous) if (nameOrSignatureOrSighash.indexOf("(") === -1) { const name = nameOrSignatureOrSighash.trim(); const matching = Object.keys(this.functions).filter((f) => (f.split("(" /* fix:) */)[0] === name)); if (matching.length === 0) { logger.throwArgumentError("no matching function", "name", name); } else if (matching.length > 1) { logger.throwArgumentError("multiple matching functions", "name", name); } return this.functions[matching[0]]; } // Normlize the signature and lookup the function const result = this.functions[FunctionFragment.fromString(nameOrSignatureOrSighash).format()]; if (!result) { logger.throwArgumentError("no matching function", "signature", nameOrSignatureOrSighash); } return result; } // Find an event definition by any means necessary (unless it is ambiguous) getEvent(nameOrSignatureOrTopic) { if (isHexString(nameOrSignatureOrTopic)) { const topichash = nameOrSignatureOrTopic.toLowerCase(); for (const name in this.events) { if (topichash === this.getEventTopic(name)) { return this.events[name]; } } logger.throwArgumentError("no matching event", "topichash", topichash); } // It is a bare name, look up the function (will return null if ambiguous) if (nameOrSignatureOrTopic.indexOf("(") === -1) { const name = nameOrSignatureOrTopic.trim(); const matching = Object.keys(this.events).filter((f) => (f.split("(" /* fix:) */)[0] === name)); if (matching.length === 0) { logger.throwArgumentError("no matching event", "name", name); } else if (matching.length > 1) { logger.throwArgumentError("multiple matching events", "name", name); } return this.events[matching[0]]; } // Normlize the signature and lookup the function const result = this.events[EventFragment.fromString(nameOrSignatureOrTopic).format()]; if (!result) { logger.throwArgumentError("no matching event", "signature", nameOrSignatureOrTopic); } return result; } // Get the sighash (the bytes4 selector) used by Solidity to identify a function getSighash(functionFragment) { if (typeof (functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } return getStatic(this.constructor, "getSighash")(functionFragment); } // Get the topic (the bytes32 hash) used by Solidity to identify an event getEventTopic(eventFragment) { if (typeof (eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); } return getStatic(this.constructor, "getTopic")(eventFragment); } _decodeParams(params, data) { return this._abiCoder.decode(params, data); } _encodeParams(params, values) { return this._abiCoder.encode(params, values); } encodeDeploy(values) { return this._encodeParams(this.deploy.inputs, values || []); } // Decode the data for a function call (e.g. tx.data) decodeFunctionData(functionFragment, data) { if (typeof (functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } const bytes = arrayify(data); if (hexlify(bytes.slice(0, 4)) !== this.getSighash(functionFragment)) { logger.throwArgumentError(`data signature does not match function ${functionFragment.name}.`, "data", hexlify(bytes)); } return this._decodeParams(functionFragment.inputs, bytes.slice(4)); } // Encode the data for a function call (e.g. tx.data) encodeFunctionData(functionFragment, values) { if (typeof (functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } return hexlify(concat([ this.getSighash(functionFragment), this._encodeParams(functionFragment.inputs, values || []) ])); } // Decode the result from a function call (e.g. from eth_call) decodeFunctionResult(functionFragment, data) { if (typeof (functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } let bytes = arrayify(data); let reason = null; let errorSignature = null; switch (bytes.length % this._abiCoder._getWordSize()) { case 0: try { return this._abiCoder.decode(functionFragment.outputs, bytes); } catch (error) { } break; case 4: if (hexlify(bytes.slice(0, 4)) === "0x08c379a0") { errorSignature = "Error(string)"; reason = this._abiCoder.decode(["string"], bytes.slice(4))[0]; } break; } return logger.throwError("call revert exception", Logger.errors.CALL_EXCEPTION, { method: functionFragment.format(), errorSignature: errorSignature, errorArgs: [reason], reason: reason }); } // Encode the result for a function call (e.g. for eth_call) encodeFunctionResult(functionFragment, values) { if (typeof (functionFragment) === "string") { functionFragment = this.getFunction(functionFragment); } return hexlify(this._abiCoder.encode(functionFragment.outputs, values || [])); } // Create the filter for the event with search criteria (e.g. for eth_filterLog) encodeFilterTopics(eventFragment, values) { if (typeof (eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); } if (values.length > eventFragment.inputs.length) { logger.throwError("too many arguments for " + eventFragment.format(), Logger.errors.UNEXPECTED_ARGUMENT, { argument: "values", value: values }); } let topics = []; if (!eventFragment.anonymous) { topics.push(this.getEventTopic(eventFragment)); } values.forEach((value, index) => { let param = eventFragment.inputs[index]; if (!param.indexed) { if (value != null) { logger.throwArgumentError("cannot filter non-indexed parameters; must be null", ("contract." + param.name), value); } return; } if (value == null) { topics.push(null); } else if (param.type === "string") { topics.push(id(value)); } else if (param.type === "bytes") { topics.push(keccak256(hexlify(value))); } else if (param.type.indexOf("[") !== -1 || param.type.substring(0, 5) === "tuple") { logger.throwArgumentError("filtering with tuples or arrays not supported", ("contract." + param.name), value); } else { // Check addresses are valid if (param.type === "address") { this._abiCoder.encode(["address"], [value]); } topics.push(hexZeroPad(hexlify(value), 32)); } }); // Trim off trailing nulls while (topics.length && topics[topics.length - 1] === null) { topics.pop(); } return topics; } // Decode a filter for the event and the search criteria decodeEventLog(eventFragment, data, topics) { if (typeof (eventFragment) === "string") { eventFragment = this.getEvent(eventFragment); } if (topics != null && !eventFragment.anonymous) { let topicHash = this.getEventTopic(eventFragment); if (!isHexString(topics[0], 32) || topics[0].toLowerCase() !== topicHash) { logger.throwError("fragment/topic mismatch", Logger.errors.INVALID_ARGUMENT, { argument: "topics[0]", expected: topicHash, value: topics[0] }); } topics = topics.slice(1); } let indexed = []; let nonIndexed = []; let dynamic = []; eventFragment.inputs.forEach((param, index) => { if (param.indexed) { if (param.type === "string" || param.type === "bytes" || param.baseType === "tuple" || param.baseType === "array") { indexed.push(ParamType.fromObject({ type: "bytes32", name: param.name })); dynamic.push(true); } else { indexed.push(param); dynamic.push(false); } } else { nonIndexed.push(param); dynamic.push(false); } }); let resultIndexed = (topics != null) ? this._abiCoder.decode(indexed, concat(topics)) : null; let resultNonIndexed = this._abiCoder.decode(nonIndexed, data); let result = []; let nonIndexedIndex = 0, indexedIndex = 0; eventFragment.inputs.forEach((param, index) => { if (param.indexed) { if (resultIndexed == null) { result[index] = new Indexed({ _isIndexed: true, hash: null }); } else if (dynamic[index]) { result[index] = new Indexed({ _isIndexed: true, hash: resultIndexed[indexedIndex++] }); } else { result[index] = resultIndexed[indexedIndex++]; } } else { result[index] = resultNonIndexed[nonIndexedIndex++]; } if (param.name && result[param.name] == null) { result[param.name] = result[index]; } }); return result; } // Given a transaction, find the matching function fragment (if any) and // determine all its properties and call parameters parseTransaction(tx) { let fragment = this.getFunction(tx.data.substring(0, 10).toLowerCase()); if (!fragment) { return null; } return new TransactionDescription({ args: this._abiCoder.decode(fragment.inputs, "0x" + tx.data.substring(10)), functionFragment: fragment, name: fragment.name, signature: fragment.format(), sighash: this.getSighash(fragment), value: BigNumber.from(tx.value || "0"), }); } // Given an event log, find the matching event fragment (if any) and // determine all its properties and values parseLog(log) { let fragment = this.getEvent(log.topics[0]); if (!fragment || fragment.anonymous) { return null; } // @TODO: If anonymous, and the only method, and the input count matches, should we parse? // Probably not, because just because it is the only event in the ABI does // not mean we have the full ABI; maybe jsut a fragment? return new LogDescription({ eventFragment: fragment, name: fragment.name, signature: fragment.format(), topic: this.getEventTopic(fragment), args: this.decodeEventLog(fragment, log.data, log.topics) }); } /* static from(value: Array | string | Interface) { if (Interface.isInterface(value)) { return value; } if (typeof(value) === "string") { return new Interface(JSON.parse(value)); } return new Interface(value); } */ static isInterface(value) { return !!(value && value._isInterface); } }