Skip to content

Commit 5144acf

Browse files
committed
Added initial support for detecting replacement transactions (ethers-io#1477).
1 parent aadc5cd commit 5144acf

File tree

2 files changed

+134
-27
lines changed

2 files changed

+134
-27
lines changed

packages/logger/src.ts/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export enum ErrorCode {
144144
// - cancelled: true if reason == "cancelled" or reason == "replaced")
145145
// - hash: original transaction hash
146146
// - replacement: the full TransactionsResponse for the replacement
147+
// - receipt: the receipt of the replacement
147148
TRANSACTION_REPLACED = "TRANSACTION_REPLACED",
148149
};
149150

packages/providers/src.ts/base-provider.ts

Lines changed: 133 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ const logger = new Logger(version);
2424

2525
import { Formatter } from "./formatter";
2626

27-
2827
//////////////////////////////
2928
// Event Serializeing
3029

@@ -925,40 +924,139 @@ export class BaseProvider extends Provider implements EnsProvider {
925924
}
926925

927926
async waitForTransaction(transactionHash: string, confirmations?: number, timeout?: number): Promise<TransactionReceipt> {
928-
if (confirmations == null) { confirmations = 1; }
927+
return this._waitForTransaction(transactionHash, (confirmations == null) ? 1: confirmations, timeout || 0, null);
928+
}
929929

930+
async _waitForTransaction(transactionHash: string, confirmations: number, timeout: number, replaceable: { data: string, from: string, nonce: number, to: string, value: BigNumber, startBlock: number }): Promise<TransactionReceipt> {
930931
const receipt = await this.getTransactionReceipt(transactionHash);
931932

932933
// Receipt is already good
933934
if ((receipt ? receipt.confirmations: 0) >= confirmations) { return receipt; }
934935

935936
// Poll until the receipt is good...
936937
return new Promise((resolve, reject) => {
937-
let timer: NodeJS.Timer = null;
938+
const cancelFuncs: Array<() => void> = [];
939+
938940
let done = false;
941+
const alreadyDone = function() {
942+
if (done) { return true; }
943+
done = true;
944+
cancelFuncs.forEach((func) => { func(); });
945+
return false;
946+
};
939947

940-
const handler = (receipt: TransactionReceipt) => {
948+
const minedHandler = (receipt: TransactionReceipt) => {
941949
if (receipt.confirmations < confirmations) { return; }
950+
if (alreadyDone()) { return; }
951+
resolve(receipt);
952+
}
953+
this.on(transactionHash, minedHandler);
954+
cancelFuncs.push(() => { this.removeListener(transactionHash, minedHandler); });
955+
956+
if (replaceable) {
957+
let lastBlockNumber = replaceable.startBlock;
958+
let scannedBlock: number = null;
959+
const replaceHandler = async (blockNumber: number) => {
960+
if (done) { return; }
961+
962+
// Wait 1 second; this is only used in the case of a fault, so
963+
// we will trade off a little bit of latency for more consistent
964+
// results and fewer JSON-RPC calls
965+
await stall(1000);
966+
967+
this.getTransactionCount(replaceable.from).then(async (nonce) => {
968+
if (done) { return; }
969+
970+
if (nonce <= replaceable.nonce) {
971+
lastBlockNumber = blockNumber;
972+
973+
} else {
974+
// First check if the transaction was mined
975+
{
976+
const mined = await this.getTransaction(transactionHash);
977+
if (mined && mined.blockNumber != null) { return; }
978+
}
979+
980+
// First time scanning. We start a little earlier for some
981+
// wiggle room here to handle the eventually consistent nature
982+
// of blockchain (e.g. the getTransactionCount was for a
983+
// different block)
984+
if (scannedBlock == null) {
985+
scannedBlock = lastBlockNumber - 3;
986+
if (scannedBlock < replaceable.startBlock) {
987+
scannedBlock = replaceable.startBlock;
988+
}
989+
}
990+
991+
while (scannedBlock <= blockNumber) {
992+
if (done) { return; }
993+
994+
const block = await this.getBlockWithTransactions(scannedBlock);
995+
for (let ti = 0; ti < block.transactions.length; ti++) {
996+
const tx = block.transactions[ti];
997+
998+
// Successfully mined!
999+
if (tx.hash === transactionHash) { return; }
1000+
1001+
// Matches our transaction from and nonce; its a replacement
1002+
if (tx.from === replaceable.from && tx.nonce === replaceable.nonce) {
1003+
if (done) { return; }
1004+
1005+
// Get the receipt of the replacement
1006+
const receipt = await this.waitForTransaction(tx.hash, confirmations);
1007+
1008+
// Already resolved or rejected (prolly a timeout)
1009+
if (alreadyDone()) { return; }
1010+
1011+
// The reason we were replaced
1012+
let reason = "replaced";
1013+
if (tx.data === replaceable.data && tx.to === replaceable.to && tx.value.eq(replaceable.value)) {
1014+
reason = "repriced";
1015+
} else if (tx.data === "0x" && tx.from === tx.to && tx.value.isZero()) {
1016+
reason = "cancelled"
1017+
}
1018+
1019+
// Explain why we were replaced
1020+
reject(logger.makeError("transaction was replaced", Logger.errors.TRANSACTION_REPLACED, {
1021+
cancelled: (reason === "replaced" || reason === "cancelled"),
1022+
reason,
1023+
replacement: this._wrapTransaction(tx),
1024+
hash: transactionHash,
1025+
receipt
1026+
}));
1027+
1028+
return;
1029+
}
1030+
}
1031+
scannedBlock++;
1032+
}
1033+
}
1034+
1035+
if (done) { return; }
1036+
this.once("block", replaceHandler);
1037+
1038+
}, (error) => {
1039+
if (done) { return; }
1040+
this.once("block", replaceHandler);
1041+
});
1042+
};
9421043

943-
if (timer) { clearTimeout(timer); }
9441044
if (done) { return; }
945-
done = true;
1045+
this.once("block", replaceHandler);
9461046

947-
this.removeListener(transactionHash, handler);
948-
resolve(receipt);
1047+
cancelFuncs.push(() => {
1048+
this.removeListener("block", replaceHandler);
1049+
});
9491050
}
950-
this.on(transactionHash, handler);
9511051

9521052
if (typeof(timeout) === "number" && timeout > 0) {
953-
timer = setTimeout(() => {
954-
if (done) { return; }
955-
timer = null;
956-
done = true;
957-
958-
this.removeListener(transactionHash, handler);
1053+
const timer = setTimeout(() => {
1054+
if (alreadyDone()) { return; }
9591055
reject(logger.makeError("timeout exceeded", Logger.errors.TIMEOUT, { timeout: timeout }));
9601056
}, timeout);
9611057
if (timer.unref) { timer.unref(); }
1058+
1059+
cancelFuncs.push(() => { clearTimeout(timer); });
9621060
}
9631061
});
9641062
}
@@ -1054,7 +1152,7 @@ export class BaseProvider extends Provider implements EnsProvider {
10541152
}
10551153

10561154
// This should be called by any subclass wrapping a TransactionResponse
1057-
_wrapTransaction(tx: Transaction, hash?: string): TransactionResponse {
1155+
_wrapTransaction(tx: Transaction, hash?: string, startBlock?: number): TransactionResponse {
10581156
if (hash != null && hexDataLength(hash) !== 32) { throw new Error("invalid response - sendTransaction"); }
10591157

10601158
const result = <TransactionResponse>tx;
@@ -1064,18 +1162,25 @@ export class BaseProvider extends Provider implements EnsProvider {
10641162
logger.throwError("Transaction hash mismatch from Provider.sendTransaction.", Logger.errors.UNKNOWN_ERROR, { expectedHash: tx.hash, returnedHash: hash });
10651163
}
10661164

1067-
// @TODO: (confirmations? number, timeout? number)
1068-
result.wait = async (confirmations?: number) => {
1069-
1070-
// We know this transaction *must* exist (whether it gets mined is
1071-
// another story), so setting an emitted value forces us to
1072-
// wait even if the node returns null for the receipt
1073-
if (confirmations !== 0) {
1074-
this._emitted["t:" + tx.hash] = "pending";
1165+
result.wait = async (confirms?: number, timeout?: number) => {
1166+
if (confirms == null) { confirms = 1; }
1167+
if (timeout == null) { timeout = 0; }
1168+
1169+
// Get the details to detect replacement
1170+
let replacement = undefined;
1171+
if (confirms !== 0 && startBlock != null) {
1172+
replacement = {
1173+
data: tx.data,
1174+
from: tx.from,
1175+
nonce: tx.nonce,
1176+
to: tx.to,
1177+
value: tx.value,
1178+
startBlock
1179+
};
10751180
}
10761181

1077-
const receipt = await this.waitForTransaction(tx.hash, confirmations)
1078-
if (receipt == null && confirmations === 0) { return null; }
1182+
const receipt = await this._waitForTransaction(tx.hash, confirms, timeout, replacement);
1183+
if (receipt == null && confirms === 0) { return null; }
10791184

10801185
// No longer pending, allow the polling loop to garbage collect this
10811186
this._emitted["t:" + tx.hash] = receipt.blockNumber;
@@ -1097,9 +1202,10 @@ export class BaseProvider extends Provider implements EnsProvider {
10971202
await this.getNetwork();
10981203
const hexTx = await Promise.resolve(signedTransaction).then(t => hexlify(t));
10991204
const tx = this.formatter.transaction(signedTransaction);
1205+
const blockNumber = await this._getInternalBlockNumber(100 + 2 * this.pollingInterval);
11001206
try {
11011207
const hash = await this.perform("sendTransaction", { signedTransaction: hexTx });
1102-
return this._wrapTransaction(tx, hash);
1208+
return this._wrapTransaction(tx, hash, blockNumber);
11031209
} catch (error) {
11041210
(<any>error).transaction = tx;
11051211
(<any>error).transactionHash = tx.hash;

0 commit comments

Comments
 (0)