import asyncio import logging import time from datetime import datetime from pathlib import Path from typing import Callable, Dict, List, Optional, Tuple from blspy import PrivateKey, G1Element from chia.cmds.init_funcs import check_keys from chia.consensus.block_rewards import calculate_base_farmer_reward from chia.protocols.protocol_message_types import ProtocolMessageTypes from chia.server.outbound_message import NodeType, make_msg from chia.simulator.simulator_protocol import FarmNewBlockProtocol from chia.types.blockchain_format.coin import Coin from chia.types.blockchain_format.sized_bytes import bytes32 from chia.util.bech32m import decode_puzzle_hash, encode_puzzle_hash from chia.util.byte_types import hexstr_to_bytes from chia.util.ints import uint32, uint64 from chia.util.keychain import bytes_to_mnemonic, generate_mnemonic from chia.util.path import path_from_root from chia.util.ws_message import WsRpcMessage, create_payload_dict from chia.wallet.cc_wallet.cc_wallet import CCWallet from chia.wallet.rl_wallet.rl_wallet import RLWallet from chia.wallet.did_wallet.did_wallet import DIDWallet from chia.wallet.trade_record import TradeRecord from chia.wallet.transaction_record import TransactionRecord from chia.wallet.util.backup_utils import download_backup, get_backup_info, upload_backup from chia.wallet.util.trade_utils import trade_record_to_dict from chia.wallet.util.transaction_type import TransactionType from chia.wallet.util.wallet_types import WalletType from chia.wallet.wallet_info import WalletInfo from chia.wallet.wallet_node import WalletNode # Timeout for response from wallet/full node for sending a transaction TIMEOUT = 30 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 { # 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 "/get_sync_status": self.get_sync_status, "/get_height_info": self.get_height_info, "/farm_block": self.farm_block, # Only when node simulator is running "/get_initial_freeze_period": self.get_initial_freeze_period, "/get_network_info": self.get_network_info, # 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, "/get_transaction_count": self.get_transaction_count, "/get_farmed_amount": self.get_farmed_amount, "/create_signed_transaction": self.create_signed_transaction, # Coloured coins and trading "/cc_set_name": self.cc_set_name, "/cc_get_name": self.cc_get_name, "/cc_spend": self.cc_spend, "/cc_get_colour": self.cc_get_colour, "/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_trade": self.get_trade, "/get_all_trades": self.get_all_trades, "/cancel_trade": self.cancel_trade, # DID Wallet "/did_update_recovery_ids": self.did_update_recovery_ids, "/did_spend": self.did_spend, "/did_get_pubkey": self.did_get_pubkey, "/did_get_did": self.did_get_did, "/did_recovery_spend": self.did_recovery_spend, "/did_get_recovery_list": self.did_get_recovery_list, "/did_create_attest": self.did_create_attest, "/did_get_information_needed_for_recovery": self.did_get_information_needed_for_recovery, "/did_create_backup_file": self.did_create_backup_file, # RL wallet "/rl_set_user_info": self.rl_set_user_info, "/send_clawback_transaction:": self.send_clawback_transaction, "/add_rate_limited_funds:": self.add_rate_limited_funds, } async def _state_changed(self, *args) -> List[WsRpcMessage]: """ 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 [] data = { "state": args[0], } if args[1] is not None: data["wallet_id"] = args[1] if args[2] is not None: data["additional_data"] = args[2] return [create_payload_dict("state_changed", data, "chia_wallet", "wallet_ui")] async def _stop_wallet(self): """ 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 not None: self.service._close() await self.service._await_closed() ########################################################################################## # Key management ########################################################################################## async def log_in(self, request): """ Logs in the wallet with a specific key. """ fingerprint = request["fingerprint"] if self.service.logged_in_fingerprint == fingerprint: return {"fingerprint": fingerprint} await self._stop_wallet() log_in_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 log_in_type == "skip": started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True) elif log_in_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 {"fingerprint": fingerprint} 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 get_public_keys(self, request: Dict): fingerprints = [sk.get_g1().get_fingerprint() 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, }, } return {"success": False, "private_key": {"fingerprint": fingerprint}} async def generate_mnemonic(self, request: Dict): return {"mnemonic": generate_mnemonic().split(" ")} async def add_key(self, request): if "mnemonic" not in request: raise ValueError("Mnemonic not 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], } 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) request_type = request["type"] if request_type == "new_wallet": started = await self.service._start(fingerprint=fingerprint, new_wallet=True) elif request_type == "skip": started = await self.service._start(fingerprint=fingerprint, skip_backup_import=True) elif request_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 {"fingerprint": fingerprint} 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}", ) if path.exists(): path.unlink() return {} 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 {} ########################################################################################## # 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 synced = await self.service.wallet_state_manager.synced() return {"synced": synced, "syncing": syncing, "genesis_initialized": True} async def get_height_info(self, request: Dict): assert self.service.wallet_state_manager is not None peak = self.service.wallet_state_manager.peak if peak is None: return {"height": 0} else: return {"height": peak.height} async def get_network_info(self, request: Dict): assert self.service.wallet_state_manager is not None network_name = self.service.config["selected_network"] address_prefix = self.service.config["network_overrides"]["config"][network_name]["address_prefix"] return {"network_name": network_name, "network_prefix": address_prefix} async def farm_block(self, request): raw_puzzle_hash = decode_puzzle_hash(request["address"]) request = FarmNewBlockProtocol(raw_puzzle_hash) msg = make_msg(ProtocolMessageTypes.farm_new_block, request) await self.service.server.send_to_all([msg], NodeType.FULL_NODE) return {} ########################################################################################## # Wallet Management ########################################################################################## async def get_wallets(self, request: Dict): assert self.service.wallet_state_manager is not None wallets: List[WalletInfo] = await self.service.wallet_state_manager.get_all_wallet_info_entries() return {"wallets": wallets} async def _create_backup_and_upload(self, host) -> None: assert self.service.wallet_state_manager is not None try: if "testing" in self.service.config and self.service.config["testing"] is True: return now = time.time() file_name = f"backup_{now}" path = path_from_root(self.service.root_path, file_name) await self.service.wallet_state_manager.create_wallet_backup(path) backup_text = path.read_text() response = await upload_backup(host, backup_text) success = response["success"] if success is False: log.error("Failed to upload backup to wallet backup service") elif success is True: log.info("Finished upload of the backup file") except Exception as e: log.error(f"Exception in upload backup. Error: {e}") async def create_new_wallet(self, request: Dict): 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": async with self.service.wallet_state_manager.lock: cc_wallet: CCWallet = await CCWallet.create_new_cc( wallet_state_manager, main_wallet, request["amount"] ) colour = cc_wallet.get_colour() asyncio.create_task(self._create_backup_and_upload(host)) return { "type": cc_wallet.type(), "colour": colour, "wallet_id": cc_wallet.id(), } elif request["mode"] == "existing": async with self.service.wallet_state_manager.lock: cc_wallet = await CCWallet.create_wallet_for_cc( wallet_state_manager, main_wallet, request["colour"] ) asyncio.create_task(self._create_backup_and_upload(host)) return {"type": cc_wallet.type()} elif request["wallet_type"] == "rl_wallet": if request["rl_type"] == "admin": log.info("Create rl admin wallet") async with self.service.wallet_state_manager.lock: 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"])), uint64(int(request["fee"])) if "fee" in request else uint64(0), ) asyncio.create_task(self._create_backup_and_upload(host)) assert rl_admin.rl_info.admin_pubkey is not None return { "success": success, "id": rl_admin.id(), "type": rl_admin.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") async with self.service.wallet_state_manager.lock: rl_user: RLWallet = await RLWallet.create_rl_user(wallet_state_manager) asyncio.create_task(self._create_backup_and_upload(host)) assert rl_user.rl_info.user_pubkey is not None return { "id": rl_user.id(), "type": rl_user.type(), "pubkey": rl_user.rl_info.user_pubkey.hex(), } elif request["wallet_type"] == "did_wallet": if request["did_type"] == "new": backup_dids = [] num_needed = 0 for d in request["backup_dids"]: backup_dids.append(hexstr_to_bytes(d)) if len(backup_dids) > 0: num_needed = uint64(request["num_of_backup_ids_needed"]) async with self.service.wallet_state_manager.lock: did_wallet: DIDWallet = await DIDWallet.create_new_did_wallet( wallet_state_manager, main_wallet, int(request["amount"]), backup_dids, uint64(num_needed), ) my_did = did_wallet.get_my_DID() return { "success": True, "type": did_wallet.type(), "my_did": my_did, "wallet_id": did_wallet.id(), } elif request["did_type"] == "recovery": async with self.service.wallet_state_manager.lock: did_wallet = await DIDWallet.create_new_did_wallet_from_recovery( wallet_state_manager, main_wallet, request["filename"] ) assert did_wallet.did_info.temp_coin is not None assert did_wallet.did_info.temp_puzhash is not None assert did_wallet.did_info.temp_pubkey is not None my_did = did_wallet.get_my_DID() coin_name = did_wallet.did_info.temp_coin.name().hex() coin_list = did_wallet.did_info.temp_coin.as_list() newpuzhash = did_wallet.did_info.temp_puzhash pubkey = did_wallet.did_info.temp_pubkey return { "success": True, "type": did_wallet.type(), "my_did": my_did, "wallet_id": did_wallet.id(), "coin_name": coin_name, "coin_list": coin_list, "newpuzhash": newpuzhash.hex(), "pubkey": pubkey.hex(), "backup_dids": did_wallet.did_info.backup_ids, "num_verifications_required": did_wallet.did_info.num_of_backup_ids_needed, } ########################################################################################## # Wallet ########################################################################################## async def get_wallet_balance(self, request: Dict) -> 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] unspent_records = await self.service.wallet_state_manager.coin_store.get_unspent_coins_for_wallet(wallet_id) balance = await wallet.get_confirmed_balance(unspent_records) pending_balance = await wallet.get_unconfirmed_balance(unspent_records) spendable_balance = await wallet.get_spendable_balance(unspent_records) pending_change = await wallet.get_pending_change_balance() max_send_amount = await wallet.get_max_send_amount(unspent_records) wallet_balance = { "wallet_id": wallet_id, "confirmed_wallet_balance": balance, "unconfirmed_wallet_balance": pending_balance, "spendable_balance": spendable_balance, "pending_change": pending_change, "max_send_amount": max_send_amount, "unspent_coin_count": len(unspent_records), } return {"wallet_balance": wallet_balance} async def get_transaction(self, request: Dict) -> Dict: assert self.service.wallet_state_manager is not None transaction_id: bytes32 = bytes32(hexstr_to_bytes(request["transaction_id"])) tr: Optional[TransactionRecord] = await self.service.wallet_state_manager.get_transaction(transaction_id) if tr is None: raise ValueError(f"Transaction 0x{transaction_id.hex()} not found") return { "transaction": tr, "transaction_id": tr.name, } async def get_transactions(self, request: Dict) -> Dict: assert self.service.wallet_state_manager is not None wallet_id = int(request["wallet_id"]) if "start" in request: start = request["start"] else: start = 0 if "end" in request: end = request["end"] else: end = 50 transactions = await self.service.wallet_state_manager.tx_store.get_transactions_between(wallet_id, start, end) formatted_transactions = [] selected = self.service.config["selected_network"] prefix = self.service.config["network_overrides"]["config"][selected]["address_prefix"] for tx in transactions: formatted = tx.to_json_dict() formatted["to_address"] = encode_puzzle_hash(tx.to_puzzle_hash, prefix) formatted_transactions.append(formatted) return { "transactions": formatted_transactions, "wallet_id": wallet_id, } async def get_initial_freeze_period(self, _: Dict): freeze_period = self.service.constants.INITIAL_FREEZE_END_TIMESTAMP return {"INITIAL_FREEZE_END_TIMESTAMP": freeze_period} async def get_next_address(self, request: Dict) -> Dict: """ Returns a new address """ assert self.service.wallet_state_manager is not None if request["new_address"] is True: create_new = True else: create_new = False wallet_id = uint32(int(request["wallet_id"])) wallet = self.service.wallet_state_manager.wallets[wallet_id] selected = self.service.config["selected_network"] prefix = self.service.config["network_overrides"]["config"][selected]["address_prefix"] if wallet.type() == WalletType.STANDARD_WALLET: raw_puzzle_hash = await wallet.get_puzzle_hash(create_new) address = encode_puzzle_hash(raw_puzzle_hash, prefix) elif wallet.type() == WalletType.COLOURED_COIN: raw_puzzle_hash = await wallet.get_puzzle_hash(create_new) address = encode_puzzle_hash(raw_puzzle_hash, prefix) else: raise ValueError(f"Wallet type {wallet.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 if await self.service.wallet_state_manager.synced() is False: raise ValueError("Wallet needs to be fully synced before sending transactions") if int(time.time()) < self.service.constants.INITIAL_FREEZE_END_TIMESTAMP: end_date = datetime.fromtimestamp(float(self.service.constants.INITIAL_FREEZE_END_TIMESTAMP)) raise ValueError(f"No transactions before: {end_date}") wallet_id = int(request["wallet_id"]) wallet = self.service.wallet_state_manager.wallets[wallet_id] if not isinstance(request["amount"], int) or not isinstance(request["amount"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) puzzle_hash: bytes32 = decode_puzzle_hash(request["address"]) if "fee" in request: fee = uint64(request["fee"]) else: fee = uint64(0) async with self.service.wallet_state_manager.lock: tx: TransactionRecord = await wallet.generate_signed_transaction(amount, puzzle_hash, fee) 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.name, } async def get_transaction_count(self, request): wallet_id = int(request["wallet_id"]) count = await self.service.wallet_state_manager.tx_store.get_transaction_count_for_wallet(wallet_id) return {"wallet_id": wallet_id, "count": count} 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"])) 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() 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] puzzle_hash: bytes32 = decode_puzzle_hash(request["inner_address"]) if not isinstance(request["amount"], int) or not isinstance(request["amount"], int): raise ValueError("An integer amount or fee is required (too many decimals)") amount: uint64 = uint64(request["amount"]) if "fee" in request: fee = uint64(request["fee"]) else: fee = uint64(0) async with self.service.wallet_state_manager.lock: tx: TransactionRecord = await wallet.generate_signed_transaction([amount], [puzzle_hash], fee) await wallet.push_transaction(tx) return { "transaction": tx, "transaction_id": tx.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() 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"] async with self.service.wallet_state_manager.lock: ( success, spend_bundle, error, ) = await self.service.wallet_state_manager.trade_manager.create_offer_for_ids(offer, file_name) if success: self.service.wallet_state_manager.trade_manager.write_offer_to_disk(Path(file_name), spend_bundle) 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) async with self.service.wallet_state_manager.lock: ( 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"]) async with self.service.wallet_state_manager.lock: ( 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"]) async with self.service.wallet_state_manager.lock: if secure: await wsm.trade_manager.cancel_pending_offer_safely(trade_id) else: await wsm.trade_manager.cancel_pending_offer(trade_id) return {} async def get_backup_info(self, request: Dict): file_path = Path(request["file_path"]) sk = None if "words" in request: mnemonic = request["words"] 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], } elif "fingerprint" in request: sk, seed = await self._get_private_key(request["fingerprint"]) if sk is None: raise ValueError("Unable to decrypt the backup file.") backup_info = get_backup_info(file_path, sk) return {"backup_info": backup_info} ########################################################################################## # Distributed Identities ########################################################################################## async def did_update_recovery_ids(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] recovery_list = [] for _ in request["new_list"]: recovery_list.append(hexstr_to_bytes(_)) if "num_verifications_required" in request: new_amount_verifications_required = uint64(request["num_verifications_required"]) else: new_amount_verifications_required = len(recovery_list) async with self.service.wallet_state_manager.lock: success = await wallet.update_recovery_list(recovery_list, new_amount_verifications_required) # Update coin with new ID info updated_puz = await wallet.get_new_puzzle() spend_bundle = await wallet.create_spend(updated_puz.get_tree_hash()) if spend_bundle is not None and success: return {"success": True} return {"success": False} async def did_spend(self, request): wallet_id = int(request["wallet_id"]) async with self.service.wallet_state_manager.lock: wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] spend_bundle = await wallet.create_spend(request["puzzlehash"]) if spend_bundle is not None: return {"success": True} return {"success": False} async def did_get_did(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] my_did: str = wallet.get_my_DID() async with self.service.wallet_state_manager.lock: coins = await wallet.select_coins(1) if coins is None or coins == set(): return {"success": True, "wallet_id": wallet_id, "my_did": my_did} else: coin = coins.pop() return {"success": True, "wallet_id": wallet_id, "my_did": my_did, "coin_id": coin.name()} async def did_get_recovery_list(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] recovery_list = wallet.did_info.backup_ids recover_hex_list = [] for _ in recovery_list: recover_hex_list.append(_.hex()) return { "success": True, "wallet_id": wallet_id, "recover_list": recover_hex_list, "num_required": wallet.did_info.num_of_backup_ids_needed, } async def did_recovery_spend(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] if len(request["attest_filenames"]) < wallet.did_info.num_of_backup_ids_needed: return {"success": False, "reason": "insufficient messages"} async with self.service.wallet_state_manager.lock: ( info_list, message_spend_bundle, ) = await wallet.load_attest_files_for_recovery_spend(request["attest_filenames"]) if "pubkey" in request: pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"])) else: assert wallet.did_info.temp_pubkey is not None pubkey = wallet.did_info.temp_pubkey if "puzhash" in request: puzhash = hexstr_to_bytes(request["puzhash"]) else: assert wallet.did_info.temp_puzhash is not None puzhash = wallet.did_info.temp_puzhash success = await wallet.recovery_spend( wallet.did_info.temp_coin, puzhash, info_list, pubkey, message_spend_bundle, ) return {"success": success} async def did_get_pubkey(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] pubkey = bytes((await wallet.wallet_state_manager.get_unused_derivation_record(wallet_id)).pubkey).hex() return {"success": True, "pubkey": pubkey} async def did_create_attest(self, request): wallet_id = int(request["wallet_id"]) wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] async with self.service.wallet_state_manager.lock: info = await wallet.get_info_for_recovery() coin = hexstr_to_bytes(request["coin_name"]) pubkey = G1Element.from_bytes(hexstr_to_bytes(request["pubkey"])) spend_bundle = await wallet.create_attestment( coin, hexstr_to_bytes(request["puzhash"]), pubkey, request["filename"] ) if spend_bundle is not None: return { "success": True, "message_spend_bundle": bytes(spend_bundle).hex(), "info": [info[0].hex(), info[1].hex(), info[2]], } else: return {"success": False} async def did_get_information_needed_for_recovery(self, request): wallet_id = int(request["wallet_id"]) did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] my_did = did_wallet.get_my_DID() coin_name = did_wallet.did_info.temp_coin.name().hex() return { "success": True, "wallet_id": wallet_id, "my_did": my_did, "coin_name": coin_name, "newpuzhash": did_wallet.did_info.temp_puzhash, "pubkey": did_wallet.did_info.temp_pubkey, "backup_dids": did_wallet.did_info.backup_ids, } async def did_create_backup_file(self, request): try: wallet_id = int(request["wallet_id"]) did_wallet: DIDWallet = self.service.wallet_state_manager.wallets[wallet_id] did_wallet.create_backup(request["filename"]) return {"wallet_id": wallet_id, "success": True} except Exception: return {"wallet_id": wallet_id, "success": False} ########################################################################################## # 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"] async with self.service.wallet_state_manager.lock: 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 {} 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] fee = int(request["fee"]) async with self.service.wallet_state_manager.lock: tx = await wallet.clawback_rl_coin_transaction(fee) 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.name, } async def add_rate_limited_funds(self, request): wallet_id = uint32(request["wallet_id"]) wallet: RLWallet = self.service.wallet_state_manager.wallets[wallet_id] puzzle_hash = wallet.rl_get_aggregation_puzzlehash(wallet.rl_info.rl_puzzle_hash) request["wallet_id"] = 1 request["puzzle_hash"] = puzzle_hash async with self.service.wallet_state_manager.lock: await wallet.rl_add_funds(request["amount"], puzzle_hash, request["fee"]) return {"status": "SUCCESS"} async def get_farmed_amount(self, request): tx_records: List[TransactionRecord] = await self.service.wallet_state_manager.tx_store.get_farming_rewards() amount = 0 pool_reward_amount = 0 farmer_reward_amount = 0 fee_amount = 0 last_height_farmed = 0 for record in tx_records: height = record.height_farmed(self.service.constants.GENESIS_CHALLENGE) if height > last_height_farmed: last_height_farmed = height if record.type == TransactionType.COINBASE_REWARD: pool_reward_amount += record.amount if record.type == TransactionType.FEE_REWARD: fee_amount += record.amount - calculate_base_farmer_reward(height) farmer_reward_amount += calculate_base_farmer_reward(height) amount += record.amount assert amount == pool_reward_amount + farmer_reward_amount + fee_amount return { "farmed_amount": amount, "pool_reward_amount": pool_reward_amount, "farmer_reward_amount": farmer_reward_amount, "fee_amount": fee_amount, "last_height_farmed": last_height_farmed, } async def create_signed_transaction(self, request): if "additions" not in request or len(request["additions"]) < 1: raise ValueError("Specify additions list") additions: List[Dict] = request["additions"] amount_0: uint64 = uint64(additions[0]["amount"]) assert amount_0 <= self.service.constants.MAX_COIN_AMOUNT puzzle_hash_0 = hexstr_to_bytes(additions[0]["puzzle_hash"]) if len(puzzle_hash_0) != 32: raise ValueError(f"Address must be 32 bytes. {puzzle_hash_0}") additional_outputs = [] for addition in additions[1:]: receiver_ph = hexstr_to_bytes(addition["puzzle_hash"]) if len(receiver_ph) != 32: raise ValueError(f"Address must be 32 bytes. {receiver_ph}") amount = uint64(addition["amount"]) if amount > self.service.constants.MAX_COIN_AMOUNT: raise ValueError(f"Coin amount cannot exceed {self.service.constants.MAX_COIN_AMOUNT}") additional_outputs.append({"puzzlehash": receiver_ph, "amount": amount}) fee = uint64(0) if "fee" in request: fee = uint64(request["fee"]) coins = None if "coins" in request and len(request["coins"]) > 0: coins = set([Coin.from_json_dict(coin_json) for coin_json in request["coins"]]) async with self.service.wallet_state_manager.lock: signed_tx = await self.service.wallet_state_manager.main_wallet.generate_signed_transaction( amount_0, puzzle_hash_0, fee, coins=coins, ignore_max_send_amount=True, primaries=additional_outputs ) return {"signed_tx": signed_tx}