Repository: ethereum/web3.py

Issue:
API for re-sending transactions
### What was wrong?

There are a few legitimate use cases for wanting to re-submit a transaction that has already been sent with modified parameters.

* Resending the same transaction with a higher gas price during times of network congestion.
* Quickly overriding a transaction sent in error (also by using a higher gas price)
* In a case where you *know* there is a signed transaction that was sent but never included in a block, ensuring that it will not be applied at some point in the future.

### How can it be fixed?

Implement a new API `web3.eth.overrideTransaction(txn_hash, txn_overrides)`

- `txn_hash` is the hash of the transaction you wish to override.
- `txn_overrides` is a dictionary of transaction parameters which should be used in place of the existing transaction parameters.

Behavior is as follows.

- *if* no transaction can be found with the given hash an exception is thrown.
- *if* the transaction has already been mined an exception is thrown.
- The `txn_overrides` may contain any/all fields that are normally accepted as transaction params except `nonce`.
- If `gasPrice` is included in the `txn_overrides`, it **must** be greater than the existing transaction gas price (TODO: should we allow a way to bypass this validation)
- if `gasPrice` is **not** included in the `txn_overrides` we should ???? (TODO: how do we choose an appropriate gas price?).


Discussion:
This is just a first stab at an API for this.  It almost definitely can be improved.
Other name options:

- `w3.eth.replaceTransaction()`
- `w3.eth.resendTransaction()`

Closes #495 
## Split API in two

I like the idea of splitting this into two APIs:

- `w3.eth.overrideTransaction(txn_hash, new_transaction_dict)` -- Use only the new transaction fields, do not reuse any values from the the current pending transaction
- `w3.eth.modifyTransaction(txn_hash, **individual_fields_to_replace)` -- Use the keyword args provided, infer `gasPrice` if not provided, and then fill in unspecified values from the current pending transaction

Examples:

```py
from web3.auto import w3

txn_b = {'from': w3.eth.accounts[0], 'to': w3.eth.accounts[1], value: 0, gas: 21000}
txn_b_hash = w3.eth.overrideTransaction(txn_a_hash, txn_b)
```

```py
new_txn_hash = w3.eth.modifyTransaction(txn_hash, gasPrice=w3.toWei('5.5', 'gwei'))
```

## Gas price inference

> if `gasPrice` is not included in the `txn_overrides` we should ???? (TODO: how do we choose an appropriate gas price?).

Proposal -- get two values, and use the maximum price of the two:

- The gas price of the pending transaction times 1.1 (the minimum required to propagate)
- The gas price as calculated by the current gas price engine

Lets use the API from @carver 's latest comment: https://github.com/ethereum/web3.py/issues/492#issuecomment-354572411
__This issue now has a funding of 0.25 ETH (254.76 USD) attached to it.__

 * If you would like to work on this issue you can claim it [here](https://gitcoin.co/funding/details?url=https://github.com/ethereum/web3.py/issues/492).
 * If you've completed this issue and want to claim the bounty you can do so [here](https://gitcoin.co/funding/details?url=https://github.com/ethereum/web3.py/issues/492)
 * Questions? Get help on the <a href='https://gitcoin.co/slack'>Gitcoin Slack</a>
 * $13838.61 more Funded OSS Work Available at: https://gitcoin.co/explorer

@vs77bb -- we have enough python talent in the gitcoin ecosystem we should be able to help get this fulfilled :) 
@owocki agree :) will pub this one out! 
Looks like a fun one 🙂 
I'd give this a go but you're probably sick of seeing me around here by now 😄  I'll give it a little while to see if anyone else is keen otherwise happy to help
> you're probably sick of seeing me around here by now

lol, not at all! :D

> I'll give it a little while to see if anyone else is keen

That's very polite of you, but not necessary IMO.

> otherwise happy to help

