def sync_unspent(self): st = time.time() # block height needs to be real time for addition to our utxos: current_blockheight = self.bci.rpc("getblockcount", []) wallet_name = self.get_wallet_name() self.reset_utxos() listunspent_args = [] if 'listunspent_args' in jm_single().config.options('POLICY'): listunspent_args = ast.literal_eval(jm_single().config.get( 'POLICY', 'listunspent_args')) unspent_list = self.bci.rpc('listunspent', listunspent_args) # filter on label, but note (a) in certain circumstances (in- # wallet transfer) it is possible for the utxo to be labeled # with the external label, and (b) the wallet will know if it # belongs or not anyway (is_known_addr): our_unspent_list = [ x for x in unspent_list if (self.bci.is_address_labeled(x, wallet_name) or self.bci.is_address_labeled(x, self.EXTERNAL_WALLET_LABEL)) ] for u in our_unspent_list: if not self.is_known_addr(u['address']): continue self._add_unspent_utxo(u, current_blockheight) et = time.time() jlog.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec')
def sync_unspent(self, wallet): from jmclient.wallet import BitcoinCoreWallet if isinstance(wallet, BitcoinCoreWallet): return st = time.time() wallet_name = self.get_wallet_name(wallet) wallet.unspent = {} listunspent_args = [] if 'listunspent_args' in jm_single().config.options('POLICY'): listunspent_args = ast.literal_eval(jm_single().config.get( 'POLICY', 'listunspent_args')) unspent_list = self.rpc('listunspent', listunspent_args) for u in unspent_list: if 'account' not in u: continue if u['account'] != wallet_name: continue if u['address'] not in wallet.addr_cache: continue wallet.unspent[u['txid'] + ':' + str(u['vout'])] = { 'address': u['address'], 'value': int(Decimal(str(u['amount'])) * Decimal('1e8')) } et = time.time() log.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec') self.wallet_synced = True
def on_auth_received(self, nick, offer, commitment, cr, amount, kphex): """Receives data on proposed transaction offer from daemon, verifies commitment, returns necessary data to send ioauth message (utxos etc) """ #deserialize the commitment revelation cr_dict = PoDLE.deserialize_revelation(cr) #check the validity of the proof of discrete log equivalence tries = jm_single().config.getint("POLICY", "taker_utxo_retries") def reject(msg): jlog.info("Counterparty commitment not accepted, reason: " + msg) return (False, ) if not verify_podle(str(cr_dict['P']), str(cr_dict['P2']), str(cr_dict['sig']), str(cr_dict['e']), str(commitment), index_range=range(tries)): reason = "verify_podle failed" return reject(reason) #finally, check that the proffered utxo is real, old enough, large enough, #and corresponds to the pubkey res = jm_single().bc_interface.query_utxo_set([cr_dict['utxo']], includeconf=True) if len(res) != 1 or not res[0]: reason = "authorizing utxo is not valid" return reject(reason) age = jm_single().config.getint("POLICY", "taker_utxo_age") if res[0]['confirms'] < age: reason = "commitment utxo not old enough: " + str( res[0]['confirms']) return reject(reason) reqd_amt = int( amount * jm_single().config.getint("POLICY", "taker_utxo_amtpercent") / 100.0) if res[0]['value'] < reqd_amt: reason = "commitment utxo too small: " + str(res[0]['value']) return reject(reason) if res[0]['address'] != self.wallet.pubkey_to_address(cr_dict['P']): reason = "Invalid podle pubkey: " + str(cr_dict['P']) return reject(reason) # authorisation of taker passed #Find utxos for the transaction now: utxos, cj_addr, change_addr = self.oid_to_order(offer, amount) if not utxos: #could not find funds return (False, ) self.wallet.update_cache_index() # Construct data for auth request back to taker. # Need to choose an input utxo pubkey to sign with # (no longer using the coinjoin pubkey from 0.2.0) # Just choose the first utxo in self.utxos and retrieve key from wallet. auth_address = utxos[utxos.keys()[0]]['address'] auth_key = self.wallet.get_key_from_addr(auth_address) auth_pub = btc.privtopub(auth_key) btc_sig = btc.ecdsa_sign(kphex, auth_key) return (True, utxos, auth_pub, cj_addr, change_addr, btc_sig)
def calculate_fidelity_bond_values(fidelity_bonds_info): if len(fidelity_bonds_info) == 0: return {} interest_rate = get_interest_rate() blocks = jm_single().bc_interface.get_current_block_height() mediantime = jm_single().bc_interface.get_best_block_median_time() validated_bonds = {} for bond_data in fidelity_bonds_info: try: fb_proof = FidelityBondProof.parse_and_verify_proof_msg( bond_data["counterparty"], bond_data["takernick"], bond_data["proof"]) except ValueError: continue if fb_proof.utxo in validated_bonds: continue utxo_data = FidelityBondMixin.get_validated_timelocked_fidelity_bond_utxo( fb_proof.utxo, fb_proof.utxo_pub, fb_proof.locktime, fb_proof.cert_expiry, blocks) if utxo_data is not None: validated_bonds[fb_proof.utxo] = (fb_proof, utxo_data) fidelity_bond_values = { bond_data.maker_nick: FidelityBondMixin.calculate_timelocked_fidelity_bond_value( utxo_data["value"], jm_single().bc_interface.get_block_time( jm_single().bc_interface.get_block_hash(blocks - utxo_data["confirms"] + 1)), bond_data.locktime, mediantime, interest_rate) for bond_data, utxo_data in validated_bonds.values() } return fidelity_bond_values
def listunspent(self, minconf=None): listunspent_args = [] if 'listunspent_args' in jm_single().config.options('POLICY'): listunspent_args = ast.literal_eval(jm_single().config.get( 'POLICY', 'listunspent_args')) if minconf is not None: listunspent_args[0] = minconf return self._rpc('listunspent', listunspent_args)
def push(self): tx = btc.serialize(self.latest_tx) jlog.debug('\n' + tx) self.txid = btc.txhash(tx) jlog.debug('txid = ' + self.txid) pushed = jm_single().bc_interface.pushtx(tx) if not pushed: self.on_finished_callback(False, fromtx=True) else: jm_single().bc_interface.add_tx_notify(self.latest_tx, self.unconfirm_callback, self.confirm_callback, self.my_cj_addr)
def sync_unspent(self): st = time.time() # block height needs to be real time for addition to our utxos: current_blockheight = self.bci.get_current_block_height() if not current_blockheight: # this failure will shut down the application elsewhere, here # just give up: return wallet_name = self.get_wallet_name() self.reset_utxos() listunspent_args = [] if 'listunspent_args' in jm_single().config.options('POLICY'): listunspent_args = ast.literal_eval(jm_single().config.get( 'POLICY', 'listunspent_args')) unspent_list = self.bci.rpc('listunspent', listunspent_args) # filter on label, but note (a) in certain circumstances (in- # wallet transfer) it is possible for the utxo to be labeled # with the external label, and (b) the wallet will know if it # belongs or not anyway (is_known_addr): our_unspent_list = [ x for x in unspent_list if (self.bci.is_address_labeled(x, wallet_name) or self.bci.is_address_labeled(x, self.EXTERNAL_WALLET_LABEL)) ] for utxo in our_unspent_list: if not self.is_known_addr(utxo['address']): continue # The result of bitcoin core's listunspent RPC call does not have # a "height" field, only "confirmations". # But the result of scantxoutset used in no-history sync does # have "height". if "height" in utxo: height = utxo["height"] else: height = None # wallet's utxo database needs to store an absolute rather # than relative height measure: confs = int(utxo['confirmations']) if confs < 0: jlog.warning("Utxo not added, has a conflict: " + str(utxo)) continue if confs >= 1: height = current_blockheight - confs + 1 self._add_unspent_txo(utxo, height) et = time.time() jlog.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec')
def inform_user_details(self): self.user_info("Your receiving address is: " + self.destination_addr) self.user_info("You will receive amount: " + str( self.receiving_amount) + " satoshis.") self.user_info("The sender also needs to know your ephemeral " "nickname: " + jm_single().nickname) self.user_info("This information has been stored in a file payjoin.txt;" " send it to your counterparty when you are ready.") with open("payjoin.txt", "w") as f: f.write("Payjoin transfer details:\n\n") f.write("Address: " + self.destination_addr + "\n") f.write("Amount (in sats): " + str(self.receiving_amount) + "\n") f.write("Receiver nick: " + jm_single().nickname + "\n") if not self.user_check("Enter 'y' to wait for the payment:"): sys.exit(EXIT_SUCCESS)
def push(self): jlog.debug('\n' + bintohex(self.latest_tx.serialize())) self.txid = bintohex(self.latest_tx.GetTxid()[::-1]) jlog.info('txid = ' + self.txid) #If we are sending to a bech32 address, in case of sweep, will #need to use that bech32 for address import, which requires #converting to script (Core does not allow import of bech32) if self.my_cj_addr.lower()[:2] in ['bc', 'tb']: notify_addr = btc.CCoinAddress(self.my_cj_addr).to_scriptPubKey() else: notify_addr = self.my_cj_addr #add the callbacks *before* pushing to ensure triggering; #this does leave a dangling notify callback if the push fails, but #that doesn't cause problems. self.wallet_service.register_callbacks([self.unconfirm_callback], self.txid, "unconfirmed") self.wallet_service.register_callbacks([self.confirm_callback], self.txid, "confirmed") task.deferLater( reactor, float(jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec")), self.handle_unbroadcast_transaction, self.txid, self.latest_tx) tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast') nick_to_use = None if tx_broadcast == 'self': pushed = self.push_ourselves() elif tx_broadcast in ['random-peer', 'not-self']: n = len(self.maker_utxo_data) if tx_broadcast == 'random-peer': i = random.randrange(n + 1) else: i = random.randrange(n) if i == n: pushed = self.push_ourselves() else: nick_to_use = list(self.maker_utxo_data.keys())[i] pushed = True else: jlog.info("Only self, random-peer and not-self broadcast " "methods supported. Reverting to self-broadcast.") pushed = self.push_ourselves() if not pushed: self.on_finished_callback(False, fromtx=True) else: if nick_to_use: return (nick_to_use, bintohex(self.latest_tx.serialize()))
def estimate_tx_fee(ins, outs, txtype='p2pkh'): '''Returns an estimate of the number of satoshis required for a transaction with the given number of inputs and outputs, based on information from the blockchain interface. ''' tx_estimated_bytes = btc.estimate_tx_size(ins, outs, txtype) log.debug("Estimated transaction size: " + str(tx_estimated_bytes)) fee_per_kb = jm_single().bc_interface.estimate_fee_per_kb( jm_single().config.getint("POLICY", "tx_fees")) absurd_fee = jm_single().config.getint("POLICY", "absurd_fee_per_kb") if fee_per_kb > absurd_fee: #This error is considered critical; for safety reasons, shut down. raise ValueError("Estimated fee per kB greater than absurd value: " + \ str(absurd_fee) + ", quitting.") log.debug("got estimated tx bytes: " + str(tx_estimated_bytes)) return int((tx_estimated_bytes * fee_per_kb) / Decimal(1000.0))
def on_sig(self, nick, sigb64): """Processes transaction signatures from counterparties. Returns True if all signatures received correctly, else returns False """ if self.aborted: return False sig = base64.b64decode(sigb64).encode('hex') inserted_sig = False txhex = btc.serialize(self.latest_tx) # batch retrieval of utxo data utxo = {} ctr = 0 for index, ins in enumerate(self.latest_tx['ins']): utxo_for_checking = ins['outpoint']['hash'] + ':' + str( ins['outpoint']['index']) #'deadbeef' markers mean our own input scripts are not '' if (ins['script'] != ''): continue utxo[ctr] = [index, utxo_for_checking] ctr += 1 utxo_data = jm_single().bc_interface.query_utxo_set( [x[1] for x in utxo.values()]) # insert signatures for i, u in utxo.iteritems(): if utxo_data[i] is None: continue sig_good = btc.verify_tx_input(txhex, u[0], utxo_data[i]['script'], *btc.deserialize_script(sig)) if sig_good: jlog.debug('found good sig at index=%d' % (u[0])) self.latest_tx['ins'][u[0]]['script'] = sig inserted_sig = True # check if maker has sent everything possible self.utxos[nick].remove(u[1]) if len(self.utxos[nick]) == 0: jlog.debug(('nick = {} sent all sigs, removing from ' 'nonrespondant list').format(nick)) self.nonrespondants.remove(nick) break if not inserted_sig: jlog.debug('signature did not match anything in the tx') # TODO what if the signature doesnt match anything # nothing really to do except drop it, carry on and wonder why the # other guy sent a failed signature tx_signed = True for ins in self.latest_tx['ins']: if ins['script'] == '': tx_signed = False if not tx_signed: return False assert not len(self.nonrespondants) jlog.debug('all makers have sent their signatures') self.taker_info_callback("INFO", "Transaction is valid, signing..") jlog.debug("schedule item was: " + str(self.schedule[self.schedule_index])) return self.self_sign_and_push()
def filter_orderbook(self, orderbook, sweep=False): if sweep: self.orderbook = orderbook #offers choosing deferred to next step else: allowed_types = ["reloffer", "absoffer"] if jm_single().config.get( "POLICY", "segwit") == "false" else ["swreloffer", "swabsoffer"] self.orderbook, self.total_cj_fee = choose_orders( orderbook, self.cjamount, self.n_counterparties, self.order_chooser, self.ignored_makers, allowed_types=allowed_types) if self.orderbook is None: #Failure to get an orderbook means order selection failed #for some reason; no action is taken, we let the stallMonitor # + the finished callback decide whether to retry. return False if self.filter_orders_callback: accepted = self.filter_orders_callback( [self.orderbook, self.total_cj_fee], self.cjamount) if accepted == "retry": #Special condition if Taker is "determined to continue" #(such as tumbler); even though these offers are rejected, #we don't trigger the finished callback; see above note on #`if self.orderbook is None` return False if not accepted: return False return True
def estimate_fee_per_kb(self, N): if not self.absurd_fees: return super(RegtestBitcoinCoreInterface, self).estimate_fee_per_kb(N) else: return jm_single().config.getint("POLICY", "absurd_fee_per_kb") + 100
def __init__(self, wallet): # The two principal member variables # are the blockchaininterface instance, # which is currently global in JM but # could be more flexible in future, and # the JM wallet object. self.bci = jm_single().bc_interface # keep track of the quasi-real-time blockheight # (updated in main monitor loop) self.update_blockheight() self.wallet = wallet self.synced = False # Dicts of registered callbacks, by type # and then by txinfo, for events # on transactions. self.callbacks = {} self.callbacks["all"] = [] self.callbacks["unconfirmed"] = {} self.callbacks["confirmed"] = {} self.restart_callback = None # transactions we are actively monitoring, # i.e. they are not new but we want to track: self.active_txids = [] # to ensure transactions are only processed once: self.processed_txids = [] self.set_autofreeze_warning_cb()
def check_for_reuse(self, added_utxos): """ (a) Check if addresses in new utxos are already in used address list, (b) record new addresses as now used (c) disable the new utxo if it returned as true for (a), and it passes the filter set in the configuration. """ to_be_frozen = set() for au in added_utxos: if self.has_address_been_used(added_utxos[au]["address"]): to_be_frozen.add(au) # any utxos actually added must have their destination address # added to the used address list for this program run: for au in added_utxos.values(): self.used_addresses.add(au["address"]) # disable those that passed the first check, before the addition, # if they satisfy configured logic for utxo in to_be_frozen: freeze_threshold = jm_single().config.getint( "POLICY", "max_sats_freeze_reuse") if freeze_threshold == -1 or added_utxos[utxo][ "value"] <= freeze_threshold: # freezing of coins must be communicated to user: self.autofreeze_warning_cb(utxo) self.disable_utxo(*utxo)
def ensure_wallet_unlocked(): wallet_info = jm_single().bc_interface.rpc('getwalletinfo', []) if 'unlocked_until' in wallet_info and wallet_info[ 'unlocked_until'] <= 0: while True: password = getpass('Enter passphrase to unlock wallet: ') if password == '': raise RuntimeError('Aborting wallet unlock') try: # TODO cleanly unlock wallet after use, not with arbitrary timeout jm_single().bc_interface.rpc('walletpassphrase', [password, 10]) break except jm_single().JsonRpcError as exc: if exc.code != -14: raise exc
def handle_unbroadcast_transaction(self, txid, tx): """ The wallet service will handle dangling callbacks for transactions but we want to reattempt broadcast in case the cause of the problem is a counterparty who refused to broadcast it for us. """ if not self.wallet_service.check_callback_called( self.txid, self.unconfirm_callback, "unconfirmed", "transaction with txid: " + str(self.txid) + " not broadcast."): # we now know the transaction was not pushed, so we reinstigate # the cancelledcallback with the same logic as explained # in Taker.push(): self.wallet_service.register_callbacks([self.unconfirm_callback], txid, "unconfirmed") if jm_single().config.get('POLICY', 'tx_broadcast') == "not-self": warnmsg = ("You have chosen not to broadcast from your own " "node. The transaction is NOT broadcast.") self.taker_info_callback("ABORT", warnmsg + "\nSee log for details.") # warning is arguably not correct but it will stand out more: jlog.warn(warnmsg) jlog.info(btc.human_readable_transaction(tx)) return if not self.push_ourselves(): jlog.error("Failed to broadcast transaction: ") jlog.info(btc.human_readable_transaction(tx))
def import_new_addresses(self, addr_list): # FIXME: same code as in taker.py bci = jm_single().bc_interface if not hasattr(bci, 'import_addresses'): return assert hasattr(bci, 'get_wallet_name') bci.import_addresses(addr_list, bci.get_wallet_name(self.wallet))
def __init__(self, fromaccount): super(BitcoinCoreWallet, self).__init__() if not isinstance(jm_single().bc_interface, BitcoinCoreInterface): raise RuntimeError('Bitcoin Core wallet can only be used when ' 'blockchain interface is BitcoinCoreInterface') self.fromaccount = fromaccount self.max_mix_depth = 1
def try_to_create_my_orders(self): if not jm_single().bc_interface.wallet_synced: return self.offerlist = self.create_my_orders() self.sync_wait_loop.stop() if not self.offerlist: jlog.info("Failed to create offers, giving up.") sys.exit(0)
def filter_orderbook(self, orderbook, sweep=False): #If honesty filter is set, we immediately filter to only the prescribed #honest makers before continuing. In this case, the number of #counterparties should already match, and this has to be set by the #script instantiating the Taker. #Note: If one or more of the honest makers has dropped out in the meantime, #we will just have insufficient offers and it will fail in the usual way #for insufficient liquidity. if self.honest_only: orderbook = [ o for o in orderbook if o['counterparty'] in self.honest_makers ] if sweep: self.orderbook = orderbook #offers choosing deferred to next step else: if jm_single().config.get("POLICY", "segwit") == "false": allowed_types = ["reloffer", "absoffer"] elif jm_single().config.get("POLICY", "native") == "false": allowed_types = ["swreloffer", "swabsoffer"] else: allowed_types = ["sw0reloffer", "sw0absoffer"] self.orderbook, self.total_cj_fee = choose_orders( orderbook, self.cjamount, self.n_counterparties, self.order_chooser, self.ignored_makers, allowed_types=allowed_types, max_cj_fee=self.max_cj_fee) if self.orderbook is None: #Failure to get an orderbook means order selection failed #for some reason; no action is taken, we let the stallMonitor # + the finished callback decide whether to retry. return False if self.filter_orders_callback: accepted = self.filter_orders_callback( [self.orderbook, self.total_cj_fee], self.cjamount) if accepted == "retry": #Special condition if Taker is "determined to continue" #(such as tumbler); even though these offers are rejected, #we don't trigger the finished callback; see above note on #`if self.orderbook is None` return False if not accepted: return False return True
def __init__(self, wallet): # The two principal member variables # are the blockchaininterface instance, # which is currently global in JM but # could be more flexible in future, and # the JM wallet object. self.bci = jm_single().bc_interface # main loop used to check for transactions, instantiated # after wallet is synced: self.monitor_loop = None self.wallet = wallet self.synced = False # used to flag RPC failure at construction of object: self.rpc_error = False # keep track of the quasi-real-time blockheight # (updated in main monitor loop) self.current_blockheight = None if self.bci is not None: if not self.update_blockheight(): # this accounts for the unusual case # where the application started up with # a functioning blockchain interface, but # that bci is now failing when we are starting # the wallet service. jlog.error("Failure of RPC connection to Bitcoin Core in " "wallet service startup. Application cannot " "continue, shutting down.") self.rpc_error = ("Failure of RPC connection to Bitcoin " "Core in wallet service startup.") # no need to call stopService as it has not yet been started. stop_reactor() else: jlog.warning("No blockchain source available, " + "wallet tools will not show correct balances.") # Dicts of registered callbacks, by type # and then by txinfo, for events # on transactions. self.callbacks = { "all": [], # note: list, not dict "unconfirmed": {}, "confirmed": {}, } self.restart_callback = None # transactions we are actively monitoring, # i.e. they are not new but we want to track: self.active_txs = {} # to ensure transactions are only processed once: self.processed_txids = set() self.set_autofreeze_warning_cb()
def sync_unspent(self, wallet): st = time.time() wallet_name = self.get_wallet_name(wallet) wallet.reset_utxos() listunspent_args = [] if 'listunspent_args' in jm_single().config.options('POLICY'): listunspent_args = ast.literal_eval(jm_single().config.get( 'POLICY', 'listunspent_args')) unspent_list = self.rpc('listunspent', listunspent_args) for u in unspent_list: if not wallet.is_known_addr(u['address']): continue self._add_unspent_utxo(wallet, u) et = time.time() log.debug('bitcoind sync_unspent took ' + str((et - st)) + 'sec') self.wallet_synced = True
def _verify_ioauth_inputs(self, nick, utxo_list, auth_pub): utxo_data = jm_single().bc_interface.query_utxo_set(utxo_list) if None in utxo_data: raise IoauthInputVerificationError([ "ERROR: outputs unconfirmed or already spent. utxo_data=" f"{pprint.pformat(utxo_data)}", "Disregarding this counterparty." ]) # Complete maker authorization: # Extract the address fields from the utxos # Construct the Bitcoin address for the auth_pub field # Ensure that at least one address from utxos corresponds. for inp in utxo_data: try: if self.wallet_service.pubkey_has_script( auth_pub, inp['script']): break except EngineError as e: pass else: raise IoauthInputVerificationError([ f"ERROR maker's ({nick}) authorising pubkey is not included " "in the transaction!" ]) total_input = sum([d['value'] for d in utxo_data]) real_cjfee = calc_cj_fee(self.orderbook[nick]['ordertype'], self.orderbook[nick]['cjfee'], self.cjamount) change_amount = (total_input - self.cjamount - self.orderbook[nick]['txfee'] + real_cjfee) # certain malicious and/or incompetent liquidity providers send # inputs totalling less than the coinjoin amount! this leads to # a change output of zero satoshis; this counterparty must be removed. if change_amount < jm_single().DUST_THRESHOLD: raise IoauthInputVerificationError([ f"ERROR counterparty requires sub-dust change. nick={nick} " f"totalin={total_input:d} cjamount={self.cjamount:d} " f"change={change_amount:d}", f"Invalid change, too small, nick={nick}" ]) return self._MakerTxData(nick, utxo_data, total_input, change_amount, real_cjfee)
def broadcast_fallback(self): self.user_info("Broadcasting non-coinjoin fallback transaction.") txid = btc.txhash(self.fallback_tx) success = jm_single().bc_interface.pushtx(self.fallback_tx) if not success: self.user_info("ERROR: the fallback transaction did not broadcast. " "The payment has NOT been made.") else: self.user_info("Payment received successfully, but it was NOT a coinjoin.") self.user_info("Txid: " + txid) reactor.stop()
def push(self): tx = btc.serialize(self.latest_tx) jlog.debug('\n' + tx) self.txid = btc.txhash(tx) jlog.debug('txid = ' + self.txid) tx_broadcast = jm_single().config.get('POLICY', 'tx_broadcast') nick_to_use = None if tx_broadcast == 'self': pushed = jm_single().bc_interface.pushtx(tx) elif tx_broadcast in ['random-peer', 'not-self']: n = len(self.maker_utxo_data) if tx_broadcast == 'random-peer': i = random.randrange(n + 1) else: i = random.randrange(n) if i == n: pushed = jm_single().bc_interface.pushtx(tx) else: nick_to_use = self.maker_utxo_data.keys()[i] pushed = True else: jlog.info("Only self, random-peer and not-self broadcast " "methods supported. Reverting to self-broadcast.") pushed = jm_single().bc_interface.pushtx(tx) if not pushed: self.on_finished_callback(False, fromtx=True) else: jm_single().bc_interface.add_tx_notify(self.latest_tx, self.unconfirm_callback, self.confirm_callback, self.my_cj_addr) if nick_to_use: return (nick_to_use, tx)
def __init__(self, wallet_service, acceptance_callback=None, info_callback=None): """ Class to manage processing of SNICKER proposals and co-signs and broadcasts in case the application level configuration permits. `acceptance_callback`, if specified, must have arguments and return type as for the default_acceptance_callback in this class. """ # This is a Joinmarket WalletService object. self.wallet_service = wallet_service # The simplest filter on accepting SNICKER joins: # that they pay a minimum of this value in satoshis, # which can be negative (e.g. to account for fees). self.income_threshold = jm_single().config.getint( "SNICKER", "lowest_net_gain") # The acceptance callback which defines if we accept # a valid proposal and sign it, or not. if acceptance_callback is None: self.acceptance_callback = self.default_acceptance_callback else: self.acceptance_callback = acceptance_callback # callback for information messages to UI if not info_callback: self.info_callback = self.default_info_callback else: self.info_callback = info_callback # A list of currently viable key candidates; these must # all be (pub)keys for which the privkey is accessible, # i.e. they must be in-wallet keys. # This list will be continuously updated by polling the # wallet. self.candidate_keys = [] # A list of already processed proposals self.processed_proposals = [] # maintain a list of all successfully broadcast # SNICKER transactions in the current run. self.successful_txs = [] # the main monitoring loop that checks for proposals: self.proposal_poll_loop = None
def unconfirm_callback(self, txd, txid): if not self.tx_match(txd): return False jlog.info("Transaction seen on network, waiting for confirmation") #To allow client to mark transaction as "done" (e.g. by persisting state) self.on_finished_callback(True, fromtx="unconfirmed") self.waiting_for_conf = True confirm_timeout_sec = float(jm_single().config.get( "TIMEOUT", "confirm_timeout_hours")) * 3600 task.deferLater(reactor, confirm_timeout_sec, self.wallet_service.check_callback_called, txid, self.confirm_callback, "confirmed", "transaction with txid " + str(txid) + " not confirmed.") return True
def get_utxos_by_mixdepth(self): unspent_list = jm_single().bc_interface.rpc('listunspent', []) result = {0: {}} for u in unspent_list: if not u['spendable']: continue if self.fromaccount and (('account' not in u) or u['account'] != self.fromaccount): continue result[0][u['txid'] + ':' + str(u['vout'])] = { 'address': u['address'], 'value': int(Decimal(str(u['amount'])) * Decimal('1e8')) } return result
def try_to_create_my_orders(self): """Because wallet syncing is not synchronous(!), we cannot calculate our offers until we know the wallet contents, so poll until BlockchainInterface.wallet_synced is flagged as True. TODO: Use a deferred, probably. Note that create_my_orders() is defined by subclasses. """ if not jm_single().bc_interface.wallet_synced: return self.offerlist = self.create_my_orders() self.sync_wait_loop.stop() if not self.offerlist: jlog.info("Failed to create offers, giving up.") sys.exit(0)