# SPDX-License-Identifier: BSD-3-Clause # Copyright (c) 2021, The Wownero Project. import logging from typing import Awaitable, Callable, Dict, Optional, Union, List import asyncio import json import aiohttp from wowlet_ws_client import WowletState, decorator_parametrized, WALLET_OPERATIONAL class WowletWSClient: def __init__(self, host: str = "127.0.0.1", port: int = 42069, password: str = None, debug: bool = False): self.host = host self.port = port self.password = password self.ses = aiohttp.ClientSession() self.debug = debug self._ws = None self._state = WowletState.IDLE self._cmd_callbacks: Dict[str, Callable] = {} self._loop_task: Callable = None self.wallets: List[str] = [] self.addresses: List[str] = [] self.address_book: List[Dict[str: str]] = [] self.unlocked_balance: float self.balance: float async def open_wallet(self, path: str, password: str = ""): """ Opens a wallet :param path: absolute path to wallet file :param password: wallet password :return: """ if self._state != WowletState.IDLE: raise Exception("You may only open wallets in IDLE " "state. Close the current wallet first.") await self.sendMessage("openWallet", { "path": path, "password": password }) async def close_wallet(self): """Close currently opened wallet""" if self._state not in WALLET_OPERATIONAL: raise Exception("There is no opened wallet to close.") await self.sendMessage("closeWallet", {}) async def address_list(self, account_index: int = 0, address_index: int = 0, limit: int = 50, offset: int = 0): if self._state not in WALLET_OPERATIONAL: raise Exception("There is no opened wallet to close.") await self.sendMessage("addressList", { "accountIndex": account_index, "addressIndex": address_index, "limit": limit, "offset": offset }) async def send_transaction(self, address: str, amount: float, description: str = "", _all: bool = False): """ Send someone some monies. :param address: address :param amount: amount :param description: optional description :param _all: send all available funds (use with caution) :return: """ if amount <= 0: raise Exception("y u send 0") if self._state not in WALLET_OPERATIONAL: raise Exception("There is no opened wallet to close.") await self.sendMessage("sendTransaction", { "address": address, "description": description, "amount": amount, "all": _all }) async def create_wallet(self, name: str, password: str = "", path: str = None): """ Automatically create a new wallet. Comes up with it's own mnemonic seed and stuffz. :param name: name of the new wallet :param password: (optional) password for the wallet :param path: (optional) absolute path *to a directory*, default is Wownero wallet directory. :return: """ if self._state != WowletState.IDLE: raise Exception("Please close the currently opened wallet first.") await self.sendMessage("createWallet", { "name": name, "path": path, "password": password }) async def get_transaction_history(self): """Get transaction history in its entirety.""" # if self._state != WALLET_OPERATIONAL: # raise Exception("The wallet is currently busy doing something else.") await self.sendMessage("transactionHistory", {}) async def get_address_book(self): """Get all address book entries.""" # if self._state != WALLET_OPERATIONAL: # raise Exception("The wallet is currently busy doing something else.") await self.sendMessage("addressBook", {}) async def address_book_insert(self, name: str, address: str): if not name or not address: raise Exception("name or address empty") await self.sendMessage("addressBookItemAdd", { "name": name, "address": address }) async def address_book_remove(self, address: str): """Remove by address""" if not address: raise Exception("address empty") await self.sendMessage("addressBookItemRemove", { "address": address }) async def create_wallet_advanced(self): pass async def loop(self): while True: buffer = await self._ws.receive() msg = json.loads(buffer.data) if "cmd" not in msg: logging.error("invalid message received") continue cmd = msg['cmd'] data = msg.get("data") if self.debug: print(msg['cmd']) for k, v in WowletState.__members__.items(): state = WowletState[k] if cmd in v.value and self._state != state: if self.debug: print(f"Changing state to {k}") self._state = state if cmd == "walletList": self.wallets = data elif cmd == "addressBook": self.address_book = data elif cmd == "addressList": if data.get("accountIndex", 0) == 0 and \ data.get("addressIndex") == 0 and \ data.get("offset") == 0: self.addresses = data elif cmd == "balanceUpdated": self.balance = data.get('balance', 0) self.balance_unlocked = data.get('spendable', 0) if cmd in self._cmd_callbacks: await self._cmd_callbacks[cmd](data) @decorator_parametrized def event(self, view_func, cmd_name): self._cmd_callbacks[cmd_name] = view_func async def connect(self): self._ws = await self.ses.ws_connect(f"ws://{self.host}:{self.port}") self._loop_task = asyncio.create_task(self.loop()) async def authenticate(self): return await self.send(json.dumps({ "cmd": "password", "data": { "password": self.password } }).encode()) async def send(self, data: bytes) -> None: return await self._ws.send_bytes(data) async def sendMessage(self, command: str, data: dict) -> None: return await self.send(json.dumps({ "cmd": command, "data": data }).encode())