A day seems like plenty of time to wait, if you decide to.
starting to look into this one, if anyone has capacity to tackle it please post here else I'll likely start writing code Monday
Lets make sure that whoever takes this on posts that they are starting work on it so that multiple people don't show up with solutions and make claiming the bounty contentious.
Good thought @pipermerriam. @amites Are you planning on claiming today? cc @owocki 
caught up on another project today, will claim tomorrow/wednesday once i've finished unless someone else wants to take it on before I get to it
Sounds like we're in a race between who is the first one able to allocate time to work on this, between @amites and @monokh . Looks like both of you are happy to cede to the other, whoever gets the time first. Just be sure to claim it before working on it. :)
Ok, in this case I'll pick it up then. Claim incoming.
@owocki i've claimed this but gitcoin hasn't picked it up. I believe it has something to do with me resending the transaction with a higher gas price. Gitcoin might be tracking a specific tx hash and therefore doesn't know if another tx went through? 
Old transaction still running: https://etherscan.io/tx/0xc9b75d10184f59d474bd019ec3ecdc47f8763869486301750b3808b8cee5f48b
New transaction that confirmed: https://etherscan.io/tx/0xe014e218f4827bbc68e48358a4eb935d195b10b5a58e896ed990f1879cf164cf
> `w3.eth.overrideTransaction(txn_hash, new_transaction_dict)`

> `w3.eth.modifyTransaction(txn_hash, **individual_fields_to_replace)` 

Is there a reason one takes a dictionary and another takes an argument list? I can understand if it's the semantics of reading `modify transaction x=1 y=2` compared to `modify transaction {x: 1, y: 2}` but on first glance it felt inconsistent

What do we feel about `w3.eth.overrideTransaction` vs `w3.eth.replaceTransaction`? I feel like `w3.eth.overrideTransaction` might give the impression that could override parts of the transaction. Is it just my english? 😄  

> If gasPrice is included in the txn_overrides, it must be greater than the existing transaction gas price (TODO: should we allow a way to bypass this validation)

Is this some limitation in how the nodes function? Otherwise I feel like it's too opinionated to have a check for something like that. I can't think of one but there could be a reason to decrease the gas price. 

> Proposal -- get two values, and use the maximum price of the two:
> The gas price of the pending transaction times 1.1 (the minimum required to propagate)
> The gas price as calculated by the current gas price engine

@carver could you clarify what the purpose of this kind of logic is? What would be the consequence of performing the same gasPrice defaulting that sending a new transaction has? i might be confused 
> Is there a reason one takes a dictionary and another takes an argument list?

Yes, I think it helps reinforce the semantic difference between the two: that `modify` reuses the values of the original transaction, and that `override` does not. You need to put everything you want in the `new_transaction_dict`. (except the `nonce`, which should raise an exception if someone specifies a different nonce than the one that `txn_hash` has).

> What do we feel about w3.eth.overrideTransaction vs w3.eth.replaceTransaction? I feel like w3.eth.overrideTransaction might give the impression that only parts of the transaction could be overwritten. Is it just my english? :smile:

I don't think `override` implies that only part will change, but it **is** a more common usage to say to override part of something than to replace part of something. So I don't think override adds that bit of confusion, but I'm happy with `replace` as an alternative. It's also a little bit easier to spell.

> Is this some limitation in how the nodes function? ... could you clarify what the purpose of this kind of logic is? What would be the consequence of performing the same gasPrice defaulting that sending a new transaction has? 

