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")
|
// - cancelled: true if reason == "cancelled" or reason == "replaced")
|
||||||
// - hash: original transaction hash
|
// - hash: original transaction hash
|
||||||
// - replacement: the full TransactionsResponse for the replacement
|
// - replacement: the full TransactionsResponse for the replacement
|
||||||
|
// - receipt: the receipt of the replacement
|
||||||
TRANSACTION_REPLACED = "TRANSACTION_REPLACED",
|
TRANSACTION_REPLACED = "TRANSACTION_REPLACED",
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -24,7 +24,6 @@ const logger = new Logger(version);
|
|||||||
|
|
||||||
import { Formatter } from "./formatter";
|
import { Formatter } from "./formatter";
|
||||||
|
|
||||||
|
|
||||||
//////////////////////////////
|
//////////////////////////////
|
||||||
// Event Serializeing
|
// Event Serializeing
|
||||||
|
|
||||||
@ -925,8 +924,10 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise<TransactionReceipt> {
|
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);
|
const receipt = await this.getTransactionReceipt(transactionHash);
|
||||||
|
|
||||||
// Receipt is already good
|
// Receipt is already good
|
||||||
@ -934,31 +935,128 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
|
|
||||||
// Poll until the receipt is good...
|
// Poll until the receipt is good...
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let timer: NodeJS.Timer = null;
|
const cancelFuncs: Array<() => void> = [];
|
||||||
|
|
||||||
let done = false;
|
let done = false;
|
||||||
|
const alreadyDone = function() {
|
||||||
const handler = (receipt: TransactionReceipt) => {
|
if (done) { return true; }
|
||||||
if (receipt.confirmations < confirmations) { return; }
|
|
||||||
|
|
||||||
if (timer) { clearTimeout(timer); }
|
|
||||||
if (done) { return; }
|
|
||||||
done = 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);
|
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) {
|
if (typeof(timeout) === "number" && timeout > 0) {
|
||||||
timer = setTimeout(() => {
|
const timer = setTimeout(() => {
|
||||||
if (done) { return; }
|
if (alreadyDone()) { return; }
|
||||||
timer = null;
|
|
||||||
done = true;
|
|
||||||
|
|
||||||
this.removeListener(transactionHash, handler);
|
|
||||||
reject(logger.makeError("timeout exceeded", Logger.errors.TIMEOUT, { timeout: timeout }));
|
reject(logger.makeError("timeout exceeded", Logger.errors.TIMEOUT, { timeout: timeout }));
|
||||||
}, timeout);
|
}, timeout);
|
||||||
if (timer.unref) { timer.unref(); }
|
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
|
// 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"); }
|
if (hash != null && hexDataLength(hash) !== 32) { throw new Error("invalid response - sendTransaction"); }
|
||||||
|
|
||||||
const result = <TransactionResponse>tx;
|
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 });
|
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 (confirms?: number, timeout?: number) => {
|
||||||
result.wait = async (confirmations?: number) => {
|
if (confirms == null) { confirms = 1; }
|
||||||
|
if (timeout == null) { timeout = 0; }
|
||||||
|
|
||||||
// We know this transaction *must* exist (whether it gets mined is
|
// Get the details to detect replacement
|
||||||
// another story), so setting an emitted value forces us to
|
let replacement = undefined;
|
||||||
// wait even if the node returns null for the receipt
|
if (confirms !== 0 && startBlock != null) {
|
||||||
if (confirmations !== 0) {
|
replacement = {
|
||||||
this._emitted["t:" + tx.hash] = "pending";
|
data: tx.data,
|
||||||
|
from: tx.from,
|
||||||
|
nonce: tx.nonce,
|
||||||
|
to: tx.to,
|
||||||
|
value: tx.value,
|
||||||
|
startBlock
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const receipt = await this.waitForTransaction(tx.hash, confirmations)
|
const receipt = await this._waitForTransaction(tx.hash, confirms, timeout, replacement);
|
||||||
if (receipt == null && confirmations === 0) { return null; }
|
if (receipt == null && confirms === 0) { return null; }
|
||||||
|
|
||||||
// No longer pending, allow the polling loop to garbage collect this
|
// No longer pending, allow the polling loop to garbage collect this
|
||||||
this._emitted["t:" + tx.hash] = receipt.blockNumber;
|
this._emitted["t:" + tx.hash] = receipt.blockNumber;
|
||||||
@ -1097,9 +1202,10 @@ export class BaseProvider extends Provider implements EnsProvider {
|
|||||||
await this.getNetwork();
|
await this.getNetwork();
|
||||||
const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t));
|
const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t));
|
||||||
const tx = this.formatter.transaction(signedTransaction);
|
const tx = this.formatter.transaction(signedTransaction);
|
||||||
|
const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
|
||||||
try {
|
try {
|
||||||
const hash = await this.perform("sendTransaction", { signedTransaction: hexTx });
|
const hash = await this.perform("sendTransaction", { signedTransaction: hexTx });
|
||||||
return this._wrapTransaction(tx, hash);
|
return this._wrapTransaction(tx, hash, blockNumber);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
(<any>error).transaction = tx;
|
(<any>error).transaction = tx;
|
||||||
(<any>error).transactionHash = tx.hash;
|
(<any>error).transactionHash = tx.hash;
|
||||||
|
Loading…
Reference in New Issue
Block a user