diff --git a/docs/account_creation.rst b/docs/account_creation.rst index b8287ad27..148325376 100644 --- a/docs/account_creation.rst +++ b/docs/account_creation.rst @@ -16,7 +16,7 @@ and then creating the transaction which will charge the fee from the address. Deploying an account with DeployAccount transaction requires the following: - class_hash of the account contract - - generating a private key and deployment salt + - private key and deployment salt - computing an address based on the account's secrets - prefunding an address with the fee tokens (e.g. using the token bridge) - creating and signing a DeployAccount transaction with generated secrets diff --git a/starknet_py/net/signer/key_pair.py b/starknet_py/net/signer/key_pair.py new file mode 100644 index 000000000..8106353ee --- /dev/null +++ b/starknet_py/net/signer/key_pair.py @@ -0,0 +1,56 @@ +from dataclasses import dataclass +from secrets import token_bytes + +from eth_keyfile.keyfile import extract_key_from_keyfile + +from starknet_py.constants import FIELD_PRIME +from starknet_py.hash.utils import private_to_stark_key +from starknet_py.net.client_models import Hash + + +@dataclass +class KeyPair: + private_key: int + public_key: int + + def __init__(self, private_key: Hash, public_key: Hash): + if isinstance(private_key, str): + self.private_key = int(private_key, 0) + else: + self.private_key = private_key + + if isinstance(public_key, str): + self.public_key = int(public_key, 0) + else: + self.public_key = public_key + + @staticmethod + def generate() -> "KeyPair": + """ + Create a key pair from a randomly generated private key. + + :return: KeyPair object. + """ + random_int = int.from_bytes(token_bytes(32), byteorder="big") + private_key = random_int % FIELD_PRIME + public_key = private_to_stark_key(private_key) + return KeyPair(private_key=private_key, public_key=public_key) + + @staticmethod + def from_private_key(key: Hash) -> "KeyPair": + if isinstance(key, str): + key = int(key, 0) + return KeyPair(private_key=key, public_key=private_to_stark_key(key)) + + @staticmethod + def from_keystore(path: str, password: str) -> "KeyPair": + """ + Create a key pair from a keystore file. + The keystore file should follow the Ethereum keystore format. + + :param path: Path to the keystore file. + :param password: Password to decrypt the keystore file. + :return: KeyPair object. + """ + key = extract_key_from_keyfile(path, password) + return KeyPair.from_private_key(int.from_bytes(key, byteorder="big")) diff --git a/starknet_py/net/signer/stark_curve_signer.py b/starknet_py/net/signer/stark_curve_signer.py index 621a1c5c9..8ffcd66e5 100644 --- a/starknet_py/net/signer/stark_curve_signer.py +++ b/starknet_py/net/signer/stark_curve_signer.py @@ -1,53 +1,14 @@ -from dataclasses import dataclass from typing import List -from eth_keyfile.keyfile import extract_key_from_keyfile - -from starknet_py.hash.utils import message_signature, private_to_stark_key -from starknet_py.net.client_models import Hash +from starknet_py.hash.utils import message_signature from starknet_py.net.models import AddressRepresentation, parse_address from starknet_py.net.models.chains import ChainId from starknet_py.net.models.transaction import AccountTransaction from starknet_py.net.signer.base_signer import BaseSigner +from starknet_py.net.signer.key_pair import KeyPair from starknet_py.utils.typed_data import TypedData -@dataclass -class KeyPair: - private_key: int - public_key: int - - def __init__(self, private_key: Hash, public_key: Hash): - if isinstance(private_key, str): - self.private_key = int(private_key, 0) - else: - self.private_key = private_key - - if isinstance(public_key, str): - self.public_key = int(public_key, 0) - else: - self.public_key = public_key - - @staticmethod - def from_private_key(key: Hash) -> "KeyPair": - if isinstance(key, str): - key = int(key, 0) - return KeyPair(private_key=key, public_key=private_to_stark_key(key)) - - @staticmethod - def from_keystore(path: str, password: str) -> "KeyPair": - """ - Create a key pair from a keystore file. - The keystore file should follow the Ethereum keystore format. - - :param path: Path to the keystore file. - :param password: Password to decrypt the keystore file. - :return: KeyPair object. - """ - key = extract_key_from_keyfile(path, password) - return KeyPair.from_private_key(int.from_bytes(key, byteorder="big")) - - class StarkCurveSigner(BaseSigner): def __init__( self, diff --git a/starknet_py/tests/e2e/docs/account_creation/test_deploy_prefunded_account.py b/starknet_py/tests/e2e/docs/account_creation/test_deploy_prefunded_account.py index 36202b050..41a263b7b 100644 --- a/starknet_py/tests/e2e/docs/account_creation/test_deploy_prefunded_account.py +++ b/starknet_py/tests/e2e/docs/account_creation/test_deploy_prefunded_account.py @@ -12,7 +12,7 @@ async def test_deploy_prefunded_account( eth_fee_contract: Contract, client: Client, ): - # pylint: disable=import-outside-toplevel, too-many-locals + # pylint: disable=import-outside-toplevel, too-many-locals, unused-variable full_node_client_fixture = client # docs: start from starknet_py.hash.address import compute_address @@ -29,6 +29,9 @@ async def test_deploy_prefunded_account( key_pair = KeyPair.from_private_key(private_key) + # You can also generate a key pair + key_pair_generated = KeyPair.generate() + # Compute an address address = compute_address( salt=salt, diff --git a/starknet_py/tests/unit/signer/test_key_pair.py b/starknet_py/tests/unit/signer/test_key_pair.py new file mode 100644 index 000000000..abb0d509b --- /dev/null +++ b/starknet_py/tests/unit/signer/test_key_pair.py @@ -0,0 +1,11 @@ +from starknet_py.constants import FIELD_PRIME +from starknet_py.net.signer.key_pair import KeyPair + + +def test_generate_key_pair(): + key_pair = KeyPair.generate() + hex_private_key = hex(key_pair.private_key)[2:] + padded_hex_private_key = hex_private_key.zfill(64) + assert len(padded_hex_private_key) == 64 + assert key_pair.private_key < FIELD_PRIME + assert key_pair.public_key < FIELD_PRIME