diff --git a/conflux_web3/utils.py b/conflux_web3/utils/__init__.py similarity index 100% rename from conflux_web3/utils.py rename to conflux_web3/utils/__init__.py diff --git a/conflux_web3/utils/address.py b/conflux_web3/utils/address.py new file mode 100644 index 0000000..ffbbdff --- /dev/null +++ b/conflux_web3/utils/address.py @@ -0,0 +1,94 @@ +from typing import ( + Union, + overload, +) + +import rlp +from hexbytes import HexBytes +from eth_utils import ( + keccak, + to_bytes, + to_checksum_address, +) + +from cfx_address import Base32Address +from cfx_address.utils import ( + normalize_to +) +from cfx_utils.types import ( + ChecksumAddress, +) + +def get_create_address(sender: Union[str, Base32Address], nonce: int, bytecode_hash: Union[bytes, str]) -> Union[ChecksumAddress, Base32Address]: + """ + Determine the resulting `CREATE` opcode contract address for a sender and a nonce. + Typically, the sender is a wallet address and the nonce is the next nonce of the sender. + NOTE: in Conflux, the contract address is computed differently from that in Ethereum + where the bytecode hash is not accounted for in the address computation. + + :param sender: The address of the sender. Can be a hex string or a base32 address. + :param nonce: The nonce of the sender. + :param bytecode_hash: The keccak256 hash of the contract bytecode, whereas the "data" field of the transaction. Can be bytes or hex string. + :return: The computed address as a hex string or base32 address, depending on the type of sender + """ + address_hex = normalize_to(sender, None) + contract_address = "0x8" + keccak( + b"\x00" + + to_bytes(hexstr=address_hex) + + nonce.to_bytes(32, "little") + + HexBytes(bytecode_hash) + ).hex()[-39:] + if Base32Address.is_valid_base32(sender): + return Base32Address(contract_address, network_id=Base32Address(sender).network_id) + return to_checksum_address(contract_address) + + +def get_create2_address( + create2_factory_address: Union[str, Base32Address], salt: bytes, bytecode_hash: Union[bytes, str] +) -> Union[ChecksumAddress, Base32Address]: + """ + Compute the address of a contract created using CREATE2. + + :param create2_factory_address: The address of the CREATE2 factory contract. On Conflux, it is deployed via `CIP-31 `_ + :param salt: A 32-byte value used as salt. Should be bytes. + :param bytecode_hash: The keccak256 hash of the contract bytecode. Can be bytes or hex string. + :return: The computed address as a hex string or base32 address, depending on the type of create2_factory_address + """ + address_hex = normalize_to(create2_factory_address, None) + contract_address = "0x8" + keccak( + b"\xff" + + to_bytes(hexstr=address_hex) + + salt + + HexBytes(bytecode_hash) + ).hex()[-39:] + if Base32Address.is_valid_base32(create2_factory_address): + return Base32Address(contract_address, network_id=Base32Address(create2_factory_address).network_id) + return to_checksum_address(contract_address) + +# def get_create2_address( +# salt: bytes, +# bytecode_hash: Union[bytes, str], +# create2_factory_address: Union[str, Base32Address] +# ) -> Union[str, Base32Address]: +# """ +# Compute the address of a contract created using CREATE2. + +# :param salt: A 32-byte value used as salt. Should be bytes. +# :param bytecode_hash: The keccak256 hash of the contract bytecode. Can be bytes or hex string. +# :param create2_factory_address: The address of the CREATE2 factory contract. On Conflux, it is deployed via `CIP-31 `_ +# :return: The computed address as a hex string or base32 address, depending on the type of create2_factory_address +# """ + +# # Ensure bytecode_hash is bytes32 +# if isinstance(bytecode_hash, str): +# bytecode_hash = bytes.fromhex(bytecode_hash.replace('0x', '')) +# elif len(bytecode_hash) != 32: +# raise ValueError("Bytecode hash must be 32 bytes long") + +# address_hex = to_bytes(hexstr=create2_factory_address.eth_checksum_address) if isinstance(create2_factory_address, Base32Address) else create2_factory_address + +# core_part = keccak( +# ["bytes1", "address", "bytes32", "bytes32"], +# ["0xff", create2_factory_address, salt, bytecode_hash], +# ) +# return "0x8" + core_part.hex()[-39:] \ No newline at end of file diff --git a/tests/transaction/test_contract.py b/tests/transaction/test_contract.py index 515ea07..1399757 100644 --- a/tests/transaction/test_contract.py +++ b/tests/transaction/test_contract.py @@ -1,5 +1,10 @@ from typing import TYPE_CHECKING import os, json, pytest + +from hexbytes import HexBytes + +from web3.exceptions import ValidationError + from conflux_web3 import Web3 from conflux_web3.contract import ( ConfluxContract, @@ -8,10 +13,13 @@ get_contract_metadata ) from cfx_utils.exceptions import Base32AddressNotMatch -from conflux_web3.middleware.wallet import Wallet + from cfx_account import LocalAccount + +from conflux_web3.middleware.wallet import Wallet from tests._test_helpers.type_check import TypeValidator -from web3.exceptions import ValidationError + +from conflux_web3.utils.address import get_create_address if TYPE_CHECKING: from conflux_web3 import Web3 @@ -34,9 +42,12 @@ def test_contract_deploy_and_transfer(self, w3_: Web3): # test deployment erc20_metadata = get_contract_metadata("ERC20") erc20 = w3_.cfx.contract(bytecode=erc20_metadata["bytecode"], abi=erc20_metadata["abi"]) - hash = erc20.constructor(name="Coin", symbol="C", initialSupply=10**18).transact() - contract_address = w3_.cfx.wait_for_transaction_receipt(hash)["contractCreated"] - assert contract_address is not None + tx_hash = erc20.constructor(name="Coin", symbol="C", initialSupply=10**18).transact() + contract_address = tx_hash.executed()["contractCreated"] + tx_data = w3_.cfx.get_transaction(tx_hash) + computed_contract_address = get_create_address(w3_.cfx.default_account, tx_data["nonce"], w3_.keccak(tx_data["data"])) # type: ignore + assert contract_address == computed_contract_address, (contract_address, computed_contract_address) + contract = w3_.cfx.contract(contract_address, abi=erc20_metadata["abi"]) # test transfer diff --git a/tests/utils/test_contract_utils.py b/tests/utils/test_contract_utils.py new file mode 100644 index 0000000..43945bf --- /dev/null +++ b/tests/utils/test_contract_utils.py @@ -0,0 +1,21 @@ +from conflux_web3 import Web3 +from conflux_web3.types import Base32Address +from cfx_address.utils import normalize_to +from conflux_web3.utils.address import get_create_address, get_create2_address + +bytecode = "0x608060405234801561001057600080fd5b506101a1806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80637cf5dab014610030575b600080fd5b61004a600480360381019061004591906100b1565b610060565b60405161005791906100ed565b60405180910390f35b600060018261006f9190610137565b9050919050565b600080fd5b6000819050919050565b61008e8161007b565b811461009957600080fd5b50565b6000813590506100ab81610085565b92915050565b6000602082840312156100c7576100c6610076565b5b60006100d58482850161009c565b91505092915050565b6100e78161007b565b82525050565b600060208201905061010260008301846100de565b92915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101428261007b565b915061014d8361007b565b925082820190508082111561016557610164610108565b5b9291505056fea26469706673582212208eb410cb79fbf08652e19d496e31d076d04be7ed242c64a44aec4c5af0f2533b64736f6c63430008130033" + +def test_get_create_address(): + address = "cfx:aamz08kfa8wsu69jhhcgrwkjkh69p85wj6222847yp" + nonce = 1 + bytecode_hash = Web3.solidity_keccak(["bytes"], [bytecode]) + assert get_create_address(address, nonce, bytecode_hash) == Base32Address("0x837f77a1e8da5b860905a07bc1921e43fbfb04ef", 1029) + assert get_create_address(Base32Address(address).hex_address, nonce, bytecode_hash) == normalize_to("0x837f77a1e8da5b860905a07bc1921e43fbfb04ef", None) + +def test_get_create2_address(): + salt = (1111).to_bytes(32, 'big') + bytecode_hash = Web3.solidity_keccak(["bytes"], [bytecode]) + create2_factory_address = "0x8A3A92281Df6497105513B18543fd3B60c778E40" + assert get_create2_address(create2_factory_address, salt, bytecode_hash) == normalize_to("0x80ac53cc16c0b58dc5bde5af47f5ef9e84693fe4", None) + assert get_create2_address(Base32Address(create2_factory_address, 1029), salt, bytecode_hash) == normalize_to("0x80ac53cc16c0b58dc5bde5af47f5ef9e84693fe4", 1029) + \ No newline at end of file