def add_tx_notify(self, txd, unconfirmfun, confirmfun, spentfun, notifyaddr, timeoutfun=None): if not self.notifythread: self.notifythread = BitcoinCoreNotifyThread(self) self.notifythread.start() one_addr_imported = False for outs in txd['outs']: addr = btc.script_to_address(outs['script'], get_p2pk_vbyte()) if self.rpc('getaccount', [addr]) != '': one_addr_imported = True break if not one_addr_imported: self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False]) tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) self.txnotify_fun.append( (btc.txhash(btc.serialize(txd)), tx_output_set, unconfirmfun, confirmfun, spentfun, timeoutfun, False)) #create unconfirm timeout here, create confirm timeout in the other thread if timeoutfun: threading.Timer(cs_single().config.getint('TIMEOUT', 'unconfirm_timeout_sec'), bitcoincore_timeout_callback, args=(False, tx_output_set, self.txnotify_fun, timeoutfun)).start()
def cli_receive(filename): wif_privkey = raw_input("Enter private key in WIF compressed format: ") try: privkey = btc.from_wif_privkey(wif_privkey, vbyte=get_p2pk_vbyte()) except: print("Could not parse WIF privkey, quitting.") return amount = raw_input("Enter amount of utxo being spent, in satoshis: ") valid_coinjoins = scan_for_coinjoins(privkey, int(amount), filename) if not valid_coinjoins: print("Found no valid coinjoins") return for vc in valid_coinjoins: addr, priv, tx = vc print("Found signable coinjoin with destination address: ", addr) #TODO find a more sensible file naming fn = btc.txhash(tx) + ".txt" with open(fn, "wb") as f: f.write("SNICKER output file for receiver\n" "================================\n") f.write("The serialized transaction in hex:\n") f.write(tx + "\n") f.write("YOUR DESTINATION: " + addr + "\n") f.write("PRIVATE KEY FOR THIS DESTINATION ADDRESS:\n") f.write( btc.wif_compressed_privkey(priv, vbyte=get_p2pk_vbyte()) + "\n") f.write("The decoded transaction:\n") f.write(pformat(btc.deserialize(tx)) + "\n") print("The partially signed transaction and the private key for your " "output are stored in the file: " + fn) print( "Pass the transaction hex to `signrawtransaction` in Bitcoin Core " "or similar if you wish to broadcast the transaction.")
def watch_for_tx3_spends(self, redeeming_txid): """Function used to check whether our, or a competing tx, successfully spends out of TX3. Meant to be polled. """ assert self.sm.state in [6, 7, 8] if self.tx3redeem.is_confirmed: self.carol_watcher_loop.stop() cslog.info( "Redeemed funds via TX3 OK, txid of redeeming transaction " "is: " + self.tx3redeem.txid) self.quit(complete=False, failed=False) return if self.tx3.is_spent: if btc.txhash(self.tx3.spending_tx) != redeeming_txid: cslog.info( "Detected TX3 spent by other party; backing out to TX2") retval = self.find_secret_from_tx3_redeem() if not retval: cslog.info( "CRITICAL ERROR: Failed to find secret from TX3 redeem." ) self.quit(False, True) return rt2s_success = self.redeem_tx2_with_secret() self.quit(False, not rt2s_success) return
def make_sign_and_push(ins_full, wallet, amount, output_addr=None, change_addr=None, hashcode=btc.SIGHASH_ALL, estimate_fee = False): """Utility function for easily building transactions from wallets """ total = sum(x['value'] for x in ins_full.values()) ins = list(ins_full.keys()) #random output address and change addr output_addr = wallet.get_new_addr(1, 1) if not output_addr else output_addr change_addr = wallet.get_new_addr(1, 0) if not change_addr else change_addr fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 outs = [{'value': amount, 'address': output_addr}, {'value': total - amount - fee_est, 'address': change_addr}] de_tx = btc.deserialize(btc.mktx(ins, outs)) scripts = {} for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) script = wallet.addr_to_script(ins_full[utxo]['address']) scripts[index] = (script, ins_full[utxo]['value']) binarize_tx(de_tx) de_tx = wallet.sign_tx(de_tx, scripts, hashcode=hashcode) #pushtx returns False on any error tx = binascii.hexlify(btc.serialize(de_tx)).decode('ascii') push_succeed = jm_single().bc_interface.pushtx(tx) if push_succeed: return btc.txhash(tx) else: return False
def set_txid(self): """Creates the txid of the transaction, if it is fully signed, else an Exception is raised. This is used for passing to counterparty for in-advance signing (e.g. backouts, or here a whole chain). """ assert self.fully_signed_tx self.txid = btc.txhash(self.fully_signed_tx)
def outputs_watcher(self, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun): """Given a key for the watcher loop (notifyaddr), a wallet name (label), a set of outputs, and unconfirm, confirm and timeout callbacks, check to see if a transaction matching that output set has appeared in the wallet. Call the callbacks and update the watcher loop state. End the loop when the confirmation has been seen (no spent monitoring here). """ wl = self.tx_watcher_loops[notifyaddr] txlist = self.rpc("listtransactions", ["*", 100, 0, True]) for tx in txlist[::-1]: #changed syntax in 0.14.0; allow both syntaxes try: res = self.rpc("gettransaction", [tx["txid"], True]) except: try: res = self.rpc("gettransaction", [tx["txid"], 1]) except JsonRpcError as e: #This should never happen (gettransaction is a wallet rpc). log.warn("Failed gettransaction call; JsonRpcError") res = None except Exception as e: log.warn("Failed gettransaction call; unexpected error:") log.warn(str(e)) res = None if not res: continue if "confirmations" not in res: log.debug("Malformed gettx result: " + str(res)) return txd = self.get_deser_from_gettransaction(res) if txd is None: continue txos = set([(sv['script'], sv['value']) for sv in txd['outs']]) if not txos == tx_output_set: continue #Here we have found a matching transaction in the wallet. real_txid = btc.txhash(btc.serialize(txd)) if not wl[1] and res["confirmations"] == 0: log.debug("Tx: " + str(real_txid) + " seen on network.") unconfirmfun(txd, real_txid) wl[1] = True return if not wl[2] and res["confirmations"] > 0: log.debug("Tx: " + str(real_txid) + " has " + str(res["confirmations"]) + " confirmations.") confirmfun(txd, real_txid, res["confirmations"]) wl[2] = True wl[0].stop() return if res["confirmations"] < 0: log.debug("Tx: " + str(real_txid) + " has a conflict. Abandoning.") wl[0].stop() return
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 cli_broadcast(wallet_name, partial_tx_hex): """Given a partially signed transaction retrieved by running this script with the -r flag, and assuming that the utxo with which the transaction was made is in a Joinmarket wallet, this function will complete the signing and then broadcast the transaction. This function is useful if the *receiver*'s wallet is Joinmarket; if it is Core then the workflow is just `signrawtransaction` then `sendrawtransaction`; should be similar for Electrum although haven't tried. """ wallet = cli_get_wallet(wallet_name) tx = btc.deserialize(partial_tx_hex) num_sigs = 0 for index, ins in enumerate(tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) #is the utxo in our utxos? in_wallet_utxos = wallet.get_utxos_by_mixdepth(False) for m, um in in_wallet_utxos.iteritems(): for k, v in um.iteritems(): if k == utxo: print("Found utxo in mixdepth: ", m) if isinstance(wallet, SegwitWallet): amount = v['value'] else: amount = None signed_tx = btc.sign(partial_tx_hex, index, wallet.get_key_from_addr( v['address']), amount=amount) num_sigs += 1 if num_sigs != 1: print("Something wrong, expected to get 1 sig, got: ", num_sigs) return #should be fully signed; broadcast? print("Signed tx in hex:") print(signed_tx) print("In decoded form:") print(pformat(btc.deserialize(signed_tx))) if not raw_input("Broadcast to network? (y/n): ") == "y": print("You chose not to broadcast, quitting.") return txid = btc.txhash(signed_tx) print('txid = ' + txid) pushed = jm_single().bc_interface.pushtx(signed_tx) if not pushed: print("Broadcast failed.") else: print("Broadcast was successful.")
def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): """Called at a polling interval, checks if the given deserialized transaction (which must be fully signed) is (a) broadcast, (b) confirmed and (c) spent from. (c, n ignored in electrum version, just supports registering first confirmation). TODO: There is no handling of conflicts here. """ txid = btc.txhash(btc.serialize(txd)) wl = self.tx_watcher_loops[txid] #first check if in mempool (unconfirmed) #choose an output address for the query. Filter out #p2pkh addresses, assume p2sh (thus would fail to find tx on #some nonstandard script type) addr = None for i in range(len(txd['outs'])): if not btc.is_p2pkh_script(txd['outs'][i]['script']): addr = btc.script_to_address(txd['outs'][i]['script'], get_p2sh_vbyte()) break if not addr: log.error("Failed to find any p2sh output, cannot be a standard " "joinmarket transaction, fatal error!") reactor.stop() return unconftxs_res = self.get_from_electrum( 'blockchain.address.get_mempool', addr, blocking=True).get('result') unconftxs = [str(t['tx_hash']) for t in unconftxs_res] if not wl[1] and txid in unconftxs: jmprint("Tx: " + str(txid) + " seen on network.", "info") unconfirmfun(txd, txid) wl[1] = True return conftx = self.get_from_electrum('blockchain.address.listunspent', addr, blocking=True).get('result') conftxs = [str(t['tx_hash']) for t in conftx] if not wl[2] and len(conftxs) and txid in conftxs: jmprint("Tx: " + str(txid) + " is confirmed.", "info") confirmfun(txd, txid, 1) wl[2] = True #Note we do not stop the monitoring loop when #confirmations occur, since we are also monitoring for spending. return if not spentfun or wl[3]: return
def make_sign_and_push(ins_full, wallet_service, amount, output_addr=None, change_addr=None, hashcode=btc.SIGHASH_ALL, estimate_fee=False): """Utility function for easily building transactions from wallets. """ assert isinstance(wallet_service, WalletService) total = sum(x['value'] for x in ins_full.values()) ins = ins_full.keys() #random output address and change addr output_addr = wallet_service.get_new_addr( 1, BaseWallet.ADDRESS_TYPE_INTERNAL) if not output_addr else output_addr change_addr = wallet_service.get_new_addr( 0, BaseWallet.ADDRESS_TYPE_INTERNAL) if not change_addr else change_addr fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 outs = [{ 'value': amount, 'address': output_addr }, { 'value': total - amount - fee_est, 'address': change_addr }] tx = btc.mktx(ins, outs) de_tx = btc.deserialize(tx) for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) addr = ins_full[utxo]['address'] priv = wallet_service.get_key_from_addr(addr) if index % 2: priv = binascii.unhexlify(priv) tx = btc.sign(tx, index, priv, hashcode=hashcode) #pushtx returns False on any error print(btc.deserialize(tx)) push_succeed = jm_single().bc_interface.pushtx(tx) if push_succeed: return btc.txhash(tx) else: return False
def make_sign_and_push(ins_full, wallet_service, amount, output_addr=None, change_addr=None, hashcode=btc.SIGHASH_ALL, estimate_fee=False): """Utility function for easily building transactions from wallets """ assert isinstance(wallet_service, WalletService) total = sum(x['value'] for x in ins_full.values()) ins = list(ins_full.keys()) #random output address and change addr output_addr = wallet_service.get_new_addr( 1, 1) if not output_addr else output_addr change_addr = wallet_service.get_new_addr( 0, 1) if not change_addr else change_addr fee_est = estimate_tx_fee(len(ins), 2) if estimate_fee else 10000 outs = [{ 'value': amount, 'address': output_addr }, { 'value': total - amount - fee_est, 'address': change_addr }] de_tx = btc.deserialize(btc.mktx(ins, outs)) scripts = {} for index, ins in enumerate(de_tx['ins']): utxo = ins['outpoint']['hash'] + ':' + str(ins['outpoint']['index']) script = wallet_service.addr_to_script(ins_full[utxo]['address']) scripts[index] = (script, ins_full[utxo]['value']) binarize_tx(de_tx) de_tx = wallet_service.sign_tx(de_tx, scripts, hashcode=hashcode) #pushtx returns False on any error push_succeed = jm_single().bc_interface.pushtx(btc.serialize(de_tx)) if push_succeed: txid = btc.txhash(btc.serialize(de_tx)) # in normal operation this happens automatically # but in some tests there is no monitoring loop: wallet_service.process_new_tx(de_tx, txid) return txid else: return False
def on_JM_TX_RECEIVED(self, nick, txhex, offer): # "none" flags p2ep protocol; pass through to the generic # on_tx handler for that: if offer == "none": return self.on_p2ep_tx_received(nick, txhex) offer = json.loads(offer) retval = self.client.on_tx_received(nick, txhex, offer) if not retval[0]: jlog.info("Maker refuses to continue on receipt of tx") else: sigs = retval[1] self.finalized_offers[nick] = offer tx = btc.deserialize(txhex) self.finalized_offers[nick]["txd"] = tx txid = btc.txhash(btc.serialize(tx)) # we index the callback by the out-set of the transaction, # because the txid is not known until all scriptSigs collected # (hence this is required for Makers, but not Takers). # For more info see WalletService.transaction_monitor(): txinfo = tuple((x["script"], x["value"]) for x in tx["outs"]) self.client.wallet_service.register_callbacks( [self.unconfirm_callback], txinfo, "unconfirmed") self.client.wallet_service.register_callbacks( [self.confirm_callback], txinfo, "confirmed") task.deferLater( reactor, float(jm_single().config.getint("TIMEOUT", "unconfirm_timeout_sec")), self.client.wallet_service.check_callback_called, txinfo, self.unconfirm_callback, "unconfirmed", "transaction with outputs: " + str(txinfo) + " not broadcast.") d = self.callRemote(commands.JMTXSigs, nick=nick, sigs=json.dumps(sigs)) self.defaultCallbacks(d) return {"accepted": True}
def mktx(self): """First, construct input and output lists as for a normal transaction construction, using the OCCTemplateTx corresponding inputs and outputs as information. To do this completely requires txids for all inputs. Thus, this must be called for this OCCTx *after* it has been called for all parent txs. We ensure that the txid for this Tx is set here, and is attached to all the Outpoint objects for its outputs. """ self.build_ins_from_template() self.build_outs_from_template() assert all([self.ins, self.outs]) self.base_form = btc.mktx([x[0] for x in self.ins], self.outs) dtx = btc.deserialize(self.base_form) if self.locktime: dtx["ins"][0]["sequence"] = 0 dtx["locktime"] = self.locktime #To set the txid, it's required that we set the #scriptSig and scriptPubkey objects. We don't yet #need to flag it segwit (we're not yet attaching #signatures) since we want txid not wtxid and the #former doesn't use segwit formatting anyway. for i, inp in enumerate(dtx["ins"]): sti = self.template.ins[i] if sti.spk_type == "p2sh-p2wpkh": inp["script"] = "16" + btc.pubkey_to_p2sh_p2wpkh_script( self.keys["ins"][i][sti.counterparty]) elif sti.spk_type == "NN": inp["script"] = "" self.txid = btc.txhash(btc.serialize(dtx)) #by setting the txid of the outpoints, we allow child #transactions to know the outpoint references for their inputs. for to in self.template.outs: to.txid = self.txid
def direct_send(wallet, amount, mixdepth, destaddr, answeryes=False, accept_callback=None, info_callback=None): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. If accept_callback is None, command line input for acceptance is assumed, else this callback is called: accept_callback: ==== args: deserialized tx, destination address, amount in satoshis, fee in satoshis returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destaddr)[0] assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >= 0 assert isinstance(wallet, BaseWallet) from pprint import pformat txtype = wallet.get_txtype() if amount == 0: utxos = wallet.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error("There are no utxos in mixdepth: " + str(mixdepth) + ", quitting.") return total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destaddr, "value": total_inputs_val - fee_est}] else: #8 inputs to be conservative initial_fee_est = estimate_tx_fee(8, 2, txtype=txtype) utxos = wallet.select_utxos(mixdepth, amount + initial_fee_est) if len(utxos) < 8: fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype) else: fee_est = initial_fee_est total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destaddr}] change_addr = wallet.get_internal_addr(mixdepth) import_new_addresses(wallet, [change_addr]) outs.append({"value": changeval, "address": change_addr}) #Now ready to construct transaction log.info("Using a fee of : " + str(fee_est) + " satoshis.") if amount != 0: log.info("Using a change value of: " + str(changeval) + " satoshis.") txsigned = sign_tx(wallet, mktx(list(utxos.keys()), outs), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + str(actual_amount) + " satoshis to address: " + destaddr) if not answeryes: if not accept_callback: if input( 'Would you like to push to the network? (y/n):')[0] != 'y': log.info( "You chose not to broadcast the transaction, quitting.") return False else: accepted = accept_callback(pformat(txsigned), destaddr, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid
def direct_send(wallet_service, amount, mixdepth, destination, answeryes=False, accept_callback=None, info_callback=None): """Send coins directly from one mixdepth to one destination address; does not need IRC. Sweep as for normal sendpayment (set amount=0). If answeryes is True, callback/command line query is not performed. If accept_callback is None, command line input for acceptance is assumed, else this callback is called: accept_callback: ==== args: deserialized tx, destination address, amount in satoshis, fee in satoshis returns: True if accepted, False if not ==== The info_callback takes one parameter, the information message (when tx is pushed), and returns nothing. This function returns: The txid if transaction is pushed, False otherwise """ #Sanity checks assert validate_address(destination)[0] or is_burn_destination(destination) assert isinstance(mixdepth, numbers.Integral) assert mixdepth >= 0 assert isinstance(amount, numbers.Integral) assert amount >=0 assert isinstance(wallet_service.wallet, BaseWallet) if is_burn_destination(destination): #Additional checks if not isinstance(wallet_service.wallet, FidelityBondMixin): log.error("Only fidelity bond wallets can burn coins") return if answeryes: log.error("Burning coins not allowed without asking for confirmation") return if mixdepth != FidelityBondMixin.FIDELITY_BOND_MIXDEPTH: log.error("Burning coins only allowed from mixdepth " + str( FidelityBondMixin.FIDELITY_BOND_MIXDEPTH)) return if amount != 0: log.error("Only sweeping allowed when burning coins, to keep the tx " + "small. Tip: use the coin control feature to freeze utxos") return from pprint import pformat txtype = wallet_service.get_txtype() if amount == 0: utxos = wallet_service.get_utxos_by_mixdepth()[mixdepth] if utxos == {}: log.error( "There are no available utxos in mixdepth: " + str(mixdepth) + ", quitting.") return total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) if is_burn_destination(destination): if len(utxos) > 1: log.error("Only one input allowed when burning coins, to keep " + "the tx small. Tip: use the coin control feature to freeze utxos") return address_type = FidelityBondMixin.BIP32_BURN_ID index = wallet_service.wallet.get_next_unused_index(mixdepth, address_type) path = wallet_service.wallet.get_path(mixdepth, address_type, index) privkey, engine = wallet_service.wallet._get_key_from_path(path) pubkey = engine.privkey_to_pubkey(privkey) pubkeyhash = bin_hash160(pubkey) #size of burn output is slightly different from regular outputs burn_script = mk_burn_script(pubkeyhash) #in hex fee_est = estimate_tx_fee(len(utxos), 0, txtype=txtype, extra_bytes=len(burn_script)/2) outs = [{"script": burn_script, "value": total_inputs_val - fee_est}] destination = "BURNER OUTPUT embedding pubkey at " \ + wallet_service.wallet.get_path_repr(path) \ + "\n\nWARNING: This transaction if broadcasted will PERMANENTLY DESTROY your bitcoins\n" else: #regular send (non-burn) fee_est = estimate_tx_fee(len(utxos), 1, txtype=txtype) outs = [{"address": destination, "value": total_inputs_val - fee_est}] else: #8 inputs to be conservative initial_fee_est = estimate_tx_fee(8,2, txtype=txtype) utxos = wallet_service.select_utxos(mixdepth, amount + initial_fee_est) if len(utxos) < 8: fee_est = estimate_tx_fee(len(utxos), 2, txtype=txtype) else: fee_est = initial_fee_est total_inputs_val = sum([va['value'] for u, va in iteritems(utxos)]) changeval = total_inputs_val - fee_est - amount outs = [{"value": amount, "address": destination}] change_addr = wallet_service.get_internal_addr(mixdepth) outs.append({"value": changeval, "address": change_addr}) #compute transaction locktime, has special case for spending timelocked coins tx_locktime = compute_tx_locktime() if mixdepth == FidelityBondMixin.FIDELITY_BOND_MIXDEPTH and \ isinstance(wallet_service.wallet, FidelityBondMixin): for outpoint, utxo in utxos.items(): path = wallet_service.script_to_path( wallet_service.addr_to_script(utxo["address"])) if not FidelityBondMixin.is_timelocked_path(path): continue path_locktime = path[-1] tx_locktime = max(tx_locktime, path_locktime+1) #compute_tx_locktime() gives a locktime in terms of block height #timelocked addresses use unix time instead #OP_CHECKLOCKTIMEVERIFY can only compare like with like, so we #must use unix time as the transaction locktime #Now ready to construct transaction log.info("Using a fee of : " + amount_to_str(fee_est) + ".") if amount != 0: log.info("Using a change value of: " + amount_to_str(changeval) + ".") txsigned = sign_tx(wallet_service, make_shuffled_tx( list(utxos.keys()), outs, False, 2, tx_locktime), utxos) log.info("Got signed transaction:\n") log.info(pformat(txsigned)) tx = serialize(txsigned) log.info("In serialized form (for copy-paste):") log.info(tx) actual_amount = amount if amount != 0 else total_inputs_val - fee_est log.info("Sends: " + amount_to_str(actual_amount) + " to destination: " + destination) if not answeryes: if not accept_callback: if input('Would you like to push to the network? (y/n):')[0] != 'y': log.info("You chose not to broadcast the transaction, quitting.") return False else: accepted = accept_callback(pformat(txsigned), destination, actual_amount, fee_est) if not accepted: return False jm_single().bc_interface.pushtx(tx) txid = txhash(tx) successmsg = "Transaction sent: " + txid cb = log.info if not info_callback else info_callback cb(successmsg) return txid
def tx_watcher(self, txd, unconfirmfun, confirmfun, spentfun, c, n): """Called at a polling interval, checks if the given deserialized transaction (which must be fully signed) is (a) broadcast, (b) confirmed and (c) spent from at index n, and notifies confirmation if number of confs = c. TODO: Deal with conflicts correctly. Here just abandons monitoring. """ txid = btc.txhash(btc.serialize(txd)) wl = self.tx_watcher_loops[txid] try: res = self.rpc('gettransaction', [txid, True]) except JsonRpcError as e: return if not res: return if "confirmations" not in res: log.debug("Malformed gettx result: " + str(res)) return if not wl[1] and res["confirmations"] == 0: log.debug("Tx: " + str(txid) + " seen on network.") unconfirmfun(txd, txid) wl[1] = True return if not wl[2] and res["confirmations"] > 0: log.debug("Tx: " + str(txid) + " has " + str(res["confirmations"]) + " confirmations.") confirmfun(txd, txid, res["confirmations"]) if c <= res["confirmations"]: wl[2] = True #Note we do not stop the monitoring loop when #confirmations occur, since we are also monitoring for spending. return if res["confirmations"] < 0: log.debug("Tx: " + str(txid) + " has a conflict. Abandoning.") wl[0].stop() return if not spentfun or wl[3]: return #To trigger the spent callback, we check if this utxo outpoint appears in #listunspent output with 0 or more confirmations. Note that this requires #we have added the destination address to the watch-only wallet, otherwise #that outpoint will not be returned by listunspent. res2 = self.rpc('listunspent', [0, 999999]) if not res2: return txunspent = False for r in res2: if "txid" not in r: continue if txid == r["txid"] and n == r["vout"]: txunspent = True break if not txunspent: #We need to find the transaction which spent this one; #assuming the address was added to the wallet, then this #transaction must be in the recent list retrieved via listunspent. #For each one, use gettransaction to check its inputs. #This is a bit expensive, but should only occur once. txlist = self.rpc("listtransactions", ["*", 1000, 0, True]) for tx in txlist[::-1]: #changed syntax in 0.14.0; allow both syntaxes try: res = self.rpc("gettransaction", [tx["txid"], True]) except: try: res = self.rpc("gettransaction", [tx["txid"], 1]) except: #This should never happen (gettransaction is a wallet rpc). log.info("Failed any gettransaction call") res = None if not res: continue deser = self.get_deser_from_gettransaction(res) if deser is None: continue for vin in deser["ins"]: if not "outpoint" in vin: #coinbases continue if vin["outpoint"]["hash"] == txid and vin["outpoint"][ "index"] == n: #recover the deserialized form of the spending transaction. log.info("We found a spending transaction: " + \ btc.txhash(binascii.unhexlify(res["hex"]))) res2 = self.rpc("gettransaction", [tx["txid"], True]) spending_deser = self.get_deser_from_gettransaction( res2) if not spending_deser: log.info( "ERROR: could not deserialize spending tx.") #Should never happen, it's a parsing bug. #No point continuing to monitor, we just hope we #can extract the secret by scanning blocks. wl[3] = True return spentfun(spending_deser, vin["outpoint"]["hash"]) wl[3] = True return
def add_tx_notify(self, txd, unconfirmfun, confirmfun, notifyaddr, wallet_name=None, timeoutfun=None, spentfun=None, txid_flag=True, n=0, c=1, vb=None): """Given a deserialized transaction txd, callback functions for broadcast and confirmation of the transaction, an address to import, and a callback function for timeout, set up a polling loop to check for events on the transaction. Also optionally set to trigger "confirmed" callback on number of confirmations c. Also checks for spending (if spentfun is not None) of the outpoint n. If txid_flag is True, we create a watcher loop on the txid (hence only really usable in a segwit context, and only on fully formed transactions), else we create a watcher loop on the output set of the transaction (taken from the outs field of the txd). """ if not vb: vb = get_p2pk_vbyte() if isinstance(self, BitcoinCoreInterface) or isinstance( self, RegtestBitcoinCoreInterface): #This code ensures that a walletnotify is triggered, by #ensuring that at least one of the output addresses is #imported into the wallet (note the sweep special case, where #none of the output addresses belong to me). one_addr_imported = False for outs in txd['outs']: addr = btc.script_to_address(outs['script'], vb) try: if self.is_address_imported(addr): one_addr_imported = True break except JsonRpcError as e: log.debug("Failed to getaccount for address: " + addr) log.debug("This is normal for bech32 addresses.") continue if not one_addr_imported: try: self.rpc('importaddress', [notifyaddr, 'joinmarket-notify', False]) except JsonRpcError as e: #In edge case of address already controlled #by another account, warn but do not quit in middle of tx. #Can occur if destination is owned in Core wallet. if e.code == -4 and e.message == "The wallet already " + \ "contains the private key for this address or script": log.warn("WARNING: Failed to import address: " + notifyaddr) #No other error should be possible else: raise #Warning! In case of txid_flag false, this is *not* a valid txid, #but only a hash of an incomplete transaction serialization. txid = btc.txhash(btc.serialize(txd)) if not txid_flag: tx_output_set = set([(sv['script'], sv['value']) for sv in txd['outs']]) loop = task.LoopingCall(self.outputs_watcher, wallet_name, notifyaddr, tx_output_set, unconfirmfun, confirmfun, timeoutfun) log.debug("Created watcher loop for address: " + notifyaddr) loopkey = notifyaddr else: loop = task.LoopingCall(self.tx_watcher, txd, unconfirmfun, confirmfun, spentfun, c, n) log.debug("Created watcher loop for txid: " + txid) loopkey = txid self.tx_watcher_loops[loopkey] = [loop, False, False, False] #Hardcoded polling interval, but in any case it can be very short. loop.start(5.0) #Give up on un-broadcast transactions and broadcast but not confirmed #transactions as per settings in the config. reactor.callLater( float(jm_single().config.get("TIMEOUT", "unconfirm_timeout_sec")), self.tx_network_timeout, loopkey) confirm_timeout_sec = int(jm_single().config.get( "TIMEOUT", "confirm_timeout_hours")) * 3600 reactor.callLater(confirm_timeout_sec, self.tx_timeout, txd, loopkey, timeoutfun)