UNPKG

17.6 kBPlain TextView Raw
1"use strict";
2
3import { Block, TransactionReceipt, TransactionResponse } from "@ethersproject/abstract-provider";
4import { getAddress, getContractAddress } from "@ethersproject/address";
5import { BigNumber } from "@ethersproject/bignumber";
6import { hexDataLength, hexDataSlice, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
7import { AddressZero } from "@ethersproject/constants";
8import { shallowCopy } from "@ethersproject/properties";
9import { AccessList, accessListify, parse as parseTransaction } from "@ethersproject/transactions";
10
11import { Logger } from "@ethersproject/logger";
12import { version } from "./_version";
13const logger = new Logger(version);
14
15export type FormatFunc = (value: any) => any;
16
17export type FormatFuncs = { [ key: string ]: FormatFunc };
18
19export type Formats = {
20 transaction: FormatFuncs,
21 transactionRequest: FormatFuncs,
22 receipt: FormatFuncs,
23 receiptLog: FormatFuncs,
24 block: FormatFuncs,
25 blockWithTransactions: FormatFuncs,
26 filter: FormatFuncs,
27 filterLog: FormatFuncs,
28};
29
30export class Formatter {
31 readonly formats: Formats;
32
33 constructor() {
34 this.formats = this.getDefaultFormats();
35 }
36
37 getDefaultFormats(): Formats {
38 const formats: Formats = <Formats>({ });
39
40 const address = this.address.bind(this);
41 const bigNumber = this.bigNumber.bind(this);
42 const blockTag = this.blockTag.bind(this);
43 const data = this.data.bind(this);
44 const hash = this.hash.bind(this);
45 const hex = this.hex.bind(this);
46 const number = this.number.bind(this);
47 const type = this.type.bind(this);
48
49 const strictData = (v: any) => { return this.data(v, true); };
50
51 formats.transaction = {
52 hash: hash,
53
54 type: type,
55 accessList: Formatter.allowNull(this.accessList.bind(this), null),
56
57 blockHash: Formatter.allowNull(hash, null),
58 blockNumber: Formatter.allowNull(number, null),
59 transactionIndex: Formatter.allowNull(number, null),
60
61 confirmations: Formatter.allowNull(number, null),
62
63 from: address,
64
65 // either (gasPrice) or (maxPriorityFeePerGas + maxFeePerGas)
66 // must be set
67 gasPrice: Formatter.allowNull(bigNumber),
68 maxPriorityFeePerGas: Formatter.allowNull(bigNumber),
69 maxFeePerGas: Formatter.allowNull(bigNumber),
70
71 gasLimit: bigNumber,
72 to: Formatter.allowNull(address, null),
73 value: bigNumber,
74 nonce: number,
75 data: data,
76
77 r: Formatter.allowNull(this.uint256),
78 s: Formatter.allowNull(this.uint256),
79 v: Formatter.allowNull(number),
80
81 creates: Formatter.allowNull(address, null),
82
83 raw: Formatter.allowNull(data),
84 };
85
86 formats.transactionRequest = {
87 from: Formatter.allowNull(address),
88 nonce: Formatter.allowNull(number),
89 gasLimit: Formatter.allowNull(bigNumber),
90 gasPrice: Formatter.allowNull(bigNumber),
91 maxPriorityFeePerGas: Formatter.allowNull(bigNumber),
92 maxFeePerGas: Formatter.allowNull(bigNumber),
93 to: Formatter.allowNull(address),
94 value: Formatter.allowNull(bigNumber),
95 data: Formatter.allowNull(strictData),
96 type: Formatter.allowNull(number),
97 accessList: Formatter.allowNull(this.accessList.bind(this), null),
98 };
99
100 formats.receiptLog = {
101 transactionIndex: number,
102 blockNumber: number,
103 transactionHash: hash,
104 address: address,
105 topics: Formatter.arrayOf(hash),
106 data: data,
107 logIndex: number,
108 blockHash: hash,
109 };
110
111 formats.receipt = {
112 to: Formatter.allowNull(this.address, null),
113 from: Formatter.allowNull(this.address, null),
114 contractAddress: Formatter.allowNull(address, null),
115 transactionIndex: number,
116 // should be allowNull(hash), but broken-EIP-658 support is handled in receipt
117 root: Formatter.allowNull(hex),
118 gasUsed: bigNumber,
119 logsBloom: Formatter.allowNull(data),// @TODO: should this be data?
120 blockHash: hash,
121 transactionHash: hash,
122 logs: Formatter.arrayOf(this.receiptLog.bind(this)),
123 blockNumber: number,
124 confirmations: Formatter.allowNull(number, null),
125 cumulativeGasUsed: bigNumber,
126 effectiveGasPrice: Formatter.allowNull(bigNumber),
127 status: Formatter.allowNull(number),
128 type: type
129 };
130
131 formats.block = {
132 hash: Formatter.allowNull(hash),
133 parentHash: hash,
134 number: number,
135
136 timestamp: number,
137 nonce: Formatter.allowNull(hex),
138 difficulty: this.difficulty.bind(this),
139
140 gasLimit: bigNumber,
141 gasUsed: bigNumber,
142
143 miner: Formatter.allowNull(address),
144 extraData: data,
145
146 transactions: Formatter.allowNull(Formatter.arrayOf(hash)),
147
148 baseFeePerGas: Formatter.allowNull(bigNumber)
149 };
150
151 formats.blockWithTransactions = shallowCopy(formats.block);
152 formats.blockWithTransactions.transactions = Formatter.allowNull(Formatter.arrayOf(this.transactionResponse.bind(this)));
153
154 formats.filter = {
155 fromBlock: Formatter.allowNull(blockTag, undefined),
156 toBlock: Formatter.allowNull(blockTag, undefined),
157 blockHash: Formatter.allowNull(hash, undefined),
158 address: Formatter.allowNull(address, undefined),
159 topics: Formatter.allowNull(this.topics.bind(this), undefined),
160 };
161
162 formats.filterLog = {
163 blockNumber: Formatter.allowNull(number),
164 blockHash: Formatter.allowNull(hash),
165 transactionIndex: number,
166
167 removed: Formatter.allowNull(this.boolean.bind(this)),
168
169 address: address,
170 data: Formatter.allowFalsish(data, "0x"),
171
172 topics: Formatter.arrayOf(hash),
173
174 transactionHash: hash,
175 logIndex: number,
176 };
177
178 return formats;
179 }
180
181 accessList(accessList: Array<any>): AccessList {
182 return accessListify(accessList || []);
183 }
184
185 // Requires a BigNumberish that is within the IEEE754 safe integer range; returns a number
186 // Strict! Used on input.
187 number(number: any): number {
188 if (number === "0x") { return 0; }
189 return BigNumber.from(number).toNumber();
190 }
191
192 type(number: any): number {
193 if (number === "0x" || number == null) { return 0; }
194 return BigNumber.from(number).toNumber();
195 }
196
197 // Strict! Used on input.
198 bigNumber(value: any): BigNumber {
199 return BigNumber.from(value);
200 }
201
202 // Requires a boolean, "true" or "false"; returns a boolean
203 boolean(value: any): boolean {
204 if (typeof(value) === "boolean") { return value; }
205 if (typeof(value) === "string") {
206 value = value.toLowerCase();
207 if (value === "true") { return true; }
208 if (value === "false") { return false; }
209 }
210 throw new Error("invalid boolean - " + value);
211 }
212
213 hex(value: any, strict?: boolean): string {
214 if (typeof(value) === "string") {
215 if (!strict && value.substring(0, 2) !== "0x") { value = "0x" + value; }
216 if (isHexString(value)) {
217 return value.toLowerCase();
218 }
219 }
220 return logger.throwArgumentError("invalid hash", "value", value);
221 }
222
223 data(value: any, strict?: boolean): string {
224 const result = this.hex(value, strict);
225 if ((result.length % 2) !== 0) {
226 throw new Error("invalid data; odd-length - " + value);
227 }
228 return result;
229 }
230
231 // Requires an address
232 // Strict! Used on input.
233 address(value: any): string {
234 return getAddress(value);
235 }
236
237 callAddress(value: any): string {
238 if (!isHexString(value, 32)) { return null; }
239 const address = getAddress(hexDataSlice(value, 12));
240 return (address === AddressZero) ? null: address;
241 }
242
243 contractAddress(value: any): string {
244 return getContractAddress(value);
245 }
246
247 // Strict! Used on input.
248 blockTag(blockTag: any): string {
249 if (blockTag == null) { return "latest"; }
250
251 if (blockTag === "earliest") { return "0x0"; }
252
253 if (blockTag === "latest" || blockTag === "pending") {
254 return blockTag;
255 }
256
257 if (typeof(blockTag) === "number" || isHexString(blockTag)) {
258 return hexValue(<number | string>blockTag);
259 }
260
261 throw new Error("invalid blockTag");
262 }
263
264 // Requires a hash, optionally requires 0x prefix; returns prefixed lowercase hash.
265 hash(value: any, strict?: boolean): string {
266 const result = this.hex(value, strict);
267 if (hexDataLength(result) !== 32) {
268 return logger.throwArgumentError("invalid hash", "value", value);
269 }
270 return result;
271 }
272
273 // Returns the difficulty as a number, or if too large (i.e. PoA network) null
274 difficulty(value: any): number {
275 if (value == null) { return null; }
276
277 const v = BigNumber.from(value);
278
279 try {
280 return v.toNumber();
281 } catch (error) { }
282
283 return null;
284 }
285
286 uint256(value: any): string {
287 if (!isHexString(value)) {
288 throw new Error("invalid uint256");
289 }
290 return hexZeroPad(value, 32);
291 }
292
293 _block(value: any, format: any): Block {
294 if (value.author != null && value.miner == null) {
295 value.miner = value.author;
296 }
297 // The difficulty may need to come from _difficulty in recursed blocks
298 const difficulty = (value._difficulty != null) ? value._difficulty: value.difficulty;
299 const result = Formatter.check(format, value);
300 result._difficulty = ((difficulty == null) ? null: BigNumber.from(difficulty));
301 return result;
302 }
303
304 block(value: any): Block {
305 return this._block(value, this.formats.block);
306 }
307
308 blockWithTransactions(value: any): Block {
309 return this._block(value, this.formats.blockWithTransactions);
310 }
311
312 // Strict! Used on input.
313 transactionRequest(value: any): any {
314 return Formatter.check(this.formats.transactionRequest, value);
315 }
316
317 transactionResponse(transaction: any): TransactionResponse {
318
319 // Rename gas to gasLimit
320 if (transaction.gas != null && transaction.gasLimit == null) {
321 transaction.gasLimit = transaction.gas;
322 }
323
324 // Some clients (TestRPC) do strange things like return 0x0 for the
325 // 0 address; correct this to be a real address
326 if (transaction.to && BigNumber.from(transaction.to).isZero()) {
327 transaction.to = "0x0000000000000000000000000000000000000000";
328 }
329
330 // Rename input to data
331 if (transaction.input != null && transaction.data == null) {
332 transaction.data = transaction.input;
333 }
334
335 // If to and creates are empty, populate the creates from the transaction
336 if (transaction.to == null && transaction.creates == null) {
337 transaction.creates = this.contractAddress(transaction);
338 }
339
340 if ((transaction.type === 1 || transaction.type === 2)&& transaction.accessList == null) {
341 transaction.accessList = [ ];
342 }
343
344 const result: TransactionResponse = Formatter.check(this.formats.transaction, transaction);
345
346 if (transaction.chainId != null) {
347 let chainId = transaction.chainId;
348
349 if (isHexString(chainId)) {
350 chainId = BigNumber.from(chainId).toNumber();
351 }
352
353 result.chainId = chainId;
354
355 } else {
356 let chainId = transaction.networkId;
357
358 // geth-etc returns chainId
359 if (chainId == null && result.v == null) {
360 chainId = transaction.chainId;
361 }
362
363 if (isHexString(chainId)) {
364 chainId = BigNumber.from(chainId).toNumber();
365 }
366
367 if (typeof(chainId) !== "number" && result.v != null) {
368 chainId = (result.v - 35) / 2;
369 if (chainId < 0) { chainId = 0; }
370 chainId = parseInt(chainId);
371 }
372
373 if (typeof(chainId) !== "number") { chainId = 0; }
374
375 result.chainId = chainId;
376 }
377
378 // 0x0000... should actually be null
379 if (result.blockHash && result.blockHash.replace(/0/g, "") === "x") {
380 result.blockHash = null;
381 }
382
383 return result;
384 }
385
386 transaction(value: any): any {
387 return parseTransaction(value);
388 }
389
390 receiptLog(value: any): any {
391 return Formatter.check(this.formats.receiptLog, value);
392 }
393
394 receipt(value: any): TransactionReceipt {
395 const result: TransactionReceipt = Formatter.check(this.formats.receipt, value);
396
397 // RSK incorrectly implemented EIP-658, so we munge things a bit here for it
398 if (result.root != null) {
399 if (result.root.length <= 4) {
400 // Could be 0x00, 0x0, 0x01 or 0x1
401 const value = BigNumber.from(result.root).toNumber();
402 if (value === 0 || value === 1) {
403 // Make sure if both are specified, they match
404 if (result.status != null && (result.status !== value)) {
405 logger.throwArgumentError("alt-root-status/status mismatch", "value", { root: result.root, status: result.status });
406 }
407 result.status = value;
408 delete result.root;
409 } else {
410 logger.throwArgumentError("invalid alt-root-status", "value.root", result.root);
411 }
412 } else if (result.root.length !== 66) {
413 // Must be a valid bytes32
414 logger.throwArgumentError("invalid root hash", "value.root", result.root);
415 }
416 }
417
418 if (result.status != null) {
419 result.byzantium = true;
420 }
421
422 return result;
423 }
424
425 topics(value: any): any {
426 if (Array.isArray(value)) {
427 return value.map((v) => this.topics(v));
428
429 } else if (value != null) {
430 return this.hash(value, true);
431 }
432
433 return null;
434 }
435
436 filter(value: any): any {
437 return Formatter.check(this.formats.filter, value);
438 }
439
440 filterLog(value: any): any {
441 return Formatter.check(this.formats.filterLog, value);
442 }
443
444 static check(format: { [ name: string ]: FormatFunc }, object: any): any {
445 const result: any = {};
446 for (const key in format) {
447 try {
448 const value = format[key](object[key]);
449 if (value !== undefined) { result[key] = value; }
450 } catch (error) {
451 error.checkKey = key;
452 error.checkValue = object[key];
453 throw error;
454 }
455 }
456 return result;
457 }
458
459 // if value is null-ish, nullValue is returned
460 static allowNull(format: FormatFunc, nullValue?: any): FormatFunc {
461 return (function(value: any) {
462 if (value == null) { return nullValue; }
463 return format(value);
464 });
465 }
466
467 // If value is false-ish, replaceValue is returned
468 static allowFalsish(format: FormatFunc, replaceValue: any): FormatFunc {
469 return (function(value: any) {
470 if (!value) { return replaceValue; }
471 return format(value);
472 });
473 }
474
475 // Requires an Array satisfying check
476 static arrayOf(format: FormatFunc): FormatFunc {
477 return (function(array: any): Array<any> {
478 if (!Array.isArray(array)) { throw new Error("not an array"); }
479
480 const result: any = [];
481
482 array.forEach(function(value) {
483 result.push(format(value));
484 });
485
486 return result;
487 });
488 }
489}
490
491export interface CommunityResourcable {
492 isCommunityResource(): boolean;
493}
494
495export function isCommunityResourcable(value: any): value is CommunityResourcable {
496 return (value && typeof(value.isCommunityResource) === "function");
497}
498
499export function isCommunityResource(value: any): boolean {
500 return (isCommunityResourcable(value) && value.isCommunityResource());
501}
502
503// Show the throttle message only once
504let throttleMessage = false;
505export function showThrottleMessage() {
506 if (throttleMessage) { return; }
507 throttleMessage = true;
508
509 console.log("========= NOTICE =========")
510 console.log("Request-Rate Exceeded (this message will not be repeated)");
511 console.log("");
512 console.log("The default API keys for each service are provided as a highly-throttled,");
513 console.log("community resource for low-traffic projects and early prototyping.");
514 console.log("");
515 console.log("While your application will continue to function, we highly recommended");
516 console.log("signing up for your own API keys to improve performance, increase your");
517 console.log("request rate/limit and enable other perks, such as metrics and advanced APIs.");
518 console.log("");
519 console.log("For more details: https:/\/docs.ethers.io/api-keys/");
520 console.log("==========================");
521}
522