Yes, once you have broadcast a transaction, the only effective way to replace it is with a transaction that has a higher gas price (by 10%, in most default implementations). Mining nodes will just ignore lower priced transaction broadcasts (why not, they get more fees for mining with your earlier broadcast transaction :smile:  ).
__The funding of 0.25 ETH (238.7 USD) attached has been claimed by @monokh.__ 

 @monokh, please leave a comment to let the funder  and the other parties involved your implementation plan.  If you don't leave a comment, the funder may expire your claim at their discretion. 

 * Learn more [on the gitcoin issue page](https://gitcoin.co/funding/details?url=https://github.com/ethereum/web3.py/issues/492)
 * Questions? Get help on the <a href='https://gitcoin.co/slack'>Gitcoin Slack</a>
 * $14321.91 more Funded OSS Work Available at: https://gitcoin.co/explorer

@monokh thx for the heads up, i just sync'd the issue from web3 => gitcoin
@carver thanks 
> Yes, once you have broadcast a transaction, the only effective way to replace it is with a transaction that has a higher gas price (by 10%, in most default implementations). Mining nodes will just ignore lower priced transaction broadcasts (why not, they get more fees for accepting your earlier broadcast transaction 😄 ).

That makes sense from a convenience perspective but I wonder if considering this in the API itself is necessary. For me, I would expect replacing a transaction to work as any other transaction, i.e. i provide the parameters and the node receives it. Now, if I provided a parameter that miners didn't like then as the consumer of the API, I should understand and handle that. That means that if miner expectations of these parameters change for whatever reason in the future then apps can adapt.

Hope i'm not being too pedantic here. I guess its a question of how much convenience vs flexibility this api wants to provide. 

I'm just getting the project up and creating an example for myself to walk through. I should hopefully have something to show tomorrow 🙂 
> That makes sense from a convenience perspective but I wonder if considering this in the API itself is necessary. For me, I would expect replacing a transaction to work as any other transaction, i.e. i provide the parameters and the node receives it. Now, if I provided a parameter that miners didn't like then as the consumer of the API, I should understand and handle that.

It goes further than this, because nodes won't relay the transaction if it doesn't have a gas price bump. Last I checked `geth` errors if you try to replace a transaction without giving it a 10% price bump. I could maybe be convinced that if someone hard-codes the gas into the replacement, that we can just let the node error out. But we absolutely need to give at least a 10% price bump when estimating the price for the user.
What is the behaviour when geth does error? Does the RPC call return something useful where we can relay back to the user the fact that their gas price didn't meet requirements? Something for me to have a look at.

I think its starting to click with me why it makes sense to provide the gas price estimation this way, given that the default gas price is the node's implementation anyway.

We still want the user to be able to override the gasPrice though right? maybe to increase it higher than 10% or for another reason. At that point it should be their responsibility the outcome of their parameters.
> We still want the user to be able to override the gasPrice though right?

Yes, for sure.
No codes today unfortunately but I have been exploring the implementation. Hit a bit of a roadblock though

There's a requirement here for doing the replacement based on an unmined transaction. 

> if no transaction can be found with the given hash an exception is thrown.
> if the transaction has already been mined an exception is thrown.

This makes sense as the checks and integration with the previous transaction is what makes it easy and useful as compared to just sending another transaction manually. However there seems to be a limitation on what can be gathered from an unmined transaction using its hash. `eth_getTransactionByHash` isn't able to return a submitted transaction until its mined. This is from my testing using testrpc and an infura node. Please correct me if i'm wrong here, I'm about to test with a parity node. But if this was the case, the only option then would be to watch a pending transaction filter running in the background which feels a little impractical. 

Thoughts? I might be going down the wrong alley 😄 
> However there seems to be a limitation on what can be gathered from an unmined transaction using its hash. eth_getTransactionByHash isn't able to return a submitted transaction until its mined.

Check out the [json-rpc docs](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_gettransactionbyhash). That method is definitely intended to work for pending transactions.

For example:

> blockHash: DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending.

Just a guess: testrpc was set to auto-mine, so it's never pending when you ask for it. Infura doesn't keep a running list of pending transactions, so Infura just won't support this ability as well as a full node.
> blockHash: DATA, 32 Bytes - hash of the block where this transaction was in. null when its pending.

Yea that's exactly what made me think initially that unmined transactions should be able to be retrieved with that rpc call. 

> Just a guess: testrpc was set to auto-mine, so it's never pending when you ask for it.

I did try configuring block time on testrpc to a certain # of seconds but wasn't able to see the pending transactions. I asked truffle people if ganache had some sort of bug and they are unsure. Issue to be raised later on with them to look into that.

I'll be trying a full node (parity) later on to see what it looks like there 🙂  🤞 
Results are in. `eth_getTransactionByHash` returns the unmined transactions fine on full nodes. Probably worth considering that some nodes may not though in which case the API won't be usable... It's strange that Infura doesn't keep the tx pool, maybe we can convince that this is helpful? 

Did I mention by testrpc i meant ganache-cli? 😄  Issue raised with them to look into it. With the the python testrpc, it seems that it doesn't support blocktimes and calling eth_getTransactionByHash just returns the immediately mined transaction which presents an issue for testing 🤔  for the tests required in the library and in general for development

Anyway, I can make a start on the implementation while following up these threads. |

Btw i found that there is a web3py gitter, I can catch you there instead of clogging up this topic.
> Btw i found that there is a web3py gitter, I can catch you there instead of clogging up this topic.

Yeah, come on over if there's something you think we can knock out faster on chat. But sometimes those conversations get lost, so it can be nice to have them grouped together under an issue. I'm totally happy to keep related discussion happening on this page, it wouldn't be "clogging up" the thread. :)
Web3.py actually uses eth-tester now which includes options for disabling auto mining. Therefore there won't be problems with the tests or development:
```
tester = EthereumTester(auto_mine_transactions=False)
web3 = Web3(EthereumTesterProvider(tester))
tx = web3.eth.sendTransaction({
    'from': '0x82A978B3f5962A5b0957d9ee9eEf472EE55B42F1',
    'to': '0x7d577a597B2742b498Cb5Cf0C26cDCD726d39E6e',
    'value': '1000'
})
print(web3.eth.getTransaction(tx.hex()))
>>> ...... 'nonce': 0, 'blockHash': None, 'blockNumber': None, ......
```

Resolving patch:
diff --git a/web3/eth.py b/web3/eth.py
--- a/web3/eth.py
+++ b/web3/eth.py
@@ -1,5 +1,6 @@
 from cytoolz.dicttoolz import (
     assoc,
+    merge,
 )
 from eth_account import (
     Account,
@@ -37,7 +38,11 @@
     TransactionFilter,
 )
 from web3.utils.transactions import (
+    assert_valid_transaction_params,
+    extract_valid_transaction_params,
     get_buffered_gas_estimate,
+    get_required_transaction,
+    replace_transaction,
 )
 
 
@@ -199,6 +204,17 @@ def getTransactionCount(self, account, block_identifier=None):
             ],
         )
 
