diff --git a/packages/providers/src.ts/base-provider.ts b/packages/providers/src.ts/base-provider.ts index 41433bd635..1e5f46de94 100644 --- a/packages/providers/src.ts/base-provider.ts +++ b/packages/providers/src.ts/base-provider.ts @@ -41,13 +41,13 @@ function serializeTopics(topics: Array>): string { if (Array.isArray(topic)) { // Only track unique OR-topics - let unique: { [ topic: string ]: boolean } = { } + const unique: { [ topic: string ]: boolean } = { } topic.forEach((topic) => { unique[checkTopic(topic)] = true; }); // The order of OR-topics does not matter - let sorted = Object.keys(unique); + const sorted = Object.keys(unique); sorted.sort(); return sorted.join("|"); @@ -133,6 +133,7 @@ let defaultFormatter: Formatter = null; let nextPollId = 1; + export class BaseProvider extends Provider { _network: Network; @@ -140,7 +141,7 @@ export class BaseProvider extends Provider { formatter: Formatter; - // To help mitigate the eventually conssitent nature of the blockchain + // To help mitigate the eventually consistent nature of the blockchain // we keep a mapping of events we emit. If we emit an event X, we expect // that a user should be able to query for that event in the callback, // if the node returns null, we stall the response until we get back a @@ -161,6 +162,9 @@ export class BaseProvider extends Provider { _fastBlockNumberPromise: Promise; _fastQueryDate: number; + _maxInternalBlockNumber: number; + _internalBlockNumber: Promise<{ blockNumber: number, reqTime: number, respTime: number }>; + /** * ready @@ -190,7 +194,7 @@ export class BaseProvider extends Provider { this.ready.catch((error) => { }); } else { - let knownNetwork = getStatic<(network: Networkish) => Network>(new.target, "getNetwork")(network); + const knownNetwork = getStatic<(network: Networkish) => Network>(new.target, "getNetwork")(network); if (knownNetwork) { defineReadOnly(this, "_network", knownNetwork); defineReadOnly(this, "ready", Promise.resolve(this._network)); @@ -200,6 +204,8 @@ export class BaseProvider extends Provider { } } + this._maxInternalBlockNumber = -1024; + this._lastBlockNumber = -2; // Events being listened to @@ -223,110 +229,134 @@ export class BaseProvider extends Provider { return getNetwork((network == null) ? "homestead": network); } - poll(): void { - let pollId = nextPollId++; + async _getInternalBlockNumber(maxAge: number): Promise { + await this.ready; + + const internalBlockNumber = this._internalBlockNumber; + + if (maxAge > 0 && this._internalBlockNumber) { + const result = await internalBlockNumber; + if ((getTime() - result.respTime) <= maxAge) { + return result.blockNumber; + } + } + + const reqTime = getTime(); + this._internalBlockNumber = this.perform("getBlockNumber", { }).then((blockNumber) => { + const respTime = getTime(); + blockNumber = BigNumber.from(blockNumber).toNumber(); + if (blockNumber < this._maxInternalBlockNumber) { blockNumber = this._maxInternalBlockNumber; } + this._maxInternalBlockNumber = blockNumber; + this._setFastBlockNumber(blockNumber); // @TODO: Still need this? + return { blockNumber, reqTime, respTime }; + }); + + return (await this._internalBlockNumber).blockNumber; + } + + async poll(): Promise { + const pollId = nextPollId++; this.emit("willPoll", pollId); // Track all running promises, so we can trigger a post-poll once they are complete - let runners: Array> = []; + const runners: Array> = []; - this.getBlockNumber().then((blockNumber) => { - this._setFastBlockNumber(blockNumber); + const blockNumber = await this._getInternalBlockNumber(100 + this.pollingInterval / 2); - // If the block has not changed, meh. - if (blockNumber === this._lastBlockNumber) { return; } + this._setFastBlockNumber(blockNumber); - // First polling cycle, trigger a "block" events - if (this._emitted.block === -2) { - this._emitted.block = blockNumber - 1; - } + // If the block has not changed, meh. + if (blockNumber === this._lastBlockNumber) { return; } - // Notify all listener for each block that has passed - for (let i = (this._emitted.block) + 1; i <= blockNumber; i++) { - this.emit("block", i); - } + // First polling cycle, trigger a "block" events + if (this._emitted.block === -2) { + this._emitted.block = blockNumber - 1; + } - // The emitted block was updated, check for obsolete events - if ((this._emitted.block) !== blockNumber) { - this._emitted.block = blockNumber; + // Notify all listener for each block that has passed + for (let i = (this._emitted.block) + 1; i <= blockNumber; i++) { + this.emit("block", i); + } - Object.keys(this._emitted).forEach((key) => { - // The block event does not expire - if (key === "block") { return; } + // The emitted block was updated, check for obsolete events + if ((this._emitted.block) !== blockNumber) { + this._emitted.block = blockNumber; - // The block we were at when we emitted this event - let eventBlockNumber = this._emitted[key]; + Object.keys(this._emitted).forEach((key) => { + // The block event does not expire + if (key === "block") { return; } - // We cannot garbage collect pending transactions or blocks here - // They should be garbage collected by the Provider when setting - // "pending" events - if (eventBlockNumber === "pending") { return; } + // The block we were at when we emitted this event + const eventBlockNumber = this._emitted[key]; - // Evict any transaction hashes or block hashes over 12 blocks - // old, since they should not return null anyways - if (blockNumber - eventBlockNumber > 12) { - delete this._emitted[key]; - } - }); - } + // We cannot garbage collect pending transactions or blocks here + // They should be garbage collected by the Provider when setting + // "pending" events + if (eventBlockNumber === "pending") { return; } - // First polling cycle - if (this._lastBlockNumber === -2) { - this._lastBlockNumber = blockNumber - 1; - } + // Evict any transaction hashes or block hashes over 12 blocks + // old, since they should not return null anyways + if (blockNumber - eventBlockNumber > 12) { + delete this._emitted[key]; + } + }); + } - // Find all transaction hashes we are waiting on - this._events.forEach((event) => { - let comps = event.tag.split(":"); - switch (comps[0]) { - case "tx": { - let hash = comps[1]; - let runner = this.getTransactionReceipt(hash).then((receipt) => { - if (!receipt || receipt.blockNumber == null) { return null; } - this._emitted["t:" + hash] = receipt.blockNumber; - this.emit(hash, receipt); - return null; - }).catch((error: Error) => { this.emit("error", error); }); - - runners.push(runner); - - break; - } + // First polling cycle + if (this._lastBlockNumber === -2) { + this._lastBlockNumber = blockNumber - 1; + } - case "filter": { - let topics = deserializeTopics(comps[2]); - let filter = { - address: comps[1], - fromBlock: this._lastBlockNumber + 1, - toBlock: blockNumber, - topics: topics - } - if (!filter.address) { delete filter.address; } - let runner = this.getLogs(filter).then((logs) => { - if (logs.length === 0) { return; } - logs.forEach((log: Log) => { - this._emitted["b:" + log.blockHash] = log.blockNumber; - this._emitted["t:" + log.transactionHash] = log.blockNumber; - this.emit(filter, log); - }); - return null; - }).catch((error: Error) => { this.emit("error", error); }); - - runners.push(runner); - - break; - } + // Find all transaction hashes we are waiting on + this._events.forEach((event) => { + const comps = event.tag.split(":"); + switch (comps[0]) { + case "tx": { + const hash = comps[1]; + let runner = this.getTransactionReceipt(hash).then((receipt) => { + if (!receipt || receipt.blockNumber == null) { return null; } + this._emitted["t:" + hash] = receipt.blockNumber; + this.emit(hash, receipt); + return null; + }).catch((error: Error) => { this.emit("error", error); }); + + runners.push(runner); + + break; } - }); - this._lastBlockNumber = blockNumber; + case "filter": { + const topics = deserializeTopics(comps[2]); + const filter = { + address: comps[1], + fromBlock: this._lastBlockNumber + 1, + toBlock: blockNumber, + topics: topics + } + if (!filter.address) { delete filter.address; } + const runner = this.getLogs(filter).then((logs) => { + if (logs.length === 0) { return; } + logs.forEach((log: Log) => { + this._emitted["b:" + log.blockHash] = log.blockNumber; + this._emitted["t:" + log.transactionHash] = log.blockNumber; + this.emit(filter, log); + }); + return null; + }).catch((error: Error) => { this.emit("error", error); }); + runners.push(runner); + + break; + } + } + }); - return null; - }).catch((error: Error) => { }); + this._lastBlockNumber = blockNumber; Promise.all(runners).then(() => { this.emit("didPoll", pollId); }); + + return null; } resetEventsBlock(blockNumber: number): void { @@ -354,6 +384,7 @@ export class BaseProvider extends Provider { setTimeout(() => { if (value && !this._poller) { this._poller = setInterval(this.poll.bind(this), this.pollingInterval); + this.poll(); } else if (!value && this._poller) { clearInterval(this._poller); @@ -380,7 +411,7 @@ export class BaseProvider extends Provider { } _getFastBlockNumber(): Promise { - let now = getTime(); + const now = getTime(); // Stale block number, request a newer value if ((now - this._fastQueryDate) > 2 * this._pollingInterval) { @@ -413,15 +444,17 @@ export class BaseProvider extends Provider { // @TODO: Add .poller which must be an event emitter with a 'start', 'stop' and 'block' event; // this will be used once we move to the WebSocket or other alternatives to polling - waitForTransaction(transactionHash: string, confirmations?: number): Promise { + async waitForTransaction(transactionHash: string, confirmations?: number): Promise { if (confirmations == null) { confirmations = 1; } - if (confirmations === 0) { - return this.getTransactionReceipt(transactionHash); - } + const receipt = await this.getTransactionReceipt(transactionHash); + // Receipt is already good + if (receipt.confirmations >= confirmations) { return receipt; } + + // Poll until the receipt is good... return new Promise((resolve) => { - let handler = (receipt: TransactionReceipt) => { + const handler = (receipt: TransactionReceipt) => { if (receipt.confirmations < confirmations) { return; } this.removeListener(transactionHash, handler); resolve(receipt); @@ -430,75 +463,57 @@ export class BaseProvider extends Provider { }); } - _runPerform(method: string, params: { [ key: string ]: () => any }): Promise { - return this.ready.then(() => { - // Execute all the functions now that we are "ready" - Object.keys(params).forEach((key) => { - params[key] = params[key](); - }); - return resolveProperties(params).then((params) => { - return this.perform(method, params); - }); - }); - } - getBlockNumber(): Promise { - return this._runPerform("getBlockNumber", { }).then((result) => { - let value = parseInt(result); - if (value != result) { throw new Error("invalid response - getBlockNumber"); } - this._setFastBlockNumber(value); - return value; - }); + return this._getInternalBlockNumber(0); } - getGasPrice(): Promise { - return this._runPerform("getGasPrice", { }).then((result) => { - return BigNumber.from(result); - }); + async getGasPrice(): Promise { + await this.ready; + return BigNumber.from(await this.perform("getGasPrice", { })); } - getBalance(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { - return this._runPerform("getBalance", { - address: () => this._getAddress(addressOrName), - blockTag: () => this._getBlockTag(blockTag) - }).then((result) => { - return BigNumber.from(result); + async getBalance(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + address: this._getAddress(addressOrName), + blockTag: this._getBlockTag(blockTag) }); + return BigNumber.from(await this.perform("getBalance", params)); } - getTransactionCount(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { - return this._runPerform("getTransactionCount", { - address: () => this._getAddress(addressOrName), - blockTag: () => this._getBlockTag(blockTag) - }).then((result) => { - return BigNumber.from(result).toNumber(); + async getTransactionCount(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + address: this._getAddress(addressOrName), + blockTag: this._getBlockTag(blockTag) }); + return BigNumber.from(await this.perform("getTransactionCount", params)).toNumber(); } - getCode(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { - return this._runPerform("getCode", { - address: () => this._getAddress(addressOrName), - blockTag: () => this._getBlockTag(blockTag) - }).then((result) => { - return hexlify(result); + async getCode(addressOrName: string | Promise, blockTag?: BlockTag | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + address: this._getAddress(addressOrName), + blockTag: this._getBlockTag(blockTag) }); + return hexlify(await this.perform("getCode", params)); } - getStorageAt(addressOrName: string | Promise, position: BigNumberish | Promise, blockTag?: BlockTag | Promise): Promise { - return this._runPerform("getStorageAt", { - address: () => this._getAddress(addressOrName), - blockTag: () => this._getBlockTag(blockTag), - position: () => Promise.resolve(position).then((p) => hexValue(p)) - }).then((result) => { - return hexlify(result); + async getStorageAt(addressOrName: string | Promise, position: BigNumberish | Promise, blockTag?: BlockTag | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + address: this._getAddress(addressOrName), + blockTag: this._getBlockTag(blockTag), + position: Promise.resolve(position).then((p) => hexValue(p)) }); + return hexlify(await this.perform("getStorageAt", params)); } // This should be called by any subclass wrapping a TransactionResponse _wrapTransaction(tx: Transaction, hash?: string): TransactionResponse { if (hash != null && hexDataLength(hash) !== 32) { throw new Error("invalid response - sendTransaction"); } - let result = tx; + const result = tx; // Check the hash we expect is the same as the hash the server reported if (hash != null && tx.hash !== hash) { @@ -506,7 +521,7 @@ export class BaseProvider extends Provider { } // @TODO: (confirmations? number, timeout? number) - result.wait = (confirmations?: number) => { + result.wait = async (confirmations?: number) => { // We know this transaction *must* exist (whether it gets mined is // another story), so setting an emitted value forces us to @@ -515,179 +530,168 @@ export class BaseProvider extends Provider { this._emitted["t:" + tx.hash] = "pending"; } - return this.waitForTransaction(tx.hash, confirmations).then((receipt) => { - if (receipt == null && confirmations === 0) { return null; } + const receipt = await this.waitForTransaction(tx.hash, confirmations) + if (receipt == null && confirmations === 0) { return null; } - // No longer pending, allow the polling loop to garbage collect this - this._emitted["t:" + tx.hash] = receipt.blockNumber; + // No longer pending, allow the polling loop to garbage collect this + this._emitted["t:" + tx.hash] = receipt.blockNumber; - if (receipt.status === 0) { - logger.throwError("transaction failed", Logger.errors.CALL_EXCEPTION, { - transactionHash: tx.hash, - transaction: tx, - receipt: receipt - }); - } - return receipt; - }); + if (receipt.status === 0) { + logger.throwError("transaction failed", Logger.errors.CALL_EXCEPTION, { + transactionHash: tx.hash, + transaction: tx, + receipt: receipt + }); + } + return receipt; }; return result; } - sendTransaction(signedTransaction: string | Promise): Promise { - return this._runPerform("sendTransaction", { - signedTransaction: () => Promise.resolve(signedTransaction).then(t => hexlify(t)) - }).then((result) => { - return this._wrapTransaction(this.formatter.transaction(signedTransaction), result); - }, (error) => { - error.transaction = this.formatter.transaction(signedTransaction); - if (error.transaction.hash) { - (error).transactionHash = error.transaction.hash; - } + async sendTransaction(signedTransaction: string | Promise): Promise { + await this.ready; + const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t)); + const tx = this.formatter.transaction(signedTransaction); + try { + const hash = await this.perform("sendTransaction", { signedTransaction: hexTx }); + return this._wrapTransaction(tx, hash); + } catch (error) { + (error).transaction = tx; + (error).transactionHash = tx.hash; throw error; - }); + } } - _getTransactionRequest(transaction: TransactionRequest | Promise): Promise { - return Promise.resolve(transaction).then((t: any) => { - let tx: any = { }; - ["from", "to"].forEach((key) => { - if (t[key] == null) { return; } - tx[key] = Promise.resolve(t[key]).then(a => (a ? this._getAddress(a): null)) - }); - ["data", "gasLimit", "gasPrice", "value"].forEach((key) => { - if (t[key] == null) { return; } - tx[key] = t[key]; - }); - return resolveProperties(tx).then((t) => this.formatter.transactionRequest(t)); + async _getTransactionRequest(transaction: TransactionRequest | Promise): Promise { + const values: any = await transaction; + + const tx: any = { }; + + ["from", "to"].forEach((key) => { + if (values[key] == null) { return; } + tx[key] = Promise.resolve(values[key]).then((v) => (v ? this._getAddress(v): null)) }); - } - _getFilter(filter: Filter | FilterByBlockHash | Promise): Promise { - return Promise.resolve(filter).then((f: any) => { - let filter: any = { }; + ["gasLimit", "gasPrice", "value"].forEach((key) => { + if (values[key] == null) { return; } + tx[key] = Promise.resolve(values[key]).then((v) => (v ? BigNumber.from(v): null)); + }); - if (f.address != null) { - filter.address = this._getAddress(f.address); - } + ["data"].forEach((key) => { + if (values[key] == null) { return; } + tx[key] = Promise.resolve(values[key]).then((v) => (v ? hexlify(v): null)); + }); - if (f.topics) { - filter.topics = f.topics; - } + return this.formatter.transactionRequest(await resolveProperties(tx)); + } - if (f.blockHash != null) { - filter.blockHash = f.blockHash; - } + async _getFilter(filter: Filter | FilterByBlockHash | Promise): Promise { + if (filter instanceof Promise) { filter = await filter; } - ["fromBlock", "toBlock"].forEach((key) => { - if (f[key] == null) { return; } - filter[key] = this._getBlockTag(f[key]); - }); + const result: any = { }; - return resolveProperties(filter).then((f) => this.formatter.filter(f)); + if (filter.address != null) { + result.address = this._getAddress(filter.address); + } + + ["blockHash", "topics"].forEach((key) => { + if ((filter)[key] == null) { return; } + result[key] = (filter)[key]; + }); + + ["fromBlock", "toBlock"].forEach((key) => { + if ((filter)[key] == null) { return; } + result[key] = this._getBlockTag((filter)[key]); }); + + return this.formatter.filter(await resolveProperties(filter)); } - call(transaction: TransactionRequest | Promise, blockTag?: BlockTag | Promise): Promise { - return this._runPerform("call", { - transaction: () => this._getTransactionRequest(transaction), - blockTag: () => this._getBlockTag(blockTag) - }).then((result) => { - return hexlify(result); + async call(transaction: TransactionRequest | Promise, blockTag?: BlockTag | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + transaction: this._getTransactionRequest(transaction), + blockTag: this._getBlockTag(blockTag) }); + return hexlify(await this.perform("call", params)); } - estimateGas(transaction: TransactionRequest | Promise) { - return this._runPerform("estimateGas", { - transaction: () => this._getTransactionRequest(transaction) - }).then((result) => { - return BigNumber.from(result); + async estimateGas(transaction: TransactionRequest | Promise): Promise { + await this.ready; + const params = await resolveProperties({ + transaction: this._getTransactionRequest(transaction) }); + return BigNumber.from(await this.perform("estimateGas", params)); } - _getAddress(addressOrName: string | Promise): Promise { - return this.resolveName(addressOrName).then((address) => { - if (address == null) { - logger.throwError("ENS name not configured", Logger.errors.UNSUPPORTED_OPERATION, { - operation: `resolveName(${ JSON.stringify(addressOrName) })` - }); - } - return address; - }); + async _getAddress(addressOrName: string | Promise): Promise { + const address = await this.resolveName(addressOrName); + if (address == null) { + logger.throwError("ENS name not configured", Logger.errors.UNSUPPORTED_OPERATION, { + operation: `resolveName(${ JSON.stringify(addressOrName) })` + }); + } + return address; } - _getBlock(blockHashOrBlockTag: BlockTag | string | Promise, includeTransactions?: boolean): Promise { + async _getBlock(blockHashOrBlockTag: BlockTag | string | Promise, includeTransactions?: boolean): Promise { + await this.ready; + if (blockHashOrBlockTag instanceof Promise) { - return blockHashOrBlockTag.then((b) => this._getBlock(b, includeTransactions)); + blockHashOrBlockTag = await blockHashOrBlockTag; } - return this.ready.then(() => { - let blockHashOrBlockTagPromise: Promise = null; - if (isHexString(blockHashOrBlockTag, 32)) { - blockHashOrBlockTagPromise = Promise.resolve(blockHashOrBlockTag); - } else { - blockHashOrBlockTagPromise = this._getBlockTag(blockHashOrBlockTag); - } + // If blockTag is a number (not "latest", etc), this is the block number + let blockNumber = -128; - return blockHashOrBlockTagPromise.then((blockHashOrBlockTag) => { - let params: { [key: string]: any } = { - includeTransactions: !!includeTransactions - }; - - // Exactly one of blockHash or blockTag will be set - let blockHash: string = null; - let blockTag: string = null; - - // If blockTag is a number (not "latest", etc), this is the block number - let blockNumber = -128; - - if (isHexString(blockHashOrBlockTag, 32)) { - params.blockHash = blockHashOrBlockTag; - } else { - try { - params.blockTag = this.formatter.blockTag(blockHashOrBlockTag); - if (isHexString(params.blockTag)) { - blockNumber = parseInt(params.blockTag.substring(2), 16); - } - } catch (error) { - logger.throwArgumentError("invalid block hash or block tag", "blockHashOrBlockTag", blockHashOrBlockTag); - } + const params: { [key: string]: any } = { + includeTransactions: !!includeTransactions + }; + + if (isHexString(blockHashOrBlockTag, 32)) { + params.blockHash = blockHashOrBlockTag; + } else { + try { + params.blockTag = this.formatter.blockTag(await this._getBlockTag(blockHashOrBlockTag)); + if (isHexString(params.blockTag)) { + blockNumber = parseInt(params.blockTag.substring(2), 16); } + } catch (error) { + logger.throwArgumentError("invalid block hash or block tag", "blockHashOrBlockTag", blockHashOrBlockTag); + } + } - return poll(() => { - return this.perform("getBlock", params).then((block) => { + return poll(async () => { + const block = await this.perform("getBlock", params); - // Block was not found - if (block == null) { + // Block was not found + if (block == null) { - // For blockhashes, if we didn't say it existed, that blockhash may - // not exist. If we did see it though, perhaps from a log, we know - // it exists, and this node is just not caught up yet. - if (blockHash) { - if (this._emitted["b:" + blockHash] == null) { return null; } - } + // For blockhashes, if we didn't say it existed, that blockhash may + // not exist. If we did see it though, perhaps from a log, we know + // it exists, and this node is just not caught up yet. + if (params.blockHash != null) { + if (this._emitted["b:" + params.blockHash] == null) { return null; } + } - // For block tags, if we are asking for a future block, we return null - if (blockTag) { - if (blockNumber > this._emitted.block) { return null; } - } + // For block tags, if we are asking for a future block, we return null + if (params.blockTag != null) { + if (blockNumber > this._emitted.block) { return null; } + } - // Retry on the next block - return undefined; - } + // Retry on the next block + return undefined; + } - // Add transactions - if (includeTransactions) { - return this.formatter.blockWithTransactions(block); - } + // Add transactions + if (includeTransactions) { + return this.formatter.blockWithTransactions(block); + } - return this.formatter.block(block); - }); - }, { onceBlock: this }); - }); - }); + return this.formatter.block(block); + }, { onceBlock: this }); } getBlock(blockHashOrBlockTag: BlockTag | string | Promise): Promise { @@ -698,101 +702,93 @@ export class BaseProvider extends Provider { return >(this._getBlock(blockHashOrBlockTag, true)); } - getTransaction(transactionHash: string): Promise { - return this.ready.then(() => { - return resolveProperties({ transactionHash: transactionHash }).then(({ transactionHash }) => { - let params = { transactionHash: this.formatter.hash(transactionHash, true) }; - return poll(() => { - return this.perform("getTransaction", params).then((result) => { - if (result == null) { - if (this._emitted["t:" + transactionHash] == null) { - return null; - } - return undefined; - } + async getTransaction(transactionHash: string | Promise): Promise { + await this.ready; + if (transactionHash instanceof Promise) { transactionHash = await transactionHash; } - let tx = this.formatter.transactionResponse(result); + const params = { transactionHash: this.formatter.hash(transactionHash, true) }; - if (tx.blockNumber == null) { - tx.confirmations = 0; + return poll(async () => { + const result = await this.perform("getTransaction", params); - } else if (tx.confirmations == null) { - return this._getFastBlockNumber().then((blockNumber) => { + if (result == null) { + if (this._emitted["t:" + transactionHash] == null) { + return null; + } + return undefined; + } - // Add the confirmations using the fast block number (pessimistic) - let confirmations = (blockNumber - tx.blockNumber) + 1; - if (confirmations <= 0) { confirmations = 1; } - tx.confirmations = confirmations; + const tx = this.formatter.transactionResponse(result); - return this._wrapTransaction(tx); - }); - } + if (tx.blockNumber == null) { + tx.confirmations = 0; - return this._wrapTransaction(tx); - }); - }, { onceBlock: this }); - }); - }); + } else if (tx.confirmations == null) { + const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval); + + // Add the confirmations using the fast block number (pessimistic) + let confirmations = (blockNumber - tx.blockNumber) + 1; + if (confirmations <= 0) { confirmations = 1; } + tx.confirmations = confirmations; + } + + return this._wrapTransaction(tx); + }, { onceBlock: this }); } - getTransactionReceipt(transactionHash: string): Promise { - return this.ready.then(() => { - return resolveProperties({ transactionHash: transactionHash }).then(({ transactionHash }) => { - let params = { transactionHash: this.formatter.hash(transactionHash, true) }; - return poll(() => { - return this.perform("getTransactionReceipt", params).then((result) => { - if (result == null) { - if (this._emitted["t:" + transactionHash] == null) { - return null; - } - return undefined; - } + async getTransactionReceipt(transactionHash: string | Promise): Promise { + await this.ready; - // "geth-etc" returns receipts before they are ready - if (result.blockHash == null) { return undefined; } + if (transactionHash instanceof Promise) { transactionHash = await transactionHash; } - let receipt = this.formatter.receipt(result); + const params = { transactionHash: this.formatter.hash(transactionHash, true) }; - if (receipt.blockNumber == null) { - receipt.confirmations = 0; + return poll(async () => { + const result = await this.perform("getTransactionReceipt", params); - } else if (receipt.confirmations == null) { - return this._getFastBlockNumber().then((blockNumber) => { + if (result == null) { + if (this._emitted["t:" + transactionHash] == null) { + return null; + } + return undefined; + } - // Add the confirmations using the fast block number (pessimistic) - let confirmations = (blockNumber - receipt.blockNumber) + 1; - if (confirmations <= 0) { confirmations = 1; } - receipt.confirmations = confirmations; + // "geth-etc" returns receipts before they are ready + if (result.blockHash == null) { return undefined; } - return receipt; - }); - } + const receipt = this.formatter.receipt(result); - return receipt; - }); - }, { onceBlock: this }); - }); - }); - } + if (receipt.blockNumber == null) { + receipt.confirmations = 0; + } else if (receipt.confirmations == null) { + const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval); - getLogs(filter: Filter | FilterByBlockHash | Promise): Promise> { - return this._runPerform("getLogs", { - filter: () => this._getFilter(filter) - }).then((result) => { - return Formatter.arrayOf(this.formatter.filterLog.bind(this.formatter))(result); - }); + // Add the confirmations using the fast block number (pessimistic) + let confirmations = (blockNumber - receipt.blockNumber) + 1; + if (confirmations <= 0) { confirmations = 1; } + receipt.confirmations = confirmations; + } + + return receipt; + }, { onceBlock: this }); } - getEtherPrice(): Promise { - return this._runPerform("getEtherPrice", { }).then((result) => { - return result; - }); + async getLogs(filter: Filter | FilterByBlockHash | Promise): Promise> { + await this.ready; + const params = await resolveProperties({ filter: this._getFilter(filter) }); + const logs = await this.perform("getLogs", params); + return Formatter.arrayOf(this.formatter.filterLog.bind(this.formatter))(logs); + } + + async getEtherPrice(): Promise { + await this.ready; + return this.perform("getEtherPrice", { }); } - _getBlockTag(blockTag: BlockTag | Promise): Promise { + async _getBlockTag(blockTag: BlockTag | Promise): Promise { if (blockTag instanceof Promise) { - return blockTag.then((b) => this._getBlockTag(b)); + blockTag = await blockTag; } if (typeof(blockTag) === "number" && blockTag < 0) { @@ -800,46 +796,41 @@ export class BaseProvider extends Provider { logger.throwArgumentError("invalid BlockTag", "blockTag", blockTag); } - return this._getFastBlockNumber().then((bn) => { - bn += blockTag; - if (bn < 0) { bn = 0; } - return this.formatter.blockTag(bn) - }); + let blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval); + blockNumber += blockTag; + if (blockNumber < 0) { blockNumber = 0; } + return this.formatter.blockTag(blockNumber) } - return Promise.resolve(this.formatter.blockTag(blockTag)); + return this.formatter.blockTag(blockTag); } - _getResolver(name: string): Promise { + async _getResolver(name: string): Promise { // Get the resolver from the blockchain - return this.getNetwork().then((network) => { - - // No ENS... - if (!network.ensAddress) { - logger.throwError( - "network does not support ENS", - Logger.errors.UNSUPPORTED_OPERATION, - { operation: "ENS", network: network.name } - ); - } + const network = await this.getNetwork(); + + // No ENS... + if (!network.ensAddress) { + logger.throwError( + "network does not support ENS", + Logger.errors.UNSUPPORTED_OPERATION, + { operation: "ENS", network: network.name } + ); + } - // keccak256("resolver(bytes32)") - let data = "0x0178b8bf" + namehash(name).substring(2); - let transaction = { to: network.ensAddress, data: data }; + // keccak256("resolver(bytes32)") + const transaction = { + to: network.ensAddress, + data: ("0x0178b8bf" + namehash(name).substring(2)) + }; - return this.call(transaction).then((data) => { - return this.formatter.callAddress(data); - }); - }); + return this.formatter.callAddress(await this.call(transaction)); } - resolveName(name: string | Promise): Promise { + async resolveName(name: string | Promise): Promise { - // If it is a promise, resolve it then recurse - if (name instanceof Promise) { - return name.then((addressOrName) => this.resolveName(addressOrName)); - } + if (name instanceof Promise) { name = await name; } // If it is already an address, nothing to resolve try { @@ -847,54 +838,55 @@ export class BaseProvider extends Provider { } catch (error) { } // Get the addr from the resovler - return this._getResolver(name).then((resolverAddress) => { - if (!resolverAddress) { return null; } - - // keccak256("addr(bytes32)") - let data = "0x3b3b57de" + namehash(name).substring(2); - let transaction = { to: resolverAddress, data: data }; - return this.call(transaction).then((data) => { - return this.formatter.callAddress(data); - }); - }); + const resolverAddress = await this._getResolver(name); + if (!resolverAddress) { return null; } + + // keccak256("addr(bytes32)") + const transaction = { + to: resolverAddress, + data: ("0x3b3b57de" + namehash(name).substring(2)) + }; + + return this.formatter.callAddress(await this.call(transaction)); } - lookupAddress(address: string | Promise): Promise { - if (address instanceof Promise) { - return address.then((address) => this.lookupAddress(address)); - } + async lookupAddress(address: string | Promise): Promise { + if (address instanceof Promise) { address = await address; } address = this.formatter.address(address); - let name = address.substring(2) + ".addr.reverse" + const reverseName = address.substring(2).toLowerCase() + ".addr.reverse"; - return this._getResolver(name).then((resolverAddress) => { - if (!resolverAddress) { return null; } + const resolverAddress = await this._getResolver(reverseName); + if (!resolverAddress) { return null; } - // keccak("name(bytes32)") - let data = "0x691f3431" + namehash(name).substring(2); - return this.call({ to: resolverAddress, data: data }).then((data) => { - let bytes = arrayify(data); + // keccak("name(bytes32)") + let bytes = arrayify(await this.call({ + to: resolverAddress, + data: ("0x691f3431" + namehash(reverseName).substring(2)) + })); - // Strip off the dynamic string pointer (0x20) - if (bytes.length < 32 || !BigNumber.from(bytes.slice(0, 32)).eq(32)) { return null; } - bytes = bytes.slice(32); + // Strip off the dynamic string pointer (0x20) + if (bytes.length < 32 || !BigNumber.from(bytes.slice(0, 32)).eq(32)) { return null; } + bytes = bytes.slice(32); - if (bytes.length < 32) { return null; } - let length = BigNumber.from(bytes.slice(0, 32)).toNumber(); - bytes = bytes.slice(32); + // Not a length-prefixed string + if (bytes.length < 32) { return null; } - if (length > bytes.length) { return null; } + // Get the length of the string (from the length-prefix) + const length = BigNumber.from(bytes.slice(0, 32)).toNumber(); + bytes = bytes.slice(32); - let name = toUtf8String(bytes.slice(0, length)); + // Length longer than available data + if (length > bytes.length) { return null; } - // Make sure the reverse record matches the foward record - return this.resolveName(name).then((addr) => { - if (addr != address) { return null; } - return name; - }); - }); - }); + const name = toUtf8String(bytes.slice(0, length)); + + // Make sure the reverse record matches the foward record + const addr = await this.resolveName(name); + if (addr != address) { return null; } + + return name; } perform(method: string, params: any): Promise { @@ -1012,5 +1004,4 @@ export class BaseProvider extends Provider { return this; } - } diff --git a/packages/providers/src.ts/etherscan-provider.ts b/packages/providers/src.ts/etherscan-provider.ts index 002bf96833..2a51eb2583 100644 --- a/packages/providers/src.ts/etherscan-provider.ts +++ b/packages/providers/src.ts/etherscan-provider.ts @@ -15,7 +15,7 @@ import { BaseProvider } from "./base-provider"; // The transaction has already been sanitized by the calls in Provider function getTransactionString(transaction: TransactionRequest): string { - let result = []; + const result = []; for (let key in transaction) { if ((transaction)[key] == null) { continue; } let value = hexlify((transaction)[key]); @@ -35,7 +35,7 @@ function getResult(result: { status?: number, message?: string, result?: any }): if (result.status != 1 || result.message != "OK") { // @TODO: not any - let error: any = new Error("invalid response"); + const error: any = new Error("invalid response"); error.result = JSON.stringify(result); throw error; } @@ -46,14 +46,14 @@ function getResult(result: { status?: number, message?: string, result?: any }): function getJsonResult(result: { jsonrpc: string, result?: any, error?: { code?: number, data?: any, message?: string} } ): any { if (result.jsonrpc != "2.0") { // @TODO: not any - let error: any = new Error("invalid response"); + const error: any = new Error("invalid response"); error.result = JSON.stringify(result); throw error; } if (result.error) { // @TODO: not any - let error: any = new Error(result.error.message || "unknown error"); + const error: any = new Error(result.error.message || "unknown error"); if (result.error.code) { error.code = result.error.code; } if (result.error.data) { error.data = result.error.data; } throw error; @@ -108,28 +108,29 @@ export class EtherscanProvider extends BaseProvider{ } - perform(method: string, params: any) { + async perform(method: string, params: any): Promise { let url = this.baseUrl; let apiKey = ""; if (this.apiKey) { apiKey += "&apikey=" + this.apiKey; } - let get = (url: string, procFunc?: (value: any) => any) => { + const get = async (url: string, procFunc?: (value: any) => any): Promise => { this.emit("debug", { action: "request", request: url, provider: this }); - return fetchJson(url, null, procFunc || getJsonResult).then((result) => { - this.emit("debug", { - action: "response", - request: url, - response: deepCopy(result), - provider: this - }); - return result; + const result = await fetchJson(url, null, procFunc || getJsonResult); + + this.emit("debug", { + action: "response", + request: url, + response: deepCopy(result), + provider: this }); + + return result; }; switch (method) { @@ -230,70 +231,64 @@ export class EtherscanProvider extends BaseProvider{ return get(url); } - case "getLogs": + case "getLogs": { url += "/api?module=logs&action=getLogs"; - try { - if (params.filter.fromBlock) { - url += "&fromBlock=" + checkLogTag(params.filter.fromBlock); - } - if (params.filter.toBlock) { - url += "&toBlock=" + checkLogTag(params.filter.toBlock); - } + if (params.filter.fromBlock) { + url += "&fromBlock=" + checkLogTag(params.filter.fromBlock); + } - if (params.filter.address) { - url += "&address=" + params.filter.address; + if (params.filter.toBlock) { + url += "&toBlock=" + checkLogTag(params.filter.toBlock); + } + + if (params.filter.address) { + url += "&address=" + params.filter.address; + } + + // @TODO: We can handle slightly more complicated logs using the logs API + if (params.filter.topics && params.filter.topics.length > 0) { + if (params.filter.topics.length > 1) { + logger.throwError("unsupported topic count", Logger.errors.UNSUPPORTED_OPERATION, { topics: params.filter.topics }); } - // @TODO: We can handle slightly more complicated logs using the logs API - if (params.filter.topics && params.filter.topics.length > 0) { - if (params.filter.topics.length > 1) { - throw new Error("unsupported topic format"); - } - let topic0 = params.filter.topics[0]; + if (params.filter.topics.length === 1) { + const topic0 = params.filter.topics[0]; if (typeof(topic0) !== "string" || topic0.length !== 66) { - throw new Error("unsupported topic0 format"); + logger.throwError("unsupported topic format", Logger.errors.UNSUPPORTED_OPERATION, { topic0: topic0 }); } url += "&topic0=" + topic0; } - } catch (error) { - return Promise.reject(error); } url += apiKey; - let self = this; - return get(url, getResult).then(function(logs: Array) { - let txs: { [hash: string]: string } = {}; - - let seq = Promise.resolve(); - logs.forEach(function(log) { - seq = seq.then(function() { - if (log.blockHash != null) { return null; } - log.blockHash = txs[log.transactionHash]; - if (log.blockHash == null) { - return self.getTransaction(log.transactionHash).then(function(tx) { - txs[log.transactionHash] = tx.blockHash; - log.blockHash = tx.blockHash; - return null; - }); - } - return null; - }); - }); + const logs: Array = await get(url, getResult); - return seq.then(function() { - return logs; - }); - }); + // Cache txHash => blockHash + let txs: { [hash: string]: string } = {}; + + // Add any missing blockHash to the logs + for (let i = 0; i < logs.length; i++) { + const log = logs[i]; + if (log.blockHash != null) { continue; } + if (txs[log.transactionHash] == null) { + const tx = await this.getTransaction(log.transactionHash); + if (tx) { + txs[log.transactionHash] = tx.blockHash + } + } + log.blockHash = txs[log.transactionHash]; + } + + return logs; + } case "getEtherPrice": - if (this.network.name !== "homestead") { return Promise.resolve(0.0); } + if (this.network.name !== "homestead") { return 0.0; } url += "/api?module=stats&action=ethprice"; url += apiKey; - return get(url, getResult).then(function(result) { - return parseFloat(result.ethusd); - }); + return parseFloat(await get(url, getResult)); default: break; diff --git a/packages/providers/src.ts/index.ts b/packages/providers/src.ts/index.ts index 4d551dc9cb..f875da4c35 100644 --- a/packages/providers/src.ts/index.ts +++ b/packages/providers/src.ts/index.ts @@ -32,6 +32,38 @@ import { AsyncSendable } from "./web3-provider"; import { Formatter } from "./formatter"; +import { Logger } from "@ethersproject/logger"; +import { version } from "./_version"; +const logger = new Logger(version); + +//////////////////////// +// Helper Functions + +function getDefaultProvider(network?: Network | string, options?: any): BaseProvider { + if (network == null) { network = "homestead"; } + const n = getNetwork(network); + if (!n || !n._defaultProvider) { + logger.throwError("unsupported getDefaultProvider network", Logger.errors.NETWORK_ERROR, { + operation: "getDefaultProvider", + network: network + }); + } + + return n._defaultProvider({ + FallbackProvider, + + AlchemyProvider, + CloudflareProvider, + EtherscanProvider, + InfuraProvider, + JsonRpcProvider, + NodesmithProvider, + Web3Provider, + + IpcProvider, + }, options); +} + //////////////////////// // Exports @@ -67,6 +99,7 @@ export { /////////////////////// // Functions + getDefaultProvider, getNetwork, diff --git a/packages/providers/src.ts/json-rpc-provider.ts b/packages/providers/src.ts/json-rpc-provider.ts index 35d98066f3..c806708d46 100644 --- a/packages/providers/src.ts/json-rpc-provider.ts +++ b/packages/providers/src.ts/json-rpc-provider.ts @@ -6,8 +6,8 @@ import { Provider, TransactionRequest, TransactionResponse } from "@ethersprojec import { Signer } from "@ethersproject/abstract-signer"; import { BigNumber } from "@ethersproject/bignumber"; import { Bytes, hexlify, hexValue } from "@ethersproject/bytes"; -import { getNetwork, Network, Networkish } from "@ethersproject/networks"; -import { checkProperties, deepCopy, defineReadOnly, resolveProperties, shallowCopy } from "@ethersproject/properties"; +import { Network, Networkish } from "@ethersproject/networks"; +import { checkProperties, deepCopy, defineReadOnly, getStatic, resolveProperties, shallowCopy } from "@ethersproject/properties"; import { toUtf8Bytes } from "@ethersproject/strings"; import { ConnectionInfo, fetchJson, poll } from "@ethersproject/web"; @@ -221,10 +221,13 @@ export class JsonRpcProvider extends BaseProvider { constructor(url?: ConnectionInfo | string, network?: Networkish) { logger.checkNew(new.target, JsonRpcProvider); + const getNetwork = getStatic<(network: Networkish) => Network>(new.target, "getNetwork"); + // One parameter, but it is a network name, so swap it with the URL if (typeof(url) === "string") { - if (network === null && getNetwork(url)) { - network = url; + if (network === null) { + const checkNetwork = getNetwork(url); + network = checkNetwork; url = null; } } @@ -236,18 +239,25 @@ export class JsonRpcProvider extends BaseProvider { } else { // The network is unknown, query the JSON-RPC for it - let ready: Promise = new Promise((resolve, reject) => { - setTimeout(() => { - this.send("eth_chainId", [ ]).then((result) => { - resolve(getNetwork(BigNumber.from(result).toNumber())); - }).catch((error) => { - this.send("net_version", [ ]).then((result) => { - resolve(getNetwork(BigNumber.from(result).toNumber())); - }).catch((error) => { - reject(logger.makeError("could not detect network", Logger.errors.NETWORK_ERROR)); - }); - }); - }); + const ready: Promise = new Promise((resolve, reject) => { + setTimeout(async () => { + let chainId = null; + try { + chainId = await this.send("eth_chainId", [ ]); + } catch (error) { + try { + chainId = await this.send("net_version", [ ]); + } catch (error) { } + } + + if (chainId != null) { + try { + return resolve(getNetwork(BigNumber.from(chainId).toNumber())); + } catch (error) { console.log("e3", error); } + } + + reject(logger.makeError("could not detect network", Logger.errors.NETWORK_ERROR)); + }, 0); }); super(ready); } @@ -359,11 +369,15 @@ export class JsonRpcProvider extends BaseProvider { case "getTransactionReceipt": return this.send("eth_getTransactionReceipt", [ params.transactionHash ]); - case "call": - return this.send("eth_call", [ (this.constructor).hexlifyTransaction(params.transaction, { from: true }), params.blockTag ]); + case "call": { + const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction"); + return this.send("eth_call", [ hexlifyTransaction(params.transaction, { from: true }), params.blockTag ]); + } - case "estimateGas": - return this.send("eth_estimateGas", [ (this.constructor).hexlifyTransaction(params.transaction, { from: true }) ]); + case "estimateGas": { + const hexlifyTransaction = getStatic<(t: TransactionRequest, a?: { [key: string]: boolean }) => { [key: string]: string }>(this.constructor, "hexlifyTransaction"); + return this.send("eth_estimateGas", [ hexlifyTransaction(params.transaction, { from: true }) ]); + } case "getLogs": if (params.filter && params.filter.address != null) { @@ -433,7 +447,7 @@ export class JsonRpcProvider extends BaseProvider { // before this is called static hexlifyTransaction(transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string } { // Check only allowed properties are given - let allowed = shallowCopy(allowedTransactionKeys); + const allowed = shallowCopy(allowedTransactionKeys); if (allowExtra) { for (let key in allowExtra) { if (allowExtra[key]) { allowed[key] = true; } @@ -441,12 +455,12 @@ export class JsonRpcProvider extends BaseProvider { } checkProperties(transaction, allowed); - let result: { [key: string]: string } = {}; + const result: { [key: string]: string } = {}; // Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros. ["gasLimit", "gasPrice", "nonce", "value"].forEach(function(key) { if ((transaction)[key] == null) { return; } - let value = hexValue((transaction)[key]); + const value = hexValue((transaction)[key]); if (key === "gasLimit") { key = "gas"; } result[key] = value; }); diff --git a/packages/providers/src.ts/url-json-rpc-provider.ts b/packages/providers/src.ts/url-json-rpc-provider.ts index c8054ad17d..b8b805c30f 100644 --- a/packages/providers/src.ts/url-json-rpc-provider.ts +++ b/packages/providers/src.ts/url-json-rpc-provider.ts @@ -1,6 +1,6 @@ "use strict"; -import { getNetwork, Network, Networkish } from "@ethersproject/networks"; +import { Network, Networkish } from "@ethersproject/networks"; import { defineReadOnly, getStatic } from "@ethersproject/properties"; import { Logger } from "@ethersproject/logger"; @@ -9,7 +9,7 @@ const logger = new Logger(version); import { JsonRpcProvider, JsonRpcSigner } from "./json-rpc-provider"; -export class UrlJsonRpcProvider extends JsonRpcProvider { +export abstract class UrlJsonRpcProvider extends JsonRpcProvider { readonly apiKey: string; constructor(network?: Networkish, apiKey?: string) { @@ -19,7 +19,7 @@ export class UrlJsonRpcProvider extends JsonRpcProvider { network = getStatic<(network: Networkish) => Network>(new.target, "getNetwork")(network); apiKey = getStatic<(apiKey: string) => string>(new.target, "getApiKey")(apiKey); - let url = getStatic<(network: Network, apiKey: string) => string>(new.target, "getUrl")(network, apiKey); + const url = getStatic<(network: Network, apiKey: string) => string>(new.target, "getUrl")(network, apiKey); super(url, network); @@ -42,11 +42,11 @@ export class UrlJsonRpcProvider extends JsonRpcProvider { listAccounts(): Promise> { return Promise.resolve([]); } - +/* static getNetwork(network?: Networkish): Network { return getNetwork((network == null) ? "homestead": network); } - +*/ // Return a defaultApiKey if null, otherwise validate the API key static getApiKey(apiKey: string): string { return apiKey;