Wallet

basicnanoclient.wallet.Wallet

Nano Wallet class.

Source code in basicnanoclient/wallet.py
class Wallet():
    """Nano Wallet class."""
    _CHARS = "13456789abcdefghijkmnopqrstuwxyz"
    NANO_ALPHABET = '13456789abcdefghijkmnopqrstuwxyz'
    base32_alphabet = '13456789abcdefghijkmnopqrstuwxyz'
    account_lookup = "13456789abcdefghijkmnopqrstuwxyz"
    account_reverse = "~0~1234567~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~89:;<=>?@AB~CDEFGHIJK~LMNO~~~~~"

    def __init__(self: Self, seed: str = None, account_count: int = 0) -> None:
        """Initialize the NanoWallet class.

        Args:
            seed (str): The seed to use for the wallet.
        """
        self.seed = seed
        self.accounts = []
        if seed:
            for i in range(account_count):
                self.accounts.append(self.generate_account_key_pair(seed, i))

    def validate_key_pairs(self: Self) -> bool:
        """Validate the key pairs in the wallet.

        Returns:
            bool: True if all key pairs are valid, False otherwise.
        """
        return all(
            self.validate_key_pair(account["private"], account["public"])
            for account in self.accounts
        )

    def validate_key_pair(self: Self, private_key: str, public_key: str) -> bool:
        """Validate that a private key matches a public key.

        Args:
            private_key (str): The private key in hexadecimal format.
            public_key (str): The public key in hexadecimal format.

        Returns:
            bool: True if the key pair is valid, False otherwise.
        """
        sk = SigningKey(binascii.unhexlify(private_key))
        vk = VerifyKey(binascii.unhexlify(public_key))
        message = b"test message"
        signed = sk.sign(message)
        try:
            vk.verify(signed.message, signed.signature)
            return True
        except Exception:
            return False

    def key_expand(self: Self, key: str) -> Dict[str, Any]:
        """Expand a private key into a public key and account address.

        Args:
            key (str): A 64-character hexadecimal string representing the
                Nano private key.

        Returns:
            A dictionary with keys 'public' and 'account', where 'public' is a
            64-character hexadecimal string representing
            the Nano public key and 'account' is the Nano account address.
        """
        signing_key = SigningKey(binascii.unhexlify(key))
        # private_key = signing_key.to_bytes().hex()
        public_key = signing_key.get_verifying_key().to_bytes()
        account = self.public_key_to_account(public_key)
        return {"public": public_key.hex(), "account": account}

    def generate_account_private_key(self: Self, seed: str, index: int) -> str:
        """Generate a new account private key from a seed and index.

        Args:
            seed (str): A 64-character hexadecimal string representing the
                Nano seed.
            index (int): The account index.

        Returns:
            str: A 64-character hexadecimal string representing the
                Nano private key.
        """
        if len(seed) != 64 or not Utils.is_hex(seed):
            raise ValueError("Seed must be a 64-character hexadecimal string")

        if not isinstance(index, int):
            raise ValueError("Index must be an integer")

        account_bytes = binascii.unhexlify(Utils.dec_to_hex(index, 4))
        context = blake2b(digest_size=32)
        context.update(binascii.unhexlify(seed))
        context.update(account_bytes)

        new_key = context.hexdigest()
        return new_key

    def generate_account_key_pair(self: Self, seed: str, index: int) -> Dict[str, str]:
        """Generate a new account key pair from a seed and index.

        Args:
            seed (str): A 64-character hexadecimal string representing the
                Nano seed.
            index (int): The account index.

        Returns:
            AccountKeyPair: The account key pair.
        """
        private_key = self.generate_account_private_key(seed, index)
        keys = self.key_expand(private_key)

        return {
            "private": private_key,
            "public": keys["public"],
            "account": keys["account"]
        }

    def public_key_to_account(self: Self, public_key: str) -> str:
        """Convert a public key to a Nano account address.

        Args:
            public_key (str): The public key.

        Returns:
            str: The Nano account address.
        """
        if len(public_key) != 32:
            raise ValueError("Public key must be 32 bytes.")

        # Encode public key to Nano base32
        encoded_public_key = Utils.encode_nano_base32(public_key).rjust(52, '1')

        # Calculate checksum
        checksum = blake2b(public_key, digest_size=5).digest()
        checksum_reversed = checksum[::-1]  # Reverse the checksum bytes

        # Encode checksum to Nano base32
        encoded_checksum = Utils.encode_nano_base32(checksum_reversed).rjust(8, '1')

        # Form the account address
        account = f"nano_{encoded_public_key}{encoded_checksum}"
        return account

    @staticmethod
    def sign_block(block: dict, private_key: str) -> str:
        """Sign a block using a private key.

        Args:
            block (dict): The block to sign.
            private_key (str): The private key to sign the block with.

        Returns:
            str: The signature of the block.
        """
        # Create signing key from the private key bytes
        signing_key = SigningKey(unhexlify(private_key))

        # Convert balance from decimal to hexadecimal and pad to 32 characters
        balance_hex = hex(int(block["balance"]))[2:].zfill(32)

        # Determine block type and prepare the block contents to be signed
        block_type = block["type"]
        if block_type == "state":
            block_contents = (
                unhexlify("0000000000000000000000000000000000000000000000000000000000000006") +
                unhexlify(Utils.nano_address_to_public_key(block["account"])) +
                unhexlify(block["previous"]) +
                unhexlify(Utils.nano_address_to_public_key(block["representative"])) +
                unhexlify(balance_hex) +
                unhexlify(block["link"])
            )
        else:
            raise ValueError("Unsupported block type: {}".format(block_type))

        # Hash the block contents
        block_hash = blake2b(block_contents, digest_size=32).digest()

        # Sign the hash
        signed_message = signing_key.sign(block_hash)

        # Convert the signature to hex and return it
        return hexlify(signed_message).decode()

    @staticmethod
    def generate_work_rpc(hash: str, rpc_network: str = "http://127.0.0.1:17076") -> str:
        """Generate work for a given hash.

        TODO: move to RPC class

        Args:
            hash (str): The hash to generate work for.
            rpc_network (str): The RPC network to use.

        Returns:
            str: The generated work.
        """
        session = requests.Session()
        response = session.post(rpc_network, json={
            "action": "work_generate",
            "hash": hash,
            "multiplier": "1.0"
        }).json()
        print(response)
        return response['work']