+    def replaceTransaction(self, transaction_hash, new_transaction):
+        current_transaction = get_required_transaction(self.web3, transaction_hash)
+        return replace_transaction(self.web3, current_transaction, new_transaction)
+
+    def modifyTransaction(self, transaction_hash, **transaction_params):
+        assert_valid_transaction_params(transaction_params)
+        current_transaction = get_required_transaction(self.web3, transaction_hash)
+        current_transaction_params = extract_valid_transaction_params(current_transaction)
+        new_transaction = merge(current_transaction_params, transaction_params)
+        return replace_transaction(self.web3, current_transaction, new_transaction)
+
     def sendTransaction(self, transaction):
         # TODO: move to middleware
         if 'from' not in transaction and is_checksum_address(self.defaultAccount):
diff --git a/web3/utils/transactions.py b/web3/utils/transactions.py
--- a/web3/utils/transactions.py
+++ b/web3/utils/transactions.py
@@ -1,6 +1,8 @@
+import math
 import random
 
 from cytoolz import (
+    assoc,
     curry,
     merge,
 )
@@ -9,6 +11,17 @@
     Timeout,
 )
 
+VALID_TRANSACTION_PARAMS = [
+    'from',
+    'to',
+    'gas',
+    'gasPrice',
+    'value',
+    'data',
+    'nonce',
+    'chainId',
+]
+
 TRANSACTION_DEFAULTS = {
     'value': 0,
     'data': b'',
@@ -69,3 +82,53 @@ def get_buffered_gas_estimate(web3, transaction, gas_buffer=100000):
         )
 
     return min(gas_limit, gas_estimate + gas_buffer)
