Here's code that waits specified number of blocks and verifies the transaction receipt is still valid. If a fork occurs and the replay fails, the receipt check should fail and the callback will call with Error set.
I've only tested this for success and timeout failures, I've not tested it on an actual fork of the blockchain, because I haven't figured out how to reliably cause that to happen yet in a test framework. Appreciate any hints on how to do that.
Per the question, it only uses web3.js calls, and no libraries. I have to tell you using callbacks instead of promises is very painful for me ;-P
I haven't implemented validating the transaction multiple RPC nodes, but there's a note in the code on where to do that. You will probably want to use at least Async.join to do that, which would be an external library.
//
// @method awaitBlockConsensus
// @param web3s[0] is the node you submitted the transaction to, the other web3s
// are for cross verification, because you shouldn't trust one node.
// @param txhash is the transaction hash from when you submitted the transaction
// @param blockCount is the number of blocks to wait for.
// @param timout in seconds
// @param callback - callback(error, transaction_receipt)
//
exports.awaitBlockConsensus = function(web3s, txhash, blockCount, timeout, callback) {
var txWeb3 = web3s[0];
var startBlock = Number.MAX_SAFE_INTEGER;
var interval;
var stateEnum = { start: 1, mined: 2, awaited: 3, confirmed: 4, unconfirmed: 5 };
var savedTxInfo;
var attempts = 0;
var pollState = stateEnum.start;
var poll = function() {
if (pollState === stateEnum.start) {
txWeb3.eth.getTransaction(txhash, function(e, txInfo) {
if (e || txInfo == null) {
return; // XXX silently drop errors
}
if (txInfo.blockHash != null) {
startBlock = txInfo.blockNumber;
savedTxInfo = txInfo;
console.log("mined");
pollState = stateEnum.mined;
}
});
}
else if (pollState == stateEnum.mined) {
txWeb3.eth.getBlockNumber(function (e, blockNum) {
if (e) {
return; // XXX silently drop errors
}
console.log("blockNum: ", blockNum);
if (blockNum >= (blockCount + startBlock)) {
pollState = stateEnum.awaited;
}
});
}
else if (pollState == stateEnum.awaited) {
txWeb3.eth.getTransactionReceipt(txhash, function(e, receipt) {
if (e || receipt == null) {
return; // XXX silently drop errors. TBD callback error?
}
// confirm we didn't run out of gas
// XXX this is where we should be checking a plurality of nodes. TBD
clearInterval(interval);
if (receipt.gasUsed >= savedTxInfo.gas) {
pollState = stateEnum.unconfirmed;
callback(new Error("we ran out of gas, not confirmed!"), null);
} else {
pollState = stateEnum.confirmed;
callback(null, receipt);
}
});
} else {
throw(new Error("We should never get here, illegal state: " + pollState));
}
// note assuming poll interval is 1 second
attempts++;
if (attempts > timeout) {
clearInterval(interval);
pollState = stateEnum.unconfirmed;
callback(new Error("Timed out, not confirmed"), null);
}
};
interval = setInterval(poll, 1000);
poll();
};
[EDIT 1] - out of gas is greater than or equal, not greater...