diff --git a/ape_starknet/accounts/_cli.py b/ape_starknet/accounts/_cli.py index 88f2bf49..3dea2fdb 100644 --- a/ape_starknet/accounts/_cli.py +++ b/ape_starknet/accounts/_cli.py @@ -1,3 +1,4 @@ +import json from typing import List, Optional, Union, cast import click @@ -6,7 +7,9 @@ from ape.cli.options import ApeCliContextObject from ape.logging import logger from ape.utils import add_padding_to_strings -from eth_utils import is_hex, to_hex +from eth_keyfile import decode_keyfile_json +from eth_utils import is_hex, text_if_str, to_bytes, to_hex +from hexbytes import HexBytes from starkware.crypto.signature.signature import EC_ORDER from starkware.starknet.definitions.fields import ContractAddressSalt @@ -294,6 +297,22 @@ def _import(cli_ctx, alias, network, address, class_hash, salt): cli_ctx.logger.success(f"Imported account '{alias}'.") +@accounts.command(short_help="Export an account private key") +@ape_cli_context() +@existing_alias_argument(account_type=StarknetKeyfileAccount) +def export(cli_ctx, alias): + account = cast(StarknetKeyfileAccount, _get_container(cli_ctx).load(alias)) + path = account.key_file_path + account_json = json.loads(path.read_text()) + passphrase = click.prompt("Enter password to decrypt account", hide_input=True) + passphrase_bytes = text_if_str(to_bytes, passphrase) + decoded_json = HexBytes(decode_keyfile_json(account_json, passphrase_bytes)) + private_key = to_int(decoded_json.hex()) + cli_ctx.logger.success( + f"Account {account.alias} private key: {click.style(private_key, bold=True)})" + ) + + @accounts.command() @ape_cli_context() @existing_alias_argument(account_type=BaseStarknetAccount) diff --git a/tests/integration/test_accounts.py b/tests/integration/test_accounts.py index 08f31c86..49cb587f 100644 --- a/tests/integration/test_accounts.py +++ b/tests/integration/test_accounts.py @@ -4,8 +4,10 @@ import pytest from eth_utils import to_hex +from starknet_py.utils.crypto.facade import message_signature from starkware.crypto.signature.signature import EC_ORDER +from ape_starknet.types import StarknetSignableMessage from ape_starknet.utils import get_random_private_key, to_int from .conftest import ApeStarknetCliRunner @@ -351,3 +353,19 @@ def test_change_password(accounts_runner, key_file_account, password, existing_k assert "SUCCESS" in accounts_runner.invoke( "change-password", existing_key_file_alias, input=[new_password, password, password] ) + + +def test_export(accounts_runner, key_file_account, password): + output = accounts_runner.invoke("export", key_file_account.alias, input=[password]) + key_from_output = int(output.split(" private key: ")[-1].strip(" )\n")) + + # Sign a message using the exported private key. + msg = StarknetSignableMessage(message="test test test") + actual_signature = message_signature(msg.hash, key_from_output) + + # Sign the same message using the account. + with accounts_runner.runner.isolation(f"y\n{password}\n"): + expected_signature = key_file_account.sign_message(msg) + + # The signatures should be the same to prove exporting works. + assert actual_signature == expected_signature