+
+
+def get_required_transaction(web3, transaction_hash):
+    current_transaction = web3.eth.getTransaction(transaction_hash)
+    if not current_transaction:
+        raise ValueError('Supplied transaction with hash {} does not exist'
+                         .format(transaction_hash))
+    return current_transaction
+
+
+def extract_valid_transaction_params(transaction_params):
+    return {key: transaction_params[key]
+            for key in VALID_TRANSACTION_PARAMS if key in transaction_params}
+
+
+def assert_valid_transaction_params(transaction_params):
+    for param in transaction_params:
+        if param not in VALID_TRANSACTION_PARAMS:
+            raise ValueError('{} is not a valid transaction parameter'.format(param))
+
+
+def prepare_replacement_transaction(web3, current_transaction, new_transaction):
+    if current_transaction['blockHash'] is not None:
+        raise ValueError('Supplied transaction with hash {} has already been mined'
+                         .format(current_transaction['hash']))
+    if 'nonce' in new_transaction and new_transaction['nonce'] != current_transaction['nonce']:
+        raise ValueError('Supplied nonce in new_transaction must match the pending transaction')
+
+    if 'nonce' not in new_transaction:
+        new_transaction = assoc(new_transaction, 'nonce', current_transaction['nonce'])
+
+    if 'gasPrice' in new_transaction:
+        if new_transaction['gasPrice'] <= current_transaction['gasPrice']:
+            raise ValueError('Supplied gas price must exceed existing transaction gas price')
+    else:
+        generated_gas_price = web3.eth.generateGasPrice(new_transaction)
+        minimum_gas_price = int(math.ceil(current_transaction['gasPrice'] * 1.1))
+        if generated_gas_price and generated_gas_price > minimum_gas_price:
+            new_transaction = assoc(new_transaction, 'gasPrice', generated_gas_price)
+        else:
+            new_transaction = assoc(new_transaction, 'gasPrice', minimum_gas_price)
+
+    return new_transaction
+
+
+def replace_transaction(web3, current_transaction, new_transaction):
+    new_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction
+    )
+    return web3.eth.sendTransaction(new_transaction)