__init__(seed=None, account_count=0)

Initialize the NanoWallet class.

Parameters:
  • seed (str, default: None ) –

    The seed to use for the wallet.

Source code in basicnanoclient/wallet.py
def __init__(self: Self, seed: str = None, account_count: int = 0) -> None:
    """Initialize the NanoWallet class.

    Args:
        seed (str): The seed to use for the wallet.
    """
    self.seed = seed
    self.accounts = []
    if seed:
        for i in range(account_count):
            self.accounts.append(self.generate_account_key_pair(seed, i))

validate_key_pairs()

Validate the key pairs in the wallet.

Returns:
  • bool( bool ) –

    True if all key pairs are valid, False otherwise.

Source code in basicnanoclient/wallet.py
def validate_key_pairs(self: Self) -> bool:
    """Validate the key pairs in the wallet.

    Returns:
        bool: True if all key pairs are valid, False otherwise.
    """
    return all(
        self.validate_key_pair(account["private"], account["public"])
        for account in self.accounts
    )

validate_key_pair(private_key, public_key)

Validate that a private key matches a public key.

Parameters:
  • private_key (str) –

    The private key in hexadecimal format.

  • public_key (str) –

    The public key in hexadecimal format.

Returns:
  • bool( bool ) –

    True if the key pair is valid, False otherwise.

Source code in basicnanoclient/wallet.py
def validate_key_pair(self: Self, private_key: str, public_key: str) -> bool:
    """Validate that a private key matches a public key.

    Args:
        private_key (str): The private key in hexadecimal format.
        public_key (str): The public key in hexadecimal format.

    Returns:
        bool: True if the key pair is valid, False otherwise.
    """
    sk = SigningKey(binascii.unhexlify(private_key))
    vk = VerifyKey(binascii.unhexlify(public_key))
    message = b"test message"
    signed = sk.sign(message)
    try:
        vk.verify(signed.message, signed.signature)
        return True
    except Exception:
        return False

key_expand(key)

Expand a private key into a public key and account address.

Parameters:
  • key (str) –

    A 64-character hexadecimal string representing the Nano private key.

Returns:
  • Dict[str, Any]

    A dictionary with keys 'public' and 'account', where 'public' is a

  • Dict[str, Any]

    64-character hexadecimal string representing

  • Dict[str, Any]

    the Nano public key and 'account' is the Nano account address.

Source code in basicnanoclient/wallet.py
def key_expand(self: Self, key: str) -> Dict[str, Any]:
    """Expand a private key into a public key and account address.

    Args:
        key (str): A 64-character hexadecimal string representing the
            Nano private key.

    Returns:
        A dictionary with keys 'public' and 'account', where 'public' is a
        64-character hexadecimal string representing
        the Nano public key and 'account' is the Nano account address.
    """
    signing_key = SigningKey(binascii.unhexlify(key))
    # private_key = signing_key.to_bytes().hex()
    public_key = signing_key.get_verifying_key().to_bytes()
    account = self.public_key_to_account(public_key)
    return {"public": public_key.hex(), "account": account}

generate_account_private_key(seed, index)

Generate a new account private key from a seed and index.

Parameters:
  • seed (str) –

    A 64-character hexadecimal string representing the Nano seed.

  • index (int) –

    The account index.

Returns:
  • str( str ) –

    A 64-character hexadecimal string representing the Nano private key.

