def utxo_selector_smallest_first(utxos_by_addr, amount, num_outputs, fees=None): f = get_fees() input_fee = f['per_input'] output_fee = f['per_output'] # Order the utxos by amount utxo_tuple_list = _get_utxos_addr_tuple_list(utxos_by_addr) ordered_utxos = sorted( utxo_tuple_list, key=lambda utxo_addr_tuple: utxo_addr_tuple[1].value) calc_fees = num_outputs * output_fee utxos_to_use = {} utxo_sum = 0 for addr, utxo in ordered_utxos: tf = fees if fees is not None else calc_fees + input_fee if utxo_sum < amount + tf: utxo_sum += utxo.value if addr in utxos_to_use: utxos_to_use[addr].append(utxo) else: utxos_to_use[addr] = [utxo] calc_fees += input_fee else: break fee = fees if fees is not None else calc_fees rv = utxos_to_use, fee if utxo_sum < amount + fee: rv = {}, fee return rv
def _buy(config, resource, data, method, data_file, output_file, payment_method, max_price, info_only): """ Buys bitcoin payable content over http Todo: reduce number of input args Exception is too general, raise a different exception when user cannot pay Args: config (Config): config object used for getting .two1 information resource (str): resource or content to purchase method (str): HTTP request method, defaults to GET data_file (str): name of the data file to send in HTTP body output_file (str): Output file name payment_method (str): Type of payment used in the purchase: offchain, onchain, channel max_price (int): Max price of resource info_only (bool): Flag which will only get info and not purcahase the resource Raises: NotImplementedError: if endpoint or resource is not valid ResourcePriceGreaterThanMaxPriceError: If the resource price is greater than the max price """ # If resource is a URL string, then bypass seller search if URL_REGEXP.match(resource): target_url = resource seller = target_url elif re.match(r'^(((\w*)(\/){0,1})(\w*)){0,2}(\/){0,1}$', resource) and resource not in DEMOS: target_url = 'https://mkt.21.co/' + resource seller = target_url elif resource in DEMOS: target_url = TWO1_MERCHANT_HOST + DEMOS[resource]["path"] data = json.dumps(data) else: # If we can't figure out the resource type, attempt to use `http` target_url = 'http://' + resource # Change default HTTP method from "GET" to "POST", if we have data if method == "GET" and (data or data_file): method = "POST" # Set default headers for making bitrequests with JSON-like data headers = {'Content-Type': 'application/json'} try: # Find the correct payment method if payment_method == 'offchain': bit_req = BitTransferRequests(config.machine_auth, config.username) elif payment_method == 'onchain': bit_req = OnChainRequests(config.wallet) elif payment_method == 'channel': bit_req = ChannelRequests(config.wallet) channel_list = bit_req._channelclient.list() if not channel_list: confirmed = click.confirm(UxString.buy_channel_warning.format( bit_req.DEFAULT_DEPOSIT_AMOUNT, PaymentChannelStateMachine.PAYMENT_TX_MIN_OUTPUT_AMOUNT), default=True) if not confirmed: raise Exception(UxString.buy_channel_aborted) else: raise Exception('Payment method does not exist.') # Make the request if info_only: res = bit_req.get_402_info(target_url) else: res = bit_req.request(method.lower(), target_url, max_price=max_price, data=data or data_file, headers=headers) except ResourcePriceGreaterThanMaxPriceError as e: config.log( UxString.Error.resource_price_greater_than_max_price.format(e)) return except Exception as e: f = get_fees() buy_fee = 2 * f['per_input'] + f['per_output'] if 'Insufficient funds.' in str(e): config.log( UxString.Error.insufficient_funds_mine_more.format(buy_fee)) else: config.log(str(e), fg="red") return # Output results to user if output_file: # Write response output file output_file.write(res.content) elif info_only: # Print headers that are related to 402 payment required for key, val in res.items(): config.log('{}: {}'.format(key, val)) elif resource in DEMOS: config.log(DEMOS[resource]["formatter"](res)) else: # Write response to console config.log(res.text) # Write the amount paid out if something was truly paid if not info_only and hasattr(res, 'amount_paid'): client = rest_client.TwentyOneRestClient(TWO1_HOST, config.machine_auth, config.username) user_balances = _get_balances(config, client) if payment_method == 'offchain': balance_amount = user_balances.twentyone balance_type = '21.co' elif payment_method == 'onchain': balance_amount = user_balances.onchain balance_type = 'blockchain' elif payment_method == 'channel': balance_amount = user_balances.channels balance_type = 'payment channels' config.log( "You spent: %s Satoshis. Remaining %s balance: %s Satoshis." % (res.amount_paid, balance_type, balance_amount)) # Record the transaction if it was a payable request if hasattr(res, 'paid_amount'): config.log_purchase(s=seller, r=resource, p=res.paid_amount, d=str(datetime.datetime.today()))