Test patch:
diff --git a/tests/core/utilities/test_prepare_transaction_replacement.py b/tests/core/utilities/test_prepare_transaction_replacement.py
new file mode 100644
--- /dev/null
+++ b/tests/core/utilities/test_prepare_transaction_replacement.py
@@ -0,0 +1,122 @@
+import pytest
+
+from web3.utils.transactions import (
+    prepare_replacement_transaction,
+)
+
+SIMPLE_CURRENT_TRANSACTION = {
+    'blockHash': None,
+    'hash': '0x0',
+    'nonce': 2,
+    'gasPrice': 10,
+}
+
+
+def test_prepare_transaction_replacement(web3):
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 1,
+        'nonce': 2,
+    }
+    replacement_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction)
+
+    assert replacement_transaction == {
+        'value': 1,
+        'nonce': 2,
+        'gasPrice': 11,
+    }
+
+
+def test_prepare_transaction_replacement_without_nonce_sets_correct_nonce(web3):
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 1,
+    }
+    replacement_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction)
+    assert replacement_transaction == {
+        'value': 1,
+        'nonce': 2,
+        'gasPrice': 11,
+    }
+
+
+def test_prepare_transaction_replacement_already_mined_raises(web3):
+    with pytest.raises(ValueError):
+        prepare_replacement_transaction(
+            web3, {'blockHash': '0xa1a1a1', 'hash': '0x0'}, {'value': 2})
+
+
+def test_prepare_transaction_replacement_nonce_mismatch_raises(web3):
+    with pytest.raises(ValueError):
+        prepare_replacement_transaction(web3, {
+            'blockHash': None,
+            'hash': '0x0',
+            'nonce': 1,
+        }, {
+            'nonce': 2,
+        })
+
+
+def test_prepare_transaction_replacement_not_higher_gas_price_raises(web3):
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 1,
+        'gasPrice': 5,
+    }
+    with pytest.raises(ValueError):
+        prepare_replacement_transaction(
+            web3, current_transaction, new_transaction)
+
+    # Also raises when equal to the current transaction
+    new_transaction['gasPrice'] = 10
+    with pytest.raises(ValueError):
+        prepare_replacement_transaction(web3, current_transaction, new_transaction)
+
+
+def test_prepare_transaction_replacement_gas_price_defaulting(web3):
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 2,
+    }
+    replacement_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction)
+
+    assert replacement_transaction['gasPrice'] == 11
+
+
+def test_prepare_transaction_replacement_gas_price_defaulting_when_strategy_higer(web3):
+
+    def higher_gas_price_strategy(web3, txn):
+        return 20
+
+    web3.eth.setGasPriceStrategy(higher_gas_price_strategy)
+
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 2,
+    }
+
+    replacement_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction)
+
+    assert replacement_transaction['gasPrice'] == 20
+
+
+def test_prepare_transaction_replacement_gas_price_defaulting_when_strategy_lower(web3):
+
+    def lower_gas_price_strategy(web3, txn):
+        return 5
+
+    web3.eth.setGasPriceStrategy(lower_gas_price_strategy)
+
+    current_transaction = SIMPLE_CURRENT_TRANSACTION
+    new_transaction = {
+        'value': 2,
+    }
+
+    replacement_transaction = prepare_replacement_transaction(
+        web3, current_transaction, new_transaction)
+
+    assert replacement_transaction['gasPrice'] == 11
diff --git a/tests/core/utilities/test_valid_transaction_params.py b/tests/core/utilities/test_valid_transaction_params.py
new file mode 100644
--- /dev/null
+++ b/tests/core/utilities/test_valid_transaction_params.py
@@ -0,0 +1,71 @@
+import pytest
+
+from web3.utils.transactions import (
+    assert_valid_transaction_params,
+    extract_valid_transaction_params,
+)
+
+
+def test_assert_valid_transaction_params_all_params():
+    assert_valid_transaction_params({
+        'from': '0x0',
+        'to': '0x0',
+        'gas': 21000,
+        'gasPrice': 5000000,
+        'value': 1,
+        'data': '0x0',
+        'nonce': 2,
+        'chainId': 1,
+    })
+
+
+def test_assert_valid_transaction_params_some_params():
+    assert_valid_transaction_params({
+        'from': '0x0',
+        'to': '0x0',
+        'value': 1,
+    })
+
+
+def test_assert_valid_transaction_params_invalid_param():
+    with pytest.raises(ValueError):
+        assert_valid_transaction_params({
+            'from': '0x0',
+            'to': '0x0',
+            'value': 1,
+            'tokens': 9000,
+        })
+
+
+def test_extract_valid_transaction_params():
+    input = {
+        'from': '0x0',
+        'to': '0x0',
+        'gas': 21000,
+        'gasPrice': 5000000,
+        'value': 1,
+        'data': '0x0',
+        'nonce': 2,
+        'chainId': 1,
+    }
+    valid_transaction_params = extract_valid_transaction_params(input)
+    assert valid_transaction_params == input
+
+
+def test_extract_valid_transaction_params_includes_invalid():
+    input = {
+        'from': '0x0',
+        'to': '0x0',
+        'gas': 21000,
+        'gasPrice': 5000000,
+        'value': 1,
+        'tokens': 9000,
+    }
+    valid_transaction_params = extract_valid_transaction_params(input)
+    assert valid_transaction_params == {
+        'from': '0x0',
+        'to': '0x0',
+        'gas': 21000,
+        'gasPrice': 5000000,
+        'value': 1,
+    }
diff --git a/tests/integration/test_ethereum_tester.py b/tests/integration/test_ethereum_tester.py
--- a/tests/integration/test_ethereum_tester.py
+++ b/tests/integration/test_ethereum_tester.py
@@ -175,18 +175,65 @@ def inner(*args, **kwargs):
     return inner
 
 
-class TestEthereumTesterEthModule(EthModuleTest):
-    test_eth_sign = not_implemented(EthModuleTest.test_eth_sign, ValueError)
-
-    def test_eth_getTransactionReceipt_unmined(self, eth_tester, web3, unlocked_account):
+def disable_auto_mine(func):
+    @functools.wraps(func)
+    def func_wrapper(self, eth_tester, *args, **kwargs):
         eth_tester.disable_auto_mine_transactions()
         try:
-            super().test_eth_getTransactionReceipt_unmined(
-                web3, unlocked_account,
-            )
+            func(self, eth_tester, *args, **kwargs)
         finally:
             eth_tester.enable_auto_mine_transactions()
             eth_tester.mine_block()
