async def create_and_broadcast(self, request): account = None tx = None try: tx, account, password = await self._create_tx_helper(request) try: result = await self._broadcast_transaction( str(tx), tx.hash(), account) except aiorpcx.jsonrpc.RPCError as e: raise Fault(Errors.AIORPCX_ERROR_CODE, e.message) self.prev_transaction = result response = {"txid": result} self.logger.debug("successful broadcast for %s", result) return good_response(response) except Fault as e: if tx and tx.is_complete( ) and e.code != Errors.ALREADY_SENT_TRANSACTION_CODE: self.cleanup_tx(tx, account) return fault_to_http_response(e) except Exception as e: self.logger.exception( "unexpected error in create_and_broadcast handler") if tx and tx.is_complete() and not ( isinstance(e, AssertionError) and str(e) == 'duplicate set not supported'): self.cleanup_tx(tx, account) return fault_to_http_response( Fault(code=Errors.GENERIC_INTERNAL_SERVER_ERROR, message=str(e)))
def raise_for_var_missing(self, vars, required_vars: List[str]): for varname in required_vars: if vars.get(varname) is None: if varname in HEADER_VARS: raise Fault(Errors.HEADER_VAR_NOT_PROVIDED_CODE, Errors.HEADER_VAR_NOT_PROVIDED_MESSAGE.format(varname)) else: raise Fault(Errors.BODY_VAR_NOT_PROVIDED_CODE, Errors.BODY_VAR_NOT_PROVIDED_MESSAGE.format(varname))
def _get_wallet_path(self, wallet_name: str) -> str: """returns parent wallet path. The wallet_name must include .sqlite extension""" wallet_path = os.path.join(self.wallets_path, wallet_name) wallet_path = os.path.normpath(wallet_path) if wallet_name != os.path.basename(wallet_path): raise Fault(Errors.BAD_WALLET_NAME_CODE, Errors.BAD_WALLET_NAME_MESSAGE) if os.path.exists(wallet_path): return wallet_path else: raise Fault(Errors.WALLET_NOT_FOUND_CODE, Errors.WALLET_NOT_FOUND_MESSAGE)
def check_if_wallet_exists(self, file_path): if os.path.exists(file_path): raise Fault(code=Errors.BAD_WALLET_NAME_CODE, message=f"'{file_path + DATABASE_EXT}' already exists") if not file_path.endswith(DATABASE_EXT): if os.path.exists(file_path + DATABASE_EXT): raise Fault( code=Errors.BAD_WALLET_NAME_CODE, message=f"'{file_path + DATABASE_EXT}' already exists")
def raise_for_wallet_availability(self, wallet_name: str) -> Union[str, Fault]: if not self._wallet_name_available(wallet_name): raise Fault( code=Errors.WALLET_NOT_FOUND_CODE, message=Errors.WALLET_NOT_FOUND_MESSAGE.format(wallet_name)) return wallet_name
async def _create_tx_helper(self, request) -> Union[Tuple, Fault]: try: vars = await self.argparser(request) self.raise_for_var_missing(vars, required_vars=[VNAME.WALLET_NAME, VNAME.ACCOUNT_ID, VNAME.OUTPUTS, VNAME.PASSWORD]) wallet_name = vars[VNAME.WALLET_NAME] index = vars[VNAME.ACCOUNT_ID] outputs = vars[VNAME.OUTPUTS] utxos = vars.get(VNAME.UTXOS, None) utxo_preselection = vars.get(VNAME.UTXO_PRESELECTION, True) password = vars.get(VNAME.PASSWORD, None) child_wallet = self._get_account(wallet_name, index) if not utxos: exclude_frozen = vars.get(VNAME.EXCLUDE_FROZEN, True) confirmed_only = vars.get(VNAME.CONFIRMED_ONLY, False) mature = vars.get(VNAME.MATURE, True) utxos = child_wallet.get_utxos(exclude_frozen=exclude_frozen, confirmed_only=confirmed_only, mature=mature) if utxo_preselection: # Defaults to True utxos = self.preselect_utxos(utxos, outputs) # Todo - loop.run_in_executor tx = child_wallet.make_unsigned_transaction(utxos, outputs, self.app_state.config) return tx, child_wallet, password except NotEnoughFunds: raise Fault(Errors.INSUFFICIENT_COINS_CODE, Errors.INSUFFICIENT_COINS_MESSAGE)
async def broadcast(self, request): """Broadcast a rawtx (hex string) to the network. """ try: required_vars = [VNAME.WALLET_NAME, VNAME.ACCOUNT_ID, VNAME.RAWTX] vars = await self.argparser(request, required_vars=required_vars) wallet_name = vars[VNAME.WALLET_NAME] index = vars[VNAME.ACCOUNT_ID] rawtx = vars[VNAME.RAWTX] account = self._get_account(wallet_name, index) tx = Transaction.from_hex(rawtx) self.raise_for_duplicate_tx(tx) frozen_utxos = self.app_state.app.get_and_set_frozen_utxos_for_tx( tx, account) result = await self._broadcast_transaction(rawtx, tx.hash(), account) self.prev_transaction = result response = {"value": {"txid": result}} return good_response(response) except Fault as e: return fault_to_http_response(e) except aiorpcx.jsonrpc.RPCError as e: account.set_frozen_coin_state(frozen_utxos, False) self.remove_signed_transaction(tx, account) return fault_to_http_response( Fault(Errors.AIORPCX_ERROR_CODE, e.message))
def raise_for_duplicate_tx(self, tx): """because the network can be very slow to give this important feedback and instead will return the txid as an http 200 response.""" if tx.txid() == self.prev_transaction: message = "You've already sent this transaction: {}".format(tx.txid()) fault = Fault(Errors.ALREADY_SENT_TRANSACTION_CODE, message) raise fault return
def _fetch_transaction_dto(self, account: AbstractAccount, tx_id) -> Optional[Dict]: tx_hash = hex_str_to_hash(tx_id) tx = account.get_transaction(tx_hash) if not tx: raise Fault(Errors.TRANSACTION_NOT_FOUND_CODE, Errors.TRANSACTION_NOT_FOUND_MESSAGE) return {"tx_hex": tx.to_hex()}
def remove_transaction(self, tx_hash: bytes, wallet: AbstractAccount): # removal of txs that are not in the StateSigned tx state is disabled for now as it may # cause issues with expunging utxos inadvertently. try: tx = wallet.get_transaction(tx_hash) tx_flags = wallet._wallet._transaction_cache.get_flags(tx_hash) is_signed_state = (tx_flags & TxFlags.StateSigned) == TxFlags.StateSigned # Todo - perhaps remove restriction to StateSigned only later (if safe for utxos state) if tx and is_signed_state: wallet.delete_transaction(tx_hash) if tx and not is_signed_state: raise Fault(Errors.DISABLED_FEATURE_CODE, Errors.DISABLED_FEATURE_MESSAGE) except MissingRowError: raise Fault(Errors.TRANSACTION_NOT_FOUND_CODE, Errors.TRANSACTION_NOT_FOUND_MESSAGE)
def _get_account(self, wallet_name: str, account_id: int=1) \ -> Union[Fault, AbstractAccount]: parent_wallet = self._get_parent_wallet(wallet_name=wallet_name) try: child_wallet = parent_wallet.get_account(account_id) except KeyError: message = f"There is no account at account_id: {account_id}." raise Fault(Errors.WALLET_NOT_FOUND_CODE, message) return child_wallet
def _get_parent_wallet(self, wallet_name: str) -> Wallet: """returns a child wallet object""" path_result = self._get_wallet_path(wallet_name) parent_wallet = self.app_state.daemon.get_wallet(path_result) if not parent_wallet: message = Errors.LOAD_BEFORE_GET_MESSAGE.format( get_network_type(), 'wallet_name.sqlite') raise Fault(code=Errors.LOAD_BEFORE_GET_CODE, message=message) return parent_wallet
async def _load_wallet(self, wallet_name: Optional[str] = None) -> Union[Fault, Wallet]: """Loads one parent wallet into the daemon and begins synchronization""" if not wallet_name.endswith(".sqlite"): wallet_name += ".sqlite" path_result = self._get_wallet_path(wallet_name) parent_wallet = self.app_state.daemon.load_wallet(path_result) if parent_wallet is None: raise Fault(Errors.WALLET_NOT_LOADED_CODE, Errors.WALLET_NOT_LOADED_MESSAGE) return parent_wallet
async def main(): async with aiohttp.ClientSession() as session: tasks = [ asyncio.create_task(self.create_and_send( session, payload2)) for _ in range(0, n_txs) ] results = await asyncio.gather(*tasks, return_exceptions=True) for result in results: error_code = result.get('code') if error_code: assert False, str(Fault(error_code, result.get('message')))
async def create_tx(self, request): """ General purpose transaction builder. - Should handle any kind of output script.( see bitcoinx.address for utilities for building p2pkh, multisig etc outputs as hex strings.) """ account = None tx = None try: tx, account, password = await self._create_tx_helper(request) response = {"txid": tx.txid(), "rawtx": str(tx)} return good_response(response) except Fault as e: if tx and tx.is_complete() and e.code != Fault( Errors.ALREADY_SENT_TRANSACTION_CODE): self.cleanup_tx(tx, account) return fault_to_http_response(e) except Exception as e: if tx and tx.is_complete(): self.cleanup_tx(tx, account) return fault_to_http_response( Fault(code=Errors.GENERIC_INTERNAL_SERVER_ERROR, message=str(e)))
async def create_and_broadcast(self, request): try: tx, account, password = await self._create_tx_helper(request) self.raise_for_duplicate_tx(tx) account.sign_transaction(tx, password) frozen_utxos = self.app_state.app.get_and_set_frozen_utxos_for_tx( tx, account) result = await self._broadcast_transaction(str(tx), tx.hash(), account) self.prev_transaction = result response = {"value": {"txid": result}} self.logger.debug("successful broadcast for %s", result) return good_response(response) except Fault as e: return fault_to_http_response(e) except aiorpcx.jsonrpc.RPCError as e: account.set_frozen_coin_state(frozen_utxos, False) self.remove_signed_transaction(tx, account) return fault_to_http_response( Fault(Errors.AIORPCX_ERROR_CODE, e.message))
def test_fault_to_http_response(): fault_negative = Fault(-1, '') fault_zero = Fault(0, '<message>') fault_4xx = Fault(40000, '<message>') fault_404 = Fault(40400, '<not found message>') fault_5xx = Fault(50000, '<message>') fault_other = Fault(60000, '<message>') assert fault_to_http_response(fault_negative)._body == \ bad_request(fault_negative.code, fault_negative.message)._body assert fault_to_http_response(fault_zero)._body == \ bad_request(fault_zero.code, fault_zero.message)._body assert fault_to_http_response(fault_4xx)._body == \ bad_request(fault_4xx.code, fault_4xx.message)._body assert fault_to_http_response(fault_404)._body == \ not_found(fault_404.code, fault_404.message)._body assert fault_to_http_response(fault_5xx)._body == \ internal_server_error(fault_5xx.code, fault_5xx.message)._body assert fault_to_http_response(fault_other)._body == \ bad_request(fault_other.code, fault_other.message)._body
def account_id_if_isdigit(self, index: str) -> Union[int, Fault]: if not index.isdigit(): message = "child wallet index in url must be an integer. You tried " \ "index='%s'." % index raise Fault(code=Errors.GENERIC_BAD_REQUEST_CODE, message=message) return int(index)
async def split_utxos(self, request) -> Union[Fault, Any]: account = None tx = None try: required_vars = [ VNAME.WALLET_NAME, VNAME.ACCOUNT_ID, VNAME.SPLIT_COUNT, VNAME.PASSWORD ] vars = await self.argparser(request, required_vars=required_vars) wallet_name = vars[VNAME.WALLET_NAME] account_id = vars[VNAME.ACCOUNT_ID] split_count = vars[VNAME.SPLIT_COUNT] # optional split_value = vars.get(VNAME.SPLIT_VALUE, 10000) password = vars.get(VNAME.PASSWORD, None) desired_utxo_count = vars.get(VNAME.DESIRED_UTXO_COUNT, 2000) require_confirmed = vars.get(VNAME.REQUIRE_CONFIRMED, False) account = self._get_account(wallet_name, account_id) # Approximate size of a transaction with one P2PKH input and one P2PKH output. base_fee = self.app_state.config.estimate_fee(203) loop = asyncio.get_event_loop() # run in thread - CPU intensive code partial_coin_selection = partial( self.select_inputs_and_outputs, self.app_state.config, account, base_fee, split_count=split_count, desired_utxo_count=desired_utxo_count, require_confirmed=require_confirmed, split_value=split_value) split_result = await loop.run_in_executor(self.txb_executor, partial_coin_selection) if isinstance(split_result, Fault): return fault_to_http_response(split_result) self.logger.debug("split result: %s", split_result) utxos, outputs, attempted_split = split_result if not attempted_split: fault = Fault(Errors.SPLIT_FAILED_CODE, Errors.SPLIT_FAILED_MESSAGE) return fault_to_http_response(fault) tx = account.make_unsigned_transaction(utxos, outputs, self.app_state.config) account.sign_transaction(tx, password) self.raise_for_duplicate_tx(tx) # broadcast result = await self._broadcast_transaction(str(tx), tx.hash(), account) self.prev_transaction = result response = {"txid": result} return good_response(response) except Fault as e: if tx and tx.is_complete() and e.code != Fault( Errors.ALREADY_SENT_TRANSACTION_CODE): self.cleanup_tx(tx, account) return fault_to_http_response(e) except InsufficientCoinsError as e: self.logger.debug(Errors.INSUFFICIENT_COINS_MESSAGE) self.logger.debug("utxos remaining: %s", account.get_utxos()) return fault_to_http_response( Fault(Errors.INSUFFICIENT_COINS_CODE, Errors.INSUFFICIENT_COINS_MESSAGE)) except Exception as e: if tx and tx.is_complete(): self.cleanup_tx(tx, account) return fault_to_http_response( Fault(code=Errors.GENERIC_INTERNAL_SERVER_ERROR, message=str(e)))
def raise_for_rawtx_size(self, rawtx): if (len(rawtx) / 2) > 99000: fault = Fault(Errors.DATA_TOO_BIG_CODE, Errors.DATA_TOO_BIG_MESSAGE) raise fault
async def get_body_vars(self, request) -> Dict: try: return await decode_request_body(request) except JSONDecodeError as e: message = "JSONDecodeError " + str(e) raise Fault(Errors.JSON_DECODE_ERROR_CODE, message)
def _fake_remove_transaction_raise_fault(tx_hash: bytes, wallet: AbstractAccount): raise Fault(Errors.DISABLED_FEATURE_CODE, Errors.DISABLED_FEATURE_MESSAGE)
def _fake_create_tx_helper_raise_exception(self, request) -> Tuple[Any, set]: raise Fault(Errors.INSUFFICIENT_COINS_CODE, Errors.INSUFFICIENT_COINS_MESSAGE)
def raise_for_type_okay(self, vars): for vname in vars: if vars.get(vname, None): if not isinstance(vars.get(vname), ARGTYPES.get(vname)): message = f"{vars.get(vname)} must be of type: '{ARGTYPES.get(vname)}'" raise Fault(Errors.GENERIC_BAD_REQUEST_CODE, message)