From 97d97bb944694ba24fbf09b68776a7252c6e4bb5 Mon Sep 17 00:00:00 2001 From: eric-volz Date: Tue, 19 Sep 2023 18:03:54 +0200 Subject: [PATCH 1/3] make transactions replaceable with TxBuilder #111 --- .../builder/rawtransactionbuilder.py | 40 +++++++++++++++---- defichain/transactions/builder/txbuilder.py | 19 +++++++-- 2 files changed, 49 insertions(+), 10 deletions(-) diff --git a/defichain/transactions/builder/rawtransactionbuilder.py b/defichain/transactions/builder/rawtransactionbuilder.py index 31f4f9e1..932ca0bf 100644 --- a/defichain/transactions/builder/rawtransactionbuilder.py +++ b/defichain/transactions/builder/rawtransactionbuilder.py @@ -2,12 +2,13 @@ from defichain.exceptions.transactions import TxBuilderError, NotYetSupportedError from defichain.transactions.address import Address -from defichain.transactions.constants import AddressTypes, DefiTxType +from defichain.transactions.constants import AddressTypes, DefiTxType, SEQUENCE from defichain.networks import Network from defichain.transactions.remotedata.remotedata import RemoteData from defichain.transactions.rawtransactions import Transaction, TxInput, TxP2PKHInput, TxP2WPKHInput, TxP2SHInput, \ TxAddressOutput, TxDefiOutput, estimate_fee from defichain.transactions.defitx.modules.basedefitx import BaseDefiTx +from defichain.transactions.utils import Converter class RawTransactionBuilder: @@ -16,12 +17,13 @@ class RawTransactionBuilder: def new_transaction() -> Transaction: return Transaction([], []) - def __init__(self, address: str, account: Account, dataSource: RemoteData, feePerByte: float): - self._address, self._account, self._dataSource, self._feePerByte = None, None, None, None + def __init__(self, address: str, account: Account, dataSource: RemoteData, feePerByte: float, replaceable: bool): + self._address, self._account, self._dataSource, self._feePerByte, self._replaceable = None, None, None, None, None self.set_address(address) self.set_account(account) self.set_dataSource(dataSource) self.set_feePerByte(feePerByte) + self.set_replaceable(replaceable) # Build Transaction def build_transactionInputs(self, inputs=[]) -> Transaction: @@ -30,18 +32,35 @@ def build_transactionInputs(self, inputs=[]) -> Transaction: if inputs or self.get_dataSource() is None: tx.set_inputs(inputs) else: + # Transaction should be replaceable + if self.get_replaceable(): + sequence: str = Converter.int_to_hex(Converter.hex_to_int(SEQUENCE) - 1, 4) + else: + sequence: str = SEQUENCE + for input in self.get_dataSource().get_unspent(self.get_address()): address = Address.from_scriptPublicKey(self.get_account().get_network(), input["scriptPubKey"]) # Build P2PKH Input if address.get_addressType() == AddressTypes.P2PKH: - tx.add_input(TxP2PKHInput(input["txid"], input["vout"], self.get_address(), input["value"])) + tx.add_input(TxP2PKHInput(input["txid"], + input["vout"], + self.get_address(), + input["value"], + sequence)) # Build P2SH Input elif address.get_addressType() == AddressTypes.P2SH: - tx.add_input(TxP2SHInput(input["txid"], input["vout"], self.get_account().get_p2wpkh(), - input["value"])) + tx.add_input(TxP2SHInput(input["txid"], + input["vout"], + self.get_account().get_p2wpkh(), + input["value"], + sequence)) # Build P2WPKH Input elif address.get_addressType() == AddressTypes.P2WPKH: - tx.add_input(TxP2WPKHInput(input["txid"], input["vout"], self.get_address(), input["value"])) + tx.add_input(TxP2WPKHInput(input["txid"], + input["vout"], + self.get_address(), + input["value"], + sequence)) # Check Inputs for masternode collateral tx.set_inputs(self.checkMasternodeInputs(tx.get_inputs())) if not tx.get_inputs(): @@ -127,6 +146,9 @@ def get_dataSource(self) -> "RemoteData": def get_feePerByte(self) -> float: return self._feePerByte + def get_replaceable(self) -> bool: + return self._replaceable + def get_network(self) -> Network: return self.get_account().get_network() @@ -142,3 +164,7 @@ def set_dataSource(self, dataSource: RemoteData) -> None: def set_feePerByte(self, feePerByte: float) -> None: self._feePerByte = feePerByte + + def set_replaceable(self, replaceable: bool) -> None: + self._replaceable = replaceable + diff --git a/defichain/transactions/builder/txbuilder.py b/defichain/transactions/builder/txbuilder.py index db5bb542..6925e432 100644 --- a/defichain/transactions/builder/txbuilder.py +++ b/defichain/transactions/builder/txbuilder.py @@ -52,15 +52,17 @@ class TxBuilder: :param feePerByte: (optional) approximate fee paid per byte :type feePerByte: float """ - def __init__(self, address: str, account: Account, dataSource: "Ocean | Node | None", feePerByte=1.0): - self._address, self._account, self._dataSource, self._feePerByte = None, None, None, None + def __init__(self, address: str, account: Account, dataSource: "Ocean | Node | None", feePerByte=1.0, + replaceable: bool = False): + self._address, self._account, self._dataSource, self._feePerByte, self._replaceable = None, None, None, None, None self._set_address(address) self._set_account(account) self._set_dataSource(dataSource) self._set_feePerByte(feePerByte) + self._set_replaceable(replaceable) self._builder = RawTransactionBuilder(self.get_address(), self.get_account(), self.get_dataSource(), - self.get_feePerByte()) + self.get_feePerByte(), self.get_replaceable()) self.utxo = UTXO(self._builder) self.data = Data(self._builder) @@ -170,6 +172,14 @@ def get_feePerByte(self) -> float: """ return self._feePerByte + def get_replaceable(self) -> bool: + """ + Returns if the builder object creates transactions that are replaceable + + :return: bool + """ + return self._replaceable + # Set Information def _set_address(self, address: str) -> None: self._address = address @@ -190,4 +200,7 @@ def _set_dataSource(self, dataSource: "Ocean | Node | None") -> None: def _set_feePerByte(self, feePerByte: float) -> None: self._feePerByte = feePerByte + def _set_replaceable(self, replaceable: bool) -> None: + self._replaceable = replaceable + From a4a274a5120d49e0c71ebe6ca32d8515ead54a8e Mon Sep 17 00:00:00 2001 From: eric-volz Date: Tue, 19 Sep 2023 18:33:31 +0200 Subject: [PATCH 2/3] change replaceable number --- defichain/transactions/builder/rawtransactionbuilder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/defichain/transactions/builder/rawtransactionbuilder.py b/defichain/transactions/builder/rawtransactionbuilder.py index 932ca0bf..796a68ee 100644 --- a/defichain/transactions/builder/rawtransactionbuilder.py +++ b/defichain/transactions/builder/rawtransactionbuilder.py @@ -34,7 +34,7 @@ def build_transactionInputs(self, inputs=[]) -> Transaction: else: # Transaction should be replaceable if self.get_replaceable(): - sequence: str = Converter.int_to_hex(Converter.hex_to_int(SEQUENCE) - 1, 4) + sequence: str = Converter.int_to_hex(Converter.hex_to_int(SEQUENCE) - 2, 4) else: sequence: str = SEQUENCE From 74be68e8badd96b3784716492051837e750c9d4a Mon Sep 17 00:00:00 2001 From: eric-volz Date: Wed, 20 Sep 2023 23:21:00 +0200 Subject: [PATCH 3/3] add test from replaceable transactions --- tests/transactions/builder/__init__.py | 10 ++++++++++ tests/transactions/builder/test_replaceable.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+) create mode 100644 tests/transactions/builder/test_replaceable.py diff --git a/tests/transactions/builder/__init__.py b/tests/transactions/builder/__init__.py index e3ff22b0..e7434278 100644 --- a/tests/transactions/builder/__init__.py +++ b/tests/transactions/builder/__init__.py @@ -25,6 +25,8 @@ class Addresses: builder_p2sh = TxBuilder(Keys.account.get_p2sh(), Keys.account, ocean) builder_p2wpkh = TxBuilder(Keys.account.get_p2wpkh(), Keys.account, ocean) +builder_replaceable = TxBuilder(Keys.account.get_p2wpkh(), Keys.account, ocean, replaceable=True) + class TestAccounts: addressAmountTo = BuildAddressAmounts() @@ -303,3 +305,11 @@ class TestVault: "23a8495022054527bcc3ea6492a094331899f28a0d5ab48a1dcb3b2f1616d984795042d1f1c0121" \ "03f110404297e471ad86d1aabc8a885bd4d1ec71bc3f31bef8ed2ff9ad3032460000000000" + +class TestReplaceableTransactions: + replaceable_transaction_serialized = "0400000000010149b8ea9b2b0224e44126b86bd1e2889a7dac0ec06fcfb0dc4dd13782e1c84" \ + "fce0100000000fdffffff01fc3b000000000000160014ad56321e69b7e2d30aeca9f49979ff" \ + "c53084296f0002483045022100f8ed99fa62c18ecde71dee21d62d8e1c62037d0fd984013c5" \ + "65ef37a5ea52090022057cd12501d5e8d7c4a82219102679dfeb751f009f3e238bfe59840f1" \ + "5debf141012103f110404297e471ad86d1aabc8a885bd4d1ec71bc3f31bef8ed2ff9ad30324" \ + "60000000000" diff --git a/tests/transactions/builder/test_replaceable.py b/tests/transactions/builder/test_replaceable.py new file mode 100644 index 00000000..aac08acc --- /dev/null +++ b/tests/transactions/builder/test_replaceable.py @@ -0,0 +1,14 @@ +import pytest +from . import builder_replaceable, TestReplaceableTransactions, Addresses + +from defichain.transactions.rawtransactions import Transaction +from defichain.networks import DefichainMainnet + + +@pytest.mark.transactions +def test_replaceable(): # 01 + tx = builder_replaceable.utxo.sendall(Addresses.P2WPKH) + + assert tx.serialize() == TestReplaceableTransactions.replaceable_transaction_serialized + + assert Transaction.deserialize(DefichainMainnet, tx.serialize()).serialize() == tx.serialize() \ No newline at end of file