+    return func_wrapper
+
+
+class TestEthereumTesterEthModule(EthModuleTest):
+    test_eth_sign = not_implemented(EthModuleTest.test_eth_sign, ValueError)
+
+    @disable_auto_mine
+    def test_eth_getTransactionReceipt_unmined(self, eth_tester, web3, unlocked_account):
+        super().test_eth_getTransactionReceipt_unmined(web3, unlocked_account)
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction(self, eth_tester, web3, unlocked_account):
+        super().test_eth_replaceTransaction(web3, unlocked_account)
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction_incorrect_nonce(self, eth_tester, web3, unlocked_account):
+        super().test_eth_replaceTransaction_incorrect_nonce(web3, unlocked_account)
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction_gas_price_too_low(self, eth_tester, web3, unlocked_account):
+        super().test_eth_replaceTransaction_gas_price_too_low(web3, unlocked_account)
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction_gas_price_defaulting_minimum(self,
+                                                                 eth_tester,
+                                                                 web3,
+                                                                 unlocked_account):
+        super().test_eth_replaceTransaction_gas_price_defaulting_minimum(web3, unlocked_account)
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(self,
+                                                                         eth_tester,
+                                                                         web3,
+                                                                         unlocked_account):
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(
+            web3, unlocked_account
+        )
+
+    @disable_auto_mine
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(self,
+                                                                        eth_tester,
+                                                                        web3,
+                                                                        unlocked_account):
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(
+            web3, unlocked_account
+        )
+
+    @disable_auto_mine
+    def test_eth_modifyTransaction(self, eth_tester, web3, unlocked_account):
+        super().test_eth_modifyTransaction(web3, unlocked_account)
 
 
 class TestEthereumTesterVersionModule(VersionModuleTest):
diff --git a/tests/integration/test_goethereum.py b/tests/integration/test_goethereum.py
--- a/tests/integration/test_goethereum.py
+++ b/tests/integration/test_goethereum.py
@@ -261,7 +261,41 @@ def _check_web3_clientVersion(self, client_version):
 
 
 class TestGoEthereumEthModule(EthModuleTest):
