Source code for cfx_account.account

from typing import (
    TYPE_CHECKING,
    Optional,
    Union,
    cast,
    Dict,
    Any,
    Tuple,
    TypeVar,
)
from typing_extensions import Literal
from eth_keys import (
    keys,
)
from eth_keys.datatypes import (
    PrivateKey,
)
from eth_account.hdaccount import (
    key_from_seed,
    seed_from_mnemonic,
)
from eth_account.account import (
    Account as EthAccount,
)
from eth_account.datastructures import (
    SignedMessage,
    SignedTransaction,
)
from eth_account.messages import (
    SignableMessage,
)
from cfx_account.signers.local import LocalAccount
from eth_utils.crypto import (
    keccak,
)
from collections.abc import (
    Mapping,
)
from cytoolz import (
    dissoc,  # type: ignore
)
from hexbytes import (
    HexBytes,
)
from eth_utils.address import to_checksum_address
from eth_account.datastructures import (
    # SignedMessage,
    SignedTransaction,
)
from cfx_account._utils.signing import (
    sign_transaction_dict,
)
from cfx_account.transactions.legacy_transactions import (
    LegacyTransaction,
)
from cfx_address import (
    Base32Address,
    eth_eoa_address_to_cfx_hex,
)
from cfx_address.utils import (
    normalize_to,
)
from cfx_utils.types import (
    TxParam,
    TxDict,
    ChecksumAddress,
    HexStr,
)
from cfx_utils.decorators import (
    combomethod,
)
from cfx_account.types import (
    KeyfileDict,
)

if TYPE_CHECKING:
    from conflux_web3 import Web3

CONFLUX_DEFAULT_PATH = "m/44'/503'/0'/0/0"
VRS = TypeVar("VRS", bytes, HexStr, int)


