ethers.js/packages/abi/lib.esm/interface.js

393 lines
16 KiB
JavaScript
Raw Normal View History

2019-05-15 01:48:48 +03:00
"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";
2020-01-08 03:58:04 +03:00
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) {
2019-06-12 00:57:04 +03:00
return !!(value && value._isIndexed);
2019-05-15 01:48:48 +03:00
}
}
export class Interface {
constructor(fragments) {
logger.checkNew(new.target, Interface);
let abi = [];
2019-05-15 01:48:48 +03:00
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", {});
2019-05-15 01:48:48 +03:00
// Add all fragments by their signature
this.fragments.forEach((fragment) => {
let bucket = null;
2019-05-15 01:48:48 +03:00
switch (fragment.type) {
case "constructor":
if (this.deploy) {
2019-08-02 09:10:58 +03:00
logger.warn("duplicate definition - constructor");
2019-05-15 01:48:48 +03:00
return;
}
defineReadOnly(this, "deploy", fragment);
2019-05-15 01:48:48 +03:00
return;
case "function":
bucket = this.functions;
2019-05-15 01:48:48 +03:00
break;
case "event":
bucket = this.events;
2019-05-15 01:48:48 +03:00
break;
default:
return;
}
let signature = fragment.format();
2019-05-15 01:48:48 +03:00
if (bucket[signature]) {
2019-08-02 09:10:58 +03:00
logger.warn("duplicate definition - " + signature);
2019-05-15 01:48:48 +03:00
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" }));
2019-05-15 01:48:48 +03:00
}
defineReadOnly(this, "_isInterface", true);
2019-05-15 01:48:48 +03:00
}
2020-01-08 03:58:04 +03:00
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);
}
2020-01-08 03:58:04 +03:00
static getSighash(functionFragment) {
return hexDataSlice(id(functionFragment.format()), 0, 4);
}
2020-01-08 03:58:04 +03:00
static getTopic(eventFragment) {
return id(eventFragment.format());
}
2020-01-08 03:58:04 +03:00
// Find a function definition by any means necessary (unless it is ambiguous)
getFunction(nameOrSignatureOrSighash) {
if (isHexString(nameOrSignatureOrSighash)) {
2019-11-20 12:57:38 +03:00
for (const name in this.functions) {
if (nameOrSignatureOrSighash === this.getSighash(name)) {
return this.functions[name];
}
}
logger.throwArgumentError("no matching function", "sighash", nameOrSignatureOrSighash);
2019-05-15 01:48:48 +03:00
}
// It is a bare name, look up the function (will return null if ambiguous)
if (nameOrSignatureOrSighash.indexOf("(") === -1) {
2019-11-20 12:57:38 +03:00
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]];
2019-05-15 01:48:48 +03:00
}
// Normlize the signature and lookup the function
2019-11-20 12:57:38 +03:00
const result = this.functions[FunctionFragment.fromString(nameOrSignatureOrSighash).format()];
if (!result) {
logger.throwArgumentError("no matching function", "signature", nameOrSignatureOrSighash);
}
return result;
}
2020-01-08 03:58:04 +03:00
// Find an event definition by any means necessary (unless it is ambiguous)
getEvent(nameOrSignatureOrTopic) {
if (isHexString(nameOrSignatureOrTopic)) {
2019-11-20 12:57:38 +03:00
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);
2019-05-15 01:48:48 +03:00
}
// It is a bare name, look up the function (will return null if ambiguous)
if (nameOrSignatureOrTopic.indexOf("(") === -1) {
2019-11-20 12:57:38 +03:00
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]];
2019-05-15 01:48:48 +03:00
}
2019-11-20 12:57:38 +03:00
// 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;
}
2020-01-08 03:58:04 +03:00
// Get the sighash (the bytes4 selector) used by Solidity to identify a function
getSighash(functionFragment) {
2019-05-15 01:48:48 +03:00
if (typeof (functionFragment) === "string") {
functionFragment = this.getFunction(functionFragment);
}
2020-01-08 03:58:04 +03:00
return getStatic(this.constructor, "getSighash")(functionFragment);
}
2020-01-08 03:58:04 +03:00
// Get the topic (the bytes32 hash) used by Solidity to identify an event
getEventTopic(eventFragment) {
2019-05-15 01:48:48 +03:00
if (typeof (eventFragment) === "string") {
eventFragment = this.getEvent(eventFragment);
}
2020-01-08 03:58:04 +03:00
return getStatic(this.constructor, "getTopic")(eventFragment);
}
2019-09-28 09:36:19 +03:00
_decodeParams(params, data) {
return this._abiCoder.decode(params, data);
}
_encodeParams(params, values) {
2019-05-15 01:48:48 +03:00
return this._abiCoder.encode(params, values);
}
encodeDeploy(values) {
2019-05-15 01:48:48 +03:00
return this._encodeParams(this.deploy.inputs, values || []);
}
2020-01-08 03:58:04 +03:00
// Decode the data for a function call (e.g. tx.data)
2019-09-28 09:36:19 +03:00
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));
}
2020-01-08 03:58:04 +03:00
// Encode the data for a function call (e.g. tx.data)
encodeFunctionData(functionFragment, values) {
2019-05-15 01:48:48 +03:00
if (typeof (functionFragment) === "string") {
functionFragment = this.getFunction(functionFragment);
}
return hexlify(concat([
2019-05-15 01:48:48 +03:00
this.getSighash(functionFragment),
this._encodeParams(functionFragment.inputs, values || [])
]));
}
2020-01-08 03:58:04 +03:00
// Decode the result from a function call (e.g. from eth_call)
decodeFunctionResult(functionFragment, data) {
2019-05-15 01:48:48 +03:00
if (typeof (functionFragment) === "string") {
functionFragment = this.getFunction(functionFragment);
}
let bytes = arrayify(data);
let reason = null;
let errorSignature = null;
2019-05-15 01:48:48 +03:00
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") {
2019-05-15 01:48:48 +03:00
errorSignature = "Error(string)";
2020-01-08 03:58:04 +03:00
reason = this._abiCoder.decode(["string"], bytes.slice(4))[0];
2019-05-15 01:48:48 +03:00
}
break;
}
return logger.throwError("call revert exception", Logger.errors.CALL_EXCEPTION, {
2019-05-15 01:48:48 +03:00
method: functionFragment.format(),
errorSignature: errorSignature,
errorArgs: [reason],
reason: reason
});
}
2020-01-08 03:58:04 +03:00
// Encode the result for a function call (e.g. for eth_call)
2019-09-28 09:36:19 +03:00
encodeFunctionResult(functionFragment, values) {
if (typeof (functionFragment) === "string") {
functionFragment = this.getFunction(functionFragment);
}
return hexlify(this._abiCoder.encode(functionFragment.outputs, values || []));
}
2020-01-08 03:58:04 +03:00
// Create the filter for the event with search criteria (e.g. for eth_filterLog)
encodeFilterTopics(eventFragment, values) {
2019-05-15 01:48:48 +03:00
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, {
2019-05-15 01:48:48 +03:00
argument: "values",
value: values
});
}
let topics = [];
2019-05-15 01:48:48 +03:00
if (!eventFragment.anonymous) {
topics.push(this.getEventTopic(eventFragment));
}
values.forEach((value, index) => {
let param = eventFragment.inputs[index];
2019-05-15 01:48:48 +03:00
if (!param.indexed) {
if (value != null) {
2019-08-02 09:10:58 +03:00
logger.throwArgumentError("cannot filter non-indexed parameters; must be null", ("contract." + param.name), value);
2019-05-15 01:48:48 +03:00
}
return;
}
if (value == null) {
topics.push(null);
}
else if (param.type === "string") {
topics.push(id(value));
2019-05-15 01:48:48 +03:00
}
else if (param.type === "bytes") {
topics.push(keccak256(hexlify(value)));
2019-05-15 01:48:48 +03:00
}
else if (param.type.indexOf("[") !== -1 || param.type.substring(0, 5) === "tuple") {
2019-08-02 09:10:58 +03:00
logger.throwArgumentError("filtering with tuples or arrays not supported", ("contract." + param.name), value);
2019-05-15 01:48:48 +03:00
}
else {
// Check addresses are valid
if (param.type === "address") {
this._abiCoder.encode(["address"], [value]);
2019-05-15 01:48:48 +03:00
}
topics.push(hexZeroPad(hexlify(value), 32));
2019-05-15 01:48:48 +03:00
}
});
// Trim off trailing nulls
while (topics.length && topics[topics.length - 1] === null) {
topics.pop();
}
return topics;
}
2020-01-08 03:58:04 +03:00
// Decode a filter for the event and the search criteria
decodeEventLog(eventFragment, data, topics) {
2019-05-15 01:48:48 +03:00
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] });
2019-05-24 02:13:44 +03:00
}
2019-05-15 01:48:48 +03:00
topics = topics.slice(1);
}
let indexed = [];
let nonIndexed = [];
let dynamic = [];
eventFragment.inputs.forEach((param, index) => {
2019-05-15 01:48:48 +03:00
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 }));
2019-05-15 01:48:48 +03:00
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) => {
2019-05-15 01:48:48 +03:00
if (param.indexed) {
if (resultIndexed == null) {
2019-06-12 00:57:04 +03:00
result[index] = new Indexed({ _isIndexed: true, hash: null });
2019-05-15 01:48:48 +03:00
}
else if (dynamic[index]) {
2019-06-12 00:57:04 +03:00
result[index] = new Indexed({ _isIndexed: true, hash: resultIndexed[indexedIndex++] });
2019-05-15 01:48:48 +03:00
}
else {
result[index] = resultIndexed[indexedIndex++];
}
}
else {
result[index] = resultNonIndexed[nonIndexedIndex++];
}
2020-01-08 03:58:04 +03:00
if (param.name && result[param.name] == null) {
result[param.name] = result[index];
}
2019-05-15 01:48:48 +03:00
});
return result;
}
2020-01-08 03:58:04 +03:00
// 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());
2019-05-15 01:48:48 +03:00
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"),
2019-05-15 01:48:48 +03:00
});
}
2020-01-08 03:58:04 +03:00
// 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]);
2019-05-15 01:48:48 +03:00
if (!fragment || fragment.anonymous) {
return null;
}
// @TODO: If anonymous, and the only method, and the input count matches, should we parse?
2020-01-08 03:58:04 +03:00
// 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?
2019-05-15 01:48:48 +03:00
return new LogDescription({
eventFragment: fragment,
name: fragment.name,
signature: fragment.format(),
topic: this.getEventTopic(fragment),
2020-01-08 03:58:04 +03:00
args: this.decodeEventLog(fragment, log.data, log.topics)
2019-05-15 01:48:48 +03:00
});
}
2019-06-12 00:57:04 +03:00
/*
static from(value: Array<Fragment | string | JsonAbi> | 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) {
2019-06-12 00:57:04 +03:00
return !!(value && value._isInterface);
}
}