-    pass
+    def test_eth_replaceTransaction(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_incorrect_nonce(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_incorrect_nonce(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_too_low(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_too_low(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_defaulting_minimum(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_minimum(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(self,
+                                                                         web3,
+                                                                         unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(
+            web3, unlocked_account
+        )
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(self,
+                                                                        web3,
+                                                                        unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(
+            web3, unlocked_account
+        )
+
+    def test_eth_modifyTransaction(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_modifyTransaction(web3, unlocked_account)
 
 
 class TestGoEthereumVersionModule(VersionModuleTest):
diff --git a/tests/integration/test_parity.py b/tests/integration/test_parity.py
--- a/tests/integration/test_parity.py
+++ b/tests/integration/test_parity.py
@@ -297,6 +297,46 @@ def test_eth_newBlockFilter(self, web3):
         pytest.xfail('Parity returns latest block on first polling for new blocks')
         super().test_eth_newBlockFilter(web3)
 
+    def test_eth_replaceTransaction(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_already_mined(self, web3, unlocked_account):
+        pytest.xfail('Parity is not setup to auto mine')
+        super().test_eth_replaceTransaction_already_mined(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_incorrect_nonce(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_incorrect_nonce(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_too_low(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_too_low(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_defaulting_minimum(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_minimum(web3, unlocked_account)
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(self,
+                                                                         web3,
+                                                                         unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(
+            web3, unlocked_account
+        )
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(self,
+                                                                        web3,
+                                                                        unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(
+            web3, unlocked_account
+        )
+
+    def test_eth_modifyTransaction(self, web3, unlocked_account):
+        pytest.xfail('Needs ability to efficiently control mining')
+        super().test_eth_modifyTransaction(web3, unlocked_account)
+
 
 class TestParityVersionModule(VersionModuleTest):
     pass
diff --git a/web3/utils/module_testing/eth_module.py b/web3/utils/module_testing/eth_module.py
--- a/web3/utils/module_testing/eth_module.py
+++ b/web3/utils/module_testing/eth_module.py
@@ -232,6 +232,167 @@ def test_eth_sendTransaction_with_nonce(self, web3, unlocked_account):
         assert txn['gasPrice'] == txn_params['gasPrice']
         assert txn['nonce'] == txn_params['nonce']
 
+    def test_eth_replaceTransaction(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': web3.eth.gasPrice,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        txn_params['gasPrice'] = web3.eth.gasPrice * 2
+        replace_txn_hash = web3.eth.replaceTransaction(txn_hash, txn_params)
+        replace_txn = web3.eth.getTransaction(replace_txn_hash)
+
+        assert is_same_address(replace_txn['from'], txn_params['from'])
+        assert is_same_address(replace_txn['to'], txn_params['to'])
+        assert replace_txn['value'] == 1
+        assert replace_txn['gas'] == 21000
+        assert replace_txn['gasPrice'] == txn_params['gasPrice']
+
+    def test_eth_replaceTransaction_non_existing_transaction(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': web3.eth.gasPrice,
+        }
+        with pytest.raises(ValueError):
+            web3.eth.replaceTransaction(
+                '0x98e8cc09b311583c5079fa600f6c2a3bea8611af168c52e4b60b5b243a441997',
+                txn_params
+            )
+
+    # auto mine is enabled for this test
+    def test_eth_replaceTransaction_already_mined(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': web3.eth.gasPrice,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        txn_params['gasPrice'] = web3.eth.gasPrice * 2
+        with pytest.raises(ValueError):
+            web3.eth.replaceTransaction(txn_hash, txn_params)
+
+    def test_eth_replaceTransaction_incorrect_nonce(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': web3.eth.gasPrice,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+        txn = web3.eth.getTransaction(txn_hash)
+
+        txn_params['gasPrice'] = web3.eth.gasPrice * 2
+        txn_params['nonce'] = txn['nonce'] + 1
+        with pytest.raises(ValueError):
+            web3.eth.replaceTransaction(txn_hash, txn_params)
+
+    def test_eth_replaceTransaction_gas_price_too_low(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': 10,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        txn_params['gasPrice'] = 9
+        with pytest.raises(ValueError):
+            web3.eth.replaceTransaction(txn_hash, txn_params)
+
+    def test_eth_replaceTransaction_gas_price_defaulting_minimum(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': 10,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        txn_params.pop('gasPrice')
+        replace_txn_hash = web3.eth.replaceTransaction(txn_hash, txn_params)
+        replace_txn = web3.eth.getTransaction(replace_txn_hash)
+
+        assert replace_txn['gasPrice'] == 11  # minimum gas price
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_higher(self,
+                                                                         web3,
+                                                                         unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': 10,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        def higher_gas_price_strategy(web3, txn):
+            return 20
+
+        web3.eth.setGasPriceStrategy(higher_gas_price_strategy)
+
+        txn_params.pop('gasPrice')
+        replace_txn_hash = web3.eth.replaceTransaction(txn_hash, txn_params)
+        replace_txn = web3.eth.getTransaction(replace_txn_hash)
+        assert replace_txn['gasPrice'] == 20  # Strategy provides higher gas price
+
+    def test_eth_replaceTransaction_gas_price_defaulting_strategy_lower(self,
+                                                                        web3,
+                                                                        unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': 10,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        def lower_gas_price_strategy(web3, txn):
+            return 5
+
+        web3.eth.setGasPriceStrategy(lower_gas_price_strategy)
+
+        txn_params.pop('gasPrice')
+        replace_txn_hash = web3.eth.replaceTransaction(txn_hash, txn_params)
+        replace_txn = web3.eth.getTransaction(replace_txn_hash)
+        # Strategy provices lower gas price - minimum preferred
+        assert replace_txn['gasPrice'] == 11
+
+    def test_eth_modifyTransaction(self, web3, unlocked_account):
+        txn_params = {
+            'from': unlocked_account,
+            'to': unlocked_account,
+            'value': 1,
+            'gas': 21000,
+            'gasPrice': web3.eth.gasPrice,
+        }
+        txn_hash = web3.eth.sendTransaction(txn_params)
+
+        modified_txn_hash = web3.eth.modifyTransaction(
+            txn_hash, gasPrice=(txn_params['gasPrice'] * 2), value=2
+        )
+        modified_txn = web3.eth.getTransaction(modified_txn_hash)
+
+        assert is_same_address(modified_txn['from'], txn_params['from'])
+        assert is_same_address(modified_txn['to'], txn_params['to'])
+        assert modified_txn['value'] == 2
+        assert modified_txn['gas'] == 21000
+        assert modified_txn['gasPrice'] == txn_params['gasPrice'] * 2
+
     @pytest.mark.parametrize(
         'raw_transaction, expected_hash',
         [