123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541 |
- import {
- defineProperties, concat, getBytesCopy, getNumber, hexlify,
- toBeArray, toBigInt, toNumber,
- assert, assertArgument
- /*, isError*/
- } from "../../utils/index.js";
- import type { BigNumberish, BytesLike } from "../../utils/index.js";
- /**
- * @_ignore:
- */
- export const WordSize: number = 32;
- const Padding = new Uint8Array(WordSize);
- // Properties used to immediate pass through to the underlying object
- // - `then` is used to detect if an object is a Promise for await
- const passProperties = [ "then" ];
- const _guard = { };
- const resultNames: WeakMap<Result, ReadonlyArray<null | string>> = new WeakMap();
- function getNames(result: Result): ReadonlyArray<null | string> {
- return resultNames.get(result)!;
- }
- function setNames(result: Result, names: ReadonlyArray<null | string>): void {
- resultNames.set(result, names);
- }
- function throwError(name: string, error: Error): never {
- const wrapped = new Error(`deferred error during ABI decoding triggered accessing ${ name }`);
- (<any>wrapped).error = error;
- throw wrapped;
- }
- function toObject(names: ReadonlyArray<null | string>, items: Result, deep?: boolean): Record<string, any> | Array<any> {
- if (names.indexOf(null) >= 0) {
- return items.map((item, index) => {
- if (item instanceof Result) {
- return toObject(getNames(item), item, deep);
- }
- return item;
- });
- }
- return (<Array<string>>names).reduce((accum, name, index) => {
- let item = items.getValue(name);
- if (!(name in accum)) {
- if (deep && item instanceof Result) {
- item = toObject(getNames(item), item, deep);
- }
- accum[name] = item;
- }
- return accum;
- }, <Record<string, any>>{ });
- }
- /**
- * A [[Result]] is a sub-class of Array, which allows accessing any
- * of its values either positionally by its index or, if keys are
- * provided by its name.
- *
- * @_docloc: api/abi
- */
- export class Result extends Array<any> {
- // No longer used; but cannot be removed as it will remove the
- // #private field from the .d.ts which may break backwards
- // compatibility
- readonly #names: ReadonlyArray<null | string>;
- [ K: string | number ]: any
- /**
- * @private
- */
- constructor(...args: Array<any>) {
- // To properly sub-class Array so the other built-in
- // functions work, the constructor has to behave fairly
- // well. So, in the event we are created via fromItems()
- // we build the read-only Result object we want, but on
- // any other input, we use the default constructor
- // constructor(guard: any, items: Array<any>, keys?: Array<null | string>);
- const guard = args[0];
- let items: Array<any> = args[1];
- let names: Array<null | string> = (args[2] || [ ]).slice();
- let wrap = true;
- if (guard !== _guard) {
- items = args;
- names = [ ];
- wrap = false;
- }
- // Can't just pass in ...items since an array of length 1
- // is a special case in the super.
- super(items.length);
- items.forEach((item, index) => { this[index] = item; });
- // Find all unique keys
- const nameCounts = names.reduce((accum, name) => {
- if (typeof(name) === "string") {
- accum.set(name, (accum.get(name) || 0) + 1);
- }
- return accum;
- }, <Map<string, number>>(new Map()));
- // Remove any key thats not unique
- setNames(this, Object.freeze(items.map((item, index) => {
- const name = names[index];
- if (name != null && nameCounts.get(name) === 1) {
- return name;
- }
- return null;
- })));
- // Dummy operations to prevent TypeScript from complaining
- this.#names = [ ];
- if (this.#names == null) { void(this.#names); }
- if (!wrap) { return; }
- // A wrapped Result is immutable
- Object.freeze(this);
- // Proxy indices and names so we can trap deferred errors
- const proxy = new Proxy(this, {
- get: (target, prop, receiver) => {
- if (typeof(prop) === "string") {
- // Index accessor
- if (prop.match(/^[0-9]+$/)) {
- const index = getNumber(prop, "%index");
- if (index < 0 || index >= this.length) {
- throw new RangeError("out of result range");
- }
- const item = target[index];
- if (item instanceof Error) {
- throwError(`index ${ index }`, item);
- }
- return item;
- }
- // Pass important checks (like `then` for Promise) through
- if (passProperties.indexOf(prop) >= 0) {
- return Reflect.get(target, prop, receiver);
- }
- const value = target[prop];
- if (value instanceof Function) {
- // Make sure functions work with private variables
- // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#no_private_property_forwarding
- return function(this: any, ...args: Array<any>) {
- return value.apply((this === receiver) ? target: this, args);
- };
- } else if (!(prop in target)) {
- // Possible name accessor
- return target.getValue.apply((this === receiver) ? target: this, [ prop ]);
- }
- }
- return Reflect.get(target, prop, receiver);
- }
- });
- setNames(proxy, getNames(this));
- return proxy;
- }
- /**
- * Returns the Result as a normal Array. If %%deep%%, any children
- * which are Result objects are also converted to a normal Array.
- *
- * This will throw if there are any outstanding deferred
- * errors.
- */
- toArray(deep?: boolean): Array<any> {
- const result: Array<any> = [ ];
- this.forEach((item, index) => {
- if (item instanceof Error) { throwError(`index ${ index }`, item); }
- if (deep && item instanceof Result) {
- item = item.toArray(deep);
- }
- result.push(item);
- });
- return result;
- }
- /**
- * Returns the Result as an Object with each name-value pair. If
- * %%deep%%, any children which are Result objects are also
- * converted to an Object.
- *
- * This will throw if any value is unnamed, or if there are
- * any outstanding deferred errors.
- */
- toObject(deep?: boolean): Record<string, any> {
- const names = getNames(this);
- return names.reduce((accum, name, index) => {
- assert(name != null, `value at index ${ index } unnamed`, "UNSUPPORTED_OPERATION", {
- operation: "toObject()"
- });
- return toObject(names, this, deep);
- }, <Record<string, any>>{});
- }
- /**
- * @_ignore
- */
- slice(start?: number | undefined, end?: number | undefined): Result {
- if (start == null) { start = 0; }
- if (start < 0) {
- start += this.length;
- if (start < 0) { start = 0; }
- }
- if (end == null) { end = this.length; }
- if (end < 0) {
- end += this.length;
- if (end < 0) { end = 0; }
- }
- if (end > this.length) { end = this.length; }
- const _names = getNames(this);
- const result: Array<any> = [ ], names: Array<null | string> = [ ];
- for (let i = start; i < end; i++) {
- result.push(this[i]);
- names.push(_names[i]);
- }
- return new Result(_guard, result, names);
- }
- /**
- * @_ignore
- */
- filter(callback: (el: any, index: number, array: Result) => boolean, thisArg?: any): Result {
- const _names = getNames(this);
- const result: Array<any> = [ ], names: Array<null | string> = [ ];
- for (let i = 0; i < this.length; i++) {
- const item = this[i];
- if (item instanceof Error) {
- throwError(`index ${ i }`, item);
- }
- if (callback.call(thisArg, item, i, this)) {
- result.push(item);
- names.push(_names[i]);
- }
- }
- return new Result(_guard, result, names);
- }
- /**
- * @_ignore
- */
- map<T extends any = any>(callback: (el: any, index: number, array: Result) => T, thisArg?: any): Array<T> {
- const result: Array<T> = [ ];
- for (let i = 0; i < this.length; i++) {
- const item = this[i];
- if (item instanceof Error) {
- throwError(`index ${ i }`, item);
- }
- result.push(callback.call(thisArg, item, i, this));
- }
- return result;
- }
- /**
- * Returns the value for %%name%%.
- *
- * Since it is possible to have a key whose name conflicts with
- * a method on a [[Result]] or its superclass Array, or any
- * JavaScript keyword, this ensures all named values are still
- * accessible by name.
- */
- getValue(name: string): any {
- const index = getNames(this).indexOf(name);
- if (index === -1) { return undefined; }
- const value = this[index];
- if (value instanceof Error) {
- throwError(`property ${ JSON.stringify(name) }`, (<any>value).error);
- }
- return value;
- }
- /**
- * Creates a new [[Result]] for %%items%% with each entry
- * also accessible by its corresponding name in %%keys%%.
- */
- static fromItems(items: Array<any>, keys?: Array<null | string>): Result {
- return new Result(_guard, items, keys);
- }
- }
- /**
- * Returns all errors found in a [[Result]].
- *
- * Since certain errors encountered when creating a [[Result]] do
- * not impact the ability to continue parsing data, they are
- * deferred until they are actually accessed. Hence a faulty string
- * in an Event that is never used does not impact the program flow.
- *
- * However, sometimes it may be useful to access, identify or
- * validate correctness of a [[Result]].
- *
- * @_docloc api/abi
- */
- export function checkResultErrors(result: Result): Array<{ path: Array<string | number>, error: Error }> {
- // Find the first error (if any)
- const errors: Array<{ path: Array<string | number>, error: Error }> = [ ];
- const checkErrors = function(path: Array<string | number>, object: any): void {
- if (!Array.isArray(object)) { return; }
- for (let key in object) {
- const childPath = path.slice();
- childPath.push(key);
- try {
- checkErrors(childPath, object[key]);
- } catch (error: any) {
- errors.push({ path: childPath, error: error });
- }
- }
- }
- checkErrors([ ], result);
- return errors;
- }
- function getValue(value: BigNumberish): Uint8Array {
- let bytes = toBeArray(value);
- assert (bytes.length <= WordSize, "value out-of-bounds",
- "BUFFER_OVERRUN", { buffer: bytes, length: WordSize, offset: bytes.length });
- if (bytes.length !== WordSize) {
- bytes = getBytesCopy(concat([ Padding.slice(bytes.length % WordSize), bytes ]));
- }
- return bytes;
- }
- /**
- * @_ignore
- */
- export abstract class Coder {
- // The coder name:
- // - address, uint256, tuple, array, etc.
- readonly name!: string;
- // The fully expanded type, including composite types:
- // - address, uint256, tuple(address,bytes), uint256[3][4][], etc.
- readonly type!: string;
- // The localName bound in the signature, in this example it is "baz":
- // - tuple(address foo, uint bar) baz
- readonly localName!: string;
- // Whether this type is dynamic:
- // - Dynamic: bytes, string, address[], tuple(boolean[]), etc.
- // - Not Dynamic: address, uint256, boolean[3], tuple(address, uint8)
- readonly dynamic!: boolean;
- constructor(name: string, type: string, localName: string, dynamic: boolean) {
- defineProperties<Coder>(this, { name, type, localName, dynamic }, {
- name: "string", type: "string", localName: "string", dynamic: "boolean"
- });
- }
- _throwError(message: string, value: any): never {
- assertArgument(false, message, this.localName, value);
- }
- abstract encode(writer: Writer, value: any): number;
- abstract decode(reader: Reader): any;
- abstract defaultValue(): any;
- }
- /**
- * @_ignore
- */
- export class Writer {
- // An array of WordSize lengthed objects to concatenation
- #data: Array<Uint8Array>;
- #dataLength: number;
- constructor() {
- this.#data = [ ];
- this.#dataLength = 0;
- }
- get data(): string {
- return concat(this.#data);
- }
- get length(): number { return this.#dataLength; }
- #writeData(data: Uint8Array): number {
- this.#data.push(data);
- this.#dataLength += data.length;
- return data.length;
- }
- appendWriter(writer: Writer): number {
- return this.#writeData(getBytesCopy(writer.data));
- }
- // Arrayish item; pad on the right to *nearest* WordSize
- writeBytes(value: BytesLike): number {
- let bytes = getBytesCopy(value);
- const paddingOffset = bytes.length % WordSize;
- if (paddingOffset) {
- bytes = getBytesCopy(concat([ bytes, Padding.slice(paddingOffset) ]))
- }
- return this.#writeData(bytes);
- }
- // Numeric item; pad on the left *to* WordSize
- writeValue(value: BigNumberish): number {
- return this.#writeData(getValue(value));
- }
- // Inserts a numeric place-holder, returning a callback that can
- // be used to asjust the value later
- writeUpdatableValue(): (value: BigNumberish) => void {
- const offset = this.#data.length;
- this.#data.push(Padding);
- this.#dataLength += WordSize;
- return (value: BigNumberish) => {
- this.#data[offset] = getValue(value);
- };
- }
- }
- /**
- * @_ignore
- */
- export class Reader {
- // Allows incomplete unpadded data to be read; otherwise an error
- // is raised if attempting to overrun the buffer. This is required
- // to deal with an old Solidity bug, in which event data for
- // external (not public thoguh) was tightly packed.
- readonly allowLoose!: boolean;
- readonly #data: Uint8Array;
- #offset: number;
- #bytesRead: number;
- #parent: null | Reader;
- #maxInflation: number;
- constructor(data: BytesLike, allowLoose?: boolean, maxInflation?: number) {
- defineProperties<Reader>(this, { allowLoose: !!allowLoose });
- this.#data = getBytesCopy(data);
- this.#bytesRead = 0;
- this.#parent = null;
- this.#maxInflation = (maxInflation != null) ? maxInflation: 1024;
- this.#offset = 0;
- }
- get data(): string { return hexlify(this.#data); }
- get dataLength(): number { return this.#data.length; }
- get consumed(): number { return this.#offset; }
- get bytes(): Uint8Array { return new Uint8Array(this.#data); }
- #incrementBytesRead(count: number): void {
- if (this.#parent) { return this.#parent.#incrementBytesRead(count); }
- this.#bytesRead += count;
- // Check for excessive inflation (see: #4537)
- assert(this.#maxInflation < 1 || this.#bytesRead <= this.#maxInflation * this.dataLength, `compressed ABI data exceeds inflation ratio of ${ this.#maxInflation } ( see: https:/\/github.com/ethers-io/ethers.js/issues/4537 )`, "BUFFER_OVERRUN", {
- buffer: getBytesCopy(this.#data), offset: this.#offset,
- length: count, info: {
- bytesRead: this.#bytesRead,
- dataLength: this.dataLength
- }
- });
- }
- #peekBytes(offset: number, length: number, loose?: boolean): Uint8Array {
- let alignedLength = Math.ceil(length / WordSize) * WordSize;
- if (this.#offset + alignedLength > this.#data.length) {
- if (this.allowLoose && loose && this.#offset + length <= this.#data.length) {
- alignedLength = length;
- } else {
- assert(false, "data out-of-bounds", "BUFFER_OVERRUN", {
- buffer: getBytesCopy(this.#data),
- length: this.#data.length,
- offset: this.#offset + alignedLength
- });
- }
- }
- return this.#data.slice(this.#offset, this.#offset + alignedLength)
- }
- // Create a sub-reader with the same underlying data, but offset
- subReader(offset: number): Reader {
- const reader = new Reader(this.#data.slice(this.#offset + offset), this.allowLoose, this.#maxInflation);
- reader.#parent = this;
- return reader;
- }
- // Read bytes
- readBytes(length: number, loose?: boolean): Uint8Array {
- let bytes = this.#peekBytes(0, length, !!loose);
- this.#incrementBytesRead(length);
- this.#offset += bytes.length;
- // @TODO: Make sure the length..end bytes are all 0?
- return bytes.slice(0, length);
- }
- // Read a numeric values
- readValue(): bigint {
- return toBigInt(this.readBytes(WordSize));
- }
- readIndex(): number {
- return toNumber(this.readBytes(WordSize));
- }
- }
|