[docs] class Account(EthAccount): # _default_network_id: Optional[int]=None w3: Optional["Web3"] = None _use_unaudited_hdwallet_features = True
[docs] @combomethod def set_w3(self, w3: "Web3") -> None: self.w3 = w3
# def set_default_network_id(self, network_id: int): # self._default_network_id = network_id
[docs] @combomethod def from_key( self, private_key: Union[bytes, str, PrivateKey], network_id: Optional[int] = None, ) -> LocalAccount: """ returns a LocalAccount object :param str private_key: the raw private key :param Optional[int] network_id: target network of the account, defaults to None :return LocalAccount: object with methods for signing and encrypting >>> acct = Account.from_key( ... 0xb25c7db31feed9122727bf0939dc769a96564b2de4c4726d035b36ecf1e5b364) >>> acct.address '0x1ce9454909639d2d17a3f753ce7d93fa0b9ab12e' >>> acct.key HexBytes('0xb25c7db31feed9122727bf0939dc769a96564b2de4c4726d035b36ecf1e5b364') # These methods are also available: sign_message(), sign_transaction(), encrypt() # They correspond to the same-named methods in Account.* # but without the private key argument """ key = self._parse_private_key(private_key) return LocalAccount( key, self, # use network_id is it is not None # then use None if self.w3 is not set # if self.w3 is set, use self.w3.cfx.chain_id network_id or (self.w3 and self.w3.cfx.chain_id), )
[docs] @combomethod def sign_transaction( self, transaction_dict: TxParam, private_key: Union[bytes, str, PrivateKey], blobs: Optional[Any] = None, ) -> SignedTransaction: """ Sign a transaction using a local private key. Produces signature details and the hex-encoded transaction suitable for broadcast using :meth:`w3.cfx.send_raw_transaction() <conflux_web3.client.ConfluxClient.send_raw_transaction>`. Refer to `Interact with a Contract <https://python-conflux-sdk.readthedocs.io/en/latest/examples/10-send_raw_transaction.html#interact-with-a-contract>`_ to see how to sign for a contract method using `build_transaction` :param TxParam transaction_dict: the transaction with keys: nonce, chainId, to, data, value, storageLimit, epochHeight, gas, and gasPrice. :param Union[bytes,str,PrivateKey] private_key: private_key to be used for signing :raises TypeError: transaction_dict is not a dict-like object :raises ValueError: transaction's from field does not match private_key :return SignedTransaction: an attribute dict contains various details about the signature - most importantly the fields: v, r, and s >>> transaction = { # Note that the address must be in Base32 format or native bytes: 'to': 'cfxtest:aak7fsws4u4yf38fk870218p1h3gxut3ku00u1k1da', 'nonce': 1, 'value': 1, 'gas': 100, 'gasPrice': 1, 'storageLimit': 100, 'epochHeight': 100, 'chainId': 1 } >>> key = '0xcc7939276283a32f60d2fad7d16cac972300308fe99ec98d0e63765d02e24863' >>> signed = Account.sign_transaction(transaction, key) {'hash': HexBytes('0x692a0ea530a264f4e80ce39f393233e90638ef929c8706802e15299fd0b042b9'), 'r': 74715349327018893060702835194036838027583623083228589573427622179540208747230, 'raw_transaction': HexBytes('0xf861dd0101649413d2ba4ed43542e7c54fbb6c5fccb9f269c1f94c016464018080a0a52f639cbed11262a7b88d0a37aef909aa7dc2c36c40689a3d52b8bd1d9482dea054f3bdeb654f73704db4cbc12451fb4c9830ef62b0f24de1a40e4b6fe10f57b2'), # noqa: E501 's': 38424933894051759888751352802050752143518665905311311986258635963723328477106, 'v': 0} >>> w3.cfx.sendRawTransaction(signed.raw_transaction) """ if not isinstance(transaction_dict, Mapping): raise TypeError( "transaction_dict must be dict-like, got %r" % transaction_dict ) account: LocalAccount = self.from_key(private_key) transaction_dict = cast(TxDict, transaction_dict) # allow from field, *only* if it matches the private key if "from" in transaction_dict: if normalize_to(transaction_dict["from"], None) == normalize_to( account.address, None ): sanitized_transaction = cast(TxDict, dissoc(transaction_dict, "from")) else: raise ValueError( "transaction[from] does match key's hex address: " f"from's hex address is {Base32Address(transaction_dict['from']).hex_address}, " f"key's hex address is {account.hex_address}" ) else: sanitized_transaction = transaction_dict # sign transaction ( v, r, s, raw_transaction, ) = sign_transaction_dict( account._key_obj, sanitized_transaction ) # type: ignore transaction_hash = keccak(raw_transaction) return SignedTransaction( raw_transaction=HexBytes(raw_transaction), hash=HexBytes(transaction_hash), r=r, s=s, v=v, )
[docs] @combomethod def from_mnemonic( self, mnemonic: str, passphrase: str = "", account_path: str = CONFLUX_DEFAULT_PATH, network_id: Optional[int] = None, ) -> LocalAccount: """ Generate an account from a mnemonic. :param str mnemonic: space-separated list of BIP39 mnemonic seed words :param str passphrase: Optional passphrase used to encrypt the mnemonic, defaults to "" :param str account_path: pecify an alternate HD path for deriving the seed using BIP32 HD wallet key derivation, defaults to CONFLUX_DEFAULT_PATH(m/44'/503'/0'/0/0) :param Optional[int] network_id: the network id of returned account, defaults to None :return LocalAccount: a LocalAccount object :examples: >>> from cfx_account import Account >>> acct = Account.from_mnemonic('faint also eye industry survey unhappy boil public lemon myself cube sense', network_id=1) >>> acct.address 'cfxtest:aargrnff46pmuy2g1mmrntctkhr5mzamh6nmg361n0' """ # acct: LocalAccount = super().from_mnemonic(mnemonic, passphrase, account_path) seed = seed_from_mnemonic(mnemonic, passphrase) private_key = key_from_seed(seed, account_path) key = self._parse_private_key(private_key) return LocalAccount(key, self, network_id or (self.w3 and self.w3.cfx.chain_id))
[docs] @combomethod def create_with_mnemonic( self, passphrase: str = "", num_words: int = 12, language: str = "english", account_path: str = CONFLUX_DEFAULT_PATH, network_id: Optional[int] = None, ) -> Tuple[LocalAccount, str]: """ Create mnemonic and derive an account using parameters :param str passphrase: Extra passphrase to encrypt the seed phrase, defaults to "" :param int num_words: Number of words to use with seed phrase. Default is 12 words. Must be one of [12, 15, 18, 21, 24]. :param str language: Language to use for BIP39 mnemonic seed phrase, defaults to "english" :param str account_path: Specify an alternate HD path for deriving the seed using BIP32 HD wallet key derivation, defaults to CONFLUX_DEFAULT_PATH(m/44'/503'/0'/0/0) :param Optional[int] network_id: the network id of returned account, defaults to None :return Tuple[LocalAccount, str]: a LocalAccount object and related mnemonic """ acct, mnemonic = super().create_with_mnemonic( passphrase, num_words, language, account_path ) if network_id is not None: acct.network_id = network_id return acct, mnemonic
[docs] @classmethod def encrypt( # type: ignore cls, private_key: Union[bytes, str, PrivateKey], password: str, kdf: Optional[Literal["scrypt", "pbkdf2"]] = None, iterations: Optional[int] = None, ) -> KeyfileDict: return super().encrypt(private_key, password, kdf, iterations) # type: ignore
[docs] @staticmethod def decrypt( keyfile_json: Union[Dict[str, Any], str, KeyfileDict], password: str ) -> HexBytes: """ Decrypts a keyfile and returns the secret key. :param Union[Dict[str,Any],str,KeyfileDict] keyfile_json: encrypted keyfile :param str password: the password that was used to encrypt the key :return HexBytes: the hex private key """ return EthAccount.decrypt(keyfile_json, password)
[docs] @combomethod def recover_transaction( self, serialized_transaction: Union[bytes, HexStr, str] ) -> ChecksumAddress: """ Get the address of the account that signed this transaction. :param Union[bytes,HexStr,str] serialized_transaction: the complete signed transaction :return ChecksumAddress: address of signer, hex-encoded & checksummed :example: >>> raw_transaction = '0xf86a8086d55698372431831e848094f0109fc8df283027b6285cc889f5aa624eac1f55843b9aca008025a009ebb6ca057a0535d6186462bc0b465b561c94a295bdb0621fc19208ab149a9ca0440ffd775ce91a833ab410777204d5341a6f9fa91216a6f3ee2c051fea6a0428' # noqa: E501 >>> Account.recover_transaction(raw_transaction) '0x1c7536e3605d9c16a7a3d7b1898e529396a65c23' """ txn_bytes = HexBytes(serialized_transaction) # TODO: replace with Transaction.from_bytes() txn = LegacyTransaction.from_bytes(txn_bytes) recovered_address = self._recover_hash(txn.hash(), vrs=txn.vrs()) # type: ignore return to_checksum_address(eth_eoa_address_to_cfx_hex(recovered_address))
[docs] @combomethod def create( self, extra_entropy: str = "", network_id: Optional[int] = None ) -> LocalAccount: """ Creates a new private key, and returns it as a :class:`~cfx_account.signers.local.LocalAccount`. :param str extra_entropy: Add extra randomness to the randomness provided by your OS, defaults to '' :param Optional[int] network_id: the network id of the generated account, which determines the address encoding, defaults to None :return LocalAccount: an object with private key and convenience methods :examples: >>> from cfx_account import Account >>> acct = Account.create() >>> acct.address '0x187EE3Cb948fFb5a34417344f7fA01c638aa2F96' >>> Account.create(network_id=1029).address 'cfx:aapapvutg83zauuexwdrgjp4v6xm3dkbm2vevh1rub' """ acct: LocalAccount = super().create(extra_entropy) if network_id is not None: acct.network_id = network_id return acct
[docs] @combomethod def sign_message( self, signable_message: SignableMessage, private_key: Union[bytes, HexStr, int, keys.PrivateKey], ) -> SignedMessage: """ Sign the provided encoded message. The message is encoded according to CIP-23_ :param SignableMessage signable_message: an encoded message generated by `encode_defunct` or `encode_structured_data` :param Union[bytes,HexStr,int,keys.PrivateKey] private_key: the private key used to sign message :return SignedMessage: a signed message object :examples: >>> from from cfx_account import Account >>> from cfx_account.messages import encode_structured_data, encode_defunct >>> encoded_message = encode_defunct(text=message) >>> acct = Account.create() >>> signed = acct.sign_message(encoded_message) >>> signed.signature '0xd72ea2020802d6dfce0d49fc1d92a16b43baa58fc152d6f437d852a014e0c5740b3563375b0b844a835be4f1521b4ae2a691048622f70026e0470acc5351043a01' >>> assert acct.hex_address == Account.recover_message(encoded_message, signature=signed.signature) >>> # https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-23.md#typed-data >>> typed_data = { "types": { "CIP23Domain": [ ... ] }, ... } >>> encoded_data = encode_structured_data(typed_data) >>> signed = acct.sign_message(encoded_data) >>> signed.signature '0xd7fb6dca3b084ae3a9bf1ea3527de7a9bc2bd40e0c38d3faf9da214f1d5637ab2944a8a993dc59365c1e74e18a1589b358e3fb81bd03892d159f221e8ac765c701' >>> assert acct.hex_address == Account.recover_message(encoded_data, signature=signed.signature) .. _CIP-23: https://github.com/Conflux-Chain/CIPs/blob/master/CIPs/cip-23.md """ return super().sign_message(signable_message, private_key)
[docs] @combomethod def recover_message( self, signable_message: SignableMessage, vrs: Optional[Tuple[VRS, VRS, VRS]] = None, signature: Optional[bytes] = None, ) -> ChecksumAddress: """ Get the address of the account that signed the given message. Must specify exactly one of vrs or signature. Refer to :meth:`~cfx_account.account.Account.sign_message` for usage examples. :param SignableMessage signable_message: an encoded message generated by `encode_defunct` or `encode_structured_data` :param Optional[Tuple[VRS,VRS,VRS]] vrs: the three pieces generated by an elliptic curve signature, defaults to None :param Optional[bytes] signature: signature bytes concatenated as r+s+v, defaults to None :return ChecksumAddress: the checksum address of the account that signed the given message """ recovered_address = super().recover_message(signable_message, vrs, signature) return to_checksum_address(eth_eoa_address_to_cfx_hex(recovered_address))