Source code in basicnanoclient/wallet.py
def generate_account_private_key(self: Self, seed: str, index: int) -> str:
    """Generate a new account private key from a seed and index.

    Args:
        seed (str): A 64-character hexadecimal string representing the
            Nano seed.
        index (int): The account index.

    Returns:
        str: A 64-character hexadecimal string representing the
            Nano private key.
    """
    if len(seed) != 64 or not Utils.is_hex(seed):
        raise ValueError("Seed must be a 64-character hexadecimal string")

    if not isinstance(index, int):
        raise ValueError("Index must be an integer")

    account_bytes = binascii.unhexlify(Utils.dec_to_hex(index, 4))
    context = blake2b(digest_size=32)
    context.update(binascii.unhexlify(seed))
    context.update(account_bytes)

    new_key = context.hexdigest()
    return new_key

generate_account_key_pair(seed, index)

Generate a new account key pair from a seed and index.

Parameters:
  • seed (str) –

    A 64-character hexadecimal string representing the Nano seed.

  • index (int) –

    The account index.

Returns:
  • AccountKeyPair( Dict[str, str] ) –

    The account key pair.

Source code in basicnanoclient/wallet.py
def generate_account_key_pair(self: Self, seed: str, index: int) -> Dict[str, str]:
    """Generate a new account key pair from a seed and index.

    Args:
        seed (str): A 64-character hexadecimal string representing the
            Nano seed.
        index (int): The account index.

    Returns:
        AccountKeyPair: The account key pair.
    """
    private_key = self.generate_account_private_key(seed, index)
    keys = self.key_expand(private_key)

    return {
        "private": private_key,
        "public": keys["public"],
        "account": keys["account"]
    }

public_key_to_account(public_key)

Convert a public key to a Nano account address.

Parameters:
  • public_key (str) –

    The public key.

Returns:
  • str( str ) –

    The Nano account address.

Source code in basicnanoclient/wallet.py
def public_key_to_account(self: Self, public_key: str) -> str:
    """Convert a public key to a Nano account address.

    Args:
        public_key (str): The public key.

    Returns:
        str: The Nano account address.
    """
    if len(public_key) != 32:
        raise ValueError("Public key must be 32 bytes.")

    # Encode public key to Nano base32
    encoded_public_key = Utils.encode_nano_base32(public_key).rjust(52, '1')

    # Calculate checksum
    checksum = blake2b(public_key, digest_size=5).digest()
    checksum_reversed = checksum[::-1]  # Reverse the checksum bytes

    # Encode checksum to Nano base32
    encoded_checksum = Utils.encode_nano_base32(checksum_reversed).rjust(8, '1')

    # Form the account address
    account = f"nano_{encoded_public_key}{encoded_checksum}"
    return account

sign_block(block, private_key) staticmethod

Sign a block using a private key.

Parameters:
  • block (dict) –

    The block to sign.

  • private_key (str) –

    The private key to sign the block with.

Returns:
  • str( str ) –

    The signature of the block.

Source code in basicnanoclient/wallet.py
@staticmethod
def sign_block(block: dict, private_key: str) -> str:
    """Sign a block using a private key.

    Args:
        block (dict): The block to sign.
        private_key (str): The private key to sign the block with.

    Returns:
        str: The signature of the block.
    """
    # Create signing key from the private key bytes
    signing_key = SigningKey(unhexlify(private_key))

    # Convert balance from decimal to hexadecimal and pad to 32 characters
    balance_hex = hex(int(block["balance"]))[2:].zfill(32)

    # Determine block type and prepare the block contents to be signed
    block_type = block["type"]
    if block_type == "state":
        block_contents = (
            unhexlify("0000000000000000000000000000000000000000000000000000000000000006") +
            unhexlify(Utils.nano_address_to_public_key(block["account"])) +
            unhexlify(block["previous"]) +
            unhexlify(Utils.nano_address_to_public_key(block["representative"])) +
            unhexlify(balance_hex) +
            unhexlify(block["link"])
        )
    else:
        raise ValueError("Unsupported block type: {}".format(block_type))

    # Hash the block contents
    block_hash = blake2b(block_contents, digest_size=32).digest()

    # Sign the hash
    signed_message = signing_key.sign(block_hash)

    # Convert the signature to hex and return it
    return hexlify(signed_message).decode()

generate_work_rpc(hash, rpc_network='http://127.0.0.1:17076') staticmethod

Generate work for a given hash.

TODO: move to RPC class

Parameters:
  • hash (str) –

    The hash to generate work for.

  • rpc_network (str, default: 'http://127.0.0.1:17076' ) –

    The RPC network to use.

Returns:
  • str( str ) –

    The generated work.

Source code in basicnanoclient/wallet.py
@staticmethod
def generate_work_rpc(hash: str, rpc_network: str = "http://127.0.0.1:17076") -> str:
    """Generate work for a given hash.

    TODO: move to RPC class

    Args:
        hash (str): The hash to generate work for.
        rpc_network (str): The RPC network to use.

    Returns:
        str: The generated work.
    """
    session = requests.Session()
    response = session.post(rpc_network, json={
        "action": "work_generate",
        "hash": hash,
        "multiplier": "1.0"
    }).json()
    print(response)
    return response['work']