UNPKG

82.9 kBPlain TextView Raw
1"use strict";
2
3import {
4 Block, BlockTag, BlockWithTransactions, EventType, Filter, FilterByBlockHash, ForkEvent,
5 Listener, Log, Provider, TransactionReceipt, TransactionRequest, TransactionResponse
6} from "@ethersproject/abstract-provider";
7import { encode as base64Encode } from "@ethersproject/base64";
8import { Base58 } from "@ethersproject/basex";
9import { BigNumber, BigNumberish } from "@ethersproject/bignumber";
10import { arrayify, BytesLike, concat, hexConcat, hexDataLength, hexDataSlice, hexlify, hexValue, hexZeroPad, isHexString } from "@ethersproject/bytes";
11import { HashZero } from "@ethersproject/constants";
12import { dnsEncode, namehash } from "@ethersproject/hash";
13import { getNetwork, Network, Networkish } from "@ethersproject/networks";
14import { Deferrable, defineReadOnly, getStatic, resolveProperties } from "@ethersproject/properties";
15import { Transaction } from "@ethersproject/transactions";
16import { sha256 } from "@ethersproject/sha2";
17import { toUtf8Bytes, toUtf8String } from "@ethersproject/strings";
18import { fetchJson, poll } from "@ethersproject/web";
19
20import bech32 from "bech32";
21
22import { Logger } from "@ethersproject/logger";
23import { version } from "./_version";
24const logger = new Logger(version);
25
26import { Formatter } from "./formatter";
27
28const MAX_CCIP_REDIRECTS = 10;
29
30//////////////////////////////
31// Event Serializeing
32
33function checkTopic(topic: string): string {
34 if (topic == null) { return "null"; }
35 if (hexDataLength(topic) !== 32) {
36 logger.throwArgumentError("invalid topic", "topic", topic);
37 }
38 return topic.toLowerCase();
39}
40
41function serializeTopics(topics: Array<string | Array<string>>): string {
42 // Remove trailing null AND-topics; they are redundant
43 topics = topics.slice();
44 while (topics.length > 0 && topics[topics.length - 1] == null) { topics.pop(); }
45
46 return topics.map((topic) => {
47 if (Array.isArray(topic)) {
48
49 // Only track unique OR-topics
50 const unique: { [ topic: string ]: boolean } = { }
51 topic.forEach((topic) => {
52 unique[checkTopic(topic)] = true;
53 });
54
55 // The order of OR-topics does not matter
56 const sorted = Object.keys(unique);
57 sorted.sort();
58
59 return sorted.join("|");
60
61 } else {
62 return checkTopic(topic);
63 }
64 }).join("&");
65}
66
67function deserializeTopics(data: string): Array<string | Array<string>> {
68 if (data === "") { return [ ]; }
69
70 return data.split(/&/g).map((topic) => {
71 if (topic === "") { return [ ]; }
72
73 const comps = topic.split("|").map((topic) => {
74 return ((topic === "null") ? null: topic);
75 });
76
77 return ((comps.length === 1) ? comps[0]: comps);
78 });
79}
80
81function getEventTag(eventName: EventType): string {
82 if (typeof(eventName) === "string") {
83 eventName = eventName.toLowerCase();
84
85 if (hexDataLength(eventName) === 32) {
86 return "tx:" + eventName;
87 }
88
89 if (eventName.indexOf(":") === -1) {
90 return eventName;
91 }
92
93 } else if (Array.isArray(eventName)) {
94 return "filter:*:" + serializeTopics(eventName);
95
96 } else if (ForkEvent.isForkEvent(eventName)) {
97 logger.warn("not implemented");
98 throw new Error("not implemented");
99
100 } else if (eventName && typeof(eventName) === "object") {
101 return "filter:" + (eventName.address || "*") + ":" + serializeTopics(eventName.topics || []);
102 }
103
104 throw new Error("invalid event - " + eventName);
105}
106
107//////////////////////////////
108// Helper Object
109
110function getTime() {
111 return (new Date()).getTime();
112}
113
114function stall(duration: number): Promise<void> {
115 return new Promise((resolve) => {
116 setTimeout(resolve, duration);
117 });
118}
119
120//////////////////////////////
121// Provider Object
122
123
124/**
125 * EventType
126 * - "block"
127 * - "poll"
128 * - "didPoll"
129 * - "pending"
130 * - "error"
131 * - "network"
132 * - filter
133 * - topics array
134 * - transaction hash
135 */
136
137const PollableEvents = [ "block", "network", "pending", "poll" ];
138
139export class Event {
140 readonly listener: Listener;
141 readonly once: boolean;
142 readonly tag: string;
143
144 _lastBlockNumber: number
145 _inflight: boolean;
146
147 constructor(tag: string, listener: Listener, once: boolean) {
148 defineReadOnly(this, "tag", tag);
149 defineReadOnly(this, "listener", listener);
150 defineReadOnly(this, "once", once);
151
152 this._lastBlockNumber = -2;
153 this._inflight = false;
154 }
155
156 get event(): EventType {
157 switch (this.type) {
158 case "tx":
159 return this.hash;
160 case "filter":
161 return this.filter;
162 }
163 return this.tag;
164 }
165
166 get type(): string {
167 return this.tag.split(":")[0]
168 }
169
170 get hash(): string {
171 const comps = this.tag.split(":");
172 if (comps[0] !== "tx") { return null; }
173 return comps[1];
174 }
175
176 get filter(): Filter {
177 const comps = this.tag.split(":");
178 if (comps[0] !== "filter") { return null; }
179 const address = comps[1];
180
181 const topics = deserializeTopics(comps[2]);
182 const filter: Filter = { };
183
184 if (topics.length > 0) { filter.topics = topics; }
185 if (address && address !== "*") { filter.address = address; }
186
187 return filter;
188 }
189
190 pollable(): boolean {
191 return (this.tag.indexOf(":") >= 0 || PollableEvents.indexOf(this.tag) >= 0);
192 }
193}
194
195export interface EnsResolver {
196
197 // Name this Resolver is associated with
198 readonly name: string;
199
200 // The address of the resolver
201 readonly address: string;
202
203 // Multichain address resolution (also normal address resolution)
204 // See: https://eips.ethereum.org/EIPS/eip-2304
205 getAddress(coinType?: 60): Promise<null | string>
206
207 // Contenthash field
208 // See: https://eips.ethereum.org/EIPS/eip-1577
209 getContentHash(): Promise<null | string>;
210
211 // Storage of text records
212 // See: https://eips.ethereum.org/EIPS/eip-634
213 getText(key: string): Promise<null | string>;
214};
215
216export interface EnsProvider {
217 resolveName(name: string): Promise<null | string>;
218 lookupAddress(address: string): Promise<null | string>;
219 getResolver(name: string): Promise<null | EnsResolver>;
220}
221
222type CoinInfo = {
223 symbol: string,
224 ilk?: string, // General family
225 prefix?: string, // Bech32 prefix
226 p2pkh?: number, // Pay-to-Public-Key-Hash Version
227 p2sh?: number, // Pay-to-Script-Hash Version
228};
229
230// https://github.com/satoshilabs/slips/blob/master/slip-0044.md
231const coinInfos: { [ coinType: string ]: CoinInfo } = {
232 "0": { symbol: "btc", p2pkh: 0x00, p2sh: 0x05, prefix: "bc" },
233 "2": { symbol: "ltc", p2pkh: 0x30, p2sh: 0x32, prefix: "ltc" },
234 "3": { symbol: "doge", p2pkh: 0x1e, p2sh: 0x16 },
235 "60": { symbol: "eth", ilk: "eth" },
236 "61": { symbol: "etc", ilk: "eth" },
237 "700": { symbol: "xdai", ilk: "eth" },
238};
239
240function bytes32ify(value: number): string {
241 return hexZeroPad(BigNumber.from(value).toHexString(), 32);
242}
243
244// Compute the Base58Check encoded data (checksum is first 4 bytes of sha256d)
245function base58Encode(data: Uint8Array): string {
246 return Base58.encode(concat([ data, hexDataSlice(sha256(sha256(data)), 0, 4) ]));
247}
248
249export interface Avatar {
250 url: string;
251 linkage: Array<{ type: string, content: string }>;
252}
253
254const matcherIpfs = new RegExp("^(ipfs):/\/(.*)$", "i");
255const matchers = [
256 new RegExp("^(https):/\/(.*)$", "i"),
257 new RegExp("^(data):(.*)$", "i"),
258 matcherIpfs,
259 new RegExp("^eip155:[0-9]+/(erc[0-9]+):(.*)$", "i"),
260];
261
262function _parseString(result: string, start: number): null | string {
263 try {
264 return toUtf8String(_parseBytes(result, start));
265 } catch(error) { }
266 return null;
267}
268
269function _parseBytes(result: string, start: number): null | string {
270 if (result === "0x") { return null; }
271
272 const offset = BigNumber.from(hexDataSlice(result, start, start + 32)).toNumber();
273 const length = BigNumber.from(hexDataSlice(result, offset, offset + 32)).toNumber();
274
275 return hexDataSlice(result, offset + 32, offset + 32 + length);
276}
277
278// Trim off the ipfs:// prefix and return the default gateway URL
279function getIpfsLink(link: string): string {
280 if (link.match(/^ipfs:\/\/ipfs\//i)) {
281 link = link.substring(12);
282 } else if (link.match(/^ipfs:\/\//i)) {
283 link = link.substring(7);
284 } else {
285 logger.throwArgumentError("unsupported IPFS format", "link", link);
286 }
287
288 return `https:/\/gateway.ipfs.io/ipfs/${ link }`;
289}
290
291function numPad(value: number): Uint8Array {
292 const result = arrayify(value);
293 if (result.length > 32) { throw new Error("internal; should not happen"); }
294
295 const padded = new Uint8Array(32);
296 padded.set(result, 32 - result.length);
297 return padded;
298}
299
300function bytesPad(value: Uint8Array): Uint8Array {
301 if ((value.length % 32) === 0) { return value; }
302
303 const result = new Uint8Array(Math.ceil(value.length / 32) * 32);
304 result.set(value);
305 return result;
306}
307
308// ABI Encodes a series of (bytes, bytes, ...)
309function encodeBytes(datas: Array<BytesLike>) {
310 const result: Array<Uint8Array> = [ ];
311
312 let byteCount = 0;
313
314 // Add place-holders for pointers as we add items
315 for (let i = 0; i < datas.length; i++) {
316 result.push(null);
317 byteCount += 32;
318 }
319
320 for (let i = 0; i < datas.length; i++) {
321 const data = arrayify(datas[i]);
322
323 // Update the bytes offset
324 result[i] = numPad(byteCount);
325
326 // The length and padded value of data
327 result.push(numPad(data.length));
328 result.push(bytesPad(data));
329 byteCount += 32 + Math.ceil(data.length / 32) * 32;
330 }
331
332 return hexConcat(result);
333}
334
335export class Resolver implements EnsResolver {
336 readonly provider: BaseProvider;
337
338 readonly name: string;
339 readonly address: string;
340
341 readonly _resolvedAddress: null | string;
342
343 // For EIP-2544 names, the ancestor that provided the resolver
344 _supportsEip2544: null | Promise<boolean>;
345
346 // The resolvedAddress is only for creating a ReverseLookup resolver
347 constructor(provider: BaseProvider, address: string, name: string, resolvedAddress?: string) {
348 defineReadOnly(this, "provider", provider);
349 defineReadOnly(this, "name", name);
350 defineReadOnly(this, "address", provider.formatter.address(address));
351 defineReadOnly(this, "_resolvedAddress", resolvedAddress);
352 }
353
354 supportsWildcard(): Promise<boolean> {
355 if (!this._supportsEip2544) {
356 // supportsInterface(bytes4 = selector("resolve(bytes,bytes)"))
357 this._supportsEip2544 = this.provider.call({
358 to: this.address,
359 data: "0x01ffc9a79061b92300000000000000000000000000000000000000000000000000000000"
360 }).then((result) => {
361 return BigNumber.from(result).eq(1);
362 }).catch((error) => {
363 if (error.code === Logger.errors.CALL_EXCEPTION) { return false; }
364 // Rethrow the error: link is down, etc. Let future attempts retry.
365 this._supportsEip2544 = null;
366 throw error;
367 });
368 }
369
370 return this._supportsEip2544;
371 }
372
373 async _fetch(selector: string, parameters?: string): Promise<null | string> {
374
375 // e.g. keccak256("addr(bytes32,uint256)")
376 const tx = {
377 to: this.address,
378 ccipReadEnabled: true,
379 data: hexConcat([ selector, namehash(this.name), (parameters || "0x") ])
380 };
381
382 // Wildcard support; use EIP-2544 to resolve the request
383 let parseBytes = false;
384 if (await this.supportsWildcard()) {
385 parseBytes = true;
386
387 // selector("resolve(bytes,bytes)")
388 tx.data = hexConcat([ "0x9061b923", encodeBytes([ dnsEncode(this.name), tx.data ]) ]);
389 }
390
391 try {
392 let result = await this.provider.call(tx);
393 if ((arrayify(result).length % 32) === 4) {
394 logger.throwError("resolver threw error", Logger.errors.CALL_EXCEPTION, {
395 transaction: tx, data: result
396 });
397 }
398 if (parseBytes) { result = _parseBytes(result, 0); }
399 return result;
400 } catch (error) {
401 if (error.code === Logger.errors.CALL_EXCEPTION) { return null; }
402 throw error;
403 }
404 }
405
406 async _fetchBytes(selector: string, parameters?: string): Promise<null | string> {
407 const result = await this._fetch(selector, parameters);
408 if (result != null) { return _parseBytes(result, 0); }
409 return null;
410 }
411
412 _getAddress(coinType: number, hexBytes: string): string {
413 const coinInfo = coinInfos[String(coinType)];
414
415 if (coinInfo == null) {
416 logger.throwError(`unsupported coin type: ${ coinType }`, Logger.errors.UNSUPPORTED_OPERATION, {
417 operation: `getAddress(${ coinType })`
418 });
419 }
420
421 if (coinInfo.ilk === "eth") {
422 return this.provider.formatter.address(hexBytes);
423 }
424
425 const bytes = arrayify(hexBytes);
426
427 // P2PKH: OP_DUP OP_HASH160 <pubKeyHash> OP_EQUALVERIFY OP_CHECKSIG
428 if (coinInfo.p2pkh != null) {
429 const p2pkh = hexBytes.match(/^0x76a9([0-9a-f][0-9a-f])([0-9a-f]*)88ac$/);
430 if (p2pkh) {
431 const length = parseInt(p2pkh[1], 16);
432 if (p2pkh[2].length === length * 2 && length >= 1 && length <= 75) {
433 return base58Encode(concat([ [ coinInfo.p2pkh ], ("0x" + p2pkh[2]) ]));
434 }
435 }
436 }
437
438 // P2SH: OP_HASH160 <scriptHash> OP_EQUAL
439 if (coinInfo.p2sh != null) {
440 const p2sh = hexBytes.match(/^0xa9([0-9a-f][0-9a-f])([0-9a-f]*)87$/);
441 if (p2sh) {
442 const length = parseInt(p2sh[1], 16);
443 if (p2sh[2].length === length * 2 && length >= 1 && length <= 75) {
444 return base58Encode(concat([ [ coinInfo.p2sh ], ("0x" + p2sh[2]) ]));
445 }
446 }
447 }
448
449 // Bech32
450 if (coinInfo.prefix != null) {
451 const length = bytes[1];
452
453 // https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#witness-program
454 let version = bytes[0];
455 if (version === 0x00) {
456 if (length !== 20 && length !== 32) {
457 version = -1;
458 }
459 } else {
460 version = -1;
461 }
462
463 if (version >= 0 && bytes.length === 2 + length && length >= 1 && length <= 75) {
464 const words = bech32.toWords(bytes.slice(2));
465 words.unshift(version);
466 return bech32.encode(coinInfo.prefix, words);
467 }
468 }
469
470 return null;
471 }
472
473
474 async getAddress(coinType?: number): Promise<string> {
475 if (coinType == null) { coinType = 60; }
476
477 // If Ethereum, use the standard `addr(bytes32)`
478 if (coinType === 60) {
479 try {
480 // keccak256("addr(bytes32)")
481 const result = await this._fetch("0x3b3b57de");
482
483 // No address
484 if (result === "0x" || result === HashZero) { return null; }
485
486 return this.provider.formatter.callAddress(result);
487 } catch (error) {
488 if (error.code === Logger.errors.CALL_EXCEPTION) { return null; }
489 throw error;
490 }
491 }
492
493 // keccak256("addr(bytes32,uint256")
494 const hexBytes = await this._fetchBytes("0xf1cb7e06", bytes32ify(coinType));
495
496 // No address
497 if (hexBytes == null || hexBytes === "0x") { return null; }
498
499 // Compute the address
500 const address = this._getAddress(coinType, hexBytes);
501
502 if (address == null) {
503 logger.throwError(`invalid or unsupported coin data`, Logger.errors.UNSUPPORTED_OPERATION, {
504 operation: `getAddress(${ coinType })`,
505 coinType: coinType,
506 data: hexBytes
507 });
508 }
509
510 return address;
511 }
512
513 async getAvatar(): Promise<null | Avatar> {
514 const linkage: Array<{ type: string, content: string }> = [ { type: "name", content: this.name } ];
515 try {
516 // test data for ricmoo.eth
517 //const avatar = "eip155:1/erc721:0x265385c7f4132228A0d54EB1A9e7460b91c0cC68/29233";
518 const avatar = await this.getText("avatar");
519 if (avatar == null) { return null; }
520
521 for (let i = 0; i < matchers.length; i++) {
522 const match = avatar.match(matchers[i]);
523 if (match == null) { continue; }
524
525 const scheme = match[1].toLowerCase();
526
527 switch (scheme) {
528 case "https":
529 linkage.push({ type: "url", content: avatar });
530 return { linkage, url: avatar };
531
532 case "data":
533 linkage.push({ type: "data", content: avatar });
534 return { linkage, url: avatar };
535
536 case "ipfs":
537 linkage.push({ type: "ipfs", content: avatar });
538 return { linkage, url: getIpfsLink(avatar) };
539
540 case "erc721":
541 case "erc1155": {
542 // Depending on the ERC type, use tokenURI(uint256) or url(uint256)
543 const selector = (scheme === "erc721") ? "0xc87b56dd": "0x0e89341c";
544 linkage.push({ type: scheme, content: avatar });
545
546 // The owner of this name
547 const owner = (this._resolvedAddress || await this.getAddress());
548
549 const comps = (match[2] || "").split("/");
550 if (comps.length !== 2) { return null; }
551
552 const addr = await this.provider.formatter.address(comps[0]);
553 const tokenId = hexZeroPad(BigNumber.from(comps[1]).toHexString(), 32);
554
555 // Check that this account owns the token
556 if (scheme === "erc721") {
557 // ownerOf(uint256 tokenId)
558 const tokenOwner = this.provider.formatter.callAddress(await this.provider.call({
559 to: addr, data: hexConcat([ "0x6352211e", tokenId ])
560 }));
561 if (owner !== tokenOwner) { return null; }
562 linkage.push({ type: "owner", content: tokenOwner });
563
564 } else if (scheme === "erc1155") {
565 // balanceOf(address owner, uint256 tokenId)
566 const balance = BigNumber.from(await this.provider.call({
567 to: addr, data: hexConcat([ "0x00fdd58e", hexZeroPad(owner, 32), tokenId ])
568 }));
569 if (balance.isZero()) { return null; }
570 linkage.push({ type: "balance", content: balance.toString() });
571 }
572
573 // Call the token contract for the metadata URL
574 const tx = {
575 to: this.provider.formatter.address(comps[0]),
576 data: hexConcat([ selector, tokenId ])
577 };
578
579 let metadataUrl = _parseString(await this.provider.call(tx), 0);
580 if (metadataUrl == null) { return null; }
581 linkage.push({ type: "metadata-url-base", content: metadataUrl });
582
583 // ERC-1155 allows a generic {id} in the URL
584 if (scheme === "erc1155") {
585 metadataUrl = metadataUrl.replace("{id}", tokenId.substring(2));
586 linkage.push({ type: "metadata-url-expanded", content: metadataUrl });
587 }
588
589 // Transform IPFS metadata links
590 if (metadataUrl.match(/^ipfs:/i)) {
591 metadataUrl = getIpfsLink(metadataUrl);
592 }
593
594 linkage.push({ type: "metadata-url", content: metadataUrl });
595
596 // Get the token metadata
597 const metadata = await fetchJson(metadataUrl);
598 if (!metadata) { return null; }
599 linkage.push({ type: "metadata", content: JSON.stringify(metadata) });
600
601 // Pull the image URL out
602 let imageUrl = metadata.image;
603 if (typeof(imageUrl) !== "string") { return null; }
604
605 if (imageUrl.match(/^(https:\/\/|data:)/i)) {
606 // Allow
607 } else {
608 // Transform IPFS link to gateway
609 const ipfs = imageUrl.match(matcherIpfs);
610 if (ipfs == null) { return null; }
611
612 linkage.push({ type: "url-ipfs", content: imageUrl });
613 imageUrl = getIpfsLink(imageUrl);
614 }
615
616 linkage.push({ type: "url", content: imageUrl });
617
618 return { linkage, url: imageUrl };
619 }
620 }
621 }
622 } catch (error) { }
623
624 return null;
625 }
626
627 async getContentHash(): Promise<string> {
628
629 // keccak256("contenthash()")
630 const hexBytes = await this._fetchBytes("0xbc1c58d1");
631
632 // No contenthash
633 if (hexBytes == null || hexBytes === "0x") { return null; }
634
635 // IPFS (CID: 1, Type: DAG-PB)
636 const ipfs = hexBytes.match(/^0xe3010170(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
637 if (ipfs) {
638 const length = parseInt(ipfs[3], 16);
639 if (ipfs[4].length === length * 2) {
640 return "ipfs:/\/" + Base58.encode("0x" + ipfs[1]);
641 }
642 }
643
644 // IPNS (CID: 1, Type: libp2p-key)
645 const ipns = hexBytes.match(/^0xe5010172(([0-9a-f][0-9a-f])([0-9a-f][0-9a-f])([0-9a-f]*))$/);
646 if (ipns) {
647 const length = parseInt(ipns[3], 16);
648 if (ipns[4].length === length * 2) {
649 return "ipns:/\/" + Base58.encode("0x" + ipns[1]);
650 }
651 }
652
653 // Swarm (CID: 1, Type: swarm-manifest; hash/length hard-coded to keccak256/32)
654 const swarm = hexBytes.match(/^0xe40101fa011b20([0-9a-f]*)$/)
655 if (swarm) {
656 if (swarm[1].length === (32 * 2)) {
657 return "bzz:/\/" + swarm[1]
658 }
659 }
660
661 const skynet = hexBytes.match(/^0x90b2c605([0-9a-f]*)$/);
662 if (skynet) {
663 if (skynet[1].length === (34 * 2)) {
664 // URL Safe base64; https://datatracker.ietf.org/doc/html/rfc4648#section-5
665 const urlSafe: Record<string, string> = { "=": "", "+": "-", "/": "_" };
666 const hash = base64Encode("0x" + skynet[1]).replace(/[=+\/]/g, (a) => (urlSafe[a]));
667 return "sia:/\/" + hash;
668 }
669 }
670
671 return logger.throwError(`invalid or unsupported content hash data`, Logger.errors.UNSUPPORTED_OPERATION, {
672 operation: "getContentHash()",
673 data: hexBytes
674 });
675 }
676
677 async getText(key: string): Promise<string> {
678
679 // The key encoded as parameter to fetchBytes
680 let keyBytes = toUtf8Bytes(key);
681
682 // The nodehash consumes the first slot, so the string pointer targets
683 // offset 64, with the length at offset 64 and data starting at offset 96
684 keyBytes = concat([ bytes32ify(64), bytes32ify(keyBytes.length), keyBytes ]);
685
686 // Pad to word-size (32 bytes)
687 if ((keyBytes.length % 32) !== 0) {
688 keyBytes = concat([ keyBytes, hexZeroPad("0x", 32 - (key.length % 32)) ])
689 }
690
691 const hexBytes = await this._fetchBytes("0x59d1d43c", hexlify(keyBytes));
692 if (hexBytes == null || hexBytes === "0x") { return null; }
693
694 return toUtf8String(hexBytes);
695 }
696}
697
698let defaultFormatter: Formatter = null;
699
700let nextPollId = 1;
701
702export class BaseProvider extends Provider implements EnsProvider {
703 _networkPromise: Promise<Network>;
704 _network: Network;
705
706 _events: Array<Event>;
707
708 formatter: Formatter;
709
710 // To help mitigate the eventually consistent nature of the blockchain
711 // we keep a mapping of events we emit. If we emit an event X, we expect
712 // that a user should be able to query for that event in the callback,
713 // if the node returns null, we stall the response until we get back a
714 // meaningful value, since we may be hitting a re-org, or a node that
715 // has not indexed the event yet.
716 // Events:
717 // - t:{hash} - Transaction hash
718 // - b:{hash} - BlockHash
719 // - block - The most recent emitted block
720 _emitted: { [ eventName: string ]: number | "pending" };
721
722 _pollingInterval: number;
723 _poller: NodeJS.Timer;
724 _bootstrapPoll: NodeJS.Timer;
725
726 _lastBlockNumber: number;
727 _maxFilterBlockRange: number;
728
729 _fastBlockNumber: number;
730 _fastBlockNumberPromise: Promise<number>;
731 _fastQueryDate: number;
732
733 _maxInternalBlockNumber: number;
734 _internalBlockNumber: Promise<{ blockNumber: number, reqTime: number, respTime: number }>;
735
736 readonly anyNetwork: boolean;
737
738 disableCcipRead: boolean;
739
740
741 /**
742 * ready
743 *
744 * A Promise<Network> that resolves only once the provider is ready.
745 *
746 * Sub-classes that call the super with a network without a chainId
747 * MUST set this. Standard named networks have a known chainId.
748 *
749 */
750
751 constructor(network: Networkish | Promise<Network>) {
752 super();
753
754 // Events being listened to
755 this._events = [];
756
757 this._emitted = { block: -2 };
758
759 this.disableCcipRead = false;
760
761 this.formatter = new.target.getFormatter();
762
763 // If network is any, this Provider allows the underlying
764 // network to change dynamically, and we auto-detect the
765 // current network
766 defineReadOnly(this, "anyNetwork", (network === "any"));
767 if (this.anyNetwork) { network = this.detectNetwork(); }
768
769 if (network instanceof Promise) {
770 this._networkPromise = network;
771
772 // Squash any "unhandled promise" errors; that do not need to be handled
773 network.catch((error) => { });
774
775 // Trigger initial network setting (async)
776 this._ready().catch((error) => { });
777
778 } else {
779 const knownNetwork = getStatic<(network: Networkish) => Network>(new.target, "getNetwork")(network);
780 if (knownNetwork) {
781 defineReadOnly(this, "_network", knownNetwork);
782 this.emit("network", knownNetwork, null);
783
784 } else {
785 logger.throwArgumentError("invalid network", "network", network);
786 }
787 }
788
789 this._maxInternalBlockNumber = -1024;
790
791 this._lastBlockNumber = -2;
792 this._maxFilterBlockRange = 10;
793
794 this._pollingInterval = 4000;
795
796 this._fastQueryDate = 0;
797 }
798
799 async _ready(): Promise<Network> {
800 if (this._network == null) {
801 let network: Network = null;
802 if (this._networkPromise) {
803 try {
804 network = await this._networkPromise;
805 } catch (error) { }
806 }
807
808 // Try the Provider's network detection (this MUST throw if it cannot)
809 if (network == null) {
810 network = await this.detectNetwork();
811 }
812
813 // This should never happen; every Provider sub-class should have
814 // suggested a network by here (or have thrown).
815 if (!network) {
816 logger.throwError("no network detected", Logger.errors.UNKNOWN_ERROR, { });
817 }
818
819 // Possible this call stacked so do not call defineReadOnly again
820 if (this._network == null) {
821 if (this.anyNetwork) {
822 this._network = network;
823 } else {
824 defineReadOnly(this, "_network", network);
825 }
826 this.emit("network", network, null);
827 }
828 }
829
830 return this._network;
831 }
832
833 // This will always return the most recently established network.
834 // For "any", this can change (a "network" event is emitted before
835 // any change is reflected); otherwise this cannot change
836 get ready(): Promise<Network> {
837 return poll(() => {
838 return this._ready().then((network) => {
839 return network;
840 }, (error) => {
841 // If the network isn't running yet, we will wait
842 if (error.code === Logger.errors.NETWORK_ERROR && error.event === "noNetwork") {
843 return undefined;
844 }
845 throw error;
846 });
847 });
848 }
849
850 // @TODO: Remove this and just create a singleton formatter
851 static getFormatter(): Formatter {
852 if (defaultFormatter == null) {
853 defaultFormatter = new Formatter();
854 }
855 return defaultFormatter;
856 }
857
858 // @TODO: Remove this and just use getNetwork
859 static getNetwork(network: Networkish): Network {
860 return getNetwork((network == null) ? "homestead": network);
861 }
862
863 async ccipReadFetch(tx: Transaction, calldata: string, urls: Array<string>): Promise<null | string> {
864 if (this.disableCcipRead || urls.length === 0) { return null; }
865
866 const sender = tx.to.toLowerCase();
867 const data = calldata.toLowerCase();
868
869 const errorMessages: Array<string> = [ ];
870
871 for (let i = 0; i < urls.length; i++) {
872 const url = urls[i];
873
874 // URL expansion
875 const href = url.replace("{sender}", sender).replace("{data}", data);
876
877 // If no {data} is present, use POST; otherwise GET
878 const json: string | null = (url.indexOf("{data}") >= 0) ? null: JSON.stringify({ data, sender });
879
880 const result = await fetchJson({ url: href, errorPassThrough: true }, json, (value, response) => {
881 value.status = response.statusCode;
882 return value;
883 });
884
885 if (result.data) { return result.data; }
886
887 const errorMessage = (result.message || "unknown error");
888
889 // 4xx indicates the result is not present; stop
890 if (result.status >= 400 && result.status < 500) {
891 return logger.throwError(`response not found during CCIP fetch: ${ errorMessage }`, Logger.errors.SERVER_ERROR, { url, errorMessage });
892 }
893
894 // 5xx indicates server issue; try the next url
895 errorMessages.push(errorMessage);
896 }
897
898 return logger.throwError(`error encountered during CCIP fetch: ${ errorMessages.map((m) => JSON.stringify(m)).join(", ") }`, Logger.errors.SERVER_ERROR, {
899 urls, errorMessages
900 });
901 }
902
903 // Fetches the blockNumber, but will reuse any result that is less
904 // than maxAge old or has been requested since the last request
905 async _getInternalBlockNumber(maxAge: number): Promise<number> {
906 await this._ready();
907
908 // Allowing stale data up to maxAge old
909 if (maxAge > 0) {
910
911 // While there are pending internal block requests...
912 while (this._internalBlockNumber) {
913
914 // ..."remember" which fetch we started with
915 const internalBlockNumber = this._internalBlockNumber;
916
917 try {
918 // Check the result is not too stale
919 const result = await internalBlockNumber;
920 if ((getTime() - result.respTime) <= maxAge) {
921 return result.blockNumber;
922 }
923
924 // Too old; fetch a new value
925 break;
926
927 } catch(error) {
928
929 // The fetch rejected; if we are the first to get the
930 // rejection, drop through so we replace it with a new
931 // fetch; all others blocked will then get that fetch
932 // which won't match the one they "remembered" and loop
933 if (this._internalBlockNumber === internalBlockNumber) {
934 break;
935 }
936 }
937 }
938 }
939
940 const reqTime = getTime();
941
942 const checkInternalBlockNumber = resolveProperties({
943 blockNumber: this.perform("getBlockNumber", { }),
944 networkError: this.getNetwork().then((network) => (null), (error) => (error))
945 }).then(({ blockNumber, networkError }) => {
946 if (networkError) {
947 // Unremember this bad internal block number
948 if (this._internalBlockNumber === checkInternalBlockNumber) {
949 this._internalBlockNumber = null;
950 }
951 throw networkError;
952 }
953
954 const respTime = getTime();
955
956 blockNumber = BigNumber.from(blockNumber).toNumber();
957 if (blockNumber < this._maxInternalBlockNumber) { blockNumber = this._maxInternalBlockNumber; }
958
959 this._maxInternalBlockNumber = blockNumber;
960 this._setFastBlockNumber(blockNumber); // @TODO: Still need this?
961 return { blockNumber, reqTime, respTime };
962 });
963
964 this._internalBlockNumber = checkInternalBlockNumber;
965
966 // Swallow unhandled exceptions; if needed they are handled else where
967 checkInternalBlockNumber.catch((error) => {
968 // Don't null the dead (rejected) fetch, if it has already been updated
969 if (this._internalBlockNumber === checkInternalBlockNumber) {
970 this._internalBlockNumber = null;
971 }
972 });
973
974 return (await checkInternalBlockNumber).blockNumber;
975 }
976
977 async poll(): Promise<void> {
978 const pollId = nextPollId++;
979
980 // Track all running promises, so we can trigger a post-poll once they are complete
981 const runners: Array<Promise<void>> = [];
982
983 let blockNumber: number = null;
984 try {
985 blockNumber = await this._getInternalBlockNumber(100 + this.pollingInterval / 2);
986 } catch (error) {
987 this.emit("error", error);
988 return;
989 }
990 this._setFastBlockNumber(blockNumber);
991
992 // Emit a poll event after we have the latest (fast) block number
993 this.emit("poll", pollId, blockNumber);
994
995 // If the block has not changed, meh.
996 if (blockNumber === this._lastBlockNumber) {
997 this.emit("didPoll", pollId);
998 return;
999 }
1000
1001 // First polling cycle, trigger a "block" events
1002 if (this._emitted.block === -2) {
1003 this._emitted.block = blockNumber - 1;
1004 }
1005
1006 if (Math.abs((<number>(this._emitted.block)) - blockNumber) > 1000) {
1007 logger.warn(`network block skew detected; skipping block events (emitted=${ this._emitted.block } blockNumber${ blockNumber })`);
1008 this.emit("error", logger.makeError("network block skew detected", Logger.errors.NETWORK_ERROR, {
1009 blockNumber: blockNumber,
1010 event: "blockSkew",
1011 previousBlockNumber: this._emitted.block
1012 }));
1013 this.emit("block", blockNumber);
1014
1015 } else {
1016 // Notify all listener for each block that has passed
1017 for (let i = (<number>this._emitted.block) + 1; i <= blockNumber; i++) {
1018 this.emit("block", i);
1019 }
1020 }
1021
1022 // The emitted block was updated, check for obsolete events
1023 if ((<number>this._emitted.block) !== blockNumber) {
1024 this._emitted.block = blockNumber;
1025
1026 Object.keys(this._emitted).forEach((key) => {
1027 // The block event does not expire
1028 if (key === "block") { return; }
1029
1030 // The block we were at when we emitted this event
1031 const eventBlockNumber = this._emitted[key];
1032
1033 // We cannot garbage collect pending transactions or blocks here
1034 // They should be garbage collected by the Provider when setting
1035 // "pending" events
1036 if (eventBlockNumber === "pending") { return; }
1037
1038 // Evict any transaction hashes or block hashes over 12 blocks
1039 // old, since they should not return null anyways
1040 if (blockNumber - eventBlockNumber > 12) {
1041 delete this._emitted[key];
1042 }
1043 });
1044 }
1045
1046 // First polling cycle
1047 if (this._lastBlockNumber === -2) {
1048 this._lastBlockNumber = blockNumber - 1;
1049 }
1050 // Find all transaction hashes we are waiting on
1051 this._events.forEach((event) => {
1052 switch (event.type) {
1053 case "tx": {
1054 const hash = event.hash;
1055 let runner = this.getTransactionReceipt(hash).then((receipt) => {
1056 if (!receipt || receipt.blockNumber == null) { return null; }
1057 this._emitted["t:" + hash] = receipt.blockNumber;
1058 this.emit(hash, receipt);
1059 return null;
1060 }).catch((error: Error) => { this.emit("error", error); });
1061
1062 runners.push(runner);
1063
1064 break;
1065 }
1066
1067 case "filter": {
1068 // We only allow a single getLogs to be in-flight at a time
1069 if (!event._inflight) {
1070 event._inflight = true;
1071
1072 // Filter from the last known event; due to load-balancing
1073 // and some nodes returning updated block numbers before
1074 // indexing events, a logs result with 0 entries cannot be
1075 // trusted and we must retry a range which includes it again
1076 const filter = event.filter;
1077 filter.fromBlock = event._lastBlockNumber + 1;
1078 filter.toBlock = blockNumber;
1079
1080 // Prevent fitler ranges from growing too wild
1081 if (filter.toBlock - this._maxFilterBlockRange > filter.fromBlock) {
1082 filter.fromBlock = filter.toBlock - this._maxFilterBlockRange;
1083 }
1084
1085 const runner = this.getLogs(filter).then((logs) => {
1086 // Allow the next getLogs
1087 event._inflight = false;
1088
1089 if (logs.length === 0) { return; }
1090
1091 logs.forEach((log: Log) => {
1092 // Only when we get an event for a given block number
1093 // can we trust the events are indexed
1094 if (log.blockNumber > event._lastBlockNumber) {
1095 event._lastBlockNumber = log.blockNumber;
1096 }
1097
1098 // Make sure we stall requests to fetch blocks and txs
1099 this._emitted["b:" + log.blockHash] = log.blockNumber;
1100 this._emitted["t:" + log.transactionHash] = log.blockNumber;
1101
1102 this.emit(filter, log);
1103 });
1104 }).catch((error: Error) => {
1105 this.emit("error", error);
1106
1107 // Allow another getLogs (the range was not updated)
1108 event._inflight = false;
1109 });
1110 runners.push(runner);
1111 }
1112
1113 break;
1114 }
1115 }
1116 });
1117
1118 this._lastBlockNumber = blockNumber;
1119
1120 // Once all events for this loop have been processed, emit "didPoll"
1121 Promise.all(runners).then(() => {
1122 this.emit("didPoll", pollId);
1123 }).catch((error) => { this.emit("error", error); });
1124
1125 return;
1126 }
1127
1128 // Deprecated; do not use this
1129 resetEventsBlock(blockNumber: number): void {
1130 this._lastBlockNumber = blockNumber - 1;
1131 if (this.polling) { this.poll(); }
1132 }
1133
1134 get network(): Network {
1135 return this._network;
1136 }
1137
1138 // This method should query the network if the underlying network
1139 // can change, such as when connected to a JSON-RPC backend
1140 async detectNetwork(): Promise<Network> {
1141 return logger.throwError("provider does not support network detection", Logger.errors.UNSUPPORTED_OPERATION, {
1142 operation: "provider.detectNetwork"
1143 });
1144 }
1145
1146 async getNetwork(): Promise<Network> {
1147 const network = await this._ready();
1148
1149 // Make sure we are still connected to the same network; this is
1150 // only an external call for backends which can have the underlying
1151 // network change spontaneously
1152 const currentNetwork = await this.detectNetwork();
1153 if (network.chainId !== currentNetwork.chainId) {
1154
1155 // We are allowing network changes, things can get complex fast;
1156 // make sure you know what you are doing if you use "any"
1157 if (this.anyNetwork) {
1158 this._network = currentNetwork;
1159
1160 // Reset all internal block number guards and caches
1161 this._lastBlockNumber = -2;
1162 this._fastBlockNumber = null;
1163 this._fastBlockNumberPromise = null;
1164 this._fastQueryDate = 0;
1165 this._emitted.block = -2;
1166 this._maxInternalBlockNumber = -1024;
1167 this._internalBlockNumber = null;
1168
1169 // The "network" event MUST happen before this method resolves
1170 // so any events have a chance to unregister, so we stall an
1171 // additional event loop before returning from /this/ call
1172 this.emit("network", currentNetwork, network);
1173 await stall(0);
1174
1175 return this._network;
1176 }
1177
1178 const error = logger.makeError("underlying network changed", Logger.errors.NETWORK_ERROR, {
1179 event: "changed",
1180 network: network,
1181 detectedNetwork: currentNetwork
1182 });
1183
1184 this.emit("error", error);
1185 throw error;
1186 }
1187
1188 return network;
1189 }
1190
1191 get blockNumber(): number {
1192 this._getInternalBlockNumber(100 + this.pollingInterval / 2).then((blockNumber) => {
1193 this._setFastBlockNumber(blockNumber);
1194 }, (error) => { });
1195
1196 return (this._fastBlockNumber != null) ? this._fastBlockNumber: -1;
1197 }
1198
1199 get polling(): boolean {
1200 return (this._poller != null);
1201 }
1202
1203 set polling(value: boolean) {
1204 if (value && !this._poller) {
1205 this._poller = setInterval(() => { this.poll(); }, this.pollingInterval);
1206
1207 if (!this._bootstrapPoll) {
1208 this._bootstrapPoll = setTimeout(() => {
1209 this.poll();
1210
1211 // We block additional polls until the polling interval
1212 // is done, to prevent overwhelming the poll function
1213 this._bootstrapPoll = setTimeout(() => {
1214 // If polling was disabled, something may require a poke
1215 // since starting the bootstrap poll and it was disabled
1216 if (!this._poller) { this.poll(); }
1217
1218 // Clear out the bootstrap so we can do another
1219 this._bootstrapPoll = null;
1220 }, this.pollingInterval);
1221 }, 0);
1222 }
1223
1224 } else if (!value && this._poller) {
1225 clearInterval(this._poller);
1226 this._poller = null;
1227 }
1228 }
1229
1230 get pollingInterval(): number {
1231 return this._pollingInterval;
1232 }
1233
1234 set pollingInterval(value: number) {
1235 if (typeof(value) !== "number" || value <= 0 || parseInt(String(value)) != value) {
1236 throw new Error("invalid polling interval");
1237 }
1238
1239 this._pollingInterval = value;
1240
1241 if (this._poller) {
1242 clearInterval(this._poller);
1243 this._poller = setInterval(() => { this.poll(); }, this._pollingInterval);
1244 }
1245 }
1246
1247 _getFastBlockNumber(): Promise<number> {
1248 const now = getTime();
1249
1250 // Stale block number, request a newer value
1251 if ((now - this._fastQueryDate) > 2 * this._pollingInterval) {
1252 this._fastQueryDate = now;
1253 this._fastBlockNumberPromise = this.getBlockNumber().then((blockNumber) => {
1254 if (this._fastBlockNumber == null || blockNumber > this._fastBlockNumber) {
1255 this._fastBlockNumber = blockNumber;
1256 }
1257 return this._fastBlockNumber;
1258 });
1259 }
1260
1261 return this._fastBlockNumberPromise;
1262 }
1263
1264 _setFastBlockNumber(blockNumber: number): void {
1265 // Older block, maybe a stale request
1266 if (this._fastBlockNumber != null && blockNumber < this._fastBlockNumber) { return; }
1267
1268 // Update the time we updated the blocknumber
1269 this._fastQueryDate = getTime();
1270
1271 // Newer block number, use it
1272 if (this._fastBlockNumber == null || blockNumber > this._fastBlockNumber) {
1273 this._fastBlockNumber = blockNumber;
1274 this._fastBlockNumberPromise = Promise.resolve(blockNumber);
1275 }
1276 }
1277
1278 async waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise<TransactionReceipt> {
1279 return this._waitForTransaction(transactionHash, (confirmations == null) ? 1: confirmations, timeout || 0, null);
1280 }
1281
1282 async _waitForTransaction(transactionHash: string, confirmations: number, timeout: number, replaceable: { data: string, from: string, nonce: number, to: string, value: BigNumber, startBlock: number }): Promise<TransactionReceipt> {
1283 const receipt = await this.getTransactionReceipt(transactionHash);
1284
1285 // Receipt is already good
1286 if ((receipt ? receipt.confirmations: 0) >= confirmations) { return receipt; }
1287
1288 // Poll until the receipt is good...
1289 return new Promise((resolve, reject) => {
1290 const cancelFuncs: Array<() => void> = [];
1291
1292 let done = false;
1293 const alreadyDone = function() {
1294 if (done) { return true; }
1295 done = true;
1296 cancelFuncs.forEach((func) => { func(); });
1297 return false;
1298 };
1299
1300 const minedHandler = (receipt: TransactionReceipt) => {
1301 if (receipt.confirmations < confirmations) { return; }
1302 if (alreadyDone()) { return; }
1303 resolve(receipt);
1304 }
1305 this.on(transactionHash, minedHandler);
1306 cancelFuncs.push(() => { this.removeListener(transactionHash, minedHandler); });
1307
1308 if (replaceable) {
1309 let lastBlockNumber = replaceable.startBlock;
1310 let scannedBlock: number = null;
1311 const replaceHandler = async (blockNumber: number) => {
1312 if (done) { return; }
1313
1314 // Wait 1 second; this is only used in the case of a fault, so
1315 // we will trade off a little bit of latency for more consistent
1316 // results and fewer JSON-RPC calls
1317 await stall(1000);
1318
1319 this.getTransactionCount(replaceable.from).then(async (nonce) => {
1320 if (done) { return; }
1321
1322 if (nonce <= replaceable.nonce) {
1323 lastBlockNumber = blockNumber;
1324
1325 } else {
1326 // First check if the transaction was mined
1327 {
1328 const mined = await this.getTransaction(transactionHash);
1329 if (mined && mined.blockNumber != null) { return; }
1330 }
1331
1332 // First time scanning. We start a little earlier for some
1333 // wiggle room here to handle the eventually consistent nature
1334 // of blockchain (e.g. the getTransactionCount was for a
1335 // different block)
1336 if (scannedBlock == null) {
1337 scannedBlock = lastBlockNumber - 3;
1338 if (scannedBlock < replaceable.startBlock) {
1339 scannedBlock = replaceable.startBlock;
1340 }
1341 }
1342
1343 while (scannedBlock <= blockNumber) {
1344 if (done) { return; }
1345
1346 const block = await this.getBlockWithTransactions(scannedBlock);
1347 for (let ti = 0; ti < block.transactions.length; ti++) {
1348 const tx = block.transactions[ti];
1349
1350 // Successfully mined!
1351 if (tx.hash === transactionHash) { return; }
1352
1353 // Matches our transaction from and nonce; its a replacement
1354 if (tx.from === replaceable.from && tx.nonce === replaceable.nonce) {
1355 if (done) { return; }
1356
1357 // Get the receipt of the replacement
1358 const receipt = await this.waitForTransaction(tx.hash, confirmations);
1359
1360 // Already resolved or rejected (prolly a timeout)
1361 if (alreadyDone()) { return; }
1362
1363 // The reason we were replaced
1364 let reason = "replaced";
1365 if (tx.data === replaceable.data && tx.to === replaceable.to && tx.value.eq(replaceable.value)) {
1366 reason = "repriced";
1367 } else if (tx.data === "0x" && tx.from === tx.to && tx.value.isZero()) {
1368 reason = "cancelled"
1369 }
1370
1371 // Explain why we were replaced
1372 reject(logger.makeError("transaction was replaced", Logger.errors.TRANSACTION_REPLACED, {
1373 cancelled: (reason === "replaced" || reason === "cancelled"),
1374 reason,
1375 replacement: this._wrapTransaction(tx),
1376 hash: transactionHash,
1377 receipt
1378 }));
1379
1380 return;
1381 }
1382 }
1383 scannedBlock++;
1384 }
1385 }
1386
1387 if (done) { return; }
1388 this.once("block", replaceHandler);
1389
1390 }, (error) => {
1391 if (done) { return; }
1392 this.once("block", replaceHandler);
1393 });
1394 };
1395
1396 if (done) { return; }
1397 this.once("block", replaceHandler);
1398
1399 cancelFuncs.push(() => {
1400 this.removeListener("block", replaceHandler);
1401 });
1402 }
1403
1404 if (typeof(timeout) === "number" && timeout > 0) {
1405 const timer = setTimeout(() => {
1406 if (alreadyDone()) { return; }
1407 reject(logger.makeError("timeout exceeded", Logger.errors.TIMEOUT, { timeout: timeout }));
1408 }, timeout);
1409 if (timer.unref) { timer.unref(); }
1410
1411 cancelFuncs.push(() => { clearTimeout(timer); });
1412 }
1413 });
1414 }
1415
1416 async getBlockNumber(): Promise<number> {
1417 return this._getInternalBlockNumber(0);
1418 }
1419
1420 async getGasPrice(): Promise<BigNumber> {
1421 await this.getNetwork();
1422
1423 const result = await this.perform("getGasPrice", { });
1424 try {
1425 return BigNumber.from(result);
1426 } catch (error) {
1427 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1428 method: "getGasPrice",
1429 result, error
1430 });
1431 }
1432 }
1433
1434 async getBalance(addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag>): Promise<BigNumber> {
1435 await this.getNetwork();
1436 const params = await resolveProperties({
1437 address: this._getAddress(addressOrName),
1438 blockTag: this._getBlockTag(blockTag)
1439 });
1440
1441 const result = await this.perform("getBalance", params);
1442 try {
1443 return BigNumber.from(result);
1444 } catch (error) {
1445 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1446 method: "getBalance",
1447 params, result, error
1448 });
1449 }
1450 }
1451
1452 async getTransactionCount(addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag>): Promise<number> {
1453 await this.getNetwork();
1454 const params = await resolveProperties({
1455 address: this._getAddress(addressOrName),
1456 blockTag: this._getBlockTag(blockTag)
1457 });
1458
1459 const result = await this.perform("getTransactionCount", params);
1460 try {
1461 return BigNumber.from(result).toNumber();
1462 } catch (error) {
1463 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1464 method: "getTransactionCount",
1465 params, result, error
1466 });
1467 }
1468 }
1469
1470 async getCode(addressOrName: string | Promise<string>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
1471 await this.getNetwork();
1472 const params = await resolveProperties({
1473 address: this._getAddress(addressOrName),
1474 blockTag: this._getBlockTag(blockTag)
1475 });
1476
1477 const result = await this.perform("getCode", params);
1478 try {
1479 return hexlify(result);
1480 } catch (error) {
1481 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1482 method: "getCode",
1483 params, result, error
1484 });
1485 }
1486 }
1487
1488 async getStorageAt(addressOrName: string | Promise<string>, position: BigNumberish | Promise<BigNumberish>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
1489 await this.getNetwork();
1490 const params = await resolveProperties({
1491 address: this._getAddress(addressOrName),
1492 blockTag: this._getBlockTag(blockTag),
1493 position: Promise.resolve(position).then((p) => hexValue(p))
1494 });
1495 const result = await this.perform("getStorageAt", params);
1496 try {
1497 return hexlify(result);
1498 } catch (error) {
1499 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1500 method: "getStorageAt",
1501 params, result, error
1502 });
1503 }
1504 }
1505
1506 // This should be called by any subclass wrapping a TransactionResponse
1507 _wrapTransaction(tx: Transaction, hash?: string, startBlock?: number): TransactionResponse {
1508 if (hash != null && hexDataLength(hash) !== 32) { throw new Error("invalid response - sendTransaction"); }
1509
1510 const result = <TransactionResponse>tx;
1511
1512 // Check the hash we expect is the same as the hash the server reported
1513 if (hash != null && tx.hash !== hash) {
1514 logger.throwError("Transaction hash mismatch from Provider.sendTransaction.", Logger.errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash });
1515 }
1516
1517 result.wait = async (confirms?: number, timeout?: number) => {
1518 if (confirms == null) { confirms = 1; }
1519 if (timeout == null) { timeout = 0; }
1520
1521 // Get the details to detect replacement
1522 let replacement = undefined;
1523 if (confirms !== 0 && startBlock != null) {
1524 replacement = {
1525 data: tx.data,
1526 from: tx.from,
1527 nonce: tx.nonce,
1528 to: tx.to,
1529 value: tx.value,
1530 startBlock
1531 };
1532 }
1533
1534 const receipt = await this._waitForTransaction(tx.hash, confirms, timeout, replacement);
1535 if (receipt == null && confirms === 0) { return null; }
1536
1537 // No longer pending, allow the polling loop to garbage collect this
1538 this._emitted["t:" + tx.hash] = receipt.blockNumber;
1539
1540 if (receipt.status === 0) {
1541 logger.throwError("transaction failed", Logger.errors.CALL_EXCEPTION, {
1542 transactionHash: tx.hash,
1543 transaction: tx,
1544 receipt: receipt
1545 });
1546 }
1547 return receipt;
1548 };
1549
1550 return result;
1551 }
1552
1553 async sendTransaction(signedTransaction: string | Promise<string>): Promise<TransactionResponse> {
1554 await this.getNetwork();
1555 const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t));
1556 const tx = this.formatter.transaction(signedTransaction);
1557 if (tx.confirmations == null) { tx.confirmations = 0; }
1558 const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
1559 try {
1560 const hash = await this.perform("sendTransaction", { signedTransaction: hexTx });
1561 return this._wrapTransaction(tx, hash, blockNumber);
1562 } catch (error) {
1563 (<any>error).transaction = tx;
1564 (<any>error).transactionHash = tx.hash;
1565 throw error;
1566 }
1567 }
1568
1569 async _getTransactionRequest(transaction: Deferrable<TransactionRequest>): Promise<Transaction> {
1570 const values: any = await transaction;
1571
1572 const tx: any = { };
1573
1574 ["from", "to"].forEach((key) => {
1575 if (values[key] == null) { return; }
1576 tx[key] = Promise.resolve(values[key]).then((v) => (v ? this._getAddress(v): null))
1577 });
1578
1579 ["gasLimit", "gasPrice", "maxFeePerGas", "maxPriorityFeePerGas", "value"].forEach((key) => {
1580 if (values[key] == null) { return; }
1581 tx[key] = Promise.resolve(values[key]).then((v) => (v ? BigNumber.from(v): null));
1582 });
1583
1584 ["type"].forEach((key) => {
1585 if (values[key] == null) { return; }
1586 tx[key] = Promise.resolve(values[key]).then((v) => ((v != null) ? v: null));
1587 });
1588
1589 if (values.accessList) {
1590 tx.accessList = this.formatter.accessList(values.accessList);
1591 }
1592
1593 ["data"].forEach((key) => {
1594 if (values[key] == null) { return; }
1595 tx[key] = Promise.resolve(values[key]).then((v) => (v ? hexlify(v): null));
1596 });
1597
1598 return this.formatter.transactionRequest(await resolveProperties(tx));
1599 }
1600
1601 async _getFilter(filter: Filter | FilterByBlockHash | Promise<Filter | FilterByBlockHash>): Promise<Filter | FilterByBlockHash> {
1602 filter = await filter;
1603
1604 const result: any = { };
1605
1606 if (filter.address != null) {
1607 result.address = this._getAddress(filter.address);
1608 }
1609
1610 ["blockHash", "topics"].forEach((key) => {
1611 if ((<any>filter)[key] == null) { return; }
1612 result[key] = (<any>filter)[key];
1613 });
1614
1615 ["fromBlock", "toBlock"].forEach((key) => {
1616 if ((<any>filter)[key] == null) { return; }
1617 result[key] = this._getBlockTag((<any>filter)[key]);
1618 });
1619
1620 return this.formatter.filter(await resolveProperties(result));
1621 }
1622
1623 async _call(transaction: TransactionRequest, blockTag: BlockTag, attempt: number): Promise<string> {
1624 if (attempt >= MAX_CCIP_REDIRECTS) {
1625 logger.throwError("CCIP read exceeded maximum redirections", Logger.errors.SERVER_ERROR, {
1626 redirects: attempt, transaction
1627 });
1628 }
1629
1630 const txSender = transaction.to;
1631
1632 const result = await this.perform("call", { transaction, blockTag });
1633
1634 // CCIP Read request via OffchainLookup(address,string[],bytes,bytes4,bytes)
1635 if (attempt >= 0 && blockTag === "latest" && txSender != null && result.substring(0, 10) === "0x556f1830" && (hexDataLength(result) % 32 === 4)) {
1636 try {
1637 const data = hexDataSlice(result, 4);
1638
1639 // Check the sender of the OffchainLookup matches the transaction
1640 const sender = hexDataSlice(data, 0, 32);
1641 if (!BigNumber.from(sender).eq(txSender)) {
1642 logger.throwError("CCIP Read sender did not match", Logger.errors.CALL_EXCEPTION, {
1643 name: "OffchainLookup",
1644 signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
1645 transaction, data: result
1646 });
1647 }
1648
1649 // Read the URLs from the response
1650 const urls: Array<string> = [];
1651 const urlsOffset = BigNumber.from(hexDataSlice(data, 32, 64)).toNumber();
1652 const urlsLength = BigNumber.from(hexDataSlice(data, urlsOffset, urlsOffset + 32)).toNumber();
1653 const urlsData = hexDataSlice(data, urlsOffset + 32);
1654 for (let u = 0; u < urlsLength; u++) {
1655 const url = _parseString(urlsData, u * 32);
1656 if (url == null) {
1657 logger.throwError("CCIP Read contained corrupt URL string", Logger.errors.CALL_EXCEPTION, {
1658 name: "OffchainLookup",
1659 signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
1660 transaction, data: result
1661 });
1662 }
1663 urls.push(url);
1664 }
1665
1666 // Get the CCIP calldata to forward
1667 const calldata = _parseBytes(data, 64);
1668
1669 // Get the callbackSelector (bytes4)
1670 if (!BigNumber.from(hexDataSlice(data, 100, 128)).isZero()) {
1671 logger.throwError("CCIP Read callback selector included junk", Logger.errors.CALL_EXCEPTION, {
1672 name: "OffchainLookup",
1673 signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
1674 transaction, data: result
1675 });
1676 }
1677 const callbackSelector = hexDataSlice(data, 96, 100);
1678
1679 // Get the extra data to send back to the contract as context
1680 const extraData = _parseBytes(data, 128);
1681
1682 const ccipResult = await this.ccipReadFetch(<Transaction>transaction, calldata, urls);
1683 if (ccipResult == null) {
1684 logger.throwError("CCIP Read disabled or provided no URLs", Logger.errors.CALL_EXCEPTION, {
1685 name: "OffchainLookup",
1686 signature: "OffchainLookup(address,string[],bytes,bytes4,bytes)",
1687 transaction, data: result
1688 });
1689 }
1690
1691 const tx = {
1692 to: txSender,
1693 data: hexConcat([ callbackSelector, encodeBytes([ ccipResult, extraData ]) ])
1694 };
1695
1696 return this._call(tx, blockTag, attempt + 1);
1697
1698 } catch (error) {
1699 if (error.code === Logger.errors.SERVER_ERROR) { throw error; }
1700 }
1701 }
1702
1703 try {
1704 return hexlify(result);
1705 } catch (error) {
1706 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1707 method: "call",
1708 params: { transaction, blockTag }, result, error
1709 });
1710 }
1711
1712 }
1713
1714 async call(transaction: Deferrable<TransactionRequest>, blockTag?: BlockTag | Promise<BlockTag>): Promise<string> {
1715 await this.getNetwork();
1716 const resolved = await resolveProperties({
1717 transaction: this._getTransactionRequest(transaction),
1718 blockTag: this._getBlockTag(blockTag),
1719 ccipReadEnabled: Promise.resolve(transaction.ccipReadEnabled)
1720 });
1721 return this._call(resolved.transaction, resolved.blockTag, resolved.ccipReadEnabled ? 0: -1);
1722 }
1723
1724 async estimateGas(transaction: Deferrable<TransactionRequest>): Promise<BigNumber> {
1725 await this.getNetwork();
1726 const params = await resolveProperties({
1727 transaction: this._getTransactionRequest(transaction)
1728 });
1729
1730 const result = await this.perform("estimateGas", params);
1731 try {
1732 return BigNumber.from(result);
1733 } catch (error) {
1734 return logger.throwError("bad result from backend", Logger.errors.SERVER_ERROR, {
1735 method: "estimateGas",
1736 params, result, error
1737 });
1738 }
1739 }
1740
1741 async _getAddress(addressOrName: string | Promise<string>): Promise<string> {
1742 addressOrName = await addressOrName;
1743 if (typeof(addressOrName) !== "string") {
1744 logger.throwArgumentError("invalid address or ENS name", "name", addressOrName);
1745 }
1746
1747 const address = await this.resolveName(addressOrName);
1748 if (address == null) {
1749 logger.throwError("ENS name not configured", Logger.errors.UNSUPPORTED_OPERATION, {
1750 operation: `resolveName(${ JSON.stringify(addressOrName) })`
1751 });
1752 }
1753 return address;
1754 }
1755
1756 async _getBlock(blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>, includeTransactions?: boolean): Promise<Block | BlockWithTransactions> {
1757 await this.getNetwork();
1758
1759 blockHashOrBlockTag = await blockHashOrBlockTag;
1760
1761 // If blockTag is a number (not "latest", etc), this is the block number
1762 let blockNumber = -128;
1763
1764 const params: { [key: string]: any } = {
1765 includeTransactions: !!includeTransactions
1766 };
1767
1768 if (isHexString(blockHashOrBlockTag, 32)) {
1769 params.blockHash = blockHashOrBlockTag;
1770 } else {
1771 try {
1772 params.blockTag = await this._getBlockTag(blockHashOrBlockTag);
1773 if (isHexString(params.blockTag)) {
1774 blockNumber = parseInt(params.blockTag.substring(2), 16);
1775 }
1776 } catch (error) {
1777 logger.throwArgumentError("invalid block hash or block tag", "blockHashOrBlockTag", blockHashOrBlockTag);
1778 }
1779 }
1780
1781 return poll(async () => {
1782 const block = await this.perform("getBlock", params);
1783
1784 // Block was not found
1785 if (block == null) {
1786
1787 // For blockhashes, if we didn't say it existed, that blockhash may
1788 // not exist. If we did see it though, perhaps from a log, we know
1789 // it exists, and this node is just not caught up yet.
1790 if (params.blockHash != null) {
1791 if (this._emitted["b:" + params.blockHash] == null) { return null; }
1792 }
1793
1794 // For block tags, if we are asking for a future block, we return null
1795 if (params.blockTag != null) {
1796 if (blockNumber > this._emitted.block) { return null; }
1797 }
1798
1799 // Retry on the next block
1800 return undefined;
1801 }
1802
1803 // Add transactions
1804 if (includeTransactions) {
1805 let blockNumber: number = null;
1806 for (let i = 0; i < block.transactions.length; i++) {
1807 const tx = block.transactions[i];
1808 if (tx.blockNumber == null) {
1809 tx.confirmations = 0;
1810
1811 } else if (tx.confirmations == null) {
1812 if (blockNumber == null) {
1813 blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
1814 }
1815
1816 // Add the confirmations using the fast block number (pessimistic)
1817 let confirmations = (blockNumber - tx.blockNumber) + 1;
1818 if (confirmations <= 0) { confirmations = 1; }
1819 tx.confirmations = confirmations;
1820 }
1821 }
1822
1823 const blockWithTxs: any = this.formatter.blockWithTransactions(block);
1824 blockWithTxs.transactions = blockWithTxs.transactions.map((tx: TransactionResponse) => this._wrapTransaction(tx));
1825 return blockWithTxs;
1826 }
1827
1828 return this.formatter.block(block);
1829
1830 }, { oncePoll: this });
1831 }
1832
1833 getBlock(blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>): Promise<Block> {
1834 return <Promise<Block>>(this._getBlock(blockHashOrBlockTag, false));
1835 }
1836
1837 getBlockWithTransactions(blockHashOrBlockTag: BlockTag | string | Promise<BlockTag | string>): Promise<BlockWithTransactions> {
1838 return <Promise<BlockWithTransactions>>(this._getBlock(blockHashOrBlockTag, true));
1839 }
1840
1841 async getTransaction(transactionHash: string | Promise<string>): Promise<TransactionResponse> {
1842 await this.getNetwork();
1843 transactionHash = await transactionHash;
1844
1845 const params = { transactionHash: this.formatter.hash(transactionHash, true) };
1846
1847 return poll(async () => {
1848 const result = await this.perform("getTransaction", params);
1849
1850 if (result == null) {
1851 if (this._emitted["t:" + transactionHash] == null) {
1852 return null;
1853 }
1854 return undefined;
1855 }
1856
1857 const tx = this.formatter.transactionResponse(result);
1858
1859 if (tx.blockNumber == null) {
1860 tx.confirmations = 0;
1861
1862 } else if (tx.confirmations == null) {
1863 const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
1864
1865 // Add the confirmations using the fast block number (pessimistic)
1866 let confirmations = (blockNumber - tx.blockNumber) + 1;
1867 if (confirmations <= 0) { confirmations = 1; }
1868 tx.confirmations = confirmations;
1869 }
1870
1871 return this._wrapTransaction(tx);
1872 }, { oncePoll: this });
1873 }
1874
1875 async getTransactionReceipt(transactionHash: string | Promise<string>): Promise<TransactionReceipt> {
1876 await this.getNetwork();
1877
1878 transactionHash = await transactionHash;
1879
1880 const params = { transactionHash: this.formatter.hash(transactionHash, true) };
1881
1882 return poll(async () => {
1883 const result = await this.perform("getTransactionReceipt", params);
1884
1885 if (result == null) {
1886 if (this._emitted["t:" + transactionHash] == null) {
1887 return null;
1888 }
1889 return undefined;
1890 }
1891
1892 // "geth-etc" returns receipts before they are ready
1893 if (result.blockHash == null) { return undefined; }
1894
1895 const receipt = this.formatter.receipt(result);
1896
1897 if (receipt.blockNumber == null) {
1898 receipt.confirmations = 0;
1899
1900 } else if (receipt.confirmations == null) {
1901 const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
1902
1903 // Add the confirmations using the fast block number (pessimistic)
1904 let confirmations = (blockNumber - receipt.blockNumber) + 1;
1905 if (confirmations <= 0) { confirmations = 1; }
1906 receipt.confirmations = confirmations;
1907 }
1908
1909 return receipt;
1910 }, { oncePoll: this });
1911 }
1912
1913 async getLogs(filter: Filter | FilterByBlockHash | Promise<Filter | FilterByBlockHash>): Promise<Array<Log>> {
1914 await this.getNetwork();
1915 const params = await resolveProperties({ filter: this._getFilter(filter) });
1916 const logs: Array<Log> = await this.perform("getLogs", params);
1917 logs.forEach((log) => {
1918 if (log.removed == null) { log.removed = false; }
1919 });
1920 return Formatter.arrayOf(this.formatter.filterLog.bind(this.formatter))(logs);
1921 }
1922
1923 async getEtherPrice(): Promise<number> {
1924 await this.getNetwork();
1925 return this.perform("getEtherPrice", { });
1926 }
1927
1928 async _getBlockTag(blockTag: BlockTag | Promise<BlockTag>): Promise<BlockTag> {
1929 blockTag = await blockTag;
1930
1931 if (typeof(blockTag) === "number" && blockTag < 0) {
1932 if (blockTag % 1) {
1933 logger.throwArgumentError("invalid BlockTag", "blockTag", blockTag);
1934 }
1935
1936 let blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
1937 blockNumber += blockTag;
1938 if (blockNumber < 0) { blockNumber = 0; }
1939 return this.formatter.blockTag(blockNumber)
1940 }
1941
1942 return this.formatter.blockTag(blockTag);
1943 }
1944
1945
1946 async getResolver(name: string): Promise<null | Resolver> {
1947 let currentName = name;
1948 while (true) {
1949 if (currentName === "" || currentName === ".") { return null; }
1950
1951 // Optimization since the eth node cannot change and does
1952 // not have a wildcard resolver
1953 if (name !== "eth" && currentName === "eth") { return null; }
1954
1955 // Check the current node for a resolver
1956 const addr = await this._getResolver(currentName, "getResolver");
1957
1958 // Found a resolver!
1959 if (addr != null) {
1960 const resolver = new Resolver(this, addr, name);
1961
1962 // Legacy resolver found, using EIP-2544 so it isn't safe to use
1963 if (currentName !== name && !(await resolver.supportsWildcard())) { return null; }
1964
1965 return resolver;
1966 }
1967
1968 // Get the parent node
1969 currentName = currentName.split(".").slice(1).join(".");
1970 }
1971
1972 }
1973
1974 async _getResolver(name: string, operation?: string): Promise<string> {
1975 if (operation == null) { operation = "ENS"; }
1976
1977 const network = await this.getNetwork();
1978
1979 // No ENS...
1980 if (!network.ensAddress) {
1981 logger.throwError(
1982 "network does not support ENS",
1983 Logger.errors.UNSUPPORTED_OPERATION,
1984 { operation, network: network.name }
1985 );
1986 }
1987
1988 try {
1989 // keccak256("resolver(bytes32)")
1990 const addrData = await this.call({
1991 to: network.ensAddress,
1992 data: ("0x0178b8bf" + namehash(name).substring(2))
1993 });
1994 return this.formatter.callAddress(addrData);
1995 } catch (error) {
1996 // ENS registry cannot throw errors on resolver(bytes32)
1997 }
1998
1999 return null;
2000 }
2001
2002 async resolveName(name: string | Promise<string>): Promise<null | string> {
2003 name = await name;
2004
2005 // If it is already an address, nothing to resolve
2006 try {
2007 return Promise.resolve(this.formatter.address(name));
2008 } catch (error) {
2009 // If is is a hexstring, the address is bad (See #694)
2010 if (isHexString(name)) { throw error; }
2011 }
2012
2013 if (typeof(name) !== "string") {
2014 logger.throwArgumentError("invalid ENS name", "name", name);
2015 }
2016
2017 // Get the addr from the resolver
2018 const resolver = await this.getResolver(name);
2019 if (!resolver) { return null; }
2020
2021 return await resolver.getAddress();
2022 }
2023
2024 async lookupAddress(address: string | Promise<string>): Promise<null | string> {
2025 address = await address;
2026 address = this.formatter.address(address);
2027
2028 const node = address.substring(2).toLowerCase() + ".addr.reverse";
2029
2030 const resolverAddr = await this._getResolver(node, "lookupAddress");
2031 if (resolverAddr == null) { return null; }
2032
2033 // keccak("name(bytes32)")
2034 const name = _parseString(await this.call({
2035 to: resolverAddr,
2036 data: ("0x691f3431" + namehash(node).substring(2))
2037 }), 0);
2038
2039 const addr = await this.resolveName(name);
2040 if (addr != address) { return null; }
2041
2042 return name;
2043 }
2044
2045 async getAvatar(nameOrAddress: string): Promise<null | string> {
2046 let resolver: Resolver = null;
2047 if (isHexString(nameOrAddress)) {
2048 // Address; reverse lookup
2049 const address = this.formatter.address(nameOrAddress);
2050
2051 const node = address.substring(2).toLowerCase() + ".addr.reverse";
2052
2053 const resolverAddress = await this._getResolver(node, "getAvatar");
2054 if (!resolverAddress) { return null; }
2055
2056 // Try resolving the avatar against the addr.reverse resolver
2057 resolver = new Resolver(this, resolverAddress, node);
2058 try {
2059 const avatar = await resolver.getAvatar();
2060 if (avatar) { return avatar.url; }
2061 } catch (error) {
2062 if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; }
2063 }
2064
2065 // Try getting the name and performing forward lookup; allowing wildcards
2066 try {
2067 // keccak("name(bytes32)")
2068 const name = _parseString(await this.call({
2069 to: resolverAddress,
2070 data: ("0x691f3431" + namehash(node).substring(2))
2071 }), 0);
2072 resolver = await this.getResolver(name);
2073 } catch (error) {
2074 if (error.code !== Logger.errors.CALL_EXCEPTION) { throw error; }
2075 return null;
2076 }
2077
2078 } else {
2079 // ENS name; forward lookup with wildcard
2080 resolver = await this.getResolver(nameOrAddress);
2081 if (!resolver) { return null; }
2082 }
2083
2084 const avatar = await resolver.getAvatar();
2085 if (avatar == null) { return null; }
2086
2087 return avatar.url;
2088 }
2089
2090 perform(method: string, params: any): Promise<any> {
2091 return logger.throwError(method + " not implemented", Logger.errors.NOT_IMPLEMENTED, { operation: method });
2092 }
2093
2094 _startEvent(event: Event): void {
2095 this.polling = (this._events.filter((e) => e.pollable()).length > 0);
2096 }
2097
2098 _stopEvent(event: Event): void {
2099 this.polling = (this._events.filter((e) => e.pollable()).length > 0);
2100 }
2101
2102 _addEventListener(eventName: EventType, listener: Listener, once: boolean): this {
2103 const event = new Event(getEventTag(eventName), listener, once)
2104 this._events.push(event);
2105 this._startEvent(event);
2106
2107 return this;
2108 }
2109
2110 on(eventName: EventType, listener: Listener): this {
2111 return this._addEventListener(eventName, listener, false);
2112 }
2113
2114 once(eventName: EventType, listener: Listener): this {
2115 return this._addEventListener(eventName, listener, true);
2116 }
2117
2118
2119 emit(eventName: EventType, ...args: Array<any>): boolean {
2120 let result = false;
2121
2122 let stopped: Array<Event> = [ ];
2123
2124 let eventTag = getEventTag(eventName);
2125 this._events = this._events.filter((event) => {
2126 if (event.tag !== eventTag) { return true; }
2127
2128 setTimeout(() => {
2129 event.listener.apply(this, args);
2130 }, 0);
2131
2132 result = true;
2133
2134 if (event.once) {
2135 stopped.push(event);
2136 return false;
2137 }
2138
2139 return true;
2140 });
2141
2142 stopped.forEach((event) => { this._stopEvent(event); });
2143
2144 return result;
2145 }
2146
2147 listenerCount(eventName?: EventType): number {
2148 if (!eventName) { return this._events.length; }
2149
2150 let eventTag = getEventTag(eventName);
2151 return this._events.filter((event) => {
2152 return (event.tag === eventTag);
2153 }).length;
2154 }
2155
2156 listeners(eventName?: EventType): Array<Listener> {
2157 if (eventName == null) {
2158 return this._events.map((event) => event.listener);
2159 }
2160
2161 let eventTag = getEventTag(eventName);
2162 return this._events
2163 .filter((event) => (event.tag === eventTag))
2164 .map((event) => event.listener);
2165 }
2166
2167 off(eventName: EventType, listener?: Listener): this {
2168 if (listener == null) {
2169 return this.removeAllListeners(eventName);
2170 }
2171
2172 const stopped: Array<Event> = [ ];
2173
2174 let found = false;
2175
2176 let eventTag = getEventTag(eventName);
2177 this._events = this._events.filter((event) => {
2178 if (event.tag !== eventTag || event.listener != listener) { return true; }
2179 if (found) { return true; }
2180 found = true;
2181 stopped.push(event);
2182 return false;
2183 });
2184
2185 stopped.forEach((event) => { this._stopEvent(event); });
2186
2187 return this;
2188 }
2189
2190 removeAllListeners(eventName?: EventType): this {
2191 let stopped: Array<Event> = [ ];
2192 if (eventName == null) {
2193 stopped = this._events;
2194
2195 this._events = [ ];
2196 } else {
2197 const eventTag = getEventTag(eventName);
2198 this._events = this._events.filter((event) => {
2199 if (event.tag !== eventTag) { return true; }
2200 stopped.push(event);
2201 return false;
2202 });
2203 }
2204
2205 stopped.forEach((event) => { this._stopEvent(event); });
2206
2207 return this;
2208 }
2209}
2210
\No newline at end of file