From 08ac82001cbff2e52f67f60cd50f044333f36ce6 Mon Sep 17 00:00:00 2001 From: Mariano Sorgente <3069354+mariano54@users.noreply.github.com> Date: Mon, 14 Sep 2020 15:52:08 +0900 Subject: [PATCH] Refactor RPCs --- .../src/middleware/middleware_api.js | 4 +- src/cmds/init.py | 8 +- src/cmds/show.py | 10 +- src/full_node/full_node.py | 4 +- src/rpc/farmer_rpc_api.py | 4 +- src/rpc/full_node_rpc_api.py | 57 +- src/rpc/full_node_rpc_client.py | 25 +- src/rpc/harvester_rpc_api.py | 20 +- src/rpc/rpc_client.py | 8 +- src/rpc/rpc_server.py | 47 +- src/rpc/wallet_rpc_api.py | 1003 ++++++++--------- src/rpc/wallet_rpc_client.py | 33 +- src/util/clvm.py | 7 +- src/wallet/cc_wallet/cc_wallet.py | 9 +- src/wallet/wallet_state_manager.py | 2 +- tests/rpc/test_wallet_rpc.py | 32 +- 16 files changed, 612 insertions(+), 661 deletions(-) diff --git a/electron-react/src/middleware/middleware_api.js b/electron-react/src/middleware/middleware_api.js index 8f3a58ab22..505b15405d 100644 --- a/electron-react/src/middleware/middleware_api.js +++ b/electron-react/src/middleware/middleware_api.js @@ -290,8 +290,8 @@ export const handle_message = (store, payload) => { } } if (payload.data.success === false) { - if (payload.data.reason) { - store.dispatch(openDialog("Error: ", payload.data.reason)); + if (payload.data.error) { + store.dispatch(openDialog("Error: ", payload.data.error)); } } }; diff --git a/src/cmds/init.py b/src/cmds/init.py index 43e5bbca37..4fc575a8c4 100644 --- a/src/cmds/init.py +++ b/src/cmds/init.py @@ -62,9 +62,11 @@ def check_keys(new_root): break for sk, _ in all_sks: all_targets.append( - encode_puzzle_hash(create_puzzlehash_for_pk( - master_sk_to_wallet_sk(sk, uint32(i)).get_g1() - )) + encode_puzzle_hash( + create_puzzlehash_for_pk( + master_sk_to_wallet_sk(sk, uint32(i)).get_g1() + ) + ) ) if all_targets[-1] == config["farmer"].get("xch_target_address"): stop_searching_for_farmer = True diff --git a/src/cmds/show.py b/src/cmds/show.py index 8c17a78016..48dfcacff5 100644 --- a/src/cmds/show.py +++ b/src/cmds/show.py @@ -319,18 +319,14 @@ async def show_async(args, parser): else: wallet_rpc_port = args.wallet_rpc_port wallet_client = await WalletRpcClient.create(self_hostname, wallet_rpc_port) - get_keys_response = await wallet_client.get_keys() - if ( - "public_key_fingerprints" not in get_keys_response - or len(get_keys_response["public_key_fingerprints"]) == 0 - ): + fingerprints = await wallet_client.get_public_keys() + if len(fingerprints) == 0: print("Error, no keys loaded") wallet_client.close() await wallet_client.await_closed() client.close() await client.await_closed() return - fingerprints = get_keys_response["public_key_fingerprints"] fingerprint = None if len(fingerprints) == 1: fingerprint = fingerprints[0][0] @@ -403,7 +399,7 @@ async def show_async(args, parser): error = log_in_response["error"] print(f"Error: {log_in_response[error]}") - summaries_response = await wallet_client.get_wallet_summaries() + summaries_response = await wallet_client.get_wallets() if "wallet_summaries" not in summaries_response: print("Wallet summary cannot be displayed") else: diff --git a/src/full_node/full_node.py b/src/full_node/full_node.py index 78c7e0241d..50b8eba12e 100644 --- a/src/full_node/full_node.py +++ b/src/full_node/full_node.py @@ -1824,7 +1824,9 @@ class FullNode: tx.transaction ) if status == MempoolInclusionStatus.SUCCESS: - self.log.info(f"Added transaction to mempool: {tx.transaction.name()}") + self.log.info( + f"Added transaction to mempool: {tx.transaction.name()}" + ) # Only broadcast successful transactions, not pending ones. Otherwise it's a DOS # vector. fees = tx.transaction.fees() diff --git a/src/rpc/farmer_rpc_api.py b/src/rpc/farmer_rpc_api.py index f3590a83c6..c868d93b9f 100644 --- a/src/rpc/farmer_rpc_api.py +++ b/src/rpc/farmer_rpc_api.py @@ -26,7 +26,7 @@ class FarmerRpcApi: response = [] seen_challenges: Set = set() if self.service.current_weight == 0: - return {"success": True, "latest_challenges": []} + return {"latest_challenges": []} for pospace_fin in self.service.challenges[self.service.current_weight]: estimates = self.service.challenge_to_estimates.get( pospace_fin.challenge_hash, [] @@ -43,4 +43,4 @@ class FarmerRpcApi: } ) seen_challenges.add(pospace_fin.challenge_hash) - return {"success": True, "latest_challenges": response} + return {"latest_challenges": response} diff --git a/src/rpc/full_node_rpc_api.py b/src/rpc/full_node_rpc_api.py index 34ca637638..e62d51aaea 100644 --- a/src/rpc/full_node_rpc_api.py +++ b/src/rpc/full_node_rpc_api.py @@ -1,8 +1,6 @@ from src.full_node.full_node import FullNode from typing import Callable, List, Optional, Dict -from aiohttp import web - from src.types.header import Header from src.types.full_block import FullBlock from src.util.ints import uint32, uint64, uint128 @@ -60,7 +58,7 @@ class FullNodeRpcApi: difficulty: uint64 = self.service.blockchain.get_next_difficulty(lca) lca_block = await self.service.block_store.get_block(lca.header_hash) if lca_block is None: - return None + raise ValueError("No LCA block is set") min_iters: uint64 = self.service.blockchain.get_next_min_iters(lca_block) ips: uint64 = uint64( min_iters @@ -93,7 +91,6 @@ class FullNodeRpcApi: space = {"space": uint128(0)} assert space is not None response: Dict = { - "success": True, "blockchain_state": { "tips": tips, "tip_hashes": tip_hashes, @@ -114,43 +111,43 @@ class FullNodeRpcApi: async def get_block(self, request: Dict) -> Optional[Dict]: if "header_hash" not in request: - return None + raise ValueError("No header_hash in request") header_hash = hexstr_to_bytes(request["header_hash"]) block: Optional[FullBlock] = await self.service.block_store.get_block( header_hash ) if block is None: - return None + raise ValueError(f"Block {header_hash.hex()} not found") - return {"success": True, "block": block} + return {"block": block} async def get_header_by_height(self, request: Dict) -> Optional[Dict]: if "height" not in request: - return None + raise ValueError("No height in request") height = request["height"] header_height = uint32(int(height)) header_hash: Optional[bytes32] = self.service.blockchain.height_to_hash.get( header_height, None ) if header_hash is None: - return None + raise ValueError(f"Height {height} not found in chain") header: Header = self.service.blockchain.headers[header_hash] - return {"success": True, "header": header} + return {"header": header} async def get_header(self, request: Dict): if "header_hash" not in request: - return None + raise ValueError("header_hash not in request") header_hash_str = request["header_hash"] header_hash = hexstr_to_bytes(header_hash_str) header: Optional[Header] = self.service.blockchain.headers.get( header_hash, None ) - return {"success": True, "header": header} + return {"header": header} async def get_unfinished_block_headers(self, request: Dict) -> Optional[Dict]: if "height" not in request: - return None + raise ValueError("height not in request") height = request["height"] response_headers: List[Header] = [] for block in ( @@ -158,7 +155,7 @@ class FullNodeRpcApi: ).values(): if block.height == height: response_headers.append(block.header) - return {"success": True, "headers": response_headers} + return {"headers": response_headers} async def get_latest_block_headers(self, request: Dict) -> Optional[Dict]: headers: Dict[bytes32, Header] = {} @@ -240,7 +237,7 @@ class FullNodeRpcApi: unfinished_with_meta.extend(finished_with_meta) - return {"success": True, "latest_blocks": unfinished_with_meta} + return {"latest_blocks": unfinished_with_meta} async def get_total_miniters(self, newer_block, older_block) -> Optional[uint64]: """ @@ -251,7 +248,7 @@ class FullNodeRpcApi: older_block.prev_header_hash ) if older_block_parent is None: - return None + raise ValueError("Older block not found") older_diff = older_block.weight - older_block_parent.weight curr_mi = calculate_min_iters_from_iterations( older_block.proof_of_space, @@ -269,17 +266,17 @@ class FullNodeRpcApi: uint32(int(curr_h)) ) if curr_b_header_hash is None: - return None + raise ValueError(f"Curr header hash {curr_h} not found") curr_b_block = await self.service.block_store.get_block( curr_b_header_hash ) if curr_b_block is None or curr_b_block.proof_of_time is None: - return None + raise ValueError("Block invalid") curr_parent = await self.service.block_store.get_block( curr_b_block.prev_header_hash ) if curr_parent is None: - return None + raise ValueError("Curr parent block invalid") curr_diff = curr_b_block.weight - curr_parent.weight curr_mi = calculate_min_iters_from_iterations( curr_b_block.proof_of_space, @@ -288,7 +285,7 @@ class FullNodeRpcApi: self.service.constants.NUMBER_ZERO_BITS_CHALLENGE_SIG, ) if curr_mi is None: - raise web.HTTPBadRequest() + raise ValueError("Curr_mi invalid") total_mi = uint64(total_mi + curr_mi) return total_mi @@ -302,29 +299,31 @@ class FullNodeRpcApi: "newer_block_header_hash" not in request or "older_block_header_hash" not in request ): - return None + raise ValueError( + "Invalid request. newer_block_header_hash and older_block_header_hash required" + ) newer_block_hex = request["newer_block_header_hash"] older_block_hex = request["older_block_header_hash"] if newer_block_hex == older_block_hex: - return None + raise ValueError("New and old must not be the same") newer_block_bytes = hexstr_to_bytes(newer_block_hex) older_block_bytes = hexstr_to_bytes(older_block_hex) newer_block = await self.service.block_store.get_block(newer_block_bytes) if newer_block is None: - raise web.HTTPNotFound() + raise ValueError("Newer block not found") older_block = await self.service.block_store.get_block(older_block_bytes) if older_block is None: - raise web.HTTPNotFound() + raise ValueError("Newer block not found") delta_weight = newer_block.header.data.weight - older_block.header.data.weight delta_iters = ( newer_block.header.data.total_iters - older_block.header.data.total_iters ) total_min_inters = await self.get_total_miniters(newer_block, older_block) if total_min_inters is None: - raise web.HTTPNotFound() + raise ValueError("Min iters invalid") delta_iters -= total_min_inters weight_div_iters = delta_weight / delta_iters tips_adjustment_constant = 0.65 @@ -338,14 +337,14 @@ class FullNodeRpcApi: * tips_adjustment_constant * eligible_plots_filter_mult ) - return {"success": True, "space": uint128(int(network_space_bytes_estimate))} + return {"space": uint128(int(network_space_bytes_estimate))} async def get_unspent_coins(self, request: Dict) -> Optional[Dict]: """ Retrieves the unspent coins for a given puzzlehash. """ if "puzzle_hash" not in request: - return None + raise ValueError("Puzzle hash not in request") puzzle_hash = hexstr_to_bytes(request["puzzle_hash"]) header_hash = request.get("header_hash", None) @@ -361,7 +360,7 @@ class FullNodeRpcApi: ) ) - return {"success": True, "coin_records": coin_records} + return {"coin_records": coin_records} async def get_heaviest_block_seen(self, request: Dict) -> Optional[Dict]: tips: List[Header] = self.service.blockchain.get_current_tips() @@ -373,4 +372,4 @@ class FullNodeRpcApi: for _, pot_block in potential_tips: if pot_block.weight > max_tip.weight: max_tip = pot_block.header - return {"success": True, "tip": max_tip} + return {"tip": max_tip} diff --git a/src/rpc/full_node_rpc_client.py b/src/rpc/full_node_rpc_client.py index 236404bdf7..a56f38a162 100644 --- a/src/rpc/full_node_rpc_client.py +++ b/src/rpc/full_node_rpc_client.py @@ -1,4 +1,3 @@ -import aiohttp from typing import Dict, Optional, List from src.types.full_block import FullBlock from src.types.header import Header @@ -30,10 +29,8 @@ class FullNodeRpcClient(RpcClient): async def get_block(self, header_hash) -> Optional[FullBlock]: try: response = await self.fetch("get_block", {"header_hash": header_hash.hex()}) - except aiohttp.client_exceptions.ClientResponseError as e: - if e.message == "Not Found": - return None - raise + except Exception: + return None return FullBlock.from_json_dict(response["block"]) async def get_header_by_height(self, header_height) -> Optional[Header]: @@ -41,10 +38,8 @@ class FullNodeRpcClient(RpcClient): response = await self.fetch( "get_header_by_height", {"height": header_height} ) - except aiohttp.client_exceptions.ClientResponseError as e: - if e.message == "Not Found": - return None - raise + except Exception: + return None return Header.from_json_dict(response["header"]) async def get_header(self, header_hash) -> Optional[Header]: @@ -54,10 +49,8 @@ class FullNodeRpcClient(RpcClient): ) if response["header"] is None: return None - except aiohttp.client_exceptions.ClientResponseError as e: - if e.message == "Not Found": - return None - raise + except Exception: + return None return Header.from_json_dict(response["header"]) async def get_unfinished_block_headers(self, height: uint32) -> List[Header]: @@ -75,10 +68,8 @@ class FullNodeRpcClient(RpcClient): "older_block_header_hash": older_block_header_hash, }, ) - except aiohttp.client_exceptions.ClientResponseError as e: - if e.message == "Not Found": - return None - raise + except Exception: + return None return network_space_bytes_estimate["space"] async def get_unspent_coins( diff --git a/src/rpc/harvester_rpc_api.py b/src/rpc/harvester_rpc_api.py index 62e7289182..dddd3e53c0 100644 --- a/src/rpc/harvester_rpc_api.py +++ b/src/rpc/harvester_rpc_api.py @@ -29,7 +29,6 @@ class HarvesterRpcApi: async def get_plots(self, request: Dict) -> Dict: plots, failed_to_open, not_found = self.service._get_plots() return { - "success": True, "plots": plots, "failed_to_open_filenames": failed_to_open, "not_found_filenames": not_found, @@ -37,23 +36,26 @@ class HarvesterRpcApi: async def refresh_plots(self, request: Dict) -> Dict: await self.service._refresh_plots() - return {"success": True} + return {} async def delete_plot(self, request: Dict) -> Dict: filename = request["filename"] - success = self.service._delete_plot(filename) - return {"success": success} + if self.service._delete_plot(filename): + return {} + raise ValueError(f"Not able to delete file {filename}") async def add_plot_directory(self, request: Dict) -> Dict: dirname = request["dirname"] - success = await self.service._add_plot_directory(dirname) - return {"success": success} + if await self.service._add_plot_directory(dirname): + return {} + raise ValueError(f"Did not add plot directory {dirname}") async def get_plot_directories(self, request: Dict) -> Dict: plot_dirs = await self.service._get_plot_directories() - return {"success": True, "directories": plot_dirs} + return {"directories": plot_dirs} async def remove_plot_directory(self, request: Dict) -> Dict: dirname = request["dirname"] - success = await self.service._remove_plot_directory(dirname) - return {"success": success} + if await self.service._remove_plot_directory(dirname): + return {} + raise ValueError(f"Did not remove plot directory {dirname}") diff --git a/src/rpc/rpc_client.py b/src/rpc/rpc_client.py index e47e8a8a34..8bdb7b0b8f 100644 --- a/src/rpc/rpc_client.py +++ b/src/rpc/rpc_client.py @@ -31,7 +31,13 @@ class RpcClient: async def fetch(self, path, request_json): async with self.session.post(self.url + path, json=request_json) as response: response.raise_for_status() - return await response.json() + res_json = await response.json() + if not res_json["success"]: + if "error" in res_json: + raise Exception(res_json["error"]) + else: + raise Exception() + return res_json async def get_connections(self) -> List[Dict]: response = await self.fetch("get_connections", {}) diff --git a/src/rpc/rpc_server.py b/src/rpc/rpc_server.py index a8f516281c..491b013af6 100644 --- a/src/rpc/rpc_server.py +++ b/src/rpc/rpc_server.py @@ -59,16 +59,27 @@ class RpcServer: def _wrap_http_handler(self, f) -> Callable: async def inner(request) -> aiohttp.web.Response: request_data = await request.json() - res_object = await f(request_data) - if res_object is None: - raise aiohttp.web.HTTPNotFound() + try: + res_object = await f(request_data) + if res_object is None: + res_object = {} + if "success" not in res_object: + res_object["success"] = True + except Exception as e: + tb = traceback.format_exc() + self.log.warning(f"Error while handling message: {tb}") + if len(e.args) > 0: + res_object = {"success": False, "error": f"{e.args[0]}"} + else: + res_object = {"success": False, "error": f"{e}"} + return obj_to_response(res_object) return inner async def get_connections(self, request: Dict) -> Dict: if self.rpc_api.service.global_connections is None: - return {"success": False} + raise ValueError("Global connections is not set") connections = self.rpc_api.service.global_connections.get_connections() con_info = [ { @@ -86,7 +97,7 @@ class RpcServer: } for con in connections ] - return {"success": True, "connections": con_info} + return {"connections": con_info} async def open_connection(self, request: Dict): host = request["host"] @@ -98,8 +109,8 @@ class RpcServer: if getattr(self.rpc_api.service, "server", None) is None or not ( await self.rpc_api.service.server.start_client(target_node, on_connect) ): - raise aiohttp.web.HTTPInternalServerError() - return {"success": True} + raise ValueError("Start client failed, or server is not set") + return {} async def close_connection(self, request: Dict): node_id = hexstr_to_bytes(request["node_id"]) @@ -111,10 +122,10 @@ class RpcServer: if c.node_id == node_id ] if len(connections_to_close) == 0: - raise aiohttp.web.HTTPNotFound() + raise ValueError(f"Connection with node_id {node_id.hex()} does not exist") for connection in connections_to_close: self.rpc_api.service.global_connections.close(connection) - return {"success": True} + return {} async def stop_node(self, request): """ @@ -122,7 +133,7 @@ class RpcServer: """ if self.stop_cb is not None: self.stop_cb() - return {"success": True} + return {} async def ws_api(self, message): """ @@ -145,8 +156,8 @@ class RpcServer: f = getattr(self.rpc_api, command, None) if f is not None: return await f(data) - else: - return {"error": f"unknown_command {command}"} + + raise ValueError(f"unknown_command {command}") async def safe_handle(self, websocket, payload): message = None @@ -154,14 +165,22 @@ class RpcServer: message = json.loads(payload) self.log.info(f"Rpc call <- {message['command']}") response = await self.ws_api(message) + + # Only respond if we return something from api call if response is not None: log.info(f"Rpc response -> {message['command']}") + # Set success to true automatically (unless it's already set) + if "success" not in response: + response["success"] = True await websocket.send_str(format_response(message, response)) except Exception as e: tb = traceback.format_exc() - self.log.error(f"Error while handling message: {tb}") - error = {"success": False, "error": f"{e}"} + self.log.warning(f"Error while handling message: {tb}") + if len(e.args) > 0: + error = {"success": False, "error": f"{e.args[0]}"} + else: + error = {"success": False, "error": f"{e}"} if message is None: return await websocket.send_str(format_response(message, error)) diff --git a/src/rpc/wallet_rpc_api.py b/src/rpc/wallet_rpc_api.py index ae3051d151..75066e300a 100644 --- a/src/rpc/wallet_rpc_api.py +++ b/src/rpc/wallet_rpc_api.py @@ -28,7 +28,7 @@ from src.wallet.rl_wallet.rl_wallet import RLWallet from src.wallet.cc_wallet.cc_wallet import CCWallet from src.wallet.wallet_info import WalletInfo from src.wallet.wallet_node import WalletNode -from src.types.mempool_inclusion_status import MempoolInclusionStatus +from src.wallet.transaction_record import TransactionRecord # Timeout for response from wallet/full node for sending a transaction TIMEOUT = 30 @@ -38,11 +38,13 @@ log = logging.getLogger(__name__) class WalletRpcApi: def __init__(self, wallet_node: WalletNode): + assert wallet_node is not None self.service = wallet_node self.service_name = "chia_wallet" def get_routes(self) -> Dict[str, Callable]: return { +<<<<<<< HEAD "/get_wallet_balance": self.get_wallet_balance, "/send_transaction": self.send_transaction, <<<<<<< HEAD @@ -53,10 +55,31 @@ class WalletRpcApi: >>>>>>> c125573c... Cleaner send_transaction flow and more wallet rpc testing "/get_transactions": self.get_transactions, "/farm_block": self.farm_block, +======= + # Key management + "/log_in": self.log_in, + "/get_public_keys": self.get_public_keys, + "/get_private_key": self.get_private_key, + "/generate_mnemonic": self.generate_mnemonic, + "/add_key": self.add_key, + "/delete_key": self.delete_key, + "/delete_all_keys": self.delete_all_keys, + # Wallet node +>>>>>>> 8908edc0... Refactor RPCs "/get_sync_status": self.get_sync_status, "/get_height_info": self.get_height_info, - "/create_new_wallet": self.create_new_wallet, + "/farm_block": self.farm_block, # Only when node simulator is running + # Wallet management "/get_wallets": self.get_wallets, + "/create_new_wallet": self.create_new_wallet, + # Wallet + "/get_wallet_balance": self.get_wallet_balance, + "/get_transaction": self.get_transaction, + "/get_transactions": self.get_transactions, + "/get_next_address": self.get_next_address, + "/send_transaction": self.send_transaction, + "/create_backup": self.create_backup, + # Coloured coins and trading "/cc_set_name": self.cc_set_name, "/cc_get_name": self.cc_get_name, "/cc_spend": self.cc_spend, @@ -64,99 +87,19 @@ class WalletRpcApi: "/create_offer_for_ids": self.create_offer_for_ids, "/get_discrepancies_for_offer": self.get_discrepancies_for_offer, "/respond_to_offer": self.respond_to_offer, - "/get_wallet_summaries": self.get_wallet_summaries, - "/get_public_keys": self.get_public_keys, - "/generate_mnemonic": self.generate_mnemonic, - "/log_in": self.log_in, - "/add_key": self.add_key, - "/delete_key": self.delete_key, - "/delete_all_keys": self.delete_all_keys, - "/get_private_key": self.get_private_key, "/get_trade": self.get_trade, "/get_all_trades": self.get_all_trades, "/cancel_trade": self.cancel_trade, - "/create_backup": self.create_backup, + # RL wallet "/rl_set_user_info": self.rl_set_user_info, "/send_clawback_transaction:": self.send_clawback_transaction, } - async def rl_set_user_info(self, request): - wallet_id = uint32(int(request["wallet_id"])) - rl_user = self.service.wallet_state_manager.wallets[wallet_id] - origin = request["origin"] - try: - success = await rl_user.set_user_info( - uint64(request["interval"]), - uint64(request["limit"]), - origin["parent_coin_info"], - origin["puzzle_hash"], - origin["amount"], - request["admin_pubkey"], - ) - return {"success": success} - except Exception as e: - data = { - "success": False, - "reason": str(e), - } - return data - - async def get_trade(self, request: Dict): - if self.service is None: - return {"success": False} - if self.service.wallet_state_manager is None: - return {"success": False} - - trade_mgr = self.service.wallet_state_manager.trade_manager - - trade_id = request["trade_id"] - trade: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(trade_id) - if trade is None: - response = { - "success": False, - "error": f"No trade with trade id: {trade_id}", - } - return response - - result = trade_record_to_dict(trade) - response = {"success": True, "trade": result} - return response - - async def get_all_trades(self, request: Dict): - if self.service is None: - return {"success": False} - if self.service.wallet_state_manager is None: - return {"success": False} - - trade_mgr = self.service.wallet_state_manager.trade_manager - - all_trades = await trade_mgr.get_all_trades() - result = [] - for trade in all_trades: - result.append(trade_record_to_dict(trade)) - - response = {"success": True, "trades": result} - return response - - async def cancel_trade(self, request: Dict): - if self.service is None: - return {"success": False} - if self.service.wallet_state_manager is None: - return {"success": False} - - wsm = self.service.wallet_state_manager - secure = request["secure"] - trade_id = hexstr_to_bytes(request["trade_id"]) - - if secure: - await wsm.trade_manager.cancel_pending_offer_safely(trade_id) - else: - await wsm.trade_manager.cancel_pending_offer(trade_id) - - response = {"success": True} - return response - async def _state_changed(self, *args) -> List[str]: + """ + Called by the WalletNode or WalletStateManager when something has changed in the wallet. This + gives us an opportunity to send notifications to all connected clients via WebSocket. + """ if len(args) < 2: return [] @@ -169,38 +112,72 @@ class WalletRpcApi: data["additional_data"] = args[2] return [create_payload("state_changed", data, "chia_wallet", "wallet_ui")] - async def get_next_address(self, request: Dict) -> Dict: + async def _stop_wallet(self): """ - Returns a new address + Stops a currently running wallet/key, which allows starting the wallet with a new key. + Each key has it's own wallet database. """ - if self.service is None: - return {"success": False} + if self.service is not None: + self.service._close() + await self.service._await_closed() - wallet_id = uint32(int(request["wallet_id"])) - if self.service.wallet_state_manager is None: - return {"success": False} - wallet = self.service.wallet_state_manager.wallets[wallet_id] + ########################################################################################## + # Key management + ########################################################################################## - if wallet.wallet_info.type == WalletType.STANDARD_WALLET.value: - raw_puzzle_hash = await wallet.get_new_puzzlehash() - address = encode_puzzle_hash(raw_puzzle_hash) - elif wallet.wallet_info.type == WalletType.COLOURED_COIN.value: - raw_puzzle_hash = await wallet.get_new_inner_hash() - address = encode_puzzle_hash(raw_puzzle_hash) + async def log_in(self, request): + """ + Logs in the wallet with a specific key. + """ + + await self._stop_wallet() + fingerprint = request["fingerprint"] + type = request["type"] + recovery_host = request["host"] + testing = False + if "testing" in self.service.config and self.service.config["testing"] is True: + testing = True + if type == "skip": + started = await self.service._start( + fingerprint=fingerprint, skip_backup_import=True + ) + elif type == "restore_backup": + file_path = Path(request["file_path"]) + started = await self.service._start( + fingerprint=fingerprint, backup_file=file_path + ) else: - return { - "success": False, - "reason": "Wallet type cannnot create puzzle hashes", - } + started = await self.service._start(fingerprint) - response = { - "success": True, - "wallet_id": wallet_id, - "address": address, - } + if started is True: + return {} + elif testing is True and self.service.backup_initialized is False: + response = {"success": False, "error": "not_initialized"} + return response + elif self.service.backup_initialized is False: + backup_info = None + backup_path = None + try: + private_key = self.service.get_key_for_fingerprint(fingerprint) + last_recovery = await download_backup(recovery_host, private_key) + backup_path = path_from_root(self.service.root_path, "last_recovery") + if backup_path.exists(): + backup_path.unlink() + backup_path.write_text(last_recovery) + backup_info = get_backup_info(backup_path, private_key) + backup_info["backup_host"] = recovery_host + backup_info["downloaded"] = True + except Exception as e: + log.error(f"error {e}") + response = {"success": False, "error": "not_initialized"} + if backup_info is not None: + response["backup_info"] = backup_info + response["backup_path"] = f"{backup_path}" + return response - return response + return {"success": False, "error": "Unknown Error"} +<<<<<<< HEAD async def send_transaction(self, request): wallet_id = int(request["wallet_id"]) wallet = self.service.wallet_state_manager.wallets[wallet_id] @@ -261,28 +238,128 @@ class WalletRpcApi: "status": "FAILED", "reason": "Timed out. Transaction may or may not have been sent.", "id": wallet_id, +======= + async def get_public_keys(self, request: Dict): + fingerprints = [ + (sk.get_g1().get_fingerprint(), seed is not None) + for (sk, seed) in self.service.keychain.get_all_private_keys() + ] + return {"public_key_fingerprints": fingerprints} + + async def _get_private_key( + self, fingerprint + ) -> Tuple[Optional[PrivateKey], Optional[bytes]]: + for sk, seed in self.service.keychain.get_all_private_keys(): + if sk.get_g1().get_fingerprint() == fingerprint: + return sk, seed + return None, None + + async def get_private_key(self, request): + fingerprint = request["fingerprint"] + sk, seed = await self._get_private_key(fingerprint) + if sk is not None: + s = bytes_to_mnemonic(seed) if seed is not None else None + return { + "private_key": { + "fingerprint": fingerprint, + "sk": bytes(sk).hex(), + "pk": bytes(sk.get_g1()).hex(), + "seed": s, + }, +>>>>>>> 8908edc0... Refactor RPCs } + return {"success": False, "private_key": {"fingerprint": fingerprint}} +<<<<<<< HEAD return data +======= + async def generate_mnemonic(self, request: Dict): + return {"mnemonic": generate_mnemonic()} +>>>>>>> 8908edc0... Refactor RPCs - async def get_transactions(self, request): - wallet_id = int(request["wallet_id"]) - transactions = await self.service.wallet_state_manager.get_all_transactions( - wallet_id + async def add_key(self, request): + if "mnemonic" in request: + # Adding a key from 24 word mnemonic + mnemonic = request["mnemonic"] + passphrase = "" + try: + sk = self.service.keychain.add_private_key( + " ".join(mnemonic), passphrase + ) + except KeyError as e: + return { + "success": False, + "error": f"The word '{e.args[0]}' is incorrect.'", + "word": e.args[0], + } + + else: + raise ValueError("Mnemonic not in request") + + fingerprint = sk.get_g1().get_fingerprint() + await self._stop_wallet() + + # Makes sure the new key is added to config properly + started = False + check_keys(self.service.root_path) + type = request["type"] + if type == "new_wallet": + started = await self.service._start( + fingerprint=fingerprint, new_wallet=True + ) + elif type == "skip": + started = await self.service._start( + fingerprint=fingerprint, skip_backup_import=True + ) + elif type == "restore_backup": + file_path = Path(request["file_path"]) + started = await self.service._start( + fingerprint=fingerprint, backup_file=file_path + ) + + if started is True: + return {} + raise ValueError("Failed to start") + + async def delete_key(self, request): + await self._stop_wallet() + fingerprint = request["fingerprint"] + self.service.keychain.delete_key_by_fingerprint(fingerprint) + path = path_from_root( + self.service.root_path, + f"{self.service.config['database_path']}-{fingerprint}", ) - formatted_transactions = [] + if path.exists(): + path.unlink() + return {} - for tx in transactions: - formatted = tx.to_json_dict() - formatted["to_address"] = encode_puzzle_hash(tx.to_address) - formatted_transactions.append(formatted) + async def delete_all_keys(self, request: Dict): + await self._stop_wallet() + self.service.keychain.delete_all_keys() + path = path_from_root( + self.service.root_path, self.service.config["database_path"] + ) + if path.exists(): + path.unlink() + return {} - response = { - "success": True, - "txs": formatted_transactions, - "wallet_id": wallet_id, - } - return response + ########################################################################################## + # Wallet Node + ########################################################################################## + + async def get_sync_status(self, request: Dict): + assert self.service.wallet_state_manager is not None + syncing = self.service.wallet_state_manager.sync_mode + + return {"syncing": syncing} + + async def get_height_info(self, request: Dict): + assert self.service.wallet_state_manager is not None + + lca = self.service.wallet_state_manager.lca + height = self.service.wallet_state_manager.block_records[lca].height + + return {"height": height} async def farm_block(self, request): puzzle_hash = request["puzzle_hash"] @@ -295,51 +372,24 @@ class WalletRpcApi: ) self.service.server.push_message(msg) - return {"success": True} + return {} - async def get_wallet_balance(self, request: Dict): - if self.service.wallet_state_manager is None: - return {"success": False} - wallet_id = uint32(int(request["wallet_id"])) - wallet = self.service.wallet_state_manager.wallets[wallet_id] - balance = await wallet.get_confirmed_balance() - pending_balance = await wallet.get_unconfirmed_balance() - spendable_balance = await wallet.get_spendable_balance() - pending_change = await wallet.get_pending_change_balance() - if wallet.wallet_info.type == WalletType.COLOURED_COIN.value: - frozen_balance = 0 - else: - frozen_balance = await wallet.get_frozen_amount() + ########################################################################################## + # Wallet Management + ########################################################################################## - wallet_balance = { - "wallet_id": wallet_id, - "confirmed_wallet_balance": balance, - "unconfirmed_wallet_balance": pending_balance, - "spendable_balance": spendable_balance, - "frozen_balance": frozen_balance, - "pending_change": pending_change, - } + async def get_wallets(self, request: Dict): + assert self.service.wallet_state_manager is not None - return {"success": True, "wallet_balance": wallet_balance} + wallets: List[ + WalletInfo + ] = await self.service.wallet_state_manager.get_all_wallets() - async def get_sync_status(self, request: Dict): - if self.service.wallet_state_manager is None: - return {"success": False} - syncing = self.service.wallet_state_manager.sync_mode - - return {"success": True, "syncing": syncing} - - async def get_height_info(self, request: Dict): - if self.service.wallet_state_manager is None: - return {"success": False} - lca = self.service.wallet_state_manager.lca - height = self.service.wallet_state_manager.block_records[lca].height - - response = {"success": True, "height": height} + response = {"wallets": wallets} return response - async def create_backup_and_upload(self, host): + async def _create_backup_and_upload(self, host): try: if ( "testing" in self.service.config @@ -361,192 +411,214 @@ class WalletRpcApi: log.error(f"Exception in upload backup. Error: {e}") async def create_new_wallet(self, request): - config, wallet_state_manager, main_wallet = self.get_wallet_config() + assert self.service.wallet_state_manager is not None + + wallet_state_manager = self.service.wallet_state_manager + main_wallet = wallet_state_manager.main_wallet host = request["host"] if request["wallet_type"] == "cc_wallet": if request["mode"] == "new": - try: - cc_wallet: CCWallet = await CCWallet.create_new_cc( - wallet_state_manager, main_wallet, request["amount"] - ) - colour = cc_wallet.get_colour() - asyncio.ensure_future(self.create_backup_and_upload(host)) - return { - "success": True, - "type": cc_wallet.wallet_info.type, - "colour": colour, - "wallet_id": cc_wallet.wallet_info.id, - } - except Exception as e: - log.error(f"FAILED {e}") - return {"success": False, "reason": str(e)} + cc_wallet: CCWallet = await CCWallet.create_new_cc( + wallet_state_manager, main_wallet, request["amount"] + ) + colour = cc_wallet.get_colour() + asyncio.ensure_future(self._create_backup_and_upload(host)) + return { + "type": cc_wallet.wallet_info.type, + "colour": colour, + "wallet_id": cc_wallet.wallet_info.id, + } elif request["mode"] == "existing": - try: - cc_wallet = await CCWallet.create_wallet_for_cc( - wallet_state_manager, main_wallet, request["colour"] - ) - asyncio.ensure_future(self.create_backup_and_upload(host)) - return {"success": True, "type": cc_wallet.wallet_info.type} - except Exception as e: - log.error(f"FAILED2 {e}") - return {"success": False, "reason": str(e)} + cc_wallet = await CCWallet.create_wallet_for_cc( + wallet_state_manager, main_wallet, request["colour"] + ) + asyncio.ensure_future(self._create_backup_and_upload(host)) + return {"type": cc_wallet.wallet_info.type} if request["wallet_type"] == "rl_wallet": if request["rl_type"] == "admin": log.info("Create rl admin wallet") - try: - rl_admin: RLWallet = await RLWallet.create_rl_admin( - wallet_state_manager - ) - success = await rl_admin.admin_create_coin( - uint64(int(request["interval"])), - uint64(int(request["limit"])), - request["pubkey"], - uint64(int(request["amount"])), - ) - asyncio.ensure_future(self.create_backup_and_upload(host)) - return { - "success": success, - "id": rl_admin.wallet_info.id, - "type": rl_admin.wallet_info.type, - "origin": rl_admin.rl_info.rl_origin, - "pubkey": rl_admin.rl_info.admin_pubkey.hex(), - } - except Exception as e: - log.error(f"FAILED {e}") - return {"success": False, "reason": str(e)} + rl_admin: RLWallet = await RLWallet.create_rl_admin( + wallet_state_manager + ) + success = await rl_admin.admin_create_coin( + uint64(int(request["interval"])), + uint64(int(request["limit"])), + request["pubkey"], + uint64(int(request["amount"])), + ) + asyncio.ensure_future(self._create_backup_and_upload(host)) + return { + "success": success, + "id": rl_admin.wallet_info.id, + "type": rl_admin.wallet_info.type, + "origin": rl_admin.rl_info.rl_origin, + "pubkey": rl_admin.rl_info.admin_pubkey.hex(), + } elif request["rl_type"] == "user": log.info("Create rl user wallet") - try: - rl_user: RLWallet = await RLWallet.create_rl_user( - wallet_state_manager - ) - asyncio.ensure_future(self.create_backup_and_upload(host)) - return { - "success": True, - "id": rl_user.wallet_info.id, - "type": rl_user.wallet_info.type, - "pubkey": rl_user.rl_info.user_pubkey.hex(), - } - except Exception as e: - log.error("FAILED {e}") - return {"success": False, "reason": str(e)} + rl_user: RLWallet = await RLWallet.create_rl_user(wallet_state_manager) + asyncio.ensure_future(self._create_backup_and_upload(host)) + return { + "id": rl_user.wallet_info.id, + "type": rl_user.wallet_info.type, + "pubkey": rl_user.rl_info.user_pubkey.hex(), + } - def get_wallet_config(self): - return ( - self.service.config, - self.service.wallet_state_manager, - self.service.wallet_state_manager.main_wallet, + ########################################################################################## + # Wallet + ########################################################################################## + + async def get_wallet_balance(self, request: Dict): + assert self.service.wallet_state_manager is not None + wallet_id = uint32(int(request["wallet_id"])) + wallet = self.service.wallet_state_manager.wallets[wallet_id] + balance = await wallet.get_confirmed_balance() + pending_balance = await wallet.get_unconfirmed_balance() + spendable_balance = await wallet.get_spendable_balance() + pending_change = await wallet.get_pending_change_balance() + if wallet.wallet_info.type == WalletType.COLOURED_COIN.value: + frozen_balance = 0 + else: + frozen_balance = await wallet.get_frozen_amount() + + wallet_balance = { + "wallet_id": wallet_id, + "confirmed_wallet_balance": balance, + "unconfirmed_wallet_balance": pending_balance, + "spendable_balance": spendable_balance, + "frozen_balance": frozen_balance, + "pending_change": pending_change, + } + + return {"wallet_balance": wallet_balance} + + async def get_transaction(self, request): + assert self.service.wallet_state_manager is not None + transaction_id: bytes32 = bytes32(bytes.fromhex(request["transaction_id"])) + tr: Optional[ + TransactionRecord + ] = await self.service.wallet_state_manager.get_transaction(transaction_id) + if tr is None: + raise ValueError(f"Transaction {transaction_id} not found") + + return { + "transaction": tr, + "transaction_id": tr.spend_bundle.name(), + } + + async def get_transactions(self, request): + assert self.service.wallet_state_manager is not None + + wallet_id = int(request["wallet_id"]) + transactions = await self.service.wallet_state_manager.get_all_transactions( + wallet_id ) + formatted_transactions = [] - async def get_wallets(self, request: Dict): - if self.service.wallet_state_manager is None: - return {"success": False} - wallets: List[ - WalletInfo - ] = await self.service.wallet_state_manager.get_all_wallets() + for tx in transactions: + formatted = tx.to_json_dict() + formatted["to_address"] = encode_puzzle_hash(tx.to_address) + formatted_transactions.append(formatted) - response = {"wallets": wallets, "success": True} + return { + "txs": formatted_transactions, + "wallet_id": wallet_id, + } - return response + async def get_next_address(self, request: Dict) -> Dict: + """ + Returns a new address + """ + assert self.service.wallet_state_manager is not None + + wallet_id = uint32(int(request["wallet_id"])) + wallet = self.service.wallet_state_manager.wallets[wallet_id] + + if wallet.wallet_info.type == WalletType.STANDARD_WALLET.value: + raw_puzzle_hash = await wallet.get_new_puzzlehash() + address = encode_puzzle_hash(raw_puzzle_hash) + elif wallet.wallet_info.type == WalletType.COLOURED_COIN.value: + raw_puzzle_hash = await wallet.get_new_inner_hash() + address = encode_puzzle_hash(raw_puzzle_hash) + else: + raise ValueError( + f"Wallet type {wallet.wallet_info.type} cannot create puzzle hashes" + ) + + return { + "wallet_id": wallet_id, + "address": address, + } + + async def send_transaction(self, request): + assert self.service.wallet_state_manager is not None + + wallet_id = int(request["wallet_id"]) + wallet = self.service.wallet_state_manager.wallets[wallet_id] + tx = await wallet.generate_signed_transaction_dict(request) + if tx is None: + raise ValueError("Failed to generate signed transaction") + + await wallet.push_transaction(tx) + + # Transaction may not have been included in the mempool yet. Use get_transaction to check. + return { + "transaction": tx, + "transaction_id": tx.spend_bundle.name(), + } + + async def create_backup(self, request): + assert self.service.wallet_state_manager is not None + file_path = Path(request["file_path"]) + await self.service.wallet_state_manager.create_wallet_backup(file_path) + return {} + + ########################################################################################## + # Coloured Coins and Trading + ########################################################################################## async def cc_set_name(self, request): + assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] await wallet.set_name(str(request["name"])) - response = {"wallet_id": wallet_id, "success": True} - return response + return {"wallet_id": wallet_id} async def cc_get_name(self, request): + assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] name: str = await wallet.get_name() - response = {"wallet_id": wallet_id, "name": name} - return response + return {"wallet_id": wallet_id, "name": name} async def cc_spend(self, request): + assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] encoded_puzzle_hash = request["inner_address"] puzzle_hash = decode_puzzle_hash(encoded_puzzle_hash) - try: - tx = await wallet.generate_signed_transaction( - request["amount"], puzzle_hash - ) - except Exception as e: - data = {"status": "FAILED", "reason": f"{e}", "id": wallet_id} - return data + tx = await wallet.generate_signed_transaction(request["amount"], puzzle_hash) if tx is None: - data = { - "success": False, - "reason": "Failed to generate signed transaction", - "id": wallet_id, - } - return data - try: - await wallet.wallet_state_manager.add_pending_transaction(tx) - except Exception as e: - data = { - "success": False, - "reason": f"Failed to push transaction {e}", - "id": wallet_id, - } - return data + raise ValueError("Failed to generate signed transaction") + await wallet.wallet_state_manager.add_pending_transaction(tx) return { - "success": True, - "transaction": tr, - "transaction_id": tr.spend_bundle.name(), + "transaction": tx, + "transaction_id": tx.spend_bundle.name(), } async def cc_get_colour(self, request): + assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) wallet: CCWallet = self.service.wallet_state_manager.wallets[wallet_id] colour: str = wallet.get_colour() - response = {"colour": colour, "wallet_id": wallet_id} - return response - - async def get_wallet_summaries(self, request: Dict): - if self.service.wallet_state_manager is None: - return {"success": False} - wallet_summaries = {} - for wallet_id in self.service.wallet_state_manager.wallets: - wallet = self.service.wallet_state_manager.wallets[wallet_id] - balance = await wallet.get_confirmed_balance() - type = wallet.wallet_info.type - if type == WalletType.COLOURED_COIN.value: - name = wallet.wallet_info.name - colour = wallet.get_colour() - wallet_summaries[wallet_id] = { - "type": type, - "balance": balance, - "name": name, - "colour": colour, - } - else: - wallet_summaries[wallet_id] = {"type": type, "balance": balance} - return {"success": True, "wallet_summaries": wallet_summaries} - - async def get_discrepancies_for_offer(self, request): - file_name = request["filename"] - file_path = Path(file_name) - ( - success, - discrepancies, - error, - ) = await self.service.wallet_state_manager.trade_manager.get_discrepancies_for_offer( - file_path - ) - - if success: - response = {"success": True, "discrepancies": discrepancies} - else: - response = {"success": False, "error": error} - - return response + return {"colour": colour, "wallet_id": wallet_id} async def create_offer_for_ids(self, request): + assert self.service.wallet_state_manager is not None + offer = request["ids"] file_name = request["filename"] ( @@ -560,17 +632,76 @@ class WalletRpcApi: self.service.wallet_state_manager.trade_manager.write_offer_to_disk( Path(file_name), spend_bundle ) - response = {"success": success} + return {} + raise ValueError(error) + + async def get_discrepancies_for_offer(self, request): + assert self.service.wallet_state_manager is not None + file_name = request["filename"] + file_path = Path(file_name) + ( + success, + discrepancies, + error, + ) = await self.service.wallet_state_manager.trade_manager.get_discrepancies_for_offer( + file_path + ) + + if success: + return {"discrepancies": discrepancies} + raise ValueError(error) + + async def respond_to_offer(self, request): + assert self.service.wallet_state_manager is not None + file_path = Path(request["filename"]) + ( + success, + trade_record, + error, + ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer( + file_path + ) + if not success: + raise ValueError(error) + return {} + + async def get_trade(self, request: Dict): + assert self.service.wallet_state_manager is not None + + trade_mgr = self.service.wallet_state_manager.trade_manager + + trade_id = request["trade_id"] + trade: Optional[TradeRecord] = await trade_mgr.get_trade_by_id(trade_id) + if trade is None: + raise ValueError(f"No trade with trade id: {trade_id}") + + result = trade_record_to_dict(trade) + return {"trade": result} + + async def get_all_trades(self, request: Dict): + assert self.service.wallet_state_manager is not None + + trade_mgr = self.service.wallet_state_manager.trade_manager + + all_trades = await trade_mgr.get_all_trades() + result = [] + for trade in all_trades: + result.append(trade_record_to_dict(trade)) + + return {"trades": result} + + async def cancel_trade(self, request: Dict): + assert self.service.wallet_state_manager is not None + + wsm = self.service.wallet_state_manager + secure = request["secure"] + trade_id = hexstr_to_bytes(request["trade_id"]) + + if secure: + await wsm.trade_manager.cancel_pending_offer_safely(trade_id) else: - response = {"success": success, "reason": error} - - return response - - async def create_backup(self, request): - file_path = Path(request["file_path"]) - await self.service.wallet_state_manager.create_wallet_backup(file_path) - response = {"success": True} - return response + await wsm.trade_manager.cancel_pending_offer(trade_id) + return {} async def get_backup_info(self, request: Dict): file_path = Path(request["file_path"]) @@ -588,233 +719,47 @@ class WalletRpcApi: "error": f"The word '{e.args[0]}' is incorrect.'", "word": e.args[0], } - except ValueError as e: - return { - "success": False, - "error": e.args[0], - } elif "fingerprint" in request: sk, seed = await self._get_private_key(request["fingerprint"]) if sk is None: - return { - "success": False, - "error": "Unable to decrypt the backup file.", - } + raise ValueError("Unable to decrypt the backup file.") backup_info = get_backup_info(file_path, sk) - response = {"success": True, "backup_info": backup_info} - return response + return {"backup_info": backup_info} - async def respond_to_offer(self, request): - file_path = Path(request["filename"]) - ( - success, - trade_record, - reason, - ) = await self.service.wallet_state_manager.trade_manager.respond_to_offer( - file_path + ########################################################################################## + # Rate Limited Wallet + ########################################################################################## + + async def rl_set_user_info(self, request): + assert self.service.wallet_state_manager is not None + + wallet_id = uint32(int(request["wallet_id"])) + rl_user = self.service.wallet_state_manager.wallets[wallet_id] + origin = request["origin"] + success = await rl_user.set_user_info( + uint64(request["interval"]), + uint64(request["limit"]), + origin["parent_coin_info"], + origin["puzzle_hash"], + origin["amount"], + request["admin_pubkey"], ) - if success: - response = {"success": success} - else: - response = {"success": success, "reason": reason} - return response - - async def get_public_keys(self, request: Dict): - fingerprints = [ - (sk.get_g1().get_fingerprint(), seed is not None) - for (sk, seed) in self.service.keychain.get_all_private_keys() - ] - response = {"success": True, "public_key_fingerprints": fingerprints} - return response - - async def _get_private_key( - self, fingerprint - ) -> Tuple[Optional[PrivateKey], Optional[bytes]]: - for sk, seed in self.service.keychain.get_all_private_keys(): - if sk.get_g1().get_fingerprint() == fingerprint: - return sk, seed - return None, None - - async def get_private_key(self, request): - fingerprint = request["fingerprint"] - sk, seed = await self._get_private_key(fingerprint) - if sk is not None: - s = bytes_to_mnemonic(seed) if seed is not None else None - return { - "success": True, - "private_key": { - "fingerprint": fingerprint, - "sk": bytes(sk).hex(), - "pk": bytes(sk.get_g1()).hex(), - "seed": s, - }, - } - return {"success": False, "private_key": {"fingerprint": fingerprint}} - - async def log_in(self, request): - await self.stop_wallet() - fingerprint = request["fingerprint"] - type = request["type"] - recovery_host = request["host"] - testing = False - if "testing" in self.service.config and self.service.config["testing"] is True: - testing = True - if type == "skip": - started = await self.service._start( - fingerprint=fingerprint, skip_backup_import=True - ) - elif type == "restore_backup": - file_path = Path(request["file_path"]) - started = await self.service._start( - fingerprint=fingerprint, backup_file=file_path - ) - else: - started = await self.service._start(fingerprint) - - if started is True: - return {"success": True} - elif testing is True and self.service.backup_initialized is False: - response = {"success": False, "error": "not_initialized"} - return response - elif self.service.backup_initialized is False: - backup_info = None - backup_path = None - try: - private_key = self.service.get_key_for_fingerprint(fingerprint) - last_recovery = await download_backup(recovery_host, private_key) - backup_path = path_from_root(self.service.root_path, "last_recovery") - if backup_path.exists(): - backup_path.unlink() - backup_path.write_text(last_recovery) - backup_info = get_backup_info(backup_path, private_key) - backup_info["backup_host"] = recovery_host - backup_info["downloaded"] = True - except Exception as e: - log.error(f"error {e}") - response = {"success": False, "error": "not_initialized"} - if backup_info is not None: - response["backup_info"] = backup_info - response["backup_path"] = f"{backup_path}" - return response - - return {"success": False, "error": "Unknown Error"} - - async def add_key(self, request): - if "mnemonic" in request: - # Adding a key from 24 word mnemonic - mnemonic = request["mnemonic"] - passphrase = "" - try: - sk = self.service.keychain.add_private_key( - " ".join(mnemonic), passphrase - ) - except KeyError as e: - return { - "success": False, - "error": f"The word '{e.args[0]}' is incorrect.'", - "word": e.args[0], - } - except ValueError as e: - return { - "success": False, - "error": e.args[0], - } - - else: - return {"success": False} - - fingerprint = sk.get_g1().get_fingerprint() - await self.stop_wallet() - - # Makes sure the new key is added to config properly - started = False - check_keys(self.service.root_path) - type = request["type"] - if type == "new_wallet": - started = await self.service._start( - fingerprint=fingerprint, new_wallet=True - ) - elif type == "skip": - started = await self.service._start( - fingerprint=fingerprint, skip_backup_import=True - ) - elif type == "restore_backup": - file_path = Path(request["file_path"]) - started = await self.service._start( - fingerprint=fingerprint, backup_file=file_path - ) - - if started is True: - return {"success": True} - else: - return {"success": False} - - async def delete_key(self, request): - await self.stop_wallet() - fingerprint = request["fingerprint"] - self.service.keychain.delete_key_by_fingerprint(fingerprint) - path = path_from_root( - self.service.root_path, - f"{self.service.config['database_path']}-{fingerprint}", - ) - if path.exists(): - path.unlink() - return {"success": True} - - async def clean_all_state(self): - self.service.keychain.delete_all_keys() - path = path_from_root( - self.service.root_path, self.service.config["database_path"] - ) - if path.exists(): - path.unlink() - - async def stop_wallet(self): - if self.service is not None: - self.service._close() - await self.service._await_closed() - - async def delete_all_keys(self, request: Dict): - await self.stop_wallet() - await self.clean_all_state() - response = {"success": True} - return response - - async def generate_mnemonic(self, request: Dict): - mnemonic = generate_mnemonic() - response = {"success": True, "mnemonic": mnemonic} - return response + return {"success": success} async def send_clawback_transaction(self, request): + assert self.service.wallet_state_manager is not None + wallet_id = int(request["wallet_id"]) wallet: RLWallet = self.service.wallet_state_manager.wallets[wallet_id] - try: - tx = await wallet.clawback_rl_coin_transaction() - except Exception as e: - data = { - "success": False, - "reason": f"Failed to generate signed transaction {e}", - } - return data + + tx = await wallet.clawback_rl_coin_transaction() if tx is None: - data = { - "success": False, - "reason": "Failed to generate signed transaction", - } - return data - try: - await wallet.push_transaction(tx) - except Exception as e: - data = { - "success": False, - "reason": f"Failed to push transaction {e}", - } - return data + raise ValueError("Failed to generate signed transaction") + await wallet.push_transaction(tx) # Transaction may not have been included in the mempool yet. Use get_transaction to check. return { - "success": True, "transaction": tx, "transaction_id": tx.spend_bundle.name(), } diff --git a/src/rpc/wallet_rpc_client.py b/src/rpc/wallet_rpc_client.py index a06a5fa5c5..da3999a23c 100644 --- a/src/rpc/wallet_rpc_client.py +++ b/src/rpc/wallet_rpc_client.py @@ -1,5 +1,8 @@ -from typing import Dict +from typing import Dict, List from src.rpc.rpc_client import RpcClient +from src.wallet.transaction_record import TransactionRecord +from src.util.ints import uint64 +from src.types.sized_bytes import bytes32 class WalletRpcClient(RpcClient): @@ -11,14 +14,12 @@ class WalletRpcClient(RpcClient): to the full node. """ - async def get_wallet_summaries(self) -> Dict: - return await self.fetch("get_wallet_summaries", {}) + async def get_wallets(self) -> Dict: + return (await self.fetch("get_wallets", {}))["wallets"] async def get_wallet_balance(self, wallet_id: str) -> Dict: return await self.fetch("get_wallet_balance", {"wallet_id": wallet_id}) -<<<<<<< HEAD -======= async def send_transaction( self, wallet_id: str, amount: uint64, address: str, fee: uint64 = uint64(0) ) -> Dict: @@ -38,9 +39,7 @@ class WalletRpcClient(RpcClient): async def get_next_address(self, wallet_id: str) -> Dict: return await self.fetch("get_next_address", {"wallet_id": wallet_id}) - async def get_transaction( - self, wallet_id: str, transaction_id: bytes32 - ) -> Dict: + async def get_transaction(self, wallet_id: str, transaction_id: bytes32) -> Dict: res = await self.fetch( "get_transaction", @@ -49,18 +48,12 @@ class WalletRpcClient(RpcClient): res["transaction"] = TransactionRecord.from_json_dict(res["transaction"]) return res - async def get_transactions( - self, wallet_id: str, - ) -> Dict: - res = await self.fetch( - "get_transactions", - {"walled_id": wallet_id}, - ) - parsed = [TransactionRecord.from_json_dict(tx) for tx in res["transactions"]) - res[transactions] = parsed + async def get_transactions(self, wallet_id: str,) -> Dict: + res = await self.fetch("get_transactions", {"walled_id": wallet_id},) + parsed = [TransactionRecord.from_json_dict(tx) for tx in res["transactions"]] + res["transactions"] = parsed return res ->>>>>>> f1c565a8... More test async def log_in(self, fingerprint) -> Dict: return await self.fetch( "log_in", @@ -92,5 +85,5 @@ class WalletRpcClient(RpcClient): }, ) - async def get_keys(self) -> Dict: - return await self.fetch("get_public_keys", {}) + async def get_public_keys(self) -> List: + return (await self.fetch("get_public_keys", {}))["public_key_fingerprints"] diff --git a/src/util/clvm.py b/src/util/clvm.py index 0a1e245254..013db2793a 100644 --- a/src/util/clvm.py +++ b/src/util/clvm.py @@ -15,10 +15,5 @@ def run_program( pre_eval_f=None, ): return default_run_program( - program, - args, - quote_kw, - operator_lookup, - max_cost, - pre_eval_f=pre_eval_f, + program, args, quote_kw, operator_lookup, max_cost, pre_eval_f=pre_eval_f, ) diff --git a/src/wallet/cc_wallet/cc_wallet.py b/src/wallet/cc_wallet/cc_wallet.py index 3cfe9a262c..76ef69d6f4 100644 --- a/src/wallet/cc_wallet/cc_wallet.py +++ b/src/wallet/cc_wallet/cc_wallet.py @@ -253,14 +253,7 @@ class CCWallet: inner_puzzle = await self.inner_puzzle_for_cc_puzhash(coin.puzzle_hash) lineage_proof = Program.to( - ( - 1, - [ - coin.parent_coin_info, - inner_puzzle.get_tree_hash(), - coin.amount, - ], - ) + (1, [coin.parent_coin_info, inner_puzzle.get_tree_hash(), coin.amount]) ) await self.add_lineage(coin.name(), lineage_proof) diff --git a/src/wallet/wallet_state_manager.py b/src/wallet/wallet_state_manager.py index 961e5c57cf..cd079c753a 100644 --- a/src/wallet/wallet_state_manager.py +++ b/src/wallet/wallet_state_manager.py @@ -376,7 +376,7 @@ class WalletStateManager: """ self.pending_tx_callback = callback - def state_changed(self, state: str, wallet_id: int = None, data_object = {}): + def state_changed(self, state: str, wallet_id: int = None, data_object={}): """ Calls the callback if it's present. """ diff --git a/tests/rpc/test_wallet_rpc.py b/tests/rpc/test_wallet_rpc.py index 73928a4918..aa0264ad0e 100644 --- a/tests/rpc/test_wallet_rpc.py +++ b/tests/rpc/test_wallet_rpc.py @@ -1,15 +1,13 @@ import asyncio -from secrets import token_bytes import pytest -from src.protocols import full_node_protocol -from src.simulator.simulator_protocol import FarmNewBlockProtocol, ReorgProtocol +from src.simulator.simulator_protocol import FarmNewBlockProtocol from src.types.peer_info import PeerInfo -from src.util.ints import uint16, uint32, uint64 +from src.util.ints import uint16, uint32 from tests.setup_nodes import setup_simulators_and_wallets, bt from src.consensus.block_rewards import calculate_base_fee, calculate_block_reward -from tests.time_out_assert import time_out_assert, time_out_assert_not_None +from tests.time_out_assert import time_out_assert from src.util.chech32 import encode_puzzle_hash from src.rpc.wallet_rpc_client import WalletRpcClient from src.rpc.wallet_rpc_api import WalletRpcApi @@ -95,25 +93,35 @@ class TestWalletRpc: async def tx_in_mempool(): tx = (await client.get_transaction("1", transaction_id))["transaction"] return tx.is_in_mempool() + await time_out_assert(5, tx_in_mempool, True) - await time_out_assert(5, wallet.get_unconfirmed_balance, initial_funds - tx_amount) - assert (await client.get_wallet_balance("1"))["wallet_balance"]["unconfirmed_wallet_balance"] == initial_funds - tx_amount - assert (await client.get_wallet_balance("1"))["wallet_balance"]["confirmed_wallet_balance"] == initial_funds + await time_out_assert( + 5, wallet.get_unconfirmed_balance, initial_funds - tx_amount + ) + assert (await client.get_wallet_balance("1"))["wallet_balance"][ + "unconfirmed_wallet_balance" + ] == initial_funds - tx_amount + assert (await client.get_wallet_balance("1"))["wallet_balance"][ + "confirmed_wallet_balance" + ] == initial_funds for i in range(0, 5): await full_node_1.farm_new_block(FarmNewBlockProtocol(ph_2)) async def eventual_balance(): - return (await client.get_wallet_balance("1"))["wallet_balance"]["confirmed_wallet_balance"] + return (await client.get_wallet_balance("1"))["wallet_balance"][ + "confirmed_wallet_balance" + ] - await time_out_assert(5, eventual_balance, initial_funds_eventually - tx_amount) + await time_out_assert( + 5, eventual_balance, initial_funds_eventually - tx_amount + ) address = (await client.get_next_address("1"))["address"] assert len(address) > 10 txs = (await client.get_transactions("1"))["transactions"] - - + assert len(txs) > 1 except Exception: # Checks that the RPC manages to stop the node