def _add_unspent_txo(self, utxo, height): """ Add a UTXO as returned by rpc's listunspent call to the wallet. Note that these are returned as little endian outpoint txids, so are converted. params: utxo: single utxo dict as returned by listunspent current_blockheight: blockheight as integer, used to set the block in which a confirmed utxo is included. """ txid = hextobin(utxo['txid']) script = hextobin(utxo['scriptPubKey']) value = int(Decimal(str(utxo['amount'])) * Decimal('1e8')) self.add_utxo(txid, int(utxo['vout']), script, value, height)
def get_all_transactions(self): """ Get all transactions (spending or receiving) that are currently recorded by our blockchain interface as relating to this wallet, as a list. """ res = [] processed_txids = [] for r in self.bci._yield_transactions( self.get_wallet_name()): txid = r["txid"] if txid not in processed_txids: tx = self.bci.get_transaction(hextobin(txid)) res.append(self.bci.get_deser_from_gettransaction(tx)) processed_txids.append(txid) return res
def test_fidelity_bond_seen(valid, fidelity_bond_proof, maker_nick, taker_nick): proof = FidelityBondProof( maker_nick, taker_nick, hextobin(fidelity_bond_proof['certificate-pubkey']), fidelity_bond_proof['certificate-expiry'], hextobin(fidelity_bond_proof['certificate-signature']), (hextobin(fidelity_bond_proof['txid']), fidelity_bond_proof['vout']), hextobin(fidelity_bond_proof['utxo-pubkey']), fidelity_bond_proof['locktime']) serialized = proof._serialize_proof_msg( fidelity_bond_proof['nick-signature']) ob = get_ob() ob.msgchan.nick = taker_nick ob.on_fidelity_bond_seen(maker_nick, fidelity_bond_cmd_list[0], serialized) rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() assert len(rows) == 1 assert rows[0]["counterparty"] == maker_nick assert rows[0]["takernick"] == taker_nick try: parsed_proof = FidelityBondProof.parse_and_verify_proof_msg( rows[0]["counterparty"], rows[0]["takernick"], rows[0]["proof"]) except ValueError: parsed_proof = None if valid: assert parsed_proof is not None assert parsed_proof.utxo[0] == hextobin(fidelity_bond_proof["txid"]) assert parsed_proof.utxo[1] == fidelity_bond_proof["vout"] assert parsed_proof.locktime == fidelity_bond_proof["locktime"] assert parsed_proof.cert_expiry == fidelity_bond_proof[ "certificate-expiry"] assert parsed_proof.utxo_pub == hextobin( fidelity_bond_proof["utxo-pubkey"]) else: assert parsed_proof is None
def test_duplicate_fidelity_bond_rejected(): fidelity_bond_info = (({ "nick-signature": (b'0E\x02!\x00\xdbb\x15\x96\xa0\x87\xb8\x1d\xe05\xddV\xa1\x1bn\x8f' + b'q\x90&\x8cG@\x89"2\xb2\x81\x9b\xc00\xa5\xb6\x02 \x03\x14l\xd7BR\xba\x8c:\x88(' + b'\x8e3l\xac\xf5`T\x87\xfa\xf5\xa9\x1f\x19\xc0\xb6\xe9\xbb\xdc\xc7y\x99' ), "certificate-signature": ("3045022100eb512af938113badb4d7b29e0c22061c51dadb113a9395e" + "9ed81a46103391213022029170de414964f07228c4f0d404b1386272bae337f0133f1329d948a" + "252fa2a0"), "certificate-pubkey": "0258efb077960d6848f001904857f062fa453de26c1ad8736f55497254f56e8a74", "certificate-expiry": 1, "utxo-pubkey": "02f54f027377e84171296453828aa863c23fc4489453025f49bd3addfb3a359b3d", "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", "vout": 0, "locktime": 1640995200 }, "J5A4k9ecQzRRDfBx", "J55VZ6U6ZyFDNeuv"), ({ "nick-signature": (b'0E\x02!\x00\x80\xc6$\x0c\xa1\x15YS\xacHB\xb33\xfa~\x9f\xb9`\xb3' + b'\xfe\xed0\xadHq\xc1~\x03.B\xbb#\x02 #y~]\xd9\xbbX2\xc0\x1b\xe57\xf4\x0f\x1f' + b'\xd6$\x01\xf9\x15Z\xc9X\xa5\x18\xbe\x83\x1a&4Y\xd4'), "certificate-signature": ("304402205669ea394f7381e9abf0b3c013fac2b79d24c02feb86ff153" + "cff83c658d7cf7402200b295ace655687f80738f3733c1dc5f1e2b8f351c017a05b8bd31983dd" + "4d723f"), "certificate-pubkey": "031d1c006a6310dbdf57341efc19c3a43c402379d7ccd2480416cadc7579f973f7", "certificate-expiry": 1, "utxo-pubkey": "02616c56412eb738a9eacfb0550b43a5a2e77e5d5205ea9e2ca8dfac34e50c9754", "txid": "84c88fafe0bb75f507fe3bfb29a93d10b2e80c15a63b2943c1a5fecb5a55cba2", "vout": 1, "locktime": 1893456000 }, "J54LS6YyJPoseqFS", "J55VZ6U6ZyFDNeuv")) ob = get_ob() fidelity_bond_proof1, maker_nick1, taker_nick1 = fidelity_bond_info[0] proof = FidelityBondProof( maker_nick1, taker_nick1, hextobin(fidelity_bond_proof1['certificate-pubkey']), fidelity_bond_proof1['certificate-expiry'], hextobin(fidelity_bond_proof1['certificate-signature']), (hextobin(fidelity_bond_proof1['txid']), fidelity_bond_proof1['vout']), hextobin(fidelity_bond_proof1['utxo-pubkey']), fidelity_bond_proof1['locktime']) serialized1 = proof._serialize_proof_msg( fidelity_bond_proof1['nick-signature']) ob.msgchan.nick = taker_nick1 ob.on_fidelity_bond_seen(maker_nick1, fidelity_bond_cmd_list[0], serialized1) rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() assert len(rows) == 1 #show the same fidelity bond message again, check it gets rejected as duplicate ob.on_fidelity_bond_seen(maker_nick1, fidelity_bond_cmd_list[0], serialized1) rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() assert len(rows) == 1 #show a different fidelity bond and check it does get accepted fidelity_bond_proof2, maker_nick2, taker_nick2 = fidelity_bond_info[1] proof2 = FidelityBondProof( maker_nick1, taker_nick1, hextobin(fidelity_bond_proof2['certificate-pubkey']), fidelity_bond_proof2['certificate-expiry'], hextobin(fidelity_bond_proof2['certificate-signature']), (hextobin(fidelity_bond_proof2['txid']), fidelity_bond_proof2['vout']), hextobin(fidelity_bond_proof2['utxo-pubkey']), fidelity_bond_proof2['locktime']) serialized2 = proof2._serialize_proof_msg( fidelity_bond_proof2['nick-signature']) ob.msgchan.nick = taker_nick2 ob.on_fidelity_bond_seen(maker_nick2, fidelity_bond_cmd_list[0], serialized2) rows = ob.db.execute("SELECT * FROM fidelitybonds;").fetchall() assert len(rows) == 2
def transaction_monitor(self): """Keeps track of any changes in the wallet (new transactions). Intended to be run as a twisted task.LoopingCall so that this Service is constantly in near-realtime sync with the blockchain. """ if not self.update_blockheight(): return txlist = self.bci.list_transactions(100) if not txlist: return new_txs = [] for x in txlist: # process either (a) a completely new tx or # (b) a tx that reached unconf status but we are still # waiting for conf (active_txids) if "txid" not in x: continue if x['txid'] in self.active_txids or x['txid'] not in self.old_txs: new_txs.append(x) # reset for next polling event: self.old_txs = [x['txid'] for x in txlist if "txid" in x] for tx in new_txs: txid = tx["txid"] res = self.bci.get_transaction(hextobin(txid)) if not res: continue confs = res["confirmations"] if not isinstance(confs, Integral): jlog.warning("Malformed gettx result: " + str(res)) continue if confs < 0: jlog.info("Transaction: " + txid + " has a conflict, abandoning.") continue if confs == 0: height = None else: height = self.current_blockheight - confs + 1 txd = self.bci.get_deser_from_gettransaction(res) if txd is None: continue removed_utxos, added_utxos = self.wallet.process_new_tx( txd, height) if txid not in self.processed_txids: # apply checks to disable/freeze utxos to reused addrs if needed: self.check_for_reuse(added_utxos) # TODO note that this log message will be missed if confirmation # is absurdly fast, this is considered acceptable compared with # additional complexity. self.log_new_tx(removed_utxos, added_utxos, txid) self.processed_txids.append(txid) # first fire 'all' type callbacks, irrespective of if the # transaction pertains to anything known (but must # have correct label per above); filter on this Joinmarket wallet label, # or the external monitoring label: if (self.bci.is_address_labeled(tx, self.get_wallet_name()) or self.bci.is_address_labeled( tx, self.EXTERNAL_WALLET_LABEL)): for f in self.callbacks["all"]: # note we need no return value as we will never # remove these from the list f(txd, txid) # The tuple given as the second possible key for the dict # is such because txid is not always available # at the time of callback registration). possible_keys = [ txid, tuple((x.scriptPubKey, x.nValue) for x in txd.vout) ] # note that len(added_utxos) > 0 is not a sufficient condition for # the tx being new, since wallet.add_new_utxos will happily re-add # a utxo that already exists; but this does not cause re-firing # of callbacks since we in these cases delete the callback after being # called once. # Note also that it's entirely possible that there are only removals, # not additions, to the utxo set, specifically in sweeps to external # addresses. In this case, since removal can by definition only # happen once, we must allow entries in self.active_txids through the # filter. if len(added_utxos) > 0 or len(removed_utxos) > 0 \ or txid in self.active_txids: if confs == 0: for k in possible_keys: if k in self.callbacks["unconfirmed"]: for f in self.callbacks["unconfirmed"][k]: # True implies success, implies removal: if f(txd, txid): self.callbacks["unconfirmed"][k].remove(f) # keep monitoring for conf > 0: self.active_txids.append(txid) elif confs > 0: for k in possible_keys: if k in self.callbacks["confirmed"]: for f in self.callbacks["confirmed"][k]: if f(txd, txid, confs): self.callbacks["confirmed"][k].remove(f) if txid in self.active_txids: self.active_txids.remove(txid)