Added initial support for detecting replacement transactions (#1477).
This commit is contained in:
parent
aadc5cd3d6
commit
5144acf456
@ -144,6 +144,7 @@ export enum ErrorCode {
|
||||
// - cancelled: true if reason == "cancelled" or reason == "replaced")
|
||||
// - hash: original transaction hash
|
||||
// - replacement: the full TransactionsResponse for the replacement
|
||||
// - receipt: the receipt of the replacement
|
||||
TRANSACTION_REPLACED = "TRANSACTION_REPLACED",
|
||||
};
|
||||
|
||||
|
@ -24,7 +24,6 @@ const logger = new Logger(version);
|
||||
|
||||
import { Formatter } from "./formatter";
|
||||
|
||||
|
||||
//////////////////////////////
|
||||
// Event Serializeing
|
||||
|
||||
@ -925,8 +924,10 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
}
|
||||
|
||||
async waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise<TransactionReceipt> {
|
||||
if (confirmations == null) { confirmations = 1; }
|
||||
return this._waitForTransaction(transactionHash, (confirmations == null) ? 1: confirmations, timeout || 0, null);
|
||||
}
|
||||
|
||||
async _waitForTransaction(transactionHash: string, confirmations: number, timeout: number, replaceable: { data: string, from: string, nonce: number, to: string, value: BigNumber, startBlock: number }): Promise<TransactionReceipt> {
|
||||
const receipt = await this.getTransactionReceipt(transactionHash);
|
||||
|
||||
// Receipt is already good
|
||||
@ -934,31 +935,128 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
|
||||
// Poll until the receipt is good...
|
||||
return new Promise((resolve, reject) => {
|
||||
let timer: NodeJS.Timer = null;
|
||||
const cancelFuncs: Array<() => void> = [];
|
||||
|
||||
let done = false;
|
||||
|
||||
const handler = (receipt: TransactionReceipt) => {
|
||||
if (receipt.confirmations < confirmations) { return; }
|
||||
|
||||
if (timer) { clearTimeout(timer); }
|
||||
if (done) { return; }
|
||||
const alreadyDone = function() {
|
||||
if (done) { return true; }
|
||||
done = true;
|
||||
cancelFuncs.forEach((func) => { func(); });
|
||||
return false;
|
||||
};
|
||||
|
||||
this.removeListener(transactionHash, handler);
|
||||
const minedHandler = (receipt: TransactionReceipt) => {
|
||||
if (receipt.confirmations < confirmations) { return; }
|
||||
if (alreadyDone()) { return; }
|
||||
resolve(receipt);
|
||||
}
|
||||
this.on(transactionHash, handler);
|
||||
this.on(transactionHash, minedHandler);
|
||||
cancelFuncs.push(() => { this.removeListener(transactionHash, minedHandler); });
|
||||
|
||||
if (replaceable) {
|
||||
let lastBlockNumber = replaceable.startBlock;
|
||||
let scannedBlock: number = null;
|
||||
const replaceHandler = async (blockNumber: number) => {
|
||||
if (done) { return; }
|
||||
|
||||
// Wait 1 second; this is only used in the case of a fault, so
|
||||
// we will trade off a little bit of latency for more consistent
|
||||
// results and fewer JSON-RPC calls
|
||||
await stall(1000);
|
||||
|
||||
this.getTransactionCount(replaceable.from).then(async (nonce) => {
|
||||
if (done) { return; }
|
||||
|
||||
if (nonce <= replaceable.nonce) {
|
||||
lastBlockNumber = blockNumber;
|
||||
|
||||
} else {
|
||||
// First check if the transaction was mined
|
||||
{
|
||||
const mined = await this.getTransaction(transactionHash);
|
||||
if (mined && mined.blockNumber != null) { return; }
|
||||
}
|
||||
|
||||
// First time scanning. We start a little earlier for some
|
||||
// wiggle room here to handle the eventually consistent nature
|
||||
// of blockchain (e.g. the getTransactionCount was for a
|
||||
// different block)
|
||||
if (scannedBlock == null) {
|
||||
scannedBlock = lastBlockNumber - 3;
|
||||
if (scannedBlock < replaceable.startBlock) {
|
||||
scannedBlock = replaceable.startBlock;
|
||||
}
|
||||
}
|
||||
|
||||
while (scannedBlock <= blockNumber) {
|
||||
if (done) { return; }
|
||||
|
||||
const block = await this.getBlockWithTransactions(scannedBlock);
|
||||
for (let ti = 0; ti < block.transactions.length; ti++) {
|
||||
const tx = block.transactions[ti];
|
||||
|
||||
// Successfully mined!
|
||||
if (tx.hash === transactionHash) { return; }
|
||||
|
||||
// Matches our transaction from and nonce; its a replacement
|
||||
if (tx.from === replaceable.from && tx.nonce === replaceable.nonce) {
|
||||
if (done) { return; }
|
||||
|
||||
// Get the receipt of the replacement
|
||||
const receipt = await this.waitForTransaction(tx.hash, confirmations);
|
||||
|
||||
// Already resolved or rejected (prolly a timeout)
|
||||
if (alreadyDone()) { return; }
|
||||
|
||||
// The reason we were replaced
|
||||
let reason = "replaced";
|
||||
if (tx.data === replaceable.data && tx.to === replaceable.to && tx.value.eq(replaceable.value)) {
|
||||
reason = "repriced";
|
||||
} else if (tx.data === "0x" && tx.from === tx.to && tx.value.isZero()) {
|
||||
reason = "cancelled"
|
||||
}
|
||||
|
||||
// Explain why we were replaced
|
||||
reject(logger.makeError("transaction was replaced", Logger.errors.TRANSACTION_REPLACED, {
|
||||
cancelled: (reason === "replaced" || reason === "cancelled"),
|
||||
reason,
|
||||
replacement: this._wrapTransaction(tx),
|
||||
hash: transactionHash,
|
||||
receipt
|
||||
}));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
scannedBlock++;
|
||||
}
|
||||
}
|
||||
|
||||
if (done) { return; }
|
||||
this.once("block", replaceHandler);
|
||||
|
||||
}, (error) => {
|
||||
if (done) { return; }
|
||||
this.once("block", replaceHandler);
|
||||
});
|
||||
};
|
||||
|
||||
if (done) { return; }
|
||||
this.once("block", replaceHandler);
|
||||
|
||||
cancelFuncs.push(() => {
|
||||
this.removeListener("block", replaceHandler);
|
||||
});
|
||||
}
|
||||
|
||||
if (typeof(timeout) === "number" && timeout > 0) {
|
||||
timer = setTimeout(() => {
|
||||
if (done) { return; }
|
||||
timer = null;
|
||||
done = true;
|
||||
|
||||
this.removeListener(transactionHash, handler);
|
||||
const timer = setTimeout(() => {
|
||||
if (alreadyDone()) { return; }
|
||||
reject(logger.makeError("timeout exceeded", Logger.errors.TIMEOUT, { timeout: timeout }));
|
||||
}, timeout);
|
||||
if (timer.unref) { timer.unref(); }
|
||||
|
||||
cancelFuncs.push(() => { clearTimeout(timer); });
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -1054,7 +1152,7 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
}
|
||||
|
||||
// This should be called by any subclass wrapping a TransactionResponse
|
||||
_wrapTransaction(tx: Transaction, hash?: string): TransactionResponse {
|
||||
_wrapTransaction(tx: Transaction, hash?: string, startBlock?: number): TransactionResponse {
|
||||
if (hash != null && hexDataLength(hash) !== 32) { throw new Error("invalid response - sendTransaction"); }
|
||||
|
||||
const result = <TransactionResponse>tx;
|
||||
@ -1064,18 +1162,25 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
logger.throwError("Transaction hash mismatch from Provider.sendTransaction.", Logger.errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash });
|
||||
}
|
||||
|
||||
// @TODO: (confirmations? number, timeout? number)
|
||||
result.wait = async (confirmations?: number) => {
|
||||
result.wait = async (confirms?: number, timeout?: number) => {
|
||||
if (confirms == null) { confirms = 1; }
|
||||
if (timeout == null) { timeout = 0; }
|
||||
|
||||
// We know this transaction *must* exist (whether it gets mined is
|
||||
// another story), so setting an emitted value forces us to
|
||||
// wait even if the node returns null for the receipt
|
||||
if (confirmations !== 0) {
|
||||
this._emitted["t:" + tx.hash] = "pending";
|
||||
// Get the details to detect replacement
|
||||
let replacement = undefined;
|
||||
if (confirms !== 0 && startBlock != null) {
|
||||
replacement = {
|
||||
data: tx.data,
|
||||
from: tx.from,
|
||||
nonce: tx.nonce,
|
||||
to: tx.to,
|
||||
value: tx.value,
|
||||
startBlock
|
||||
};
|
||||
}
|
||||
|
||||
const receipt = await this.waitForTransaction(tx.hash, confirmations)
|
||||
if (receipt == null && confirmations === 0) { return null; }
|
||||
const receipt = await this._waitForTransaction(tx.hash, confirms, timeout, replacement);
|
||||
if (receipt == null && confirms === 0) { return null; }
|
||||
|
||||
// No longer pending, allow the polling loop to garbage collect this
|
||||
this._emitted["t:" + tx.hash] = receipt.blockNumber;
|
||||
@ -1097,9 +1202,10 @@ export class BaseProvider extends Provider implements EnsProvider {
|
||||
await this.getNetwork();
|
||||
const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t));
|
||||
const tx = this.formatter.transaction(signedTransaction);
|
||||
const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
|
||||
try {
|
||||
const hash = await this.perform("sendTransaction", { signedTransaction: hexTx });
|
||||
return this._wrapTransaction(tx, hash);
|
||||
return this._wrapTransaction(tx, hash, blockNumber);
|
||||
} catch (error) {
|
||||
(<any>error).transaction = tx;
|
||||
(<any>error).transactionHash = tx.hash;
|
||||
|
Loading…
Reference in New Issue
Block a user