UNPKG

17.8 kBJavaScriptView Raw
1"use strict";
2Object.defineProperty(exports, "__esModule", { value: true });
3exports.RawRevertError = exports.AnyRevertError = exports.StringRevertError = exports.getThrownErrorRevertErrorBytes = exports.RevertError = exports.coerceThrownErrorAsRevertError = exports.decodeThrownErrorAsRevertError = exports.decodeBytesAsRevertError = exports.registerRevertErrorType = void 0;
4const ethUtil = require("ethereumjs-util");
5const _ = require("lodash");
6const util_1 = require("util");
7const AbiEncoder = require("./abi_encoder");
8const configured_bignumber_1 = require("./configured_bignumber");
9/**
10 * Register a RevertError type so that it can be decoded by
11 * `decodeRevertError`.
12 * @param revertClass A class that inherits from RevertError.
13 * @param force Allow overwriting registered types.
14 */
15function registerRevertErrorType(revertClass, force = false) {
16 RevertError.registerType(revertClass, force);
17}
18exports.registerRevertErrorType = registerRevertErrorType;
19/**
20 * Decode an ABI encoded revert error.
21 * Throws if the data cannot be decoded as a known RevertError type.
22 * @param bytes The ABI encoded revert error. Either a hex string or a Buffer.
23 * @param coerce Coerce unknown selectors into a `RawRevertError` type.
24 * @return A RevertError object.
25 */
26function decodeBytesAsRevertError(bytes, coerce = false) {
27 return RevertError.decode(bytes, coerce);
28}
29exports.decodeBytesAsRevertError = decodeBytesAsRevertError;
30/**
31 * Decode a thrown error.
32 * Throws if the data cannot be decoded as a known RevertError type.
33 * @param error Any thrown error.
34 * @param coerce Coerce unknown selectors into a `RawRevertError` type.
35 * @return A RevertError object.
36 */
37function decodeThrownErrorAsRevertError(error, coerce = false) {
38 if (error instanceof RevertError) {
39 return error;
40 }
41 return RevertError.decode(getThrownErrorRevertErrorBytes(error), coerce);
42}
43exports.decodeThrownErrorAsRevertError = decodeThrownErrorAsRevertError;
44/**
45 * Coerce a thrown error into a `RevertError`. Always succeeds.
46 * @param error Any thrown error.
47 * @return A RevertError object.
48 */
49function coerceThrownErrorAsRevertError(error) {
50 if (error instanceof RevertError) {
51 return error;
52 }
53 try {
54 return decodeThrownErrorAsRevertError(error, true);
55 }
56 catch (err) {
57 if (isGanacheTransactionRevertError(error)) {
58 throw err;
59 }
60 // Handle geth transaction reverts.
61 if (isGethTransactionRevertError(error)) {
62 // Geth transaction reverts are opaque, meaning no useful data is returned,
63 // so we just return an AnyRevertError type.
64 return new AnyRevertError();
65 }
66 // Coerce plain errors into a StringRevertError.
67 return new StringRevertError(error.message);
68 }
69}
70exports.coerceThrownErrorAsRevertError = coerceThrownErrorAsRevertError;
71/**
72 * Base type for revert errors.
73 */
74class RevertError extends Error {
75 /**
76 * Create a RevertError instance with optional parameter values.
77 * Parameters that are left undefined will not be tested in equality checks.
78 * @param declaration Function-style declaration of the revert (e.g., Error(string message))
79 * @param values Optional mapping of parameters to values.
80 * @param raw Optional encoded form of the revert error. If supplied, this
81 * instance will be treated as a `RawRevertError`, meaning it can only
82 * match other `RawRevertError` types with the same encoded payload.
83 */
84 constructor(name, declaration, values, raw) {
85 super(createErrorMessage(name, values));
86 this.values = {};
87 if (declaration !== undefined) {
88 this.abi = declarationToAbi(declaration);
89 if (values !== undefined) {
90 _.assign(this.values, _.cloneDeep(values));
91 }
92 }
93 this._raw = raw;
94 // Extending Error is tricky; we need to explicitly set the prototype.
95 Object.setPrototypeOf(this, new.target.prototype);
96 }
97 /**
98 * Decode an ABI encoded revert error.
99 * Throws if the data cannot be decoded as a known RevertError type.
100 * @param bytes The ABI encoded revert error. Either a hex string or a Buffer.
101 * @param coerce Whether to coerce unknown selectors into a `RawRevertError` type.
102 * @return A RevertError object.
103 */
104 static decode(bytes, coerce = false) {
105 if (bytes instanceof RevertError) {
106 return bytes;
107 }
108 const _bytes = bytes instanceof Buffer ? ethUtil.bufferToHex(bytes) : ethUtil.addHexPrefix(bytes);
109 // tslint:disable-next-line: custom-no-magic-numbers
110 const selector = _bytes.slice(2, 10);
111 if (!(selector in RevertError._typeRegistry)) {
112 if (coerce) {
113 return new RawRevertError(bytes);
114 }
115 throw new Error(`Unknown selector: ${selector}`);
116 }
117 const { type, decoder } = RevertError._typeRegistry[selector];
118 const instance = new type();
119 try {
120 Object.assign(instance, { values: decoder(_bytes) });
121 instance.message = instance.toString();
122 return instance;
123 }
124 catch (err) {
125 throw new Error(`Bytes ${_bytes} cannot be decoded as a revert error of type ${instance.signature}: ${err.message}`);
126 }
127 }
128 /**
129 * Register a RevertError type so that it can be decoded by
130 * `RevertError.decode`.
131 * @param revertClass A class that inherits from RevertError.
132 * @param force Allow overwriting existing registrations.
133 */
134 static registerType(revertClass, force = false) {
135 const instance = new revertClass();
136 if (!force && instance.selector in RevertError._typeRegistry) {
137 throw new Error(`RevertError type with signature "${instance.signature}" is already registered`);
138 }
139 if (_.isNil(instance.abi)) {
140 throw new Error(`Attempting to register a RevertError class with no ABI`);
141 }
142 RevertError._typeRegistry[instance.selector] = {
143 type: revertClass,
144 decoder: createDecoder(instance.abi),
145 };
146 }
147 /**
148 * Get the ABI name for this revert.
149 */
150 get name() {
151 if (!_.isNil(this.abi)) {
152 return this.abi.name;
153 }
154 return `<${this.typeName}>`;
155 }
156 /**
157 * Get the class name of this type.
158 */
159 get typeName() {
160 // tslint:disable-next-line: no-string-literal
161 return this.constructor.name;
162 }
163 /**
164 * Get the hex selector for this revert (without leading '0x').
165 */
166 get selector() {
167 if (!_.isNil(this.abi)) {
168 return toSelector(this.abi);
169 }
170 if (this._isRawType) {
171 // tslint:disable-next-line: custom-no-magic-numbers
172 return this._raw.slice(2, 10);
173 }
174 return '';
175 }
176 /**
177 * Get the signature for this revert: e.g., 'Error(string)'.
178 */
179 get signature() {
180 if (!_.isNil(this.abi)) {
181 return toSignature(this.abi);
182 }
183 return '';
184 }
185 /**
186 * Get the ABI arguments for this revert.
187 */
188 get arguments() {
189 if (!_.isNil(this.abi)) {
190 return this.abi.arguments || [];
191 }
192 return [];
193 }
194 get [Symbol.toStringTag]() {
195 return this.toString();
196 }
197 /**
198 * Compares this instance with another.
199 * Fails if instances are not of the same type.
200 * Only fields/values defined in both instances are compared.
201 * @param other Either another RevertError instance, hex-encoded bytes, or a Buffer of the ABI encoded revert.
202 * @return True if both instances match.
203 */
204 equals(other) {
205 let _other = other;
206 if (_other instanceof Buffer) {
207 _other = ethUtil.bufferToHex(_other);
208 }
209 if (typeof _other === 'string') {
210 _other = RevertError.decode(_other);
211 }
212 if (!(_other instanceof RevertError)) {
213 return false;
214 }
215 // If either is of the `AnyRevertError` type, always succeed.
216 if (this._isAnyType || _other._isAnyType) {
217 return true;
218 }
219 // If either are raw types, they must match their raw data.
220 if (this._isRawType || _other._isRawType) {
221 return this._raw === _other._raw;
222 }
223 // Must be of same type.
224 if (this.constructor !== _other.constructor) {
225 return false;
226 }
227 // Must share the same parameter values if defined in both instances.
228 for (const name of Object.keys(this.values)) {
229 const a = this.values[name];
230 const b = _other.values[name];
231 if (a === b) {
232 continue;
233 }
234 if (!_.isNil(a) && !_.isNil(b)) {
235 const { type } = this._getArgumentByName(name);
236 if (!checkArgEquality(type, a, b)) {
237 return false;
238 }
239 }
240 }
241 return true;
242 }
243 encode() {
244 if (this._raw !== undefined) {
245 return this._raw;
246 }
247 if (!this._hasAllArgumentValues) {
248 throw new Error(`Instance of ${this.typeName} does not have all its parameter values set.`);
249 }
250 const encoder = createEncoder(this.abi);
251 return encoder(this.values);
252 }
253 toString() {
254 if (this._isRawType) {
255 return `${this.constructor.name}(${this._raw})`;
256 }
257 const values = _.omitBy(this.values, (v) => _.isNil(v));
258 // tslint:disable-next-line: forin
259 for (const k in values) {
260 const { type: argType } = this._getArgumentByName(k);
261 if (argType === 'bytes') {
262 // Try to decode nested revert errors.
263 try {
264 values[k] = RevertError.decode(values[k]);
265 }
266 catch (err) { } // tslint:disable-line:no-empty
267 }
268 }
269 const inner = _.isEmpty(values) ? '' : util_1.inspect(values);
270 return `${this.constructor.name}(${inner})`;
271 }
272 _getArgumentByName(name) {
273 const arg = _.find(this.arguments, (a) => a.name === name);
274 if (_.isNil(arg)) {
275 throw new Error(`RevertError ${this.signature} has no argument named ${name}`);
276 }
277 return arg;
278 }
279 get _isAnyType() {
280 return _.isNil(this.abi) && _.isNil(this._raw);
281 }
282 get _isRawType() {
283 return !_.isNil(this._raw);
284 }
285 get _hasAllArgumentValues() {
286 if (_.isNil(this.abi) || _.isNil(this.abi.arguments)) {
287 return false;
288 }
289 for (const arg of this.abi.arguments) {
290 if (_.isNil(this.values[arg.name])) {
291 return false;
292 }
293 }
294 return true;
295 }
296}
297exports.RevertError = RevertError;
298// Map of types registered via `registerType`.
299RevertError._typeRegistry = {};
300const PARITY_TRANSACTION_REVERT_ERROR_MESSAGE = /^VM execution error/;
301const GANACHE_TRANSACTION_REVERT_ERROR_MESSAGE = /^VM Exception while processing transaction: revert/;
302const GETH_TRANSACTION_REVERT_ERROR_MESSAGE = /always failing transaction$/;
303/**
304 * Try to extract the ecnoded revert error bytes from a thrown `Error`.
305 */
306function getThrownErrorRevertErrorBytes(error) {
307 // Handle ganache transaction reverts.
308 if (isGanacheTransactionRevertError(error)) {
309 return error.data;
310 }
311 else if (isParityTransactionRevertError(error)) {
312 // Parity returns { data: 'Reverted 0xa6bcde47...', ... }
313 const { data } = error;
314 const hexDataIndex = data.indexOf('0x');
315 if (hexDataIndex !== -1) {
316 return data.slice(hexDataIndex);
317 }
318 }
319 else {
320 // Handle geth transaction reverts.
321 if (isGethTransactionRevertError(error)) {
322 // Geth transaction reverts are opaque, meaning no useful data is returned,
323 // so we do nothing.
324 }
325 }
326 throw new Error(`Cannot decode thrown Error "${error.message}" as a RevertError`);
327}
328exports.getThrownErrorRevertErrorBytes = getThrownErrorRevertErrorBytes;
329function isParityTransactionRevertError(error) {
330 if (PARITY_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message) && 'code' in error && 'data' in error) {
331 return true;
332 }
333 return false;
334}
335function isGanacheTransactionRevertError(error) {
336 if (GANACHE_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message) && 'data' in error) {
337 return true;
338 }
339 return false;
340}
341function isGethTransactionRevertError(error) {
342 return GETH_TRANSACTION_REVERT_ERROR_MESSAGE.test(error.message);
343}
344/**
345 * RevertError type for standard string reverts.
346 */
347class StringRevertError extends RevertError {
348 constructor(message) {
349 super('StringRevertError', 'Error(string message)', { message });
350 }
351}
352exports.StringRevertError = StringRevertError;
353/**
354 * Special RevertError type that matches with any other RevertError instance.
355 */
356class AnyRevertError extends RevertError {
357 constructor() {
358 super('AnyRevertError');
359 }
360}
361exports.AnyRevertError = AnyRevertError;
362/**
363 * Special RevertError type that is not decoded.
364 */
365class RawRevertError extends RevertError {
366 constructor(encoded) {
367 super('RawRevertError', undefined, undefined, typeof encoded === 'string' ? encoded : ethUtil.bufferToHex(encoded));
368 }
369}
370exports.RawRevertError = RawRevertError;
371/**
372 * Create an error message for a RevertError.
373 * @param name The name of the RevertError.
374 * @param values The values for the RevertError.
375 */
376function createErrorMessage(name, values) {
377 if (values === undefined) {
378 return `${name}()`;
379 }
380 const _values = _.omitBy(values, (v) => _.isNil(v));
381 const inner = _.isEmpty(_values) ? '' : util_1.inspect(_values);
382 return `${name}(${inner})`;
383}
384/**
385 * Parse a solidity function declaration into a RevertErrorAbi object.
386 * @param declaration Function declaration (e.g., 'foo(uint256 bar)').
387 * @return A RevertErrorAbi object.
388 */
389function declarationToAbi(declaration) {
390 let m = /^\s*([_a-z][a-z0-9_]*)\((.*)\)\s*$/i.exec(declaration);
391 if (!m) {
392 throw new Error(`Invalid Revert Error signature: "${declaration}"`);
393 }
394 const [name, args] = m.slice(1);
395 const argList = _.filter(args.split(','));
396 const argData = _.map(argList, (a) => {
397 // Match a function parameter in the format 'TYPE ID', where 'TYPE' may be
398 // an array type.
399 m = /^\s*(([_a-z][a-z0-9_]*)(\[\d*\])*)\s+([_a-z][a-z0-9_]*)\s*$/i.exec(a);
400 if (!m) {
401 throw new Error(`Invalid Revert Error signature: "${declaration}"`);
402 }
403 // tslint:disable: custom-no-magic-numbers
404 return {
405 name: m[4],
406 type: m[1],
407 };
408 // tslint:enable: custom-no-magic-numbers
409 });
410 const r = {
411 type: 'error',
412 name,
413 arguments: _.isEmpty(argData) ? [] : argData,
414 };
415 return r;
416}
417function checkArgEquality(type, lhs, rhs) {
418 // Try to compare as decoded revert errors first.
419 try {
420 return RevertError.decode(lhs).equals(RevertError.decode(rhs));
421 }
422 catch (err) {
423 // no-op
424 }
425 if (type === 'address') {
426 return normalizeAddress(lhs) === normalizeAddress(rhs);
427 }
428 else if (type === 'bytes' || /^bytes(\d+)$/.test(type)) {
429 return normalizeBytes(lhs) === normalizeBytes(rhs);
430 }
431 else if (type === 'string') {
432 return lhs === rhs;
433 }
434 else if (/\[\d*\]$/.test(type)) {
435 // An array type.
436 // tslint:disable: custom-no-magic-numbers
437 // Arguments must be arrays and have the same dimensions.
438 if (lhs.length !== rhs.length) {
439 return false;
440 }
441 const m = /^(.+)\[(\d*)\]$/.exec(type);
442 const baseType = m[1];
443 const isFixedLength = m[2].length !== 0;
444 if (isFixedLength) {
445 const length = parseInt(m[2], 10);
446 // Fixed-size arrays have a fixed dimension.
447 if (lhs.length !== length) {
448 return false;
449 }
450 }
451 // Recurse into sub-elements.
452 for (const [slhs, srhs] of _.zip(lhs, rhs)) {
453 if (!checkArgEquality(baseType, slhs, srhs)) {
454 return false;
455 }
456 }
457 return true;
458 // tslint:enable: no-magic-numbers
459 }
460 // tslint:disable-next-line
461 return new configured_bignumber_1.BigNumber(lhs || 0).eq(rhs);
462}
463function normalizeAddress(addr) {
464 const ADDRESS_SIZE = 20;
465 return ethUtil.bufferToHex(ethUtil.setLengthLeft(ethUtil.toBuffer(ethUtil.addHexPrefix(addr)), ADDRESS_SIZE));
466}
467function normalizeBytes(bytes) {
468 return ethUtil.addHexPrefix(bytes).toLowerCase();
469}
470function createEncoder(abi) {
471 const encoder = AbiEncoder.createMethod(abi.name, abi.arguments || []);
472 return (values) => {
473 const valuesArray = _.map(abi.arguments, (arg) => values[arg.name]);
474 return encoder.encode(valuesArray);
475 };
476}
477function createDecoder(abi) {
478 const encoder = AbiEncoder.createMethod(abi.name, abi.arguments || []);
479 return (hex) => {
480 return encoder.decode(hex);
481 };
482}
483function toSignature(abi) {
484 const argTypes = _.map(abi.arguments, (a) => a.type);
485 const args = argTypes.join(',');
486 return `${abi.name}(${args})`;
487}
488function toSelector(abi) {
489 return (ethUtil
490 .keccak256(Buffer.from(toSignature(abi)))
491 // tslint:disable-next-line: custom-no-magic-numbers
492 .slice(0, 4)
493 .toString('hex'));
494}
495// Register StringRevertError
496RevertError.registerType(StringRevertError);
497// tslint:disable-next-line max-file-line-count
498//# sourceMappingURL=revert_error.js.map
\No newline at end of file