abstract-coder.ts 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541
  1. import {
  2. defineProperties, concat, getBytesCopy, getNumber, hexlify,
  3. toBeArray, toBigInt, toNumber,
  4. assert, assertArgument
  5. /*, isError*/
  6. } from "../../utils/index.js";
  7. import type { BigNumberish, BytesLike } from "../../utils/index.js";
  8. /**
  9. * @_ignore:
  10. */
  11. export const WordSize: number = 32;
  12. const Padding = new Uint8Array(WordSize);
  13. // Properties used to immediate pass through to the underlying object
  14. // - `then` is used to detect if an object is a Promise for await
  15. const passProperties = [ "then" ];
  16. const _guard = { };
  17. const resultNames: WeakMap<Result, ReadonlyArray<null | string>> = new WeakMap();
  18. function getNames(result: Result): ReadonlyArray<null | string> {
  19. return resultNames.get(result)!;
  20. }
  21. function setNames(result: Result, names: ReadonlyArray<null | string>): void {
  22. resultNames.set(result, names);
  23. }
  24. function throwError(name: string, error: Error): never {
  25. const wrapped = new Error(`deferred error during ABI decoding triggered accessing ${ name }`);
  26. (<any>wrapped).error = error;
  27. throw wrapped;
  28. }
  29. function toObject(names: ReadonlyArray<null | string>, items: Result, deep?: boolean): Record<string, any> | Array<any> {
  30. if (names.indexOf(null) >= 0) {
  31. return items.map((item, index) => {
  32. if (item instanceof Result) {
  33. return toObject(getNames(item), item, deep);
  34. }
  35. return item;
  36. });
  37. }
  38. return (<Array<string>>names).reduce((accum, name, index) => {
  39. let item = items.getValue(name);
  40. if (!(name in accum)) {
  41. if (deep && item instanceof Result) {
  42. item = toObject(getNames(item), item, deep);
  43. }
  44. accum[name] = item;
  45. }
  46. return accum;
  47. }, <Record<string, any>>{ });
  48. }
  49. /**
  50. * A [[Result]] is a sub-class of Array, which allows accessing any
  51. * of its values either positionally by its index or, if keys are
  52. * provided by its name.
  53. *
  54. * @_docloc: api/abi
  55. */
  56. export class Result extends Array<any> {
  57. // No longer used; but cannot be removed as it will remove the
  58. // #private field from the .d.ts which may break backwards
  59. // compatibility
  60. readonly #names: ReadonlyArray<null | string>;
  61. [ K: string | number ]: any
  62. /**
  63. * @private
  64. */
  65. constructor(...args: Array<any>) {
  66. // To properly sub-class Array so the other built-in
  67. // functions work, the constructor has to behave fairly
  68. // well. So, in the event we are created via fromItems()
  69. // we build the read-only Result object we want, but on
  70. // any other input, we use the default constructor
  71. // constructor(guard: any, items: Array<any>, keys?: Array<null | string>);
  72. const guard = args[0];
  73. let items: Array<any> = args[1];
  74. let names: Array<null | string> = (args[2] || [ ]).slice();
  75. let wrap = true;
  76. if (guard !== _guard) {
  77. items = args;
  78. names = [ ];
  79. wrap = false;
  80. }
  81. // Can't just pass in ...items since an array of length 1
  82. // is a special case in the super.
  83. super(items.length);
  84. items.forEach((item, index) => { this[index] = item; });
  85. // Find all unique keys
  86. const nameCounts = names.reduce((accum, name) => {
  87. if (typeof(name) === "string") {
  88. accum.set(name, (accum.get(name) || 0) + 1);
  89. }
  90. return accum;
  91. }, <Map<string, number>>(new Map()));
  92. // Remove any key thats not unique
  93. setNames(this, Object.freeze(items.map((item, index) => {
  94. const name = names[index];
  95. if (name != null && nameCounts.get(name) === 1) {
  96. return name;
  97. }
  98. return null;
  99. })));
  100. // Dummy operations to prevent TypeScript from complaining
  101. this.#names = [ ];
  102. if (this.#names == null) { void(this.#names); }
  103. if (!wrap) { return; }
  104. // A wrapped Result is immutable
  105. Object.freeze(this);
  106. // Proxy indices and names so we can trap deferred errors
  107. const proxy = new Proxy(this, {
  108. get: (target, prop, receiver) => {
  109. if (typeof(prop) === "string") {
  110. // Index accessor
  111. if (prop.match(/^[0-9]+$/)) {
  112. const index = getNumber(prop, "%index");
  113. if (index < 0 || index >= this.length) {
  114. throw new RangeError("out of result range");
  115. }
  116. const item = target[index];
  117. if (item instanceof Error) {
  118. throwError(`index ${ index }`, item);
  119. }
  120. return item;
  121. }
  122. // Pass important checks (like `then` for Promise) through
  123. if (passProperties.indexOf(prop) >= 0) {
  124. return Reflect.get(target, prop, receiver);
  125. }
  126. const value = target[prop];
  127. if (value instanceof Function) {
  128. // Make sure functions work with private variables
  129. // See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy#no_private_property_forwarding
  130. return function(this: any, ...args: Array<any>) {
  131. return value.apply((this === receiver) ? target: this, args);
  132. };
  133. } else if (!(prop in target)) {
  134. // Possible name accessor
  135. return target.getValue.apply((this === receiver) ? target: this, [ prop ]);
  136. }
  137. }
  138. return Reflect.get(target, prop, receiver);
  139. }
  140. });
  141. setNames(proxy, getNames(this));
  142. return proxy;
  143. }
  144. /**
  145. * Returns the Result as a normal Array. If %%deep%%, any children
  146. * which are Result objects are also converted to a normal Array.
  147. *
  148. * This will throw if there are any outstanding deferred
  149. * errors.
  150. */
  151. toArray(deep?: boolean): Array<any> {
  152. const result: Array<any> = [ ];
  153. this.forEach((item, index) => {
  154. if (item instanceof Error) { throwError(`index ${ index }`, item); }
  155. if (deep && item instanceof Result) {
  156. item = item.toArray(deep);
  157. }
  158. result.push(item);
  159. });
  160. return result;
  161. }
  162. /**
  163. * Returns the Result as an Object with each name-value pair. If
  164. * %%deep%%, any children which are Result objects are also
  165. * converted to an Object.
  166. *
  167. * This will throw if any value is unnamed, or if there are
  168. * any outstanding deferred errors.
  169. */
  170. toObject(deep?: boolean): Record<string, any> {
  171. const names = getNames(this);
  172. return names.reduce((accum, name, index) => {
  173. assert(name != null, `value at index ${ index } unnamed`, "UNSUPPORTED_OPERATION", {
  174. operation: "toObject()"
  175. });
  176. return toObject(names, this, deep);
  177. }, <Record<string, any>>{});
  178. }
  179. /**
  180. * @_ignore
  181. */
  182. slice(start?: number | undefined, end?: number | undefined): Result {
  183. if (start == null) { start = 0; }
  184. if (start < 0) {
  185. start += this.length;
  186. if (start < 0) { start = 0; }
  187. }
  188. if (end == null) { end = this.length; }
  189. if (end < 0) {
  190. end += this.length;
  191. if (end < 0) { end = 0; }
  192. }
  193. if (end > this.length) { end = this.length; }
  194. const _names = getNames(this);
  195. const result: Array<any> = [ ], names: Array<null | string> = [ ];
  196. for (let i = start; i < end; i++) {
  197. result.push(this[i]);
  198. names.push(_names[i]);
  199. }
  200. return new Result(_guard, result, names);
  201. }
  202. /**
  203. * @_ignore
  204. */
  205. filter(callback: (el: any, index: number, array: Result) => boolean, thisArg?: any): Result {
  206. const _names = getNames(this);
  207. const result: Array<any> = [ ], names: Array<null | string> = [ ];
  208. for (let i = 0; i < this.length; i++) {
  209. const item = this[i];
  210. if (item instanceof Error) {
  211. throwError(`index ${ i }`, item);
  212. }
  213. if (callback.call(thisArg, item, i, this)) {
  214. result.push(item);
  215. names.push(_names[i]);
  216. }
  217. }
  218. return new Result(_guard, result, names);
  219. }
  220. /**
  221. * @_ignore
  222. */
  223. map<T extends any = any>(callback: (el: any, index: number, array: Result) => T, thisArg?: any): Array<T> {
  224. const result: Array<T> = [ ];
  225. for (let i = 0; i < this.length; i++) {
  226. const item = this[i];
  227. if (item instanceof Error) {
  228. throwError(`index ${ i }`, item);
  229. }
  230. result.push(callback.call(thisArg, item, i, this));
  231. }
  232. return result;
  233. }
  234. /**
  235. * Returns the value for %%name%%.
  236. *
  237. * Since it is possible to have a key whose name conflicts with
  238. * a method on a [[Result]] or its superclass Array, or any
  239. * JavaScript keyword, this ensures all named values are still
  240. * accessible by name.
  241. */
  242. getValue(name: string): any {
  243. const index = getNames(this).indexOf(name);
  244. if (index === -1) { return undefined; }
  245. const value = this[index];
  246. if (value instanceof Error) {
  247. throwError(`property ${ JSON.stringify(name) }`, (<any>value).error);
  248. }
  249. return value;
  250. }
  251. /**
  252. * Creates a new [[Result]] for %%items%% with each entry
  253. * also accessible by its corresponding name in %%keys%%.
  254. */
  255. static fromItems(items: Array<any>, keys?: Array<null | string>): Result {
  256. return new Result(_guard, items, keys);
  257. }
  258. }
  259. /**
  260. * Returns all errors found in a [[Result]].
  261. *
  262. * Since certain errors encountered when creating a [[Result]] do
  263. * not impact the ability to continue parsing data, they are
  264. * deferred until they are actually accessed. Hence a faulty string
  265. * in an Event that is never used does not impact the program flow.
  266. *
  267. * However, sometimes it may be useful to access, identify or
  268. * validate correctness of a [[Result]].
  269. *
  270. * @_docloc api/abi
  271. */
  272. export function checkResultErrors(result: Result): Array<{ path: Array<string | number>, error: Error }> {
  273. // Find the first error (if any)
  274. const errors: Array<{ path: Array<string | number>, error: Error }> = [ ];
  275. const checkErrors = function(path: Array<string | number>, object: any): void {
  276. if (!Array.isArray(object)) { return; }
  277. for (let key in object) {
  278. const childPath = path.slice();
  279. childPath.push(key);
  280. try {
  281. checkErrors(childPath, object[key]);
  282. } catch (error: any) {
  283. errors.push({ path: childPath, error: error });
  284. }
  285. }
  286. }
  287. checkErrors([ ], result);
  288. return errors;
  289. }
  290. function getValue(value: BigNumberish): Uint8Array {
  291. let bytes = toBeArray(value);
  292. assert (bytes.length <= WordSize, "value out-of-bounds",
  293. "BUFFER_OVERRUN", { buffer: bytes, length: WordSize, offset: bytes.length });
  294. if (bytes.length !== WordSize) {
  295. bytes = getBytesCopy(concat([ Padding.slice(bytes.length % WordSize), bytes ]));
  296. }
  297. return bytes;
  298. }
  299. /**
  300. * @_ignore
  301. */
  302. export abstract class Coder {
  303. // The coder name:
  304. // - address, uint256, tuple, array, etc.
  305. readonly name!: string;
  306. // The fully expanded type, including composite types:
  307. // - address, uint256, tuple(address,bytes), uint256[3][4][], etc.
  308. readonly type!: string;
  309. // The localName bound in the signature, in this example it is "baz":
  310. // - tuple(address foo, uint bar) baz
  311. readonly localName!: string;
  312. // Whether this type is dynamic:
  313. // - Dynamic: bytes, string, address[], tuple(boolean[]), etc.
  314. // - Not Dynamic: address, uint256, boolean[3], tuple(address, uint8)
  315. readonly dynamic!: boolean;
  316. constructor(name: string, type: string, localName: string, dynamic: boolean) {
  317. defineProperties<Coder>(this, { name, type, localName, dynamic }, {
  318. name: "string", type: "string", localName: "string", dynamic: "boolean"
  319. });
  320. }
  321. _throwError(message: string, value: any): never {
  322. assertArgument(false, message, this.localName, value);
  323. }
  324. abstract encode(writer: Writer, value: any): number;
  325. abstract decode(reader: Reader): any;
  326. abstract defaultValue(): any;
  327. }
  328. /**
  329. * @_ignore
  330. */
  331. export class Writer {
  332. // An array of WordSize lengthed objects to concatenation
  333. #data: Array<Uint8Array>;
  334. #dataLength: number;
  335. constructor() {
  336. this.#data = [ ];
  337. this.#dataLength = 0;
  338. }
  339. get data(): string {
  340. return concat(this.#data);
  341. }
  342. get length(): number { return this.#dataLength; }
  343. #writeData(data: Uint8Array): number {
  344. this.#data.push(data);
  345. this.#dataLength += data.length;
  346. return data.length;
  347. }
  348. appendWriter(writer: Writer): number {
  349. return this.#writeData(getBytesCopy(writer.data));
  350. }
  351. // Arrayish item; pad on the right to *nearest* WordSize
  352. writeBytes(value: BytesLike): number {
  353. let bytes = getBytesCopy(value);
  354. const paddingOffset = bytes.length % WordSize;
  355. if (paddingOffset) {
  356. bytes = getBytesCopy(concat([ bytes, Padding.slice(paddingOffset) ]))
  357. }
  358. return this.#writeData(bytes);
  359. }
  360. // Numeric item; pad on the left *to* WordSize
  361. writeValue(value: BigNumberish): number {
  362. return this.#writeData(getValue(value));
  363. }
  364. // Inserts a numeric place-holder, returning a callback that can
  365. // be used to asjust the value later
  366. writeUpdatableValue(): (value: BigNumberish) => void {
  367. const offset = this.#data.length;
  368. this.#data.push(Padding);
  369. this.#dataLength += WordSize;
  370. return (value: BigNumberish) => {
  371. this.#data[offset] = getValue(value);
  372. };
  373. }
  374. }
  375. /**
  376. * @_ignore
  377. */
  378. export class Reader {
  379. // Allows incomplete unpadded data to be read; otherwise an error
  380. // is raised if attempting to overrun the buffer. This is required
  381. // to deal with an old Solidity bug, in which event data for
  382. // external (not public thoguh) was tightly packed.
  383. readonly allowLoose!: boolean;
  384. readonly #data: Uint8Array;
  385. #offset: number;
  386. #bytesRead: number;
  387. #parent: null | Reader;
  388. #maxInflation: number;
  389. constructor(data: BytesLike, allowLoose?: boolean, maxInflation?: number) {
  390. defineProperties<Reader>(this, { allowLoose: !!allowLoose });
  391. this.#data = getBytesCopy(data);
  392. this.#bytesRead = 0;
  393. this.#parent = null;
  394. this.#maxInflation = (maxInflation != null) ? maxInflation: 1024;
  395. this.#offset = 0;
  396. }
  397. get data(): string { return hexlify(this.#data); }
  398. get dataLength(): number { return this.#data.length; }
  399. get consumed(): number { return this.#offset; }
  400. get bytes(): Uint8Array { return new Uint8Array(this.#data); }
  401. #incrementBytesRead(count: number): void {
  402. if (this.#parent) { return this.#parent.#incrementBytesRead(count); }
  403. this.#bytesRead += count;
  404. // Check for excessive inflation (see: #4537)
  405. 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", {
  406. buffer: getBytesCopy(this.#data), offset: this.#offset,
  407. length: count, info: {
  408. bytesRead: this.#bytesRead,
  409. dataLength: this.dataLength
  410. }
  411. });
  412. }
  413. #peekBytes(offset: number, length: number, loose?: boolean): Uint8Array {
  414. let alignedLength = Math.ceil(length / WordSize) * WordSize;
  415. if (this.#offset + alignedLength > this.#data.length) {
  416. if (this.allowLoose && loose && this.#offset + length <= this.#data.length) {
  417. alignedLength = length;
  418. } else {
  419. assert(false, "data out-of-bounds", "BUFFER_OVERRUN", {
  420. buffer: getBytesCopy(this.#data),
  421. length: this.#data.length,
  422. offset: this.#offset + alignedLength
  423. });
  424. }
  425. }
  426. return this.#data.slice(this.#offset, this.#offset + alignedLength)
  427. }
  428. // Create a sub-reader with the same underlying data, but offset
  429. subReader(offset: number): Reader {
  430. const reader = new Reader(this.#data.slice(this.#offset + offset), this.allowLoose, this.#maxInflation);
  431. reader.#parent = this;
  432. return reader;
  433. }
  434. // Read bytes
  435. readBytes(length: number, loose?: boolean): Uint8Array {
  436. let bytes = this.#peekBytes(0, length, !!loose);
  437. this.#incrementBytesRead(length);
  438. this.#offset += bytes.length;
  439. // @TODO: Make sure the length..end bytes are all 0?
  440. return bytes.slice(0, length);
  441. }
  442. // Read a numeric values
  443. readValue(): bigint {
  444. return toBigInt(this.readBytes(WordSize));
  445. }
  446. readIndex(): number {
  447. return toNumber(this.readBytes(WordSize));
  448. }
  449. }