UNPKG

@caict/bop-typescript-sdk

Version:
922 lines (808 loc) 27.7 kB
"use strict"; import { getAddress } from "./address"; import { BigNumber, BigNumberish } from "./bignumber"; import { arrayify, BytesLike, concat, hexDataSlice, hexlify, hexZeroPad, isHexString, } from "./bytes"; import { id } from "./id"; import { keccak256 } from "./keccak256"; import { defineReadOnly, Description, getStatic } from "./properties"; import { AbiCoder, defaultAbiCoder } from "./abi-coder"; import { checkResultErrors, Result } from "./coders/abstract-coder"; import { ConstructorFragment, ErrorFragment, EventFragment, FormatTypes, Fragment, FunctionFragment, JsonFragment, ParamType, } from "./fragments"; import { Logger } from "../logger"; const version = "interface/5.7.0"; const logger = new Logger(version); export { checkResultErrors, Result }; export class LogDescription extends Description<LogDescription> { readonly eventFragment: EventFragment; readonly name: string; readonly signature: string; readonly topic: string; readonly args: Result; } export class TransactionDescription extends Description<TransactionDescription> { readonly functionFragment: FunctionFragment; readonly name: string; readonly args: Result; readonly signature: string; readonly sighash: string; readonly value: BigNumber; } export class ErrorDescription extends Description<ErrorDescription> { readonly errorFragment: ErrorFragment; readonly name: string; readonly args: Result; readonly signature: string; readonly sighash: string; } export class Indexed extends Description<Indexed> { readonly hash: string; readonly _isIndexed: boolean; static isIndexed(value: any): value is Indexed { return !!(value && value._isIndexed); } } const BuiltinErrors: Record< string, { signature: string; inputs: Array<string>; name: string; reason?: boolean } > = { "0x08c379a0": { signature: "Error(string)", name: "Error", inputs: ["string"], reason: true, }, "0x4e487b71": { signature: "Panic(uint256)", name: "Panic", inputs: ["uint256"], }, }; function wrapAccessError(property: string, error: Error): Error { const wrap = new Error( `deferred error during ABI decoding triggered accessing ${property}`, ); (<any>wrap).error = error; return wrap; } /* function checkNames(fragment: Fragment, type: "input" | "output", params: Array<ParamType>): void { params.reduce((accum, param) => { if (param.name) { if (accum[param.name]) { logger.throwArgumentError(`duplicate ${ type } parameter ${ JSON.stringify(param.name) } in ${ fragment.format("full") }`, "fragment", fragment); } accum[param.name] = true; } return accum; }, <{ [ name: string ]: boolean }>{ }); } */ export class Interface { readonly fragments: ReadonlyArray<Fragment>; readonly errors: { [name: string]: ErrorFragment }; readonly events: { [name: string]: EventFragment }; readonly functions: { [name: string]: FunctionFragment }; readonly structs: { [name: string]: any }; readonly deploy: ConstructorFragment; readonly _abiCoder: AbiCoder; readonly _isInterface: boolean; constructor( fragments: string | ReadonlyArray<Fragment | JsonFragment | string>, ) { let abi: ReadonlyArray<Fragment | JsonFragment | string> = []; 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<() => AbiCoder>(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: { [name: string]: Fragment } = null; switch (fragment.type) { case "constructor": if (this.deploy) { logger.warn("duplicate definition - constructor"); return; } //checkNames(fragment, "input", fragment.inputs); defineReadOnly(this, "deploy", <ConstructorFragment>fragment); return; case "function": //checkNames(fragment, "input", fragment.inputs); //checkNames(fragment, "output", (<FunctionFragment>fragment).outputs); bucket = this.functions; break; case "event": //checkNames(fragment, "input", fragment.inputs); bucket = this.events; break; case "error": bucket = this.errors; 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 add a default if (!this.deploy) { defineReadOnly( this, "deploy", ConstructorFragment.from({ payable: false, type: "constructor", }), ); } defineReadOnly(this, "_isInterface", true); } format(format?: string): string | Array<string> { if (!format) { format = FormatTypes.full; } if (format === FormatTypes.sighash) { logger.throwArgumentError( "interface does not support formatting 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(): AbiCoder { return defaultAbiCoder; } static getAddress(address: string): string { return getAddress(address); } static getSighash(fragment: ErrorFragment | FunctionFragment): string { return hexDataSlice(id(fragment.format()), 0, 4); } static getEventTopic(eventFragment: EventFragment): string { return id(eventFragment.format()); } // Find a function definition by any means necessary (unless it is ambiguous) getFunction(nameOrSignatureOrSighash: string): FunctionFragment { 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]]; } // Normalize 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: string): EventFragment { 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]]; } // Normalize 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; } // Find a function definition by any means necessary (unless it is ambiguous) getError(nameOrSignatureOrSighash: string): ErrorFragment { if (isHexString(nameOrSignatureOrSighash)) { const getSighash = getStatic< (f: ErrorFragment | FunctionFragment) => string >(this.constructor, "getSighash"); for (const name in this.errors) { const error = this.errors[name]; if (nameOrSignatureOrSighash === getSighash(error)) { return this.errors[name]; } } logger.throwArgumentError( "no matching error", "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.errors).filter( (f) => f.split("(" /* fix:) */)[0] === name, ); if (matching.length === 0) { logger.throwArgumentError("no matching error", "name", name); } else if (matching.length > 1) { logger.throwArgumentError("multiple matching errors", "name", name); } return this.errors[matching[0]]; } // Normalize the signature and lookup the function const result = this.errors[ FunctionFragment.fromString(nameOrSignatureOrSighash).format() ]; if (!result) { logger.throwArgumentError( "no matching error", "signature", nameOrSignatureOrSighash, ); } return result; } // Get the sighash (the bytes4 selector) used by Solidity to identify a function getSighash(fragment: ErrorFragment | FunctionFragment | string): string { if (typeof fragment === "string") { try { fragment = this.getFunction(fragment); } catch (error) { try { fragment = this.getError(<string>fragment); } catch (_) { throw error; } } } return getStatic<(f: ErrorFragment | FunctionFragment) => string>( this.constructor, "getSighash", )(fragment); } // Get the topic (the bytes32 hash) used by Solidity to identify an event getEventTopic(eventFragment: EventFragment | string): string { if (typeof eventFragment === "string") { eventFragment = this.getEvent(eventFragment); } return getStatic<(e: EventFragment) => string>( this.constructor, "getEventTopic", )(eventFragment); } _decodeParams(params: ReadonlyArray<ParamType>, data: BytesLike): Result { return this._abiCoder.decode(params, data); } _encodeParams( params: ReadonlyArray<ParamType>, values: ReadonlyArray<any>, ): string { logger.debug( `_encodeParams:${JSON.stringify(params)}, values:${JSON.stringify(values)}`, ); return this._abiCoder.encode(params, values); } encodeDeploy(values?: ReadonlyArray<any>): string { return this._encodeParams(this.deploy.inputs, values || []); } decodeErrorResult(fragment: ErrorFragment | string, data: BytesLike): Result { if (typeof fragment === "string") { fragment = this.getError(fragment); } const bytes = arrayify(data); if (hexlify(bytes.slice(0, 4)) !== this.getSighash(fragment)) { logger.throwArgumentError( `data signature does not match error ${fragment.name}.`, "data", hexlify(bytes), ); } return this._decodeParams(fragment.inputs, bytes.slice(4)); } encodeErrorResult( fragment: ErrorFragment | string, values?: ReadonlyArray<any>, ): string { if (typeof fragment === "string") { fragment = this.getError(fragment); } return hexlify( concat([ this.getSighash(fragment), this._encodeParams(fragment.inputs, values || []), ]), ); } // Decode the data for a function call (e.g. tx.data) decodeFunctionData( functionFragment: FunctionFragment | string, data: BytesLike, ): Result { 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: FunctionFragment | string, values?: ReadonlyArray<any>, ): string { 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: FunctionFragment | string, data: BytesLike, ): Result { if (typeof functionFragment === "string") { functionFragment = this.getFunction(functionFragment); } let bytes = arrayify(data); let reason: string = null; let message = ""; let errorArgs: Result = null; let errorName: string = null; let errorSignature: string = null; switch (bytes.length % this._abiCoder._getWordSize()) { case 0: try { return this._abiCoder.decode(functionFragment.outputs, bytes); } catch (error) {} break; case 4: { const selector = hexlify(bytes.slice(0, 4)); const builtin = BuiltinErrors[selector]; if (builtin) { errorArgs = this._abiCoder.decode(builtin.inputs, bytes.slice(4)); errorName = builtin.name; errorSignature = builtin.signature; if (builtin.reason) { reason = errorArgs[0]; } if (errorName === "Error") { message = `; VM Exception while processing transaction: reverted with reason string ${JSON.stringify(errorArgs[0])}`; } else if (errorName === "Panic") { message = `; VM Exception while processing transaction: reverted with panic code ${errorArgs[0]}`; } } else { try { const error = this.getError(selector); errorArgs = this._abiCoder.decode(error.inputs, bytes.slice(4)); errorName = error.name; errorSignature = error.format(); } catch (error) {} } break; } } return logger.throwError( "call revert exception" + message, Logger.errors.CALL_EXCEPTION, { method: functionFragment.format(), data: hexlify(data), errorArgs, errorName, errorSignature, reason, }, ); } // Encode the result for a function call (e.g. for eth_call) encodeFunctionResult( functionFragment: FunctionFragment | string, values?: ReadonlyArray<any>, ): string { 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: EventFragment | string, // values: ReadonlyArray<any>, // ): Array<string | Array<string>> { // 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: Array<string | Array<string>> = []; // if (!eventFragment.anonymous) { // topics.push(this.getEventTopic(eventFragment)); // } // const encodeTopic = (param: ParamType, value: any): string => { // if (param.type === "string") { // return id(value); // } else if (param.type === "bytes") { // return keccak256(hexlify(value)); // } // if (param.type === "bool" && typeof value === "boolean") { // value = value ? "0x01" : "0x00"; // } // if (param.type.match(/^u?int/)) { // value = BigNumber.from(value).toHexString(); // } // // Check addresses are valid // if (param.type === "address") { // value = this._abiCoder.encode(["address"], [value]); // } // logger.debug( // `encodeTopic: param:${JSON.stringify(param)}, value:${JSON.stringify(value)}`, // ); // return hexZeroPad(hexlify(value), 32); // }; // values.forEach((value, index) => { // let param = (<EventFragment>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.baseType === "array" || param.baseType === "tuple") { // logger.throwArgumentError( // "filtering with tuples or arrays not supported", // "contract." + param.name, // value, // ); // } else if (Array.isArray(value)) { // topics.push(value.map((value) => encodeTopic(param, value))); // } else { // topics.push(encodeTopic(param, value)); // } // }); // // Trim off trailing nulls // while (topics.length && topics[topics.length - 1] === null) { // topics.pop(); // } // return topics; // } encodeEventLog( eventFragment: EventFragment | string, values: ReadonlyArray<any>, ): { data: string; topics: Array<string> } { if (typeof eventFragment === "string") { eventFragment = this.getEvent(eventFragment); } const topics: Array<string> = []; const dataTypes: Array<ParamType> = []; const dataValues: Array<string> = []; if (!eventFragment.anonymous) { topics.push(this.getEventTopic(eventFragment)); } if (values.length !== eventFragment.inputs.length) { logger.throwArgumentError( "event arguments/values mismatch", "values", values, ); } eventFragment.inputs.forEach((param, index) => { const value = values[index]; if (param.indexed) { if (param.type === "string") { topics.push(id(value)); } else if (param.type === "bytes") { topics.push(keccak256(value)); } else if (param.baseType === "tuple" || param.baseType === "array") { // @TODO throw new Error("not implemented"); } else { topics.push(this._abiCoder.encode([param.type], [value])); } } else { dataTypes.push(param); dataValues.push(value); } }); return { data: this._abiCoder.encode(dataTypes, dataValues), topics: topics, }; } // Decode a filter for the event and the search criteria decodeEventLog( eventFragment: EventFragment | string, data: BytesLike, topics?: ReadonlyArray<string>, ): Result { 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: Array<ParamType> = []; let nonIndexed: Array<ParamType> = []; let dynamic: Array<boolean> = []; 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, true); let result: Array<any> & { [key: string]: any } = []; 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 { try { result[index] = resultIndexed[indexedIndex++]; } catch (error) { result[index] = error; } } } else { try { result[index] = resultNonIndexed[nonIndexedIndex++]; } catch (error) { result[index] = error; } } // Add the keyword argument if named and safe if (param.name && result[param.name] == null) { const value = result[index]; // Make error named values throw on access if (value instanceof Error) { Object.defineProperty(result, param.name, { enumerable: true, get: () => { throw wrapAccessError( `property ${JSON.stringify(param.name)}`, value, ); }, }); } else { result[param.name] = value; } } }); // Make all error indexed values throw on access for (let i = 0; i < result.length; i++) { const value = result[i]; if (value instanceof Error) { Object.defineProperty(result, i, { enumerable: true, get: () => { throw wrapAccessError(`index ${i}`, value); }, }); } } return Object.freeze(result); } // Given a transaction, find the matching function fragment (if any) and // determine all its properties and call parameters // parseTransaction(tx: { // data: string; // value?: BigNumberish; // }): TransactionDescription { // 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"), // }); // } // @TODO //parseCallResult(data: BytesLike): ?? // Given an event log, find the matching event fragment (if any) and // determine all its properties and values parseLog(log: { topics: Array<string>; data: string }): LogDescription { 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 just 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), }); } parseError(data: BytesLike): ErrorDescription { const hexData = hexlify(data); let fragment = this.getError(hexData.substring(0, 10).toLowerCase()); if (!fragment) { return null; } return new ErrorDescription({ args: this._abiCoder.decode( fragment.inputs, "0x" + hexData.substring(10), ), errorFragment: fragment, name: fragment.name, signature: fragment.format(), sighash: this.getSighash(fragment), }); } /* 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: any): value is Interface { return !!(value && value._isInterface); } }