electrum-personal-server

Maximally lightweight electrum server for a single user
git clone https://git.parazyd.org/electrum-personal-server
Log | Files | Refs | README

commit c18d6c9b136a10dc94869e744bb2544bdce5a72e
parent 2c8cd9aa12ae7d5344e9ecf2c518a642f7a0da6d
Author: chris-belcher <chris-belcher@users.noreply.github.com>
Date:   Tue, 27 Mar 2018 20:23:17 +0100

moved python modules and ssl certs to other directories to make root less cluttered

Diffstat:
Rcert.crt -> certs/cert.crt | 0
Rcert.key -> certs/cert.key | 0
Ddeterministicwallet.py | 429-------------------------------------------------------------------------------
Aelectrumpersonalserver/deterministicwallet.py | 429+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rhashes.py -> electrumpersonalserver/hashes.py | 0
Rjsonrpc.py -> electrumpersonalserver/jsonrpc.py | 0
Aelectrumpersonalserver/merkleproof.py | 271+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aelectrumpersonalserver/transactionmonitor.py | 723+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Dmerkleproof.py | 270-------------------------------------------------------------------------------
Mrescan-script.py | 2+-
Mserver.py | 31++++++++-----------------------
Dtransactionmonitor.py | 705-------------------------------------------------------------------------------
12 files changed, 1432 insertions(+), 1428 deletions(-)

diff --git a/cert.crt b/certs/cert.crt diff --git a/cert.key b/certs/cert.key diff --git a/deterministicwallet.py b/deterministicwallet.py @@ -1,429 +0,0 @@ - -import bitcoin as btc -from hashes import bh2u, hash_160, bfh, sha256 - -# the class hierarchy for deterministic wallets in this file: -# subclasses are written towards the right -# each class knows how to create the scriptPubKeys of that wallet -# -# |-- SingleSigOldMnemonicWallet -# |-- SingleSigP2PKHWallet -# |-- SingleSigP2WPKHWallet -# SingleSigWallet --| -# / |-- SingleSigP2WPKH_P2SHWallet -# DeterministicWallet -# \ |-- MultisigP2SHWallet -# MultisigWallet --| -# |-- MultisigP2WSHWallet -# |-- MultisigP2WSH_P2SHWallet - -#the wallet types are here -#https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES -#and -#https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst - -def is_string_parsable_as_hex_int(s): - try: - int(s, 16) - return True - except: - return False - -def parse_electrum_master_public_key(keydata, gaplimit): - if keydata[:4] in ("xpub", "tpub"): - wallet = SingleSigP2PKHWallet(keydata) - elif keydata[:4] in ("zpub", "vpub"): - wallet = SingleSigP2WPKHWallet(keydata) - elif keydata[:4] in ("ypub", "upub"): - wallet = SingleSigP2WPKH_P2SHWallet(keydata) - elif keydata.find(" ") != -1: #multiple keys = multisig - chunks = keydata.split(" ") - try: - m = int(chunks[0]) - except ValueError: - raise ValueError("Unable to parse m in multisig key data: " - + chunks[0]) - pubkeys = chunks[1:] - if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]): - raise ValueError("inconsistent bip32 pubkey types") - if pubkeys[0][:4] in ("xpub", "tpub"): - wallet = MultisigP2SHWallet(m, pubkeys) - elif pubkeys[0][:4] in ("Zpub", "Vpub"): - wallet = MultisigP2WSHWallet(m, pubkeys) - elif pubkeys[0][:4] in ("Ypub", "Upub"): - wallet = MultisigP2WSH_P2SHWallet(m, pubkeys) - elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128: - wallet = SingleSigOldMnemonicWallet(keydata) - else: - raise ValueError("Unrecognized electrum mpk format: " + keydata[:4]) - wallet.gaplimit = gaplimit - return wallet - -class DeterministicWallet(object): - def __init__(self): - self.gaplimit = 0 - self.next_index = [0, 0] - self.scriptpubkey_index = {} - - def get_new_scriptpubkeys(self, change, count): - """Returns newly-generated addresses from this deterministic wallet""" - return self.get_scriptpubkeys(change, self.next_index[change], - count) - - def get_scriptpubkeys(self, change, from_index, count): - """Returns addresses from this deterministic wallet""" - pass - - #called in check_for_new_txes() when a new tx of ours arrives - #to see if we need to import more addresses - def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): - """Return None if they havent, or how many addresses to - import if they have""" - result = {} - for spk in scriptpubkeys: - if spk not in self.scriptpubkey_index: - continue - change, index = self.scriptpubkey_index[spk] - distance_from_next = self.next_index[change] - index - if distance_from_next > self.gaplimit: - continue - #need to import more - if change in result: - result[change] = max(result[change], self.gaplimit - - distance_from_next + 1) - else: - result[change] = self.gaplimit - distance_from_next + 1 - if len(result) > 0: - return result - else: - return None - - def rewind_one(self, change): - """Go back one pubkey in a branch""" - self.next_index[change] -= 1 - -class SingleSigWallet(DeterministicWallet): - def __init__(self, mpk): - super(SingleSigWallet, self).__init__() - self.branches = (btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1)) - #m/change/i - - def pubkey_to_scriptpubkey(self, pubkey): - raise RuntimeError() - - def get_pubkey(self, change, index): - return btc.bip32_extract_key(btc.bip32_ckd(self.branches[change], - index)) - - def get_scriptpubkeys(self, change, from_index, count): - result = [] - for index in range(from_index, from_index + count): - pubkey = self.get_pubkey(change, index) - scriptpubkey = self.pubkey_to_scriptpubkey(pubkey) - self.scriptpubkey_index[scriptpubkey] = (change, index) - result.append(scriptpubkey) - self.next_index[change] = max(self.next_index[change], from_index+count) - return result - -class SingleSigP2PKHWallet(SingleSigWallet): - def pubkey_to_scriptpubkey(self, pubkey): - pkh = bh2u(hash_160(bfh(pubkey))) - #op_dup op_hash_160 length hash160 op_equalverify op_checksig - return "76a914" + pkh + "88ac" - -class SingleSigP2WPKHWallet(SingleSigWallet): - def pubkey_to_scriptpubkey(self, pubkey): - pkh = bh2u(hash_160(bfh(pubkey))) - #witness-version length hash160 - #witness version is always 0, length is always 0x14 - return "0014" + pkh - -class SingleSigP2WPKH_P2SHWallet(SingleSigWallet): - def pubkey_to_scriptpubkey(self, pubkey): - #witness-version length pubkeyhash - #witness version is always 0, length is always 0x14 - redeem_script = '0014' + bh2u(hash_160(bfh(pubkey))) - sh = bh2u(hash_160(bfh(redeem_script))) - return "a914" + sh + "87" - -class SingleSigOldMnemonicWallet(SingleSigWallet): - def __init__(self, mpk): - super(SingleSigWallet, self).__init__() - self.mpk = mpk - - def get_pubkey(self, change, index): - return btc.electrum_pubkey(self.mpk, index, change) - - def pubkey_to_scriptpubkey(self, pubkey): - pkh = bh2u(hash_160(bfh(pubkey))) - #op_dup op_hash_160 length hash160 op_equalverify op_checksig - return "76a914" + pkh + "88ac" - -class MultisigWallet(DeterministicWallet): - def __init__(self, m, mpk_list): - super(MultisigWallet, self).__init__() - self.m = m - self.pubkey_branches = [(btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1)) - for mpk in mpk_list] - #derivation path for pubkeys is m/change/index - - def redeem_script_to_scriptpubkey(self, redeem_script): - raise RuntimeError() - - def get_scriptpubkeys(self, change, from_index, count): - result = [] - for index in range(from_index, from_index + count): - pubkeys = [btc.bip32_extract_key(btc.bip32_ckd(branch[change], - index)) for branch in self.pubkey_branches] - pubkeys = sorted(pubkeys) - redeemScript = "" - redeemScript += "%x"%(0x50 + self.m) #op_m - for p in pubkeys: - redeemScript += "21" #length - redeemScript += p - redeemScript += "%x"%(0x50 + len(pubkeys)) #op_n - redeemScript += "ae" # op_checkmultisig - scriptpubkey = self.redeem_script_to_scriptpubkey(redeemScript) - self.scriptpubkey_index[scriptpubkey] = (change, index) - result.append(scriptpubkey) - self.next_index[change] = max(self.next_index[change], from_index+count) - return result - -class MultisigP2SHWallet(MultisigWallet): - def redeem_script_to_scriptpubkey(self, redeem_script): - sh = bh2u(hash_160(bfh(redeem_script))) - #op_hash160 length hash160 op_equal - return "a914" + sh + "87" - -class MultisigP2WSHWallet(MultisigWallet): - def redeem_script_to_scriptpubkey(self, redeem_script): - sh = bh2u(sha256(bfh(redeem_script))) - #witness-version length sha256 - #witness version is always 0, length is always 0x20 - return "0020" + sh - -class MultisigP2WSH_P2SHWallet(MultisigWallet): - def redeem_script_to_scriptpubkey(self, redeem_script): - #witness-version length sha256 - #witness version is always 0, length is always 0x20 - nested_redeemScript = "0020" + bh2u(sha256(bfh(redeem_script))) - sh = bh2u(hash_160(bfh(nested_redeemScript))) - #op_hash160 length hash160 op_equal - return "a914" + sh + "87" - -# electrum has its own tests here -#https://github.com/spesmilo/electrum/blob/03b40a3c0a7dd84e76bc0d0ea2ad390dafc92250/lib/tests/test_wallet_vertical.py - -electrum_keydata_test_vectors = [ - #p2pkh wallet - ("xpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" + - "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #pubkey - ["76a914b1847c763c9a9b12631ab42335751c1bf843880c88ac" #recv scriptpubkeys - ,"76a914d8b6b932e892fad5132ea888111adac2171c5af588ac" - ,"76a914e44b19ef74814f977ae4e2823dd0a0b33480472a88ac"], - ["76a914d2c2905ca383a5b8f94818cb7903498061a6286688ac" #change scriptpubkeys - ,"76a914e7b4ddb7cede132e84ba807defc092cf52e005b888ac" - ,"76a91433bdb046a1d373728d7844df89aa24f788443a4588ac"]) - , #p2wpkh wallet - ("zpub6mr7wBKy3oJn89TCiXUAPBWpTTTx58BgEjPLzDNf5kMThvd6xchrobPTsJ5mP" + - "w3NJ7zRhckN8cv4FhQBfwurZzNE5uTW5C5PYqNTkRAnTkP", #pubkey - ['00142b82c61a7a48b7b10801f0eb247af46821bd33f5' #recv scriptpubkeys - ,'0014073dc6bcbb18d6468c5996bdeba926f6805b74b1' - ,'001400fa0b5cb21e8d442a7bd61af3d558a62be0c9aa'], - ['00144f4a0655a4b586be1e08d97a2f55125120b84c69' #change scriptpubkeys - ,'0014ef7967a7a56c23bbc9f317e612c93a5e23d25ffe' - ,'0014ad768a11730bf54d10c72184d53239de0f310bc9']) - ,#p2sh 2of2 multisig wallet - ("2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + - "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" + - "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" + - "T74sszGaxaf64o5s", #m=2, 2 pubkeys, n=len(pubkeys) - ['a914fe30a46a4e1b41f9bb758448fd84ee4628c103e187' #recv - ,'a914dad5dd605871560ae5d219cd6275e6ad19bc6b9987' - ,'a914471e158e2db190acdd8c76ed6d2ade102fe1e8ac87' - ,'a914013449715a32f21d1a8a2b95a01b40eb41ada16f87' - ,'a914ae3dd25567fb7c2f87be41220dd14025ca68b0e087' - ,'a91462b90344947b610c4eadb7dd460fee3f32fefe7687' - ,'a914d4388c7d5771ebf26b6e650c42e60e4cf7d4c5a187' - ,'a914e4f0832e56591d01b71c72b9a3777dc8f9d9a92e87' - ,'a914a5d5accd96d27403c7663b92fdb57299d7a871eb87' - ,'a914f8f2c6ef2d80f972e4d8b418a15337a3c38af37f87' - ,'a914a2bd2f67fac7c24e609b574ccc8cfaa2f90ebf8c87' - ,'a914a56298a7decde1d18306f55d9305577c3fce690187' - ,'a91430f2f83238ac29125a539055fa59efc86a73a23987' - ,'a914263b4585d0735c5065987922af359d5eabeb880d87' - ,'a91455d9d47113fb8b37705bdf6d4107d438afd63e4687' - ,'a914970d754163b8957b73f4e8baaf23dea5f6e3db2287' - ,'a914facbc921203a9ffd751cc246a884918beaac21b687' - ,'a914fc7556833eca1e0f84c6d7acb875e645f7ed4e9687' - ,'a914bbfe6a032d633f113b5d605e3a97cc08a47cc87d87' - ,'a91403d733c4ca337b5fa1de95970ba6f898a9d36c4887' - ,'a9148af27dc7c950e17c11e164065e672cd60ae3d48d87' - ,'a914c026aa45377f2a4a62136bac1d3350c318fee5c587' - ,'a9146337f59e3ea55e73725c9f2fc52a5ca5d68c361687'], - ['a914aeaebf9d567ab8a6813e89668e16f40bf419408e87' #change - ,'a914f2a6264dd3975297fa2a5a8e17321299a44f76d987' - ,'a9142067a6c47958090a645137cc0898c0c7bbc69b5387' - ,'a914210840f77ea5b7eb11cb55e5d719a93b7746fb9387' - ,'a914163db6b8ca00362be63a26502c5f7bf64787506b87' - ,'a91479b2c527594059c056e5367965ae92bbcf63512187']) - ,#p2sh 2of3 multisig wallet - ("2 tpubD6NzVbkrYhZ4WwaMJ3od4hANxdMVpb63Du3ERq1xjtowxVJEcTbGH2rFd9TFXxw" + - "KJRKDn9vQjDPxFeaku6BHW6wHn2KPF1ijS4LwgwQFJ3B tpubD6NzVbkrYhZ4Wjv4ZRPD6" + - "MNdiLmfvXztbKuuatkqHjukU3S6GXhmKnbAF5eU9bR2Nryiq8v67emUUSM1VUrAx5wcZ19" + - "AsaGg3ZLmjbbwLXr tpubD6NzVbkrYhZ4Xxa2fEp7YsbnFnwuQNaogijbiX42Deqd4NiAD" + - "tqNU6AXCU2d2kPFWBpAGG7K3HAKYwUfZBPgTLkfQp2dDg9SLVnkgYPgEXN", - ['a914167c95beb25b984ace517d4346e6cdbf1381793687', #recv addrs - 'a914378bbda1ba7a713de18c3ba3c366f42212bfb45087', - 'a9142a5c9881c70906180f37dd02d8c830e9b6328d4a87', - 'a914ffe0832375b72ee5307bfa502896ba28cc470ee987', - 'a9147607d40e039fbea57d9c04e48b198c9fcf3356c187', - 'a9148d9582ad4cf0581c6e0697e4cba6a12e66ca1a0087', - 'a914d153a743b315ba19690823119019e16e3762104d87', - 'a914b4accc89e48610043e70371153fd8cb5a3eef34287', - 'a91406febca615e3631253fd75a1d819436e1d046e0487', - 'a914b863cbb888c6b28291cb87a2390539e28be37a9587', - 'a914ec39094e393184d2c352a29b9d7a3caddaccb6cf87', - 'a914da4faa4babbdf611caf511d287133f06c1c3244a87', - 'a9146e64561d0c5e2e9159ecff65db02e04b3277402487', - 'a914377d66386972492192ae827fb2208596af0941d187', - 'a914448d364ff2374449e57df13db33a40f5b099997c87', - 'a914f24b875d2cb99e0b138ab0e6dd65027932b3c6e787', - 'a914aa4bcee53406b1ef6c83852e3844e38a3a9d9f3087', - 'a9145e5ec40fdab54be0d6e21107bc38c39df97e37fc87', - 'a9141de4d402c82f4e9b0e6b792b331232a5405ebd3f87', - 'a9148873ee280e51f9c64d257dd6dedc8712fd652cc687'], - ['a9142cc87d7562a85029a57cc37026e12dab72223db287', #change - 'a91499f4aee0b274f0b3ab48549a2c58cd667a62c0cb87', - 'a91497a89cd5ada3a766a1275f8151e9256fcf537f6c87', - 'a9147ffc9f3a3b60635ea1783243274f4d07ab617cb487', - 'a9143423113ab913d86fd47e55488a0c559e18b457b987', - 'a914a28a3773a37c52ff6fd7dff497d0eaf80a46febb87']) - , #p2wsh 1of2 multisig wallet - ("1 Vpub5fAqpSRkLmvXwqbuR61MaKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHp" + - "dvrg2LHHXkKxSXBHNWNpZz9b1VqADjmcCs3arSoxN3F3r Vpub5fvEo4MUpbVs9sZqr45" + - "zmRVEsTcQ49MA9m3MLht3XzdZvS9eMXLLu1H6TL1j2SMnykHqXNzG5ycMyQmFDvEE5B32" + - "sP8TmRe6wW8HjBgMssh", - #recv scriptpubkeys - ['002031fbaa839e96fc1abaf3453b9f770e0ccfe2d8e3e990bb381fdcb7db4722986a', - '0020820ae739b36f4feb1c299ced201db383bbcf1634e0071e489b385f43c2323761', - '0020eff05f4d14aa1968a7142b1009aa57a6208fb01b212f8b8f7df63645d26a1292', - '002049c6e17979dca380ffb66295d27f609bea2879d4f0b590c96c70ff12260a8721', - '002002bf2430fc7ebc6fb27da1cb80e52702edcc62a29f65c997e5c924dcd98411bd', - '0020c7a58dcf9633453ba12860b57c14af67d87d022be5c52bf6be7a6abdc295c6e0', - '0020136696059a5e932c72f4f0a05fa7f52faf9b54f1b7694e15acce710e6cc9e89d', - '0020c372e880227f35c2ee35d0724bf05cea95e74dcb3e6aa67ff15f561a29c0645d', - '002095c705590e2b84996fa44bff64179b26669e53bbd58d76bb6bbb5c5498a981ce', - '00207217754dae083c3c365c7e1ce3ad889ca2bd88e4f809cec66b9987adc390aa26', - '0020bee30906450e099357cc96a1f472c1ef70089cd4a0cba96749adfe1c9a2f9e87', - '0020b1838b3d5a386ad6c90eeae9a27a9b812e32ce06376f261dea89e405bc8209d9', - '0020231a3d05886efff601f0702d4c8450dfcce8d6a4bd90f17f7ff76f5c25c632de', - '002071220f3941b5f65aca90e464db4291cd5ea63f37fa858fd5b66d5019f0dbab0f', - '0020fc3c7db9f0e773f9f9c725d4286ddcc88db9575c45b2441d458018150eb4ef10', - '00209f037bfc98dee2fc0d3cca54df09b2d20e92a0133fa381a4dd74c49e4d0a89f5', - '0020c9060d0554ba2ca92048e1772e806d796ba41f10bf6aee2653a9eba96b05c944', - '0020a7cb1dd2730dba564f414ed8d9312370ff89c34df1441b83125cb4d97a96005a', - '00209fddc9b4e070b887dec034ed74f15f62d075a3ac8cf6eb95a88c635e0207534c', - '0020c48f9c50958ab8e386a8bd3888076f31d12e5cf011ff46cc83c6fadfe6d47d20', - '0020a659f4621dca404571917e73dedb26b6d7c49a07dacbf15890760ac0583d3267'], - #change scriptpubkeys - ['002030213b5d3b6988b86aa13a9eaca08e718d51f32dc130c70981abb0102173c791', - '002027bd198f9783a58e9bc4d3fdbd1c75cc74154905cce1d23c7bd3e051695418fe', - '0020c1fd2cdebf120d3b1dc990dfdaca62382ff9525beeb6a79a908ddecb40e2162c', - '00207a3e478266e5fe49fe22e3d8f04d3adda3b6a0835806a0db1f77b84d0ba7f79c', - '002059e66462023ecd54e20d4dce286795e7d5823af511989736edc0c7a844e249f5', - '0020bd8077906dd367d6d107d960397e46db2daba5793249f1f032d8d7e12e6f193c']) - , #p2wpkh-p2sh - ("upub5E4QEumGPNTmSKD95TrYX2xqLwwvBULbRzzHkrpW9WKKCB1y9DEfPXDnUyQjLjmVs" + - "7gSd7k5vRb1FoSb6BjyiWNg4arkJLaqk1jULzbwA5q", - ["a914ae8f84a06668742f713d0743c1f54d248040e63387", #recv - "a914c2e9bdcc48596b8cce418042ade72198fddf3cd987", - "a914a44b6ad63ccef0ae1741eaccee99bf2fa83f842987", - "a9148cf1c891d96a0be07893d0bddcf00ed5dad2c46e87", - "a91414d677b32f2409f4dfb3073d382c302bcd6ed33587", - "a9141b284bee7198d5134512f37ef60e4048864b4bd687"], - ["a914a5aacff65860440893107b01912dc8f60cadab2b87", #change - "a914dcd74ebc8bfc5cf0535717a3e833592d54b3c48687", - "a91446793cae4c2b8149ade61c1627b96b90599bc08787", - "a91439f3776831f321125bdb5099fbbd654923f8316c87"]) - , #p2wpkh-p2sh - ("ypub6XrRLtXNB7NQo3vDaMNnffXVJe1WVaebXcb4ncpTHHADLuFYmf2CcPn96YzUbMt8s" + - "HSMmtr1mCcMgCBLqNdY2hrXXcdiLxCdD9e2dChBLun", - ["a91429c2ad045bbb162ef3c2d9cacb9812bec463061787", #recv - "a91433ec6bb67b113978d9cfd307a97fd15bc0a5a62087", - "a91450523020275ccbf4e916a0d8523ae42391ad988a87", - "a91438c2e5e76a874d86cfc914fe9fc1868b6afb5c5487"], - ["a91475f608698bb735120a17699fee854bce9a8dc8d387", - "a91477e69344ef53587051c85a06a52a646457b44e6c87", - "a914607c98ea34fbdffe39fee161ae2ffd5517bf1a5587"]) - , #old mnemonic mpk - ("e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d" + - "5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3", - ["76a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac", #recv - "76a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac", - "76a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac", - "76a914f96669095e6df76cfdf5c7e49a1909f002e123d088ac"], - ["76a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac", #change - "76a9148942ac692ace81019176c4fb0ac408b18b49237f88ac", - "76a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ac"]) - , #p2wsh-p2sh 2of2 multisig - ("2 Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4u" + - "mELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1 Ypub6iNDhL4WWq5kFZcdFqHHw" + - "X4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2Q" + - "vT3zFbBCDiSDLkau", - ["a91428060ade179c792fac07fc8817fd150ce7cdd3f987", #recv - "a9145ba5ed441b9f3e22f71193d4043b645183e6aeee87", - "a91484cc1f317b7d5afff115916f1e27319919601d0187", - "a9144001695a154cac4d118af889d3fdcaf929af315787", - "a914897888f3152a27cbd7611faf6aa01085931e542a87"], - ["a91454dbb52de65795d144f3c4faeba0e37d9765c85687", #change - "a914f725cbd61c67f34ed40355f243b5bb0650ce61c587", - "a9143672bcd3d02d3ea7c3205ddbc825028a0d2a781987"]) -] - -electrum_bad_keydata_test_vectors = [ - "zpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" + - "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #bad checksum - "a tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + - "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" + - "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" + - "T74sszGaxaf64o5s", #unparsable m number - "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + - "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ Vpub5fAqpSRkLmvXwqbuR61M" + - "aKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHpdvrg2LHHXkKxSXBHNWNpZz9b1Vq" + - "ADjmcCs3arSoxN3F3r" #inconsistent magic -] - -def test(): - for keydata, recv_spks, change_spks in electrum_keydata_test_vectors: - initial_count = 15 - gaplimit = 5 - wal = parse_electrum_master_public_key(keydata, gaplimit) - spks = wal.get_scriptpubkeys(0, 0, initial_count) - #for test, generate 15, check that the last 5 lead to gap limit overrun - for i in range(initial_count - gaplimit): - ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[i]]) - assert ret == None - for i in range(gaplimit): - index = i + initial_count - gaplimit - ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[index]]) - assert ret != None and ret[0] == i+1 - last_index_add = 3 - last_index = initial_count - gaplimit + last_index_add - ret = wal.have_scriptpubkeys_overrun_gaplimit(spks[2:last_index]) - assert ret[0] == last_index_add - assert wal.get_scriptpubkeys(0, 0, len(recv_spks)) == recv_spks - assert wal.get_scriptpubkeys(1, 0, len(change_spks)) == change_spks - for keydata in electrum_bad_keydata_test_vectors: - try: - parse_electrum_master_public_key(keydata, 5) - raised_error = False - except (ValueError, Exception): - raised_error = True - assert raised_error - print("All tests passed successfully") - -if __name__ == "__main__": - test() - pass - diff --git a/electrumpersonalserver/deterministicwallet.py b/electrumpersonalserver/deterministicwallet.py @@ -0,0 +1,429 @@ + +import bitcoin as btc +from electrumpersonalserver.hashes import bh2u, hash_160, bfh, sha256 + +# the class hierarchy for deterministic wallets in this file: +# subclasses are written towards the right +# each class knows how to create the scriptPubKeys of that wallet +# +# |-- SingleSigOldMnemonicWallet +# |-- SingleSigP2PKHWallet +# |-- SingleSigP2WPKHWallet +# SingleSigWallet --| +# / |-- SingleSigP2WPKH_P2SHWallet +# DeterministicWallet +# \ |-- MultisigP2SHWallet +# MultisigWallet --| +# |-- MultisigP2WSHWallet +# |-- MultisigP2WSH_P2SHWallet + +#the wallet types are here +#https://github.com/spesmilo/electrum/blob/3.0.6/RELEASE-NOTES +#and +#https://github.com/spesmilo/electrum-docs/blob/master/xpub_version_bytes.rst + +def is_string_parsable_as_hex_int(s): + try: + int(s, 16) + return True + except: + return False + +def parse_electrum_master_public_key(keydata, gaplimit): + if keydata[:4] in ("xpub", "tpub"): + wallet = SingleSigP2PKHWallet(keydata) + elif keydata[:4] in ("zpub", "vpub"): + wallet = SingleSigP2WPKHWallet(keydata) + elif keydata[:4] in ("ypub", "upub"): + wallet = SingleSigP2WPKH_P2SHWallet(keydata) + elif keydata.find(" ") != -1: #multiple keys = multisig + chunks = keydata.split(" ") + try: + m = int(chunks[0]) + except ValueError: + raise ValueError("Unable to parse m in multisig key data: " + + chunks[0]) + pubkeys = chunks[1:] + if not all([pubkeys[0][:4] == pub[:4] for pub in pubkeys[1:]]): + raise ValueError("inconsistent bip32 pubkey types") + if pubkeys[0][:4] in ("xpub", "tpub"): + wallet = MultisigP2SHWallet(m, pubkeys) + elif pubkeys[0][:4] in ("Zpub", "Vpub"): + wallet = MultisigP2WSHWallet(m, pubkeys) + elif pubkeys[0][:4] in ("Ypub", "Upub"): + wallet = MultisigP2WSH_P2SHWallet(m, pubkeys) + elif is_string_parsable_as_hex_int(keydata) and len(keydata) == 128: + wallet = SingleSigOldMnemonicWallet(keydata) + else: + raise ValueError("Unrecognized electrum mpk format: " + keydata[:4]) + wallet.gaplimit = gaplimit + return wallet + +class DeterministicWallet(object): + def __init__(self): + self.gaplimit = 0 + self.next_index = [0, 0] + self.scriptpubkey_index = {} + + def get_new_scriptpubkeys(self, change, count): + """Returns newly-generated addresses from this deterministic wallet""" + return self.get_scriptpubkeys(change, self.next_index[change], + count) + + def get_scriptpubkeys(self, change, from_index, count): + """Returns addresses from this deterministic wallet""" + pass + + #called in check_for_new_txes() when a new tx of ours arrives + #to see if we need to import more addresses + def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): + """Return None if they havent, or how many addresses to + import if they have""" + result = {} + for spk in scriptpubkeys: + if spk not in self.scriptpubkey_index: + continue + change, index = self.scriptpubkey_index[spk] + distance_from_next = self.next_index[change] - index + if distance_from_next > self.gaplimit: + continue + #need to import more + if change in result: + result[change] = max(result[change], self.gaplimit + - distance_from_next + 1) + else: + result[change] = self.gaplimit - distance_from_next + 1 + if len(result) > 0: + return result + else: + return None + + def rewind_one(self, change): + """Go back one pubkey in a branch""" + self.next_index[change] -= 1 + +class SingleSigWallet(DeterministicWallet): + def __init__(self, mpk): + super(SingleSigWallet, self).__init__() + self.branches = (btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1)) + #m/change/i + + def pubkey_to_scriptpubkey(self, pubkey): + raise RuntimeError() + + def get_pubkey(self, change, index): + return btc.bip32_extract_key(btc.bip32_ckd(self.branches[change], + index)) + + def get_scriptpubkeys(self, change, from_index, count): + result = [] + for index in range(from_index, from_index + count): + pubkey = self.get_pubkey(change, index) + scriptpubkey = self.pubkey_to_scriptpubkey(pubkey) + self.scriptpubkey_index[scriptpubkey] = (change, index) + result.append(scriptpubkey) + self.next_index[change] = max(self.next_index[change], from_index+count) + return result + +class SingleSigP2PKHWallet(SingleSigWallet): + def pubkey_to_scriptpubkey(self, pubkey): + pkh = bh2u(hash_160(bfh(pubkey))) + #op_dup op_hash_160 length hash160 op_equalverify op_checksig + return "76a914" + pkh + "88ac" + +class SingleSigP2WPKHWallet(SingleSigWallet): + def pubkey_to_scriptpubkey(self, pubkey): + pkh = bh2u(hash_160(bfh(pubkey))) + #witness-version length hash160 + #witness version is always 0, length is always 0x14 + return "0014" + pkh + +class SingleSigP2WPKH_P2SHWallet(SingleSigWallet): + def pubkey_to_scriptpubkey(self, pubkey): + #witness-version length pubkeyhash + #witness version is always 0, length is always 0x14 + redeem_script = '0014' + bh2u(hash_160(bfh(pubkey))) + sh = bh2u(hash_160(bfh(redeem_script))) + return "a914" + sh + "87" + +class SingleSigOldMnemonicWallet(SingleSigWallet): + def __init__(self, mpk): + super(SingleSigWallet, self).__init__() + self.mpk = mpk + + def get_pubkey(self, change, index): + return btc.electrum_pubkey(self.mpk, index, change) + + def pubkey_to_scriptpubkey(self, pubkey): + pkh = bh2u(hash_160(bfh(pubkey))) + #op_dup op_hash_160 length hash160 op_equalverify op_checksig + return "76a914" + pkh + "88ac" + +class MultisigWallet(DeterministicWallet): + def __init__(self, m, mpk_list): + super(MultisigWallet, self).__init__() + self.m = m + self.pubkey_branches = [(btc.bip32_ckd(mpk, 0), btc.bip32_ckd(mpk, 1)) + for mpk in mpk_list] + #derivation path for pubkeys is m/change/index + + def redeem_script_to_scriptpubkey(self, redeem_script): + raise RuntimeError() + + def get_scriptpubkeys(self, change, from_index, count): + result = [] + for index in range(from_index, from_index + count): + pubkeys = [btc.bip32_extract_key(btc.bip32_ckd(branch[change], + index)) for branch in self.pubkey_branches] + pubkeys = sorted(pubkeys) + redeemScript = "" + redeemScript += "%x"%(0x50 + self.m) #op_m + for p in pubkeys: + redeemScript += "21" #length + redeemScript += p + redeemScript += "%x"%(0x50 + len(pubkeys)) #op_n + redeemScript += "ae" # op_checkmultisig + scriptpubkey = self.redeem_script_to_scriptpubkey(redeemScript) + self.scriptpubkey_index[scriptpubkey] = (change, index) + result.append(scriptpubkey) + self.next_index[change] = max(self.next_index[change], from_index+count) + return result + +class MultisigP2SHWallet(MultisigWallet): + def redeem_script_to_scriptpubkey(self, redeem_script): + sh = bh2u(hash_160(bfh(redeem_script))) + #op_hash160 length hash160 op_equal + return "a914" + sh + "87" + +class MultisigP2WSHWallet(MultisigWallet): + def redeem_script_to_scriptpubkey(self, redeem_script): + sh = bh2u(sha256(bfh(redeem_script))) + #witness-version length sha256 + #witness version is always 0, length is always 0x20 + return "0020" + sh + +class MultisigP2WSH_P2SHWallet(MultisigWallet): + def redeem_script_to_scriptpubkey(self, redeem_script): + #witness-version length sha256 + #witness version is always 0, length is always 0x20 + nested_redeemScript = "0020" + bh2u(sha256(bfh(redeem_script))) + sh = bh2u(hash_160(bfh(nested_redeemScript))) + #op_hash160 length hash160 op_equal + return "a914" + sh + "87" + +# electrum has its own tests here +#https://github.com/spesmilo/electrum/blob/03b40a3c0a7dd84e76bc0d0ea2ad390dafc92250/lib/tests/test_wallet_vertical.py + +electrum_keydata_test_vectors = [ + #p2pkh wallet + ("xpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" + + "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #pubkey + ["76a914b1847c763c9a9b12631ab42335751c1bf843880c88ac" #recv scriptpubkeys + ,"76a914d8b6b932e892fad5132ea888111adac2171c5af588ac" + ,"76a914e44b19ef74814f977ae4e2823dd0a0b33480472a88ac"], + ["76a914d2c2905ca383a5b8f94818cb7903498061a6286688ac" #change scriptpubkeys + ,"76a914e7b4ddb7cede132e84ba807defc092cf52e005b888ac" + ,"76a91433bdb046a1d373728d7844df89aa24f788443a4588ac"]) + , #p2wpkh wallet + ("zpub6mr7wBKy3oJn89TCiXUAPBWpTTTx58BgEjPLzDNf5kMThvd6xchrobPTsJ5mP" + + "w3NJ7zRhckN8cv4FhQBfwurZzNE5uTW5C5PYqNTkRAnTkP", #pubkey + ['00142b82c61a7a48b7b10801f0eb247af46821bd33f5' #recv scriptpubkeys + ,'0014073dc6bcbb18d6468c5996bdeba926f6805b74b1' + ,'001400fa0b5cb21e8d442a7bd61af3d558a62be0c9aa'], + ['00144f4a0655a4b586be1e08d97a2f55125120b84c69' #change scriptpubkeys + ,'0014ef7967a7a56c23bbc9f317e612c93a5e23d25ffe' + ,'0014ad768a11730bf54d10c72184d53239de0f310bc9']) + ,#p2sh 2of2 multisig wallet + ("2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + + "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" + + "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" + + "T74sszGaxaf64o5s", #m=2, 2 pubkeys, n=len(pubkeys) + ['a914fe30a46a4e1b41f9bb758448fd84ee4628c103e187' #recv + ,'a914dad5dd605871560ae5d219cd6275e6ad19bc6b9987' + ,'a914471e158e2db190acdd8c76ed6d2ade102fe1e8ac87' + ,'a914013449715a32f21d1a8a2b95a01b40eb41ada16f87' + ,'a914ae3dd25567fb7c2f87be41220dd14025ca68b0e087' + ,'a91462b90344947b610c4eadb7dd460fee3f32fefe7687' + ,'a914d4388c7d5771ebf26b6e650c42e60e4cf7d4c5a187' + ,'a914e4f0832e56591d01b71c72b9a3777dc8f9d9a92e87' + ,'a914a5d5accd96d27403c7663b92fdb57299d7a871eb87' + ,'a914f8f2c6ef2d80f972e4d8b418a15337a3c38af37f87' + ,'a914a2bd2f67fac7c24e609b574ccc8cfaa2f90ebf8c87' + ,'a914a56298a7decde1d18306f55d9305577c3fce690187' + ,'a91430f2f83238ac29125a539055fa59efc86a73a23987' + ,'a914263b4585d0735c5065987922af359d5eabeb880d87' + ,'a91455d9d47113fb8b37705bdf6d4107d438afd63e4687' + ,'a914970d754163b8957b73f4e8baaf23dea5f6e3db2287' + ,'a914facbc921203a9ffd751cc246a884918beaac21b687' + ,'a914fc7556833eca1e0f84c6d7acb875e645f7ed4e9687' + ,'a914bbfe6a032d633f113b5d605e3a97cc08a47cc87d87' + ,'a91403d733c4ca337b5fa1de95970ba6f898a9d36c4887' + ,'a9148af27dc7c950e17c11e164065e672cd60ae3d48d87' + ,'a914c026aa45377f2a4a62136bac1d3350c318fee5c587' + ,'a9146337f59e3ea55e73725c9f2fc52a5ca5d68c361687'], + ['a914aeaebf9d567ab8a6813e89668e16f40bf419408e87' #change + ,'a914f2a6264dd3975297fa2a5a8e17321299a44f76d987' + ,'a9142067a6c47958090a645137cc0898c0c7bbc69b5387' + ,'a914210840f77ea5b7eb11cb55e5d719a93b7746fb9387' + ,'a914163db6b8ca00362be63a26502c5f7bf64787506b87' + ,'a91479b2c527594059c056e5367965ae92bbcf63512187']) + ,#p2sh 2of3 multisig wallet + ("2 tpubD6NzVbkrYhZ4WwaMJ3od4hANxdMVpb63Du3ERq1xjtowxVJEcTbGH2rFd9TFXxw" + + "KJRKDn9vQjDPxFeaku6BHW6wHn2KPF1ijS4LwgwQFJ3B tpubD6NzVbkrYhZ4Wjv4ZRPD6" + + "MNdiLmfvXztbKuuatkqHjukU3S6GXhmKnbAF5eU9bR2Nryiq8v67emUUSM1VUrAx5wcZ19" + + "AsaGg3ZLmjbbwLXr tpubD6NzVbkrYhZ4Xxa2fEp7YsbnFnwuQNaogijbiX42Deqd4NiAD" + + "tqNU6AXCU2d2kPFWBpAGG7K3HAKYwUfZBPgTLkfQp2dDg9SLVnkgYPgEXN", + ['a914167c95beb25b984ace517d4346e6cdbf1381793687', #recv addrs + 'a914378bbda1ba7a713de18c3ba3c366f42212bfb45087', + 'a9142a5c9881c70906180f37dd02d8c830e9b6328d4a87', + 'a914ffe0832375b72ee5307bfa502896ba28cc470ee987', + 'a9147607d40e039fbea57d9c04e48b198c9fcf3356c187', + 'a9148d9582ad4cf0581c6e0697e4cba6a12e66ca1a0087', + 'a914d153a743b315ba19690823119019e16e3762104d87', + 'a914b4accc89e48610043e70371153fd8cb5a3eef34287', + 'a91406febca615e3631253fd75a1d819436e1d046e0487', + 'a914b863cbb888c6b28291cb87a2390539e28be37a9587', + 'a914ec39094e393184d2c352a29b9d7a3caddaccb6cf87', + 'a914da4faa4babbdf611caf511d287133f06c1c3244a87', + 'a9146e64561d0c5e2e9159ecff65db02e04b3277402487', + 'a914377d66386972492192ae827fb2208596af0941d187', + 'a914448d364ff2374449e57df13db33a40f5b099997c87', + 'a914f24b875d2cb99e0b138ab0e6dd65027932b3c6e787', + 'a914aa4bcee53406b1ef6c83852e3844e38a3a9d9f3087', + 'a9145e5ec40fdab54be0d6e21107bc38c39df97e37fc87', + 'a9141de4d402c82f4e9b0e6b792b331232a5405ebd3f87', + 'a9148873ee280e51f9c64d257dd6dedc8712fd652cc687'], + ['a9142cc87d7562a85029a57cc37026e12dab72223db287', #change + 'a91499f4aee0b274f0b3ab48549a2c58cd667a62c0cb87', + 'a91497a89cd5ada3a766a1275f8151e9256fcf537f6c87', + 'a9147ffc9f3a3b60635ea1783243274f4d07ab617cb487', + 'a9143423113ab913d86fd47e55488a0c559e18b457b987', + 'a914a28a3773a37c52ff6fd7dff497d0eaf80a46febb87']) + , #p2wsh 1of2 multisig wallet + ("1 Vpub5fAqpSRkLmvXwqbuR61MaKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHp" + + "dvrg2LHHXkKxSXBHNWNpZz9b1VqADjmcCs3arSoxN3F3r Vpub5fvEo4MUpbVs9sZqr45" + + "zmRVEsTcQ49MA9m3MLht3XzdZvS9eMXLLu1H6TL1j2SMnykHqXNzG5ycMyQmFDvEE5B32" + + "sP8TmRe6wW8HjBgMssh", + #recv scriptpubkeys + ['002031fbaa839e96fc1abaf3453b9f770e0ccfe2d8e3e990bb381fdcb7db4722986a', + '0020820ae739b36f4feb1c299ced201db383bbcf1634e0071e489b385f43c2323761', + '0020eff05f4d14aa1968a7142b1009aa57a6208fb01b212f8b8f7df63645d26a1292', + '002049c6e17979dca380ffb66295d27f609bea2879d4f0b590c96c70ff12260a8721', + '002002bf2430fc7ebc6fb27da1cb80e52702edcc62a29f65c997e5c924dcd98411bd', + '0020c7a58dcf9633453ba12860b57c14af67d87d022be5c52bf6be7a6abdc295c6e0', + '0020136696059a5e932c72f4f0a05fa7f52faf9b54f1b7694e15acce710e6cc9e89d', + '0020c372e880227f35c2ee35d0724bf05cea95e74dcb3e6aa67ff15f561a29c0645d', + '002095c705590e2b84996fa44bff64179b26669e53bbd58d76bb6bbb5c5498a981ce', + '00207217754dae083c3c365c7e1ce3ad889ca2bd88e4f809cec66b9987adc390aa26', + '0020bee30906450e099357cc96a1f472c1ef70089cd4a0cba96749adfe1c9a2f9e87', + '0020b1838b3d5a386ad6c90eeae9a27a9b812e32ce06376f261dea89e405bc8209d9', + '0020231a3d05886efff601f0702d4c8450dfcce8d6a4bd90f17f7ff76f5c25c632de', + '002071220f3941b5f65aca90e464db4291cd5ea63f37fa858fd5b66d5019f0dbab0f', + '0020fc3c7db9f0e773f9f9c725d4286ddcc88db9575c45b2441d458018150eb4ef10', + '00209f037bfc98dee2fc0d3cca54df09b2d20e92a0133fa381a4dd74c49e4d0a89f5', + '0020c9060d0554ba2ca92048e1772e806d796ba41f10bf6aee2653a9eba96b05c944', + '0020a7cb1dd2730dba564f414ed8d9312370ff89c34df1441b83125cb4d97a96005a', + '00209fddc9b4e070b887dec034ed74f15f62d075a3ac8cf6eb95a88c635e0207534c', + '0020c48f9c50958ab8e386a8bd3888076f31d12e5cf011ff46cc83c6fadfe6d47d20', + '0020a659f4621dca404571917e73dedb26b6d7c49a07dacbf15890760ac0583d3267'], + #change scriptpubkeys + ['002030213b5d3b6988b86aa13a9eaca08e718d51f32dc130c70981abb0102173c791', + '002027bd198f9783a58e9bc4d3fdbd1c75cc74154905cce1d23c7bd3e051695418fe', + '0020c1fd2cdebf120d3b1dc990dfdaca62382ff9525beeb6a79a908ddecb40e2162c', + '00207a3e478266e5fe49fe22e3d8f04d3adda3b6a0835806a0db1f77b84d0ba7f79c', + '002059e66462023ecd54e20d4dce286795e7d5823af511989736edc0c7a844e249f5', + '0020bd8077906dd367d6d107d960397e46db2daba5793249f1f032d8d7e12e6f193c']) + , #p2wpkh-p2sh + ("upub5E4QEumGPNTmSKD95TrYX2xqLwwvBULbRzzHkrpW9WKKCB1y9DEfPXDnUyQjLjmVs" + + "7gSd7k5vRb1FoSb6BjyiWNg4arkJLaqk1jULzbwA5q", + ["a914ae8f84a06668742f713d0743c1f54d248040e63387", #recv + "a914c2e9bdcc48596b8cce418042ade72198fddf3cd987", + "a914a44b6ad63ccef0ae1741eaccee99bf2fa83f842987", + "a9148cf1c891d96a0be07893d0bddcf00ed5dad2c46e87", + "a91414d677b32f2409f4dfb3073d382c302bcd6ed33587", + "a9141b284bee7198d5134512f37ef60e4048864b4bd687"], + ["a914a5aacff65860440893107b01912dc8f60cadab2b87", #change + "a914dcd74ebc8bfc5cf0535717a3e833592d54b3c48687", + "a91446793cae4c2b8149ade61c1627b96b90599bc08787", + "a91439f3776831f321125bdb5099fbbd654923f8316c87"]) + , #p2wpkh-p2sh + ("ypub6XrRLtXNB7NQo3vDaMNnffXVJe1WVaebXcb4ncpTHHADLuFYmf2CcPn96YzUbMt8s" + + "HSMmtr1mCcMgCBLqNdY2hrXXcdiLxCdD9e2dChBLun", + ["a91429c2ad045bbb162ef3c2d9cacb9812bec463061787", #recv + "a91433ec6bb67b113978d9cfd307a97fd15bc0a5a62087", + "a91450523020275ccbf4e916a0d8523ae42391ad988a87", + "a91438c2e5e76a874d86cfc914fe9fc1868b6afb5c5487"], + ["a91475f608698bb735120a17699fee854bce9a8dc8d387", + "a91477e69344ef53587051c85a06a52a646457b44e6c87", + "a914607c98ea34fbdffe39fee161ae2ffd5517bf1a5587"]) + , #old mnemonic mpk + ("e9d4b7866dd1e91c862aebf62a49548c7dbf7bcc6e4b7b8c9da820c7737968df9c09d" + + "5a3e271dc814a29981f81b3faaf2737b551ef5dcc6189cf0f8252c442b3", + ["76a9149cd3dfb0d87a861770ae4e268e74b45335cf00ab88ac", #recv + "76a914c30f2af6a79296b6531bf34dba14c8419be8fb7d88ac", + "76a9145eb4eeaefcf9a709f8671444933243fbd05366a388ac", + "76a914f96669095e6df76cfdf5c7e49a1909f002e123d088ac"], + ["76a914ca14915184a2662b5d1505ce7142c8ca066c70e288ac", #change + "76a9148942ac692ace81019176c4fb0ac408b18b49237f88ac", + "76a914e1232622a96a04f5e5a24ca0792bb9c28b089d6e88ac"]) + , #p2wsh-p2sh 2of2 multisig + ("2 Ypub6hWbqA2p47QgsLt5J4nxrR3ngu8xsPGb7PdV8CDh48KyNngNqPKSqertAqYhQ4u" + + "mELu1UsZUCYfj9XPA6AdSMZWDZQobwF7EJ8uNrECaZg1 Ypub6iNDhL4WWq5kFZcdFqHHw" + + "X4YTH4rYGp8xbndpRrY7WNZFFRfogSrL7wRTajmVHgR46AT1cqUG1mrcRd7h1WXwBsgX2Q" + + "vT3zFbBCDiSDLkau", + ["a91428060ade179c792fac07fc8817fd150ce7cdd3f987", #recv + "a9145ba5ed441b9f3e22f71193d4043b645183e6aeee87", + "a91484cc1f317b7d5afff115916f1e27319919601d0187", + "a9144001695a154cac4d118af889d3fdcaf929af315787", + "a914897888f3152a27cbd7611faf6aa01085931e542a87"], + ["a91454dbb52de65795d144f3c4faeba0e37d9765c85687", #change + "a914f725cbd61c67f34ed40355f243b5bb0650ce61c587", + "a9143672bcd3d02d3ea7c3205ddbc825028a0d2a781987"]) +] + +electrum_bad_keydata_test_vectors = [ + "zpub661MyMwAqRbcGVQTLtBFzc3ENvyZHoUEhWRdGwoqLZaf5wXP9VcDY2VJV7usvsFLZz" + + "2RUTVhCVXYXc3S8zpLyAFbDFcfrpUiwLoE9VWH2yz", #bad checksum + "a tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + + "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ tpubD6NzVbkrYhZ4WjgNYq2nF" + + "TbiSLW2SZAzs4g5JHLqwQ3AmR3tCWpqsZJJEoZuP5HAEBNxgYQhtWMezszoaeTCg6FWGQB" + + "T74sszGaxaf64o5s", #unparsable m number + "2 tpubD6NzVbkrYhZ4YVMVzC7wZeRfz3bhqcHvV8M3UiULCfzFtLtp5nwvi6LnBQegrkx" + + "YGPkSzXUEvcPEHcKdda8W1YShVBkhFBGkLxjSQ1Nx3cJ Vpub5fAqpSRkLmvXwqbuR61M" + + "aKMSwj5z5xUBwanaz3qnJ5MgaBDpFSLUvKTiNK9zHpdvrg2LHHXkKxSXBHNWNpZz9b1Vq" + + "ADjmcCs3arSoxN3F3r" #inconsistent magic +] + +def test(): + for keydata, recv_spks, change_spks in electrum_keydata_test_vectors: + initial_count = 15 + gaplimit = 5 + wal = parse_electrum_master_public_key(keydata, gaplimit) + spks = wal.get_scriptpubkeys(0, 0, initial_count) + #for test, generate 15, check that the last 5 lead to gap limit overrun + for i in range(initial_count - gaplimit): + ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[i]]) + assert ret == None + for i in range(gaplimit): + index = i + initial_count - gaplimit + ret = wal.have_scriptpubkeys_overrun_gaplimit([spks[index]]) + assert ret != None and ret[0] == i+1 + last_index_add = 3 + last_index = initial_count - gaplimit + last_index_add + ret = wal.have_scriptpubkeys_overrun_gaplimit(spks[2:last_index]) + assert ret[0] == last_index_add + assert wal.get_scriptpubkeys(0, 0, len(recv_spks)) == recv_spks + assert wal.get_scriptpubkeys(1, 0, len(change_spks)) == change_spks + for keydata in electrum_bad_keydata_test_vectors: + try: + parse_electrum_master_public_key(keydata, 5) + raised_error = False + except (ValueError, Exception): + raised_error = True + assert raised_error + print("All tests passed successfully") + +if __name__ == "__main__": + test() + pass + diff --git a/hashes.py b/electrumpersonalserver/hashes.py diff --git a/jsonrpc.py b/electrumpersonalserver/jsonrpc.py diff --git a/electrumpersonalserver/merkleproof.py b/electrumpersonalserver/merkleproof.py @@ -0,0 +1,271 @@ + +import bitcoin as btc +import binascii +from math import ceil, log + +from electrumpersonalserver.hashes import hash_encode, hash_decode +from electrumpersonalserver.hashes import Hash, hash_merkle_root + +#lots of ideas and code taken from bitcoin core and breadwallet +#https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h +#https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c + +def calc_tree_width(height, txcount): + """Efficently calculates the number of nodes at given merkle tree height""" + return (txcount + (1 << height) - 1) >> height + +def decend_merkle_tree(hashes, flags, height, txcount, pos): + """Function recursively follows the flags bitstring down into the + tree, building up a tree in memory""" + flag = next(flags) + if height > 0: + #non-txid node + if flag: + left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2) + #bitcoin's merkle tree format has a rule that if theres an + # odd number of nodes in then the tree, the last hash is duplicated + #in the electrum format we must hash together the duplicate + # tree branch + if pos*2+1 < calc_tree_width(height-1, txcount): + right = decend_merkle_tree(hashes, flags, height-1, + txcount, pos*2+1) + else: + if isinstance(left, tuple): + right = expand_tree_hashing(left) + else: + right = left + return (left, right) + else: + hs = next(hashes) + return hs + else: + #txid node + hs = next(hashes) + if flag: + #for the actual transaction, also store its position with a flag + return "tx:" + str(pos) + ":" + hs + else: + return hs + +def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount): + """Converts core's format for a merkle proof into a tree in memory""" + tree_depth = int(ceil(log(txcount, 2))) + hashes = iter(hash_list) + #one-liner which converts the flags value to a list of True/False bits + flags = (flag_value[i//8]&1 << i%8 != 0 for i in range(len(flag_value)*8)) + try: + root_node = decend_merkle_tree(hashes, flags, tree_depth, txcount, 0) + return root_node + except StopIteration: + raise ValueError + +def expand_tree_electrum_format_merkle_proof(node, result): + """Recurse down into the tree, adding hashes to the result list + in depth order""" + left, right = node + if isinstance(left, tuple): + expand_tree_electrum_format_merkle_proof(left, result) + if isinstance(right, tuple): + expand_tree_electrum_format_merkle_proof(right, result) + if not isinstance(left, tuple): + result.append(left) + if not isinstance(right, tuple): + result.append(right) + +def get_node_hash(node): + if node.startswith("tx"): + return node.split(":")[2] + else: + return node + +def expand_tree_hashing(node): + """Recurse down into the tree, hashing everything and + returning root hash""" + left, right = node + if isinstance(left, tuple): + hash_left = expand_tree_hashing(left) + else: + hash_left = get_node_hash(left) + if isinstance(right, tuple): + hash_right = expand_tree_hashing(right) + else: + hash_right = get_node_hash(right) + return hash_encode(Hash(hash_decode(hash_left) + hash_decode(hash_right))) + +def convert_core_to_electrum_merkle_proof(proof): + """Bitcoin Core and Electrum use different formats for merkle + proof, this function converts from Core's format to Electrum's format""" + proof = binascii.unhexlify(proof) + pos = [0] + def read_as_int(bytez): + pos[0] += bytez + return btc.decode(proof[pos[0] - bytez:pos[0]][::-1], 256) + def read_var_int(): + pos[0] += 1 + val = btc.from_byte_to_int(proof[pos[0] - 1]) + if val < 253: + return val + return read_as_int(pow(2, val - 252)) + def read_bytes(bytez): + pos[0] += bytez + return proof[pos[0] - bytez:pos[0]] + + merkle_root = proof[36:36+32] + pos[0] = 80 + txcount = read_as_int(4) + hash_count = read_var_int() + hashes = [hash_encode(read_bytes(32)) for i in range(hash_count)] + flags_count = read_var_int() + flags = read_bytes(flags_count) + + root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount) + #check special case of a tree of zero height, block with only coinbase tx + if not isinstance(root_node, tuple): + root_node = root_node[5:] #remove the "tx:0:" + result = {"pos": 0, "merkle": [], "txid": root_node, + "merkleroot": hash_encode(merkle_root)} + return result + + hashes_list = [] + expand_tree_electrum_format_merkle_proof(root_node, hashes_list) + #remove the first or second element which is the txhash + tx = hashes_list[0] + if hashes_list[1].startswith("tx"): + tx = hashes_list[1] + assert(tx.startswith("tx")) + hashes_list.remove(tx) + #if the txhash was duplicated, that _is_ included in electrum's format + if hashes_list[0].startswith("tx"): + hashes_list[0] = tx.split(":")[2] + tx_pos, txid = tx.split(":")[1:3] + tx_pos = int(tx_pos) + result = {"pos": tx_pos, "merkle": hashes_list, "txid": txid, + "merkleroot": hash_encode(merkle_root)} + return result + +merkle_proof_test_vectors = [ + #txcount 819, pos 5 + "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f000000000000000" + + "04f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431acd0b07f" + + "5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0fe098f6f9e090" + + "f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815dc237f77118a45d7" + + "5ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819198212216b5d62d75be5" + + "9221ada17ba4fb2476b689cccd3be54732fd5630832a94f11fa3f0dafd6f904d43219e0" + + "d7de110158446b5b598bd241f7b5df4da0ebc7d30e7748d487917b718df51c681174e6a" + + "bab8042cc7c1c436221c098f06a56134f9247a812126d675d69c82ba1c715cfc0cde462" + + "fd1fbe5dc87f6b8db2b9c060fcd59a20e7fe8e921c3676937a873ff88684f4be4d015f2" + + "4f26af6d2cf78335e9218bcceba4507d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69" + + "590fb0f5118ac937c02ccd47e9d90be78becd11ecf854d7d268eeb479b74d137278c0a5" + + "017d29e90cd5b35a4680201824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb210" + + "5f3d2e8bcc94d547214a9d03ff1600", + #txcount 47, pos 9 + "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab80100000000000" + + "01532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce05577c67" + + "508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff2ba638d8d400" + + "b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd990ce150b6e2dc9e9e" + + "56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568fb117674ad23050e29311" + + "97494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7a67ee03837319e098b43edad" + + "3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38982e03bd292f5bd02f28137d343f" + + "908c7d6417379afe8349a257af3ca1f74f623be6a416fe1aa96a8f259983f2cf32121bc" + + "e203955a378b3b44f132ea6ab94c7829a6c3b360c9f8da8e74027701", + #txcount 2582, pos 330 + "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb17800000000000000000" + + "01032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd4078c771849" + + "5a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d85e56491ed6d" + + "204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3efb7773d457369c228" + + "fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a3b221b47ea9c8e6f4870" + + "7b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0fbcba4c741bb5c75cac58ca0" + + "4161a7377d70f3fd19a3e248ed918c91709b49afd3760f89ed2fefbcc9c23447ccb40a2" + + "be7aba22b07189b0bf90c62db48a9fe37227e12c7af8c1d4c22f9f223530dacdd5f3ad8" + + "50ad4badf16cc24049a65334f59bf28c15cecda1a4cf3f2937bd70ee84f66569ce8ef95" + + "1d50cca46d60337e6c697685b38ad217967bbe6801d03c44fcb808cd035be31888380a2" + + "df1be14b6ff100de83cab0dce250e2b40ca3b47e8309f848646bee63b6185c176d84f15" + + "46a482e7a65a87d1a2d0d5a2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4" + + "ecaeea7d5988745fa421a8604a99857426957a2ccfa7cd8df145aa8293701989dd20750" + + "5923fcb339843944ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74" + + "c0bdd4b6046fb70100", + #txcount 2861, pos 2860, last tx with many duplicated nodes down the tree + "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e00000000000000000" + + "0cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d86159" + + "5ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29" + + "f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc7" + + "2df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974b" + + "df42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290" + + "bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f" + + "171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6" + + "c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07", + #txcount 7, pos 6, duplicated entry in the last depth, at tx level + "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a346200000000000" + + "0af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb4" + + "4dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f" + + "888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22" + + "719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d" + + "8be0bc40135", + #txcount 6, pos 5, duplicated entry in the last-but-one depth + "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b0300000000000" + + "0e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa1" + + "4e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751" + + "ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b5" + + "8a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc" + + "6edefea012d", + #txcount 5, pos 4, duplicated on the last and second last depth + "010000004d891de57908d89e9e5585648978d7650adc67e856c2d8c18c1800000000000" + + "04746fd317bffecd4ffb320239caa06685bafe0c1b5463b24d636e45788796657843d1b" + + "4d4c86041be68355c40500000002d8e26c89c46477f2407d866d2badbd98e43e732a670" + + "e96001faf1744b27e5fdd018733d72e31a2d6a0d94f2a3b35fcc66fb110c40c5bbff82b" + + "f87606553d541d011d", + #txcount 2739, pos 0, coinbase tx + "000000209f283da030c6e6d0ff5087a87c430d140ed6b4564fa34d00000000000000000" + + "0ec1513723e3652c6b8e777c41eb267ad8dd2025e85228840f5cfca7ffe1fb331afff8a" + + "5af8e961175e0f7691b30a00000df403e21a4751fbd52457f535378ac2dcf111199e9ea" + + "6f78f6c2663cb99b58203438d8f3b26f7f2804668c1df7d394a4726363d4873b2d85b71" + + "2e44cf4f5e4f33f22a8f3a1672846bd7c4570c668e6ee12befda23bfa3d0fcd30b1b079" + + "19b01c40b1e31b6d34fcdbb99539d46eb97a3ae15386f1ab0f28ecacadd9fc3fa4ce49a" + + "1a1839d815229f54036c8a3035d91e80e8dc127b62032b4e652550b4fc0aee0f6e85a14" + + "307d85ed9dde62acff9a0f7e3b52370a10d6c83ec13a0b4a8fafe87af368a167d7e9b63" + + "3b84b6ea65f1ce5e8ccc1840be0a4dab0099e25afccc7f2fdbda54cd65ecbac8d9a550c" + + "108b4e18d3af59129d373fde4c80848858fd6f7fc1e27387a38833473ca8a47729fa6e1" + + "cc14b584c14dad768108ff18cc6acdc9c31d32dc71c3c80856664a3fff870fe419a59aa" + + "9033356590475d36086f0b3c0ece34c0f3756675c610fb980ff3363af6f9c0918a7c677" + + "23371849de9c1026515c2900a80b3aee4f2625c8f48cd5eb967560ee8ebe58a8d41c331" + + "f6d5199795735d4f0494bdf592d166fa291062733619f0f133605087365639de2d9d5d6" + + "921f4b4204ff1f0000", + #txcount 1, pos 0, coinbase tx in an empty block, tree with height 1 + "010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d0000000" + + "0112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd66" + + "49ffff001d1e2de5650100000001112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98" + + "173ac9799a852fa39add30101", + #txcount 2, pos 1, tree with height 2 + "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc30010000000" + + "02793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b" + + "4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa2" + + "1aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a190" + + "2360baa5e4328e0105" +] + +def test(): + for proof in merkle_proof_test_vectors: + try: + electrum_proof = convert_core_to_electrum_merkle_proof(proof) + #print(electrum_proof) + implied_merkle_root = hash_merkle_root( + electrum_proof["merkle"], electrum_proof["txid"], + electrum_proof["pos"]) + assert implied_merkle_root == electrum_proof["merkleroot"] + except ValueError: + import traceback + traceback.print_exc() + assert 0 + print("All tests passed") + +''' +proof = "" +def chunks(d, n): + return [d[x:x + n] for x in range(0, len(d), n)] +#print(proof) +print("\" + \n\"".join(chunks(proof, 71))) +''' + +if __name__ == "__main__": + test() + diff --git a/electrumpersonalserver/transactionmonitor.py b/electrumpersonalserver/transactionmonitor.py @@ -0,0 +1,723 @@ + +import time, pprint, math, sys +from decimal import Decimal + +from electrumpersonalserver.jsonrpc import JsonRpcError +import electrumpersonalserver.hashes as hashes + +#internally this code uses scriptPubKeys, it only converts to bitcoin addresses +# when importing to bitcoind or checking whether enough addresses have been +# imported +#the electrum protocol uses sha256(scriptpubkey) as a key for lookups +# this code calls them scripthashes + +#code will generate the first address from each deterministic wallet +# and check whether they have been imported into the bitcoin node +# if no then initial_import_count addresses will be imported, then exit +# if yes then initial_import_count addresses will be generated and extra +# addresses will be generated one-by-one, each time checking whether they have +# been imported into the bitcoin node +# when an address has been reached that has not been imported, that means +# we've reached the end, then rewind the deterministic wallet index by one + +#when a transaction happens paying to an address from a deterministic wallet +# lookup the position of that address, if its less than gap_limit then +# import more addresses + +ADDRESSES_LABEL = "electrum-watchonly-addresses" + +def import_addresses(rpc, addrs, debug, log): + debug("importing addrs = " + str(addrs)) + log("Importing " + str(len(addrs)) + " addresses in total") + addr_i = iter(addrs) + notifications = 10 + for i in range(notifications): + pc = int(100.0 * i / notifications) + sys.stdout.write("[" + str(pc) + "%]... ") + sys.stdout.flush() + for j in range(int(len(addrs) / notifications)): + rpc.call("importaddress", [next(addr_i), ADDRESSES_LABEL, False]) + for a in addr_i: #import the reminder of addresses + rpc.call("importaddress", [a, ADDRESSES_LABEL, False]) + print("[100%]") + log("Importing done") + +class TransactionMonitor(object): + """ + Class which monitors the bitcoind wallet for new transactions + and builds a history datastructure for sending to electrum + """ + def __init__(self, rpc, deterministic_wallets, debug, log): + self.rpc = rpc + self.deterministic_wallets = deterministic_wallets + self.debug = debug + self.log = log + self.last_known_wallet_txid = None + self.address_history = None + self.unconfirmed_txes = None + + def get_electrum_history_hash(self, scrhash): + return hashes.get_status_electrum( ((h["tx_hash"], h["height"]) + for h in self.address_history[scrhash]["history"]) ) + + def get_electrum_history(self, scrhash): + if scrhash in self.address_history: + return self.address_history[scrhash]["history"] + else: + return None + + def subscribe_address(self, scrhash): + if scrhash in self.address_history: + self.address_history[scrhash]["subscribed"] = True + return True + else: + return False + + def unsubscribe_all_addresses(self): + for scrhash, his in self.address_history.items(): + his["subscribed"] = False + + def build_address_history(self, monitored_scriptpubkeys): + self.log("Building history with " + str(len(monitored_scriptpubkeys)) + + " addresses") + st = time.time() + address_history = {} + for spk in monitored_scriptpubkeys: + address_history[hashes.script_to_scripthash(spk)] = {'history': [], + 'subscribed': False} + wallet_addr_scripthashes = set(address_history.keys()) + #populate history + #which is a blockheight-ordered list of ("txhash", height) + #unconfirmed transactions go at the end as ("txhash", 0, fee) + # 0=unconfirmed -1=unconfirmed with unconfirmed parents + + BATCH_SIZE = 1000 + ret = list(range(BATCH_SIZE)) + t = 0 + count = 0 + obtained_txids = set() + while len(ret) == BATCH_SIZE: + ret = self.rpc.call("listtransactions", ["*", BATCH_SIZE, t, True]) + self.debug("listtransactions skip=" + str(t) + " len(ret)=" + + str(len(ret))) + t += len(ret) + for tx in ret: + if "txid" not in tx or "category" not in tx: + continue + if tx["category"] not in ("receive", "send"): + continue + if tx["txid"] in obtained_txids: + continue + self.debug("adding obtained tx=" + str(tx["txid"])) + obtained_txids.add(tx["txid"]) + + #obtain all the addresses this transaction is involved with + output_scriptpubkeys, input_scriptpubkeys, txd = \ + self.get_input_and_output_scriptpubkeys(tx["txid"]) + output_scripthashes = [hashes.script_to_scripthash(sc) + for sc in output_scriptpubkeys] + sh_to_add = wallet_addr_scripthashes.intersection(set( + output_scripthashes)) + input_scripthashes = [hashes.script_to_scripthash(sc) + for sc in input_scriptpubkeys] + sh_to_add |= wallet_addr_scripthashes.intersection(set( + input_scripthashes)) + if len(sh_to_add) == 0: + continue + + for wal in self.deterministic_wallets: + overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( + output_scriptpubkeys) + if overrun_depths != None: + self.log("ERROR: Not enough addresses imported.") + self.log("Delete wallet.dat and increase the value " + + "of `initial_import_count` in the file " + + "`config.cfg` then reimport and rescan") + #TODO make it so users dont have to delete wallet.dat + # check whether all initial_import_count addresses are + # imported rather than just the first one + return False + new_history_element = self.generate_new_history_element(tx, txd) + for scripthash in sh_to_add: + address_history[scripthash][ + "history"].append(new_history_element) + count += 1 + + unconfirmed_txes = {} + for scrhash, his in address_history.items(): + uctx = self.sort_address_history_list(his) + for u in uctx: + if u["tx_hash"] in unconfirmed_txes: + unconfirmed_txes[u["tx_hash"]].append(scrhash) + else: + unconfirmed_txes[u["tx_hash"]] = [scrhash] + self.debug("unconfirmed_txes = " + str(unconfirmed_txes)) + if len(ret) > 0: + #txid doesnt uniquely identify transactions from listtransactions + #but the tuple (txid, address) does + self.last_known_wallet_txid = (ret[-1]["txid"], ret[-1]["address"]) + else: + self.last_known_wallet_txid = None + self.debug("last_known_wallet_txid = " + str( + self.last_known_wallet_txid)) + + et = time.time() + self.debug("address_history =\n" + pprint.pformat(address_history)) + self.log("Found " + str(count) + " txes. History built in " + + str(et - st) + "sec") + self.address_history = address_history + self.unconfirmed_txes = unconfirmed_txes + return True + + def get_input_and_output_scriptpubkeys(self, txid): + gettx = self.rpc.call("gettransaction", [txid]) + txd = self.rpc.call("decoderawtransaction", [gettx["hex"]]) + output_scriptpubkeys = [out["scriptPubKey"]["hex"] + for out in txd["vout"]] + input_scriptpubkeys = [] + for inn in txd["vin"]: + try: + wallet_tx = self.rpc.call("gettransaction", [inn["txid"]]) + except JsonRpcError: + #wallet doesnt know about this tx, so the input isnt ours + continue + input_decoded = self.rpc.call("decoderawtransaction", [wallet_tx[ + "hex"]]) + script = input_decoded["vout"][inn["vout"]]["scriptPubKey"]["hex"] + input_scriptpubkeys.append(script) + return output_scriptpubkeys, input_scriptpubkeys, txd + + def generate_new_history_element(self, tx, txd): + if tx["confirmations"] == 0: + unconfirmed_input = False + total_input_value = 0 + for inn in txd["vin"]: + utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"], + True]) + if utxo is None: + utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"], + False]) + if utxo is None: + self.debug("utxo not found(!)") + #TODO detect this and figure out how to tell + # electrum that we dont know the fee + total_input_value += int(Decimal(utxo["value"]) * Decimal(1e8)) + unconfirmed_input = (unconfirmed_input or + utxo["confirmations"] == 0) + self.debug("total_input_value = " + str(total_input_value)) + + fee = total_input_value - sum([int(Decimal(out["value"]) + * Decimal(1e8)) for out in txd["vout"]]) + height = -1 if unconfirmed_input else 0 + new_history_element = ({"tx_hash": tx["txid"], "height": height, + "fee": fee}) + else: + blockheader = self.rpc.call("getblockheader", [tx['blockhash']]) + new_history_element = ({"tx_hash": tx["txid"], + "height": blockheader["height"]}) + return new_history_element + + def sort_address_history_list(self, his): + unconfirm_txes = list(filter(lambda h:h["height"] == 0, his["history"])) + confirm_txes = filter(lambda h:h["height"] != 0, his["history"]) + #TODO txes must be "in blockchain order" + # the order they appear in the block + # it might be "blockindex" in listtransactions and gettransaction + #so must sort with key height+':'+blockindex + #maybe check if any heights are the same then get the pos only for those + #better way to do this is to have a separate dict that isnt in history + # which maps txid => blockindex + # and then sort by key height+":"+idx[txid] + his["history"] = sorted(confirm_txes, key=lambda h:h["height"]) + his["history"].extend(unconfirm_txes) + return unconfirm_txes + + def check_for_updated_txes(self): + updated_scrhashes1 = self.check_for_new_txes() + updated_scrhashes2 = self.check_for_confirmations() + updated_scrhashes = updated_scrhashes1 | updated_scrhashes2 + for ush in updated_scrhashes: + his = self.address_history[ush] + self.sort_address_history_list(his) + if len(updated_scrhashes) > 0: + self.debug("new tx address_history =\n" + + pprint.pformat(self.address_history)) + self.debug("unconfirmed txes = " + + pprint.pformat(self.unconfirmed_txes)) + self.debug("updated_scripthashes = " + str(updated_scrhashes)) + updated_scrhashes = filter(lambda sh:self.address_history[sh][ + "subscribed"], updated_scrhashes) + return updated_scrhashes + + def check_for_confirmations(self): + confirmed_txes_scrhashes = [] + self.debug("check4con unconfirmed_txes = " + + pprint.pformat(self.unconfirmed_txes)) + for uc_txid, scrhashes in self.unconfirmed_txes.items(): + tx = self.rpc.call("gettransaction", [uc_txid]) + self.debug("uc_txid=" + uc_txid + " => " + str(tx)) + if tx["confirmations"] == 0: + continue #still unconfirmed + self.log("A transaction confirmed: " + uc_txid) + confirmed_txes_scrhashes.append((uc_txid, scrhashes)) + block = self.rpc.call("getblockheader", [tx["blockhash"]]) + for scrhash in scrhashes: + #delete the old unconfirmed entry in address_history + deleted_entries = [h for h in self.address_history[scrhash][ + "history"] if h["tx_hash"] == uc_txid] + for d_his in deleted_entries: + self.address_history[scrhash]["history"].remove(d_his) + #create the new confirmed entry in address_history + self.address_history[scrhash]["history"].append({"height": + block["height"], "tx_hash": uc_txid}) + updated_scrhashes = set() + for tx, scrhashes in confirmed_txes_scrhashes: + del self.unconfirmed_txes[tx] + updated_scrhashes.update(set(scrhashes)) + return updated_scrhashes + + def check_for_new_txes(self): + MAX_TX_REQUEST_COUNT = 256 + tx_request_count = 2 + max_attempts = int(math.log(MAX_TX_REQUEST_COUNT, 2)) + for i in range(max_attempts): + self.debug("listtransactions tx_request_count=" + + str(tx_request_count)) + ret = self.rpc.call("listtransactions", ["*", tx_request_count, 0, + True]) + ret = ret[::-1] + if self.last_known_wallet_txid == None: + recent_tx_index = len(ret) #=0 means no new txes + break + else: + txid_list = [(tx["txid"], tx["address"]) for tx in ret] + recent_tx_index = next((i for i, (txid, addr) + in enumerate(txid_list) if + txid == self.last_known_wallet_txid[0] and + addr == self.last_known_wallet_txid[1]), -1) + if recent_tx_index != -1: + break + tx_request_count *= 2 + + #TODO low priority: handle a user getting more than 255 new + # transactions in 15 seconds + self.debug("recent tx index = " + str(recent_tx_index) + " ret = " + + str([(t["txid"], t["address"]) for t in ret])) + if len(ret) > 0: + self.last_known_wallet_txid = (ret[0]["txid"], ret[0]["address"]) + self.debug("last_known_wallet_txid = " + str( + self.last_known_wallet_txid)) + assert(recent_tx_index != -1) + if recent_tx_index == 0: + return set() + new_txes = ret[:recent_tx_index][::-1] + self.debug("new txes = " + str(new_txes)) + obtained_txids = set() + updated_scripthashes = [] + for tx in new_txes: + if "txid" not in tx or "category" not in tx: + continue + if tx["category"] not in ("receive", "send"): + continue + if tx["txid"] in obtained_txids: + continue + obtained_txids.add(tx["txid"]) + output_scriptpubkeys, input_scriptpubkeys, txd = \ + self.get_input_and_output_scriptpubkeys(tx["txid"]) + matching_scripthashes = [] + for spk in (output_scriptpubkeys + input_scriptpubkeys): + scripthash = hashes.script_to_scripthash(spk) + if scripthash in self.address_history: + matching_scripthashes.append(scripthash) + if len(matching_scripthashes) == 0: + continue + + for wal in self.deterministic_wallets: + overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( + output_scriptpubkeys) + if overrun_depths != None: + for change, import_count in overrun_depths.items(): + spks = wal.get_new_scriptpubkeys(change, import_count) + for spk in spks: + self.address_history[hashes.script_to_scripthash( + spk)] = {'history': [], 'subscribed': False} + new_addrs = [hashes.script_to_address(s, self.rpc) + for s in spks] + self.debug("importing " + str(len(spks)) + + " into change=" + str(change)) + import_addresses(self.rpc, new_addrs, self.debug, + self.log) + + updated_scripthashes.extend(matching_scripthashes) + new_history_element = self.generate_new_history_element(tx, txd) + self.log("Found new tx: " + str(new_history_element)) + for scrhash in matching_scripthashes: + self.address_history[scrhash]["history"].append( + new_history_element) + if new_history_element["height"] == 0: + if tx["txid"] in self.unconfirmed_txes: + self.unconfirmed_txes[tx["txid"]].append(scrhash) + else: + self.unconfirmed_txes[tx["txid"]] = [scrhash] + #check whether gap limits have been overrun and import more addrs + return set(updated_scripthashes) + + +## start tests here + +class TestJsonRpc(object): + def __init__(self, txlist, utxoset, block_heights): + self.txlist = txlist + self.utxoset = utxoset + self.block_heights = block_heights + self.imported_addresses = [] + + def call(self, method, params): + if method == "listtransactions": + count = int(params[1]) + skip = int(params[2]) + return self.txlist[skip:skip + count] + elif method == "gettransaction": + for t in self.txlist: + if t["txid"] == params[0]: + return t + raise JsonRpcError({"code": None, "message": None}) + elif method == "decoderawtransaction": + for t in self.txlist: + if t["hex"] == params[0]: + return t + assert 0 + elif method == "gettxout": + for u in self.utxoset: + if u["txid"] == params[0] and u["vout"] == params[1]: + return u + assert 0 + elif method == "getblockheader": + if params[0] not in self.block_heights: + assert 0 + return {"height": self.block_heights[params[0]]} + elif method == "decodescript": + return {"addresses": [test_spk_to_address(params[0])]} + elif method == "importaddress": + self.imported_addresses.append(params[0]) + else: + raise ValueError("unknown method in test jsonrpc") + + def add_transaction(self, tx): + self.txlist.append(tx) + + def get_imported_addresses(self): + return self.imported_addresses + +from electrumpersonalserver.deterministicwallet import DeterministicWallet + +class TestDeterministicWallet(DeterministicWallet): + """Empty deterministic wallets""" + def __init__(self): + pass + + def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): + return None #not overrun + + def get_new_scriptpubkeys(self, change, count): + pass + +def test_spk_to_address(spk): + return spk + "-address" + +def assert_address_history_tx(address_history, spk, height, txid, subscribed): + history_element = address_history[hashes.script_to_scripthash(spk)] + assert history_element["history"][0]["height"] == height + assert history_element["history"][0]["tx_hash"] == txid + #fee always zero, its easier to test because otherwise you have + # to use Decimal to stop float weirdness + if height == 0: + assert history_element["history"][0]["fee"] == 0 + assert history_element["subscribed"] == subscribed + +def test(): + debugf = lambda x: x + logf = lambda x: x + #empty deterministic wallets + deterministic_wallets = [TestDeterministicWallet()] + test_spk1 = "deadbeefdeadbeefdeadbeefdeadbeef" + test_containing_block1 = "blockhash-placeholder1" + test_paying_in_tx1 = { + "txid": "placeholder-test-txid1", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk1}}], + "address": test_spk_to_address(test_spk1), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block1, + "hex": "placeholder-test-txhex1" + } + test_spk2 = "deadbeefdeadbeefdeadbeef" + test_containing_block2 = "blockhash-placeholder2" + test_paying_in_tx2 = { + "txid": "placeholder-test-txid2", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk2}}], + "address": test_spk_to_address(test_spk2), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block2, + "hex": "placeholder-test-txhex2" + } + + ###single confirmed tx in wallet belonging to us, address history built + rpc = TestJsonRpc([test_paying_in_tx1], [], + {test_containing_block1: 420000}) + txmonitor1 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor1.build_address_history([test_spk1]) + assert len(txmonitor1.address_history) == 1 + assert_address_history_tx(txmonitor1.address_history, spk=test_spk1, + height=420000, txid=test_paying_in_tx1["txid"], subscribed=False) + + ###two confirmed txes in wallet belonging to us, addr history built + rpc = TestJsonRpc([test_paying_in_tx1, test_paying_in_tx2], [], + {test_containing_block1: 1, test_containing_block2: 2}) + deterministic_wallets = [TestDeterministicWallet()] + txmonitor2 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor2.build_address_history([test_spk1, test_spk2]) + assert len(txmonitor2.address_history) == 2 + assert_address_history_tx(txmonitor2.address_history, spk=test_spk1, + height=1, txid=test_paying_in_tx1["txid"], subscribed=False) + assert_address_history_tx(txmonitor2.address_history, spk=test_spk2, + height=2, txid=test_paying_in_tx2["txid"], subscribed=False) + + ###one unconfirmed tx in wallet belonging to us, with confirmed inputs, + ### addr history built, then tx confirms, not subscribed to address + test_spk3 = "deadbeefdeadbeef" + test_containing_block3 = "blockhash-placeholder3" + input_utxo3 = {"txid": "placeholder-unknown-input-txid", "vout": 0, + "value": 1, "confirmations": 1} + test_paying_in_tx3 = { + "txid": "placeholder-test-txid3", + "vin": [input_utxo3], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk3}}], + "address": test_spk_to_address(test_spk3), + "category": "receive", + "confirmations": 0, + "blockhash": test_containing_block3, + "hex": "placeholder-test-txhex3" + } + rpc = TestJsonRpc([test_paying_in_tx3], [input_utxo3], + {test_containing_block3: 10}) + txmonitor3 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor3.build_address_history([test_spk3]) + assert len(txmonitor3.address_history) == 1 + assert_address_history_tx(txmonitor3.address_history, spk=test_spk3, + height=0, txid=test_paying_in_tx3["txid"], subscribed=False) + assert len(list(txmonitor3.check_for_updated_txes())) == 0 + test_paying_in_tx3["confirmations"] = 1 #tx confirms + #not subscribed so still only returns an empty list + assert len(list(txmonitor3.check_for_updated_txes())) == 0 + assert_address_history_tx(txmonitor3.address_history, spk=test_spk3, + height=10, txid=test_paying_in_tx3["txid"], subscribed=False) + + ###build empty address history, subscribe one address + ### an unconfirmed tx appears, then confirms + test_spk4 = "deadbeefdeadbeefaa" + test_containing_block4 = "blockhash-placeholder4" + input_utxo4 = {"txid": "placeholder-unknown-input-txid", "vout": 0, + "value": 1, "confirmations": 1} + test_paying_in_tx4 = { + "txid": "placeholder-test-txid4", + "vin": [input_utxo4], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk4}}], + "address": test_spk_to_address(test_spk4), + "category": "receive", + "confirmations": 0, + "blockhash": test_containing_block4, + "hex": "placeholder-test-txhex4" + } + rpc = TestJsonRpc([], [input_utxo4], {test_containing_block4: 10}) + txmonitor4 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor4.build_address_history([test_spk4]) + assert len(txmonitor4.address_history) == 1 + sh4 = hashes.script_to_scripthash(test_spk4) + assert len(txmonitor4.get_electrum_history(sh4)) == 0 + txmonitor4.subscribe_address(sh4) + # unconfirm transaction appears + assert len(list(txmonitor4.check_for_updated_txes())) == 0 + rpc.add_transaction(test_paying_in_tx4) + assert len(list(txmonitor4.check_for_updated_txes())) == 1 + assert_address_history_tx(txmonitor4.address_history, spk=test_spk4, + height=0, txid=test_paying_in_tx4["txid"], subscribed=True) + # transaction confirms + test_paying_in_tx4["confirmations"] = 1 + assert len(list(txmonitor4.check_for_updated_txes())) == 1 + assert_address_history_tx(txmonitor4.address_history, spk=test_spk4, + height=10, txid=test_paying_in_tx4["txid"], subscribed=True) + + ###transaction that has nothing to do with our wallet + test_spk5 = "deadbeefdeadbeefbb" + test_containing_block5 = "blockhash-placeholder5" + test_paying_in_tx5 = { + "txid": "placeholder-test-txid5", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk5}}], + "address": test_spk_to_address(test_spk5), + "category": "receive", + "confirmations": 0, + "blockhash": test_containing_block5, + "hex": "placeholder-test-txhex5" + } + test_spk5_1 = "deadbeefdeadbeefcc" + rpc = TestJsonRpc([test_paying_in_tx5], [], {test_containing_block4: 10}) + txmonitor5 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor5.build_address_history([test_spk5_1]) + assert len(txmonitor5.address_history) == 1 + assert len(txmonitor5.get_electrum_history(hashes.script_to_scripthash( + test_spk5_1))) == 0 + + ###transaction which arrives to an address which already has a tx on it + test_spk6 = "deadbeefdeadbeefdd" + test_containing_block6 = "blockhash-placeholder6" + test_paying_in_tx6 = { + "txid": "placeholder-test-txid6", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}], + "address": test_spk_to_address(test_spk6), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block6, + "hex": "placeholder-test-txhex6" + } + test_paying_in_tx6_1 = { + "txid": "placeholder-test-txid6_1", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}], + "address": test_spk_to_address(test_spk6), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block6, + "hex": "placeholder-test-txhex6" + } + rpc = TestJsonRpc([test_paying_in_tx6], [], {test_containing_block6: 10}) + txmonitor6 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor6.build_address_history([test_spk6]) + sh = hashes.script_to_scripthash(test_spk6) + assert len(txmonitor6.get_electrum_history(sh)) == 1 + rpc.add_transaction(test_paying_in_tx6_1) + assert len(txmonitor6.get_electrum_history(sh)) == 1 + txmonitor6.check_for_updated_txes() + assert len(txmonitor6.get_electrum_history(sh)) == 2 + + ###transaction spending FROM one of our addresses + test_spk7 = "deadbeefdeadbeefee" + test_input_containing_block7 = "blockhash-input-placeholder7" + test_input_tx7 = { + "txid": "placeholder-input-test-txid7", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk7}}], + "address": test_spk_to_address(test_spk7), + "category": "send", + "confirmations": 2, + "blockhash": test_input_containing_block7, + "hex": "placeholder-input-test-txhex7" + } + test_containing_block7 = "blockhash-placeholder7" + test_paying_from_tx7 = { + "txid": "placeholder-test-txid7", + "vin": [{"txid": test_input_tx7["txid"], "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": "deadbeef"}}], + "address": test_spk_to_address(test_spk7), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block7, + "hex": "placeholder-test-txhex7" + } + rpc = TestJsonRpc([test_input_tx7, test_paying_from_tx7], [], + {test_containing_block7: 9, test_input_containing_block7: 8}) + txmonitor7 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor7.build_address_history([test_spk7]) + sh = hashes.script_to_scripthash(test_spk7) + assert len(txmonitor7.get_electrum_history(sh)) == 2 + + ###transaction from one address to the other, both addresses in wallet + test_spk8 = "deadbeefdeadbeefee" + test_spk8_1 = "deadbeefdeadbeefff" + test_input_containing_block8 = "blockhash-input-placeholder8" + test_input_tx8 = { + "txid": "placeholder-input-test-txid8", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8}}], + "address": test_spk_to_address(test_spk8), + "category": "send", + "confirmations": 2, + "blockhash": test_input_containing_block8, + "hex": "placeholder-input-test-txhex8" + } + test_containing_block8 = "blockhash-placeholder8" + test_paying_from_tx8 = { + "txid": "placeholder-test-txid8", + "vin": [{"txid": test_input_tx8["txid"], "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8_1}}], + "address": test_spk_to_address(test_spk8), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block8, + "hex": "placeholder-test-txhex8" + } + rpc = TestJsonRpc([test_input_tx8, test_paying_from_tx8], [], + {test_containing_block8: 9, test_input_containing_block8: 8}) + txmonitor8 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) + assert txmonitor8.build_address_history([test_spk8, test_spk8_1]) + assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash( + test_spk8))) == 2 + assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash( + test_spk8_1))) == 1 + + ###overrun gap limit so import address is needed + test_spk9 = "deadbeefdeadbeef00" + test_containing_block9 = "blockhash-placeholder9" + test_paying_in_tx9 = { + "txid": "placeholder-test-txid9", + "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], + "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk9}}], + "address": test_spk_to_address(test_spk9), + "category": "receive", + "confirmations": 1, + "blockhash": test_containing_block9, + "hex": "placeholder-test-txhex9" + } + test_spk9_imported = "deadbeefdeadbeef11" + class TestImportDeterministicWallet(DeterministicWallet): + def __init__(self): + pass + + def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): + return {0: 1} #overrun by one + + def get_new_scriptpubkeys(self, change, count): + return [test_spk9_imported] + + rpc = TestJsonRpc([], [], {test_containing_block9: 10}) + txmonitor9 = TransactionMonitor(rpc, [TestImportDeterministicWallet()], + debugf, logf) + assert txmonitor9.build_address_history([test_spk9]) + assert len(txmonitor9.address_history) == 1 + assert len(list(txmonitor9.check_for_updated_txes())) == 0 + assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( + test_spk9))) == 0 + rpc.add_transaction(test_paying_in_tx9) + assert len(list(txmonitor9.check_for_updated_txes())) == 0 + assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( + test_spk9))) == 1 + assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( + test_spk9_imported))) == 0 + assert len(rpc.get_imported_addresses()) == 1 + assert rpc.get_imported_addresses()[0] == test_spk_to_address( + test_spk9_imported) + + #other possible stuff to test: + #finding confirmed and unconfirmed tx, in that order, then both confirm + #finding unconfirmed and confirmed tx, in that order, then both confirm + + print("\nAll tests passed") + +if __name__ == "__main__": + test() + diff --git a/merkleproof.py b/merkleproof.py @@ -1,270 +0,0 @@ - -import bitcoin as btc -import binascii -from math import ceil, log - -from hashes import hash_encode, hash_decode, Hash, hash_merkle_root - -#lots of ideas and code taken from bitcoin core and breadwallet -#https://github.com/bitcoin/bitcoin/blob/master/src/merkleblock.h -#https://github.com/breadwallet/breadwallet-core/blob/master/BRMerkleBlock.c - -def calc_tree_width(height, txcount): - """Efficently calculates the number of nodes at given merkle tree height""" - return (txcount + (1 << height) - 1) >> height - -def decend_merkle_tree(hashes, flags, height, txcount, pos): - """Function recursively follows the flags bitstring down into the - tree, building up a tree in memory""" - flag = next(flags) - if height > 0: - #non-txid node - if flag: - left = decend_merkle_tree(hashes, flags, height-1, txcount, pos*2) - #bitcoin's merkle tree format has a rule that if theres an - # odd number of nodes in then the tree, the last hash is duplicated - #in the electrum format we must hash together the duplicate - # tree branch - if pos*2+1 < calc_tree_width(height-1, txcount): - right = decend_merkle_tree(hashes, flags, height-1, - txcount, pos*2+1) - else: - if isinstance(left, tuple): - right = expand_tree_hashing(left) - else: - right = left - return (left, right) - else: - hs = next(hashes) - return hs - else: - #txid node - hs = next(hashes) - if flag: - #for the actual transaction, also store its position with a flag - return "tx:" + str(pos) + ":" + hs - else: - return hs - -def deserialize_core_format_merkle_proof(hash_list, flag_value, txcount): - """Converts core's format for a merkle proof into a tree in memory""" - tree_depth = int(ceil(log(txcount, 2))) - hashes = iter(hash_list) - #one-liner which converts the flags value to a list of True/False bits - flags = (flag_value[i//8]&1 << i%8 != 0 for i in range(len(flag_value)*8)) - try: - root_node = decend_merkle_tree(hashes, flags, tree_depth, txcount, 0) - return root_node - except StopIteration: - raise ValueError - -def expand_tree_electrum_format_merkle_proof(node, result): - """Recurse down into the tree, adding hashes to the result list - in depth order""" - left, right = node - if isinstance(left, tuple): - expand_tree_electrum_format_merkle_proof(left, result) - if isinstance(right, tuple): - expand_tree_electrum_format_merkle_proof(right, result) - if not isinstance(left, tuple): - result.append(left) - if not isinstance(right, tuple): - result.append(right) - -def get_node_hash(node): - if node.startswith("tx"): - return node.split(":")[2] - else: - return node - -def expand_tree_hashing(node): - """Recurse down into the tree, hashing everything and - returning root hash""" - left, right = node - if isinstance(left, tuple): - hash_left = expand_tree_hashing(left) - else: - hash_left = get_node_hash(left) - if isinstance(right, tuple): - hash_right = expand_tree_hashing(right) - else: - hash_right = get_node_hash(right) - return hash_encode(Hash(hash_decode(hash_left) + hash_decode(hash_right))) - -def convert_core_to_electrum_merkle_proof(proof): - """Bitcoin Core and Electrum use different formats for merkle - proof, this function converts from Core's format to Electrum's format""" - proof = binascii.unhexlify(proof) - pos = [0] - def read_as_int(bytez): - pos[0] += bytez - return btc.decode(proof[pos[0] - bytez:pos[0]][::-1], 256) - def read_var_int(): - pos[0] += 1 - val = btc.from_byte_to_int(proof[pos[0] - 1]) - if val < 253: - return val - return read_as_int(pow(2, val - 252)) - def read_bytes(bytez): - pos[0] += bytez - return proof[pos[0] - bytez:pos[0]] - - merkle_root = proof[36:36+32] - pos[0] = 80 - txcount = read_as_int(4) - hash_count = read_var_int() - hashes = [hash_encode(read_bytes(32)) for i in range(hash_count)] - flags_count = read_var_int() - flags = read_bytes(flags_count) - - root_node = deserialize_core_format_merkle_proof(hashes, flags, txcount) - #check special case of a tree of zero height, block with only coinbase tx - if not isinstance(root_node, tuple): - root_node = root_node[5:] #remove the "tx:0:" - result = {"pos": 0, "merkle": [], "txid": root_node, - "merkleroot": hash_encode(merkle_root)} - return result - - hashes_list = [] - expand_tree_electrum_format_merkle_proof(root_node, hashes_list) - #remove the first or second element which is the txhash - tx = hashes_list[0] - if hashes_list[1].startswith("tx"): - tx = hashes_list[1] - assert(tx.startswith("tx")) - hashes_list.remove(tx) - #if the txhash was duplicated, that _is_ included in electrum's format - if hashes_list[0].startswith("tx"): - hashes_list[0] = tx.split(":")[2] - tx_pos, txid = tx.split(":")[1:3] - tx_pos = int(tx_pos) - result = {"pos": tx_pos, "merkle": hashes_list, "txid": txid, - "merkleroot": hash_encode(merkle_root)} - return result - -merkle_proof_test_vectors = [ - #txcount 819, pos 5 - "0300000026e696fba00f0a43907239305eed9e55824e0e376636380f000000000000000" + - "04f8a2ce51d6c69988029837688cbfc2f580799fa1747456b9c80ab808c1431acd0b07f" + - "5543201618cadcfbf7330300000b0ff1e0050fed22ca360e0935e053b0fe098f6f9e090" + - "f5631013361620d964fe2fd88544ae10b40621e1cd24bb4306e3815dc237f77118a45d7" + - "5ada9ee362314b70573732bce59615a3bcc1bbacd04b33b7819198212216b5d62d75be5" + - "9221ada17ba4fb2476b689cccd3be54732fd5630832a94f11fa3f0dafd6f904d43219e0" + - "d7de110158446b5b598bd241f7b5df4da0ebc7d30e7748d487917b718df51c681174e6a" + - "bab8042cc7c1c436221c098f06a56134f9247a812126d675d69c82ba1c715cfc0cde462" + - "fd1fbe5dc87f6b8db2b9c060fcd59a20e7fe8e921c3676937a873ff88684f4be4d015f2" + - "4f26af6d2cf78335e9218bcceba4507d0b4ba6cb933aa01ef77ae5eb411893ec0f74b69" + - "590fb0f5118ac937c02ccd47e9d90be78becd11ecf854d7d268eeb479b74d137278c0a5" + - "017d29e90cd5b35a4680201824fb0eb4f404e20dfeaec4d50549030b7e7e220b02eb210" + - "5f3d2e8bcc94d547214a9d03ff1600", - #txcount 47, pos 9 - "0100000053696a625fbd16df418575bce0c4148886c422774fca5fcab80100000000000" + - "01532bfe4f9c4f56cd141028e5b59384c133740174b74b1982c7f01020b90ce05577c67" + - "508bdb051a7ec2ef942f000000076cde2eb7efa90b36d48aed612e559ff2ba638d8d400" + - "b14b0c58df00c6a6c33b65dc8fa02f4ca56e1f4dcf17186fa9bbd990ce150b6e2dc9e9e" + - "56bb4f270fe56fde6bdd73a7a7e82767714862888e6b759568fb117674ad23050e29311" + - "97494d457efb72efdb9cb79cd4a435724908a0eb31ec7f7a67ee03837319e098b43edad" + - "3be9af75ae7b30db6f4f93ba0fdd941fdf70fe8cc38982e03bd292f5bd02f28137d343f" + - "908c7d6417379afe8349a257af3ca1f74f623be6a416fe1aa96a8f259983f2cf32121bc" + - "e203955a378b3b44f132ea6ab94c7829a6c3b360c9f8da8e74027701", - #txcount 2582, pos 330 - "000000206365d5e1d8b7fdf0b846cfa902115c1b8ced9dd49cb17800000000000000000" + - "01032e829e1f9a5a09d0492f9cd3ec0762b7facea555989c3927d3d975fd4078c771849" + - "5a45960018edd3b9e0160a00000dfe856a7d5d77c23ebf85c68b5eb303d85e56491ed6d" + - "204372625d0b4383df5a44d6e46d2db09d936b9f5d0b53e0dbcb3efb7773d457369c228" + - "fd1ce6e11645e366a58b3fc1e8a7c916710ce29a87265a6729a3b221b47ea9c8e6f4870" + - "7b112b8d67e5cfb3db5f88b042dc49e4e5bc2e61c28e1e0fbcba4c741bb5c75cac58ca0" + - "4161a7377d70f3fd19a3e248ed918c91709b49afd3760f89ed2fefbcc9c23447ccb40a2" + - "be7aba22b07189b0bf90c62db48a9fe37227e12c7af8c1d4c22f9f223530dacdd5f3ad8" + - "50ad4badf16cc24049a65334f59bf28c15cecda1a4cf3f2937bd70ee84f66569ce8ef95" + - "1d50cca46d60337e6c697685b38ad217967bbe6801d03c44fcb808cd035be31888380a2" + - "df1be14b6ff100de83cab0dce250e2b40ca3b47e8309f848646bee63b6185c176d84f15" + - "46a482e7a65a87d1a2d0d5a2b683e2cae0520df1e3525a71d71e1f551abd7d238c3bcb4" + - "ecaeea7d5988745fa421a8604a99857426957a2ccfa7cd8df145aa8293701989dd20750" + - "5923fcb339843944ce3d21dc259bcda9c251ed90d4e55af2cf5b15432050084f513ac74" + - "c0bdd4b6046fb70100", - #txcount 2861, pos 2860, last tx with many duplicated nodes down the tree - "00000020c656c90b521a2bbca14174f2939b882a28d23d86144b0e00000000000000000" + - "0cf5185a8e369c3de5f15e039e777760994fd66184b619d839dace3aec9953fd6d86159" + - "5ac1910018ee097a972d0b0000078d20d71c3114dbf52bb13f2c18f987891e8854d2d29" + - "f61c0b3d3256afcef7c0b1e6f76d6431f93390ebd28dbb81ad7c8f08459e85efeb23cc7" + - "2df2c5612215bf53dd4ab3703886bc8c82cb78ba761855e495fb5dc371cd8fe25ae974b" + - "df42269e267caf898a9f34cbf2350eaaa4afbeaea70636b5a3b73682186817db5b33290" + - "bd5c696bd8d0322671ff70c5447fcd7bdc127e5b84350c6b14b5a3b86b424d7db38d39f" + - "171f57e255a31c6c53415e3d65408b6519a40aacc49cad8e70646d4cb0d23d4a63068e6" + - "c220efc8a2781e9e774efdd334108d7453043bd3c8070d0e5903ad5b07", - #txcount 7, pos 6, duplicated entry in the last depth, at tx level - "0100000056e02c6d3278c754e0699517834741f7c4ad3dcbfeb7803a346200000000000" + - "0af3bdd5dd465443fd003e9281455e60aae573dd4d46304d7ba17276ea33d506488cbb4" + - "4dacb5001b9ebb193b0700000003cd3abb2eb7583165f36b56add7268be9027ead4cc8f" + - "888ec650d3b1c1f4de28a0ff7c8b463b2042d09598f0e5e5905de362aa1cf75252adc22" + - "719b8e1bc969adcfbc4782b8eafc9352263770b91a0f189ae051cbe0e26046c2b14cf3d" + - "8be0bc40135", - #txcount 6, pos 5, duplicated entry in the last-but-one depth - "01000000299edfd28524eae4fb6012e4087afdb6e1b912db85e612374b0300000000000" + - "0e16572394f8578a47bf36e15cd16faa5e3b9e18805cf4e271ae4ef95aa8cea7eb31fa1" + - "4e4b6d0b1a42857d960600000003f52b45ed953924366dab3e92707145c78615b639751" + - "ecb7be1c5ecc09b592ed588ca0e15a89e9049a2dbcadf4d8362bd1f74a6972f176617b5" + - "8a5466c8a4121fc3e2d6fa66c8637b387ef190ab46d6e9c9dae4bbccd871c72372b3dbc" + - "6edefea012d", - #txcount 5, pos 4, duplicated on the last and second last depth - "010000004d891de57908d89e9e5585648978d7650adc67e856c2d8c18c1800000000000" + - "04746fd317bffecd4ffb320239caa06685bafe0c1b5463b24d636e45788796657843d1b" + - "4d4c86041be68355c40500000002d8e26c89c46477f2407d866d2badbd98e43e732a670" + - "e96001faf1744b27e5fdd018733d72e31a2d6a0d94f2a3b35fcc66fb110c40c5bbff82b" + - "f87606553d541d011d", - #txcount 2739, pos 0, coinbase tx - "000000209f283da030c6e6d0ff5087a87c430d140ed6b4564fa34d00000000000000000" + - "0ec1513723e3652c6b8e777c41eb267ad8dd2025e85228840f5cfca7ffe1fb331afff8a" + - "5af8e961175e0f7691b30a00000df403e21a4751fbd52457f535378ac2dcf111199e9ea" + - "6f78f6c2663cb99b58203438d8f3b26f7f2804668c1df7d394a4726363d4873b2d85b71" + - "2e44cf4f5e4f33f22a8f3a1672846bd7c4570c668e6ee12befda23bfa3d0fcd30b1b079" + - "19b01c40b1e31b6d34fcdbb99539d46eb97a3ae15386f1ab0f28ecacadd9fc3fa4ce49a" + - "1a1839d815229f54036c8a3035d91e80e8dc127b62032b4e652550b4fc0aee0f6e85a14" + - "307d85ed9dde62acff9a0f7e3b52370a10d6c83ec13a0b4a8fafe87af368a167d7e9b63" + - "3b84b6ea65f1ce5e8ccc1840be0a4dab0099e25afccc7f2fdbda54cd65ecbac8d9a550c" + - "108b4e18d3af59129d373fde4c80848858fd6f7fc1e27387a38833473ca8a47729fa6e1" + - "cc14b584c14dad768108ff18cc6acdc9c31d32dc71c3c80856664a3fff870fe419a59aa" + - "9033356590475d36086f0b3c0ece34c0f3756675c610fb980ff3363af6f9c0918a7c677" + - "23371849de9c1026515c2900a80b3aee4f2625c8f48cd5eb967560ee8ebe58a8d41c331" + - "f6d5199795735d4f0494bdf592d166fa291062733619f0f133605087365639de2d9d5d6" + - "921f4b4204ff1f0000", - #txcount 1, pos 0, coinbase tx in an empty block, tree with height 1 - "010000000508085c47cc849eb80ea905cc7800a3be674ffc57263cf210c59d8d0000000" + - "0112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98173ac9799a852fa39add320cd66" + - "49ffff001d1e2de5650100000001112ba175a1e04b14ba9e7ea5f76ab640affeef5ec98" + - "173ac9799a852fa39add30101", - #txcount 2, pos 1, tree with height 2 - "010000004e24a2880cd72d9bde7502087bd3756819794dc7548f68dd68dc30010000000" + - "02793fce9cdf91b4f84760571bf6009d5f0ffaddbfdc9234ef58a036096092117b10f4b" + - "4cfd68011c903e350b0200000002ee50562fc6f995eff2df61be0d5f943bac941149aa2" + - "1aacb32adc130c0f17d6a2077a642b1eabbc5120e31566a11e2689aa4d39b01cce9a190" + - "2360baa5e4328e0105" -] - -def test(): - for proof in merkle_proof_test_vectors: - try: - electrum_proof = convert_core_to_electrum_merkle_proof(proof) - #print(electrum_proof) - implied_merkle_root = hash_merkle_root( - electrum_proof["merkle"], electrum_proof["txid"], - electrum_proof["pos"]) - assert implied_merkle_root == electrum_proof["merkleroot"] - except ValueError: - import traceback - traceback.print_exc() - assert 0 - print("All tests passed") - -''' -proof = "" -def chunks(d, n): - return [d[x:x + n] for x in range(0, len(d), n)] -#print(proof) -print("\" + \n\"".join(chunks(proof, 71))) -''' - -if __name__ == "__main__": - test() - diff --git a/rescan-script.py b/rescan-script.py @@ -1,7 +1,7 @@ #! /usr/bin/python3 from configparser import ConfigParser, NoSectionError -from jsonrpc import JsonRpc, JsonRpcError +from electrumpersonalserver.jsonrpc import JsonRpc, JsonRpcError from datetime import datetime import server diff --git a/server.py b/server.py @@ -1,13 +1,13 @@ #! /usr/bin/python3 import socket, time, json, datetime, struct, binascii, ssl, os.path, platform -import sys from configparser import ConfigParser, NoSectionError -from jsonrpc import JsonRpc, JsonRpcError -import hashes, merkleproof, deterministicwallet, transactionmonitor - -ADDRESSES_LABEL = "electrum-watchonly-addresses" +from electrumpersonalserver.jsonrpc import JsonRpc, JsonRpcError +import electrumpersonalserver.hashes as hashes +import electrumpersonalserver.merkleproof as merkleproof +import electrumpersonalserver.deterministicwallet as deterministicwallet +import electrumpersonalserver.transactionmonitor as transactionmonitor VERSION_NUMBER = "0.1" @@ -304,7 +304,7 @@ def run_electrum_server(hostport, rpc, txmonitor, poll_interval_listening, def get_scriptpubkeys_to_monitor(rpc, config): imported_addresses = set(rpc.call("getaddressesbyaccount", - [ADDRESSES_LABEL])) + [transactionmonitor.ADDRESSES_LABEL])) deterministic_wallets = [] for key in config.options("master-public-keys"): @@ -373,22 +373,6 @@ def get_scriptpubkeys_to_monitor(rpc, config): for addr in watch_only_addresses]) return False, spks_to_monitor, deterministic_wallets -def import_addresses(rpc, addrs): - debug("importing addrs = " + str(addrs)) - log("Importing " + str(len(addrs)) + " addresses in total") - addr_i = iter(addrs) - notifications = 10 - for i in range(notifications): - pc = int(100.0 * i / notifications) - sys.stdout.write("[" + str(pc) + "%]... ") - sys.stdout.flush() - for j in range(int(len(addrs) / notifications)): - rpc.call("importaddress", [next(addr_i), ADDRESSES_LABEL, False]) - for a in addr_i: #import the reminder of addresses - rpc.call("importaddress", [a, ADDRESSES_LABEL, False]) - print("[100%]") - log("Importing done") - def obtain_rpc_username_password(datadir): if len(datadir.strip()) == 0: debug("no datadir configuration, checking in default location") @@ -446,7 +430,8 @@ def main(): import_needed, relevant_spks_addrs, deterministic_wallets = \ get_scriptpubkeys_to_monitor(rpc, config) if import_needed: - import_addresses(rpc, relevant_spks_addrs) + transactionmonitor.import_addresses(rpc, relevant_spks_addrs, debug, + log) log("Done.\nIf recovering a wallet which already has existing " + "transactions, then\nrun the rescan script. If you're confident " + "that the wallets are new\nand empty then there's no need to " + diff --git a/transactionmonitor.py b/transactionmonitor.py @@ -1,705 +0,0 @@ - -import time, pprint, math -from decimal import Decimal - -from jsonrpc import JsonRpcError -import server as s -import hashes - -#internally this code uses scriptPubKeys, it only converts to bitcoin addresses -# when importing to bitcoind or checking whether enough addresses have been -# imported -#the electrum protocol uses sha256(scriptpubkey) as a key for lookups -# this code calls them scripthashes - -#code will generate the first address from each deterministic wallet -# and check whether they have been imported into the bitcoin node -# if no then initial_import_count addresses will be imported, then exit -# if yes then initial_import_count addresses will be generated and extra -# addresses will be generated one-by-one, each time checking whether they have -# been imported into the bitcoin node -# when an address has been reached that has not been imported, that means -# we've reached the end, then rewind the deterministic wallet index by one - -#when a transaction happens paying to an address from a deterministic wallet -# lookup the position of that address, if its less than gap_limit then -# import more addresses - -class TransactionMonitor(object): - """ - Class which monitors the bitcoind wallet for new transactions - and builds a history datastructure for sending to electrum - """ - def __init__(self, rpc, deterministic_wallets, debug, log): - self.rpc = rpc - self.deterministic_wallets = deterministic_wallets - self.debug = debug - self.log = log - self.last_known_wallet_txid = None - self.address_history = None - self.unconfirmed_txes = None - - def get_electrum_history_hash(self, scrhash): - return hashes.get_status_electrum( ((h["tx_hash"], h["height"]) - for h in self.address_history[scrhash]["history"]) ) - - def get_electrum_history(self, scrhash): - if scrhash in self.address_history: - return self.address_history[scrhash]["history"] - else: - return None - - def subscribe_address(self, scrhash): - if scrhash in self.address_history: - self.address_history[scrhash]["subscribed"] = True - return True - else: - return False - - def unsubscribe_all_addresses(self): - for scrhash, his in self.address_history.items(): - his["subscribed"] = False - - def build_address_history(self, monitored_scriptpubkeys): - self.log("Building history with " + str(len(monitored_scriptpubkeys)) + - " addresses") - st = time.time() - address_history = {} - for spk in monitored_scriptpubkeys: - address_history[hashes.script_to_scripthash(spk)] = {'history': [], - 'subscribed': False} - wallet_addr_scripthashes = set(address_history.keys()) - #populate history - #which is a blockheight-ordered list of ("txhash", height) - #unconfirmed transactions go at the end as ("txhash", 0, fee) - # 0=unconfirmed -1=unconfirmed with unconfirmed parents - - BATCH_SIZE = 1000 - ret = list(range(BATCH_SIZE)) - t = 0 - count = 0 - obtained_txids = set() - while len(ret) == BATCH_SIZE: - ret = self.rpc.call("listtransactions", ["*", BATCH_SIZE, t, True]) - self.debug("listtransactions skip=" + str(t) + " len(ret)=" - + str(len(ret))) - t += len(ret) - for tx in ret: - if "txid" not in tx or "category" not in tx: - continue - if tx["category"] not in ("receive", "send"): - continue - if tx["txid"] in obtained_txids: - continue - self.debug("adding obtained tx=" + str(tx["txid"])) - obtained_txids.add(tx["txid"]) - - #obtain all the addresses this transaction is involved with - output_scriptpubkeys, input_scriptpubkeys, txd = \ - self.get_input_and_output_scriptpubkeys(tx["txid"]) - output_scripthashes = [hashes.script_to_scripthash(sc) - for sc in output_scriptpubkeys] - sh_to_add = wallet_addr_scripthashes.intersection(set( - output_scripthashes)) - input_scripthashes = [hashes.script_to_scripthash(sc) - for sc in input_scriptpubkeys] - sh_to_add |= wallet_addr_scripthashes.intersection(set( - input_scripthashes)) - if len(sh_to_add) == 0: - continue - - for wal in self.deterministic_wallets: - overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( - output_scriptpubkeys) - if overrun_depths != None: - self.log("ERROR: Not enough addresses imported.") - self.log("Delete wallet.dat and increase the value " + - "of `initial_import_count` in the file " + - "`config.cfg` then reimport and rescan") - #TODO make it so users dont have to delete wallet.dat - # check whether all initial_import_count addresses are - # imported rather than just the first one - return False - new_history_element = self.generate_new_history_element(tx, txd) - for scripthash in sh_to_add: - address_history[scripthash][ - "history"].append(new_history_element) - count += 1 - - unconfirmed_txes = {} - for scrhash, his in address_history.items(): - uctx = self.sort_address_history_list(his) - for u in uctx: - if u["tx_hash"] in unconfirmed_txes: - unconfirmed_txes[u["tx_hash"]].append(scrhash) - else: - unconfirmed_txes[u["tx_hash"]] = [scrhash] - self.debug("unconfirmed_txes = " + str(unconfirmed_txes)) - if len(ret) > 0: - #txid doesnt uniquely identify transactions from listtransactions - #but the tuple (txid, address) does - self.last_known_wallet_txid = (ret[-1]["txid"], ret[-1]["address"]) - else: - self.last_known_wallet_txid = None - self.debug("last_known_wallet_txid = " + str( - self.last_known_wallet_txid)) - - et = time.time() - self.debug("address_history =\n" + pprint.pformat(address_history)) - self.log("Found " + str(count) + " txes. History built in " + - str(et - st) + "sec") - self.address_history = address_history - self.unconfirmed_txes = unconfirmed_txes - return True - - def get_input_and_output_scriptpubkeys(self, txid): - gettx = self.rpc.call("gettransaction", [txid]) - txd = self.rpc.call("decoderawtransaction", [gettx["hex"]]) - output_scriptpubkeys = [out["scriptPubKey"]["hex"] - for out in txd["vout"]] - input_scriptpubkeys = [] - for inn in txd["vin"]: - try: - wallet_tx = self.rpc.call("gettransaction", [inn["txid"]]) - except JsonRpcError: - #wallet doesnt know about this tx, so the input isnt ours - continue - input_decoded = self.rpc.call("decoderawtransaction", [wallet_tx[ - "hex"]]) - script = input_decoded["vout"][inn["vout"]]["scriptPubKey"]["hex"] - input_scriptpubkeys.append(script) - return output_scriptpubkeys, input_scriptpubkeys, txd - - def generate_new_history_element(self, tx, txd): - if tx["confirmations"] == 0: - unconfirmed_input = False - total_input_value = 0 - for inn in txd["vin"]: - utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"], - True]) - if utxo is None: - utxo = self.rpc.call("gettxout", [inn["txid"], inn["vout"], - False]) - if utxo is None: - self.debug("utxo not found(!)") - #TODO detect this and figure out how to tell - # electrum that we dont know the fee - total_input_value += int(Decimal(utxo["value"]) * Decimal(1e8)) - unconfirmed_input = (unconfirmed_input or - utxo["confirmations"] == 0) - self.debug("total_input_value = " + str(total_input_value)) - - fee = total_input_value - sum([int(Decimal(out["value"]) - * Decimal(1e8)) for out in txd["vout"]]) - height = -1 if unconfirmed_input else 0 - new_history_element = ({"tx_hash": tx["txid"], "height": height, - "fee": fee}) - else: - blockheader = self.rpc.call("getblockheader", [tx['blockhash']]) - new_history_element = ({"tx_hash": tx["txid"], - "height": blockheader["height"]}) - return new_history_element - - def sort_address_history_list(self, his): - unconfirm_txes = list(filter(lambda h:h["height"] == 0, his["history"])) - confirm_txes = filter(lambda h:h["height"] != 0, his["history"]) - #TODO txes must be "in blockchain order" - # the order they appear in the block - # it might be "blockindex" in listtransactions and gettransaction - #so must sort with key height+':'+blockindex - #maybe check if any heights are the same then get the pos only for those - #better way to do this is to have a separate dict that isnt in history - # which maps txid => blockindex - # and then sort by key height+":"+idx[txid] - his["history"] = sorted(confirm_txes, key=lambda h:h["height"]) - his["history"].extend(unconfirm_txes) - return unconfirm_txes - - def check_for_updated_txes(self): - updated_scrhashes1 = self.check_for_new_txes() - updated_scrhashes2 = self.check_for_confirmations() - updated_scrhashes = updated_scrhashes1 | updated_scrhashes2 - for ush in updated_scrhashes: - his = self.address_history[ush] - self.sort_address_history_list(his) - if len(updated_scrhashes) > 0: - self.debug("new tx address_history =\n" - + pprint.pformat(self.address_history)) - self.debug("unconfirmed txes = " + - pprint.pformat(self.unconfirmed_txes)) - self.debug("updated_scripthashes = " + str(updated_scrhashes)) - updated_scrhashes = filter(lambda sh:self.address_history[sh][ - "subscribed"], updated_scrhashes) - return updated_scrhashes - - def check_for_confirmations(self): - confirmed_txes_scrhashes = [] - self.debug("check4con unconfirmed_txes = " - + pprint.pformat(self.unconfirmed_txes)) - for uc_txid, scrhashes in self.unconfirmed_txes.items(): - tx = self.rpc.call("gettransaction", [uc_txid]) - self.debug("uc_txid=" + uc_txid + " => " + str(tx)) - if tx["confirmations"] == 0: - continue #still unconfirmed - self.log("A transaction confirmed: " + uc_txid) - confirmed_txes_scrhashes.append((uc_txid, scrhashes)) - block = self.rpc.call("getblockheader", [tx["blockhash"]]) - for scrhash in scrhashes: - #delete the old unconfirmed entry in address_history - deleted_entries = [h for h in self.address_history[scrhash][ - "history"] if h["tx_hash"] == uc_txid] - for d_his in deleted_entries: - self.address_history[scrhash]["history"].remove(d_his) - #create the new confirmed entry in address_history - self.address_history[scrhash]["history"].append({"height": - block["height"], "tx_hash": uc_txid}) - updated_scrhashes = set() - for tx, scrhashes in confirmed_txes_scrhashes: - del self.unconfirmed_txes[tx] - updated_scrhashes.update(set(scrhashes)) - return updated_scrhashes - - def check_for_new_txes(self): - MAX_TX_REQUEST_COUNT = 256 - tx_request_count = 2 - max_attempts = int(math.log(MAX_TX_REQUEST_COUNT, 2)) - for i in range(max_attempts): - self.debug("listtransactions tx_request_count=" - + str(tx_request_count)) - ret = self.rpc.call("listtransactions", ["*", tx_request_count, 0, - True]) - ret = ret[::-1] - if self.last_known_wallet_txid == None: - recent_tx_index = len(ret) #=0 means no new txes - break - else: - txid_list = [(tx["txid"], tx["address"]) for tx in ret] - recent_tx_index = next((i for i, (txid, addr) - in enumerate(txid_list) if - txid == self.last_known_wallet_txid[0] and - addr == self.last_known_wallet_txid[1]), -1) - if recent_tx_index != -1: - break - tx_request_count *= 2 - - #TODO low priority: handle a user getting more than 255 new - # transactions in 15 seconds - self.debug("recent tx index = " + str(recent_tx_index) + " ret = " + - str([(t["txid"], t["address"]) for t in ret])) - if len(ret) > 0: - self.last_known_wallet_txid = (ret[0]["txid"], ret[0]["address"]) - self.debug("last_known_wallet_txid = " + str( - self.last_known_wallet_txid)) - assert(recent_tx_index != -1) - if recent_tx_index == 0: - return set() - new_txes = ret[:recent_tx_index][::-1] - self.debug("new txes = " + str(new_txes)) - obtained_txids = set() - updated_scripthashes = [] - for tx in new_txes: - if "txid" not in tx or "category" not in tx: - continue - if tx["category"] not in ("receive", "send"): - continue - if tx["txid"] in obtained_txids: - continue - obtained_txids.add(tx["txid"]) - output_scriptpubkeys, input_scriptpubkeys, txd = \ - self.get_input_and_output_scriptpubkeys(tx["txid"]) - matching_scripthashes = [] - for spk in (output_scriptpubkeys + input_scriptpubkeys): - scripthash = hashes.script_to_scripthash(spk) - if scripthash in self.address_history: - matching_scripthashes.append(scripthash) - if len(matching_scripthashes) == 0: - continue - - for wal in self.deterministic_wallets: - overrun_depths = wal.have_scriptpubkeys_overrun_gaplimit( - output_scriptpubkeys) - if overrun_depths != None: - for change, import_count in overrun_depths.items(): - spks = wal.get_new_scriptpubkeys(change, import_count) - for spk in spks: - self.address_history[hashes.script_to_scripthash( - spk)] = {'history': [], 'subscribed': False} - new_addrs = [hashes.script_to_address(s, self.rpc) - for s in spks] - self.debug("importing " + str(len(spks)) + - " into change=" + str(change)) - s.import_addresses(self.rpc, new_addrs) - - updated_scripthashes.extend(matching_scripthashes) - new_history_element = self.generate_new_history_element(tx, txd) - self.log("Found new tx: " + str(new_history_element)) - for scrhash in matching_scripthashes: - self.address_history[scrhash]["history"].append( - new_history_element) - if new_history_element["height"] == 0: - if tx["txid"] in self.unconfirmed_txes: - self.unconfirmed_txes[tx["txid"]].append(scrhash) - else: - self.unconfirmed_txes[tx["txid"]] = [scrhash] - #check whether gap limits have been overrun and import more addrs - return set(updated_scripthashes) - - -## start tests here - -class TestJsonRpc(object): - def __init__(self, txlist, utxoset, block_heights): - self.txlist = txlist - self.utxoset = utxoset - self.block_heights = block_heights - self.imported_addresses = [] - - def call(self, method, params): - if method == "listtransactions": - count = int(params[1]) - skip = int(params[2]) - return self.txlist[skip:skip + count] - elif method == "gettransaction": - for t in self.txlist: - if t["txid"] == params[0]: - return t - raise JsonRpcError({"code": None, "message": None}) - elif method == "decoderawtransaction": - for t in self.txlist: - if t["hex"] == params[0]: - return t - assert 0 - elif method == "gettxout": - for u in self.utxoset: - if u["txid"] == params[0] and u["vout"] == params[1]: - return u - assert 0 - elif method == "getblockheader": - if params[0] not in self.block_heights: - assert 0 - return {"height": self.block_heights[params[0]]} - elif method == "decodescript": - return {"addresses": [test_spk_to_address(params[0])]} - elif method == "importaddress": - self.imported_addresses.append(params[0]) - else: - raise ValueError("unknown method in test jsonrpc") - - def add_transaction(self, tx): - self.txlist.append(tx) - - def get_imported_addresses(self): - return self.imported_addresses - -from deterministicwallet import DeterministicWallet - -class TestDeterministicWallet(DeterministicWallet): - """Empty deterministic wallets""" - def __init__(self): - pass - - def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): - return None #not overrun - - def get_new_scriptpubkeys(self, change, count): - pass - -def test_spk_to_address(spk): - return spk + "-address" - -def assert_address_history_tx(address_history, spk, height, txid, subscribed): - history_element = address_history[hashes.script_to_scripthash(spk)] - assert history_element["history"][0]["height"] == height - assert history_element["history"][0]["tx_hash"] == txid - #fee always zero, its easier to test because otherwise you have - # to use Decimal to stop float weirdness - if height == 0: - assert history_element["history"][0]["fee"] == 0 - assert history_element["subscribed"] == subscribed - -def test(): - debugf = lambda x: x - logf = lambda x: x - #empty deterministic wallets - deterministic_wallets = [TestDeterministicWallet()] - test_spk1 = "deadbeefdeadbeefdeadbeefdeadbeef" - test_containing_block1 = "blockhash-placeholder1" - test_paying_in_tx1 = { - "txid": "placeholder-test-txid1", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk1}}], - "address": test_spk_to_address(test_spk1), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block1, - "hex": "placeholder-test-txhex1" - } - test_spk2 = "deadbeefdeadbeefdeadbeef" - test_containing_block2 = "blockhash-placeholder2" - test_paying_in_tx2 = { - "txid": "placeholder-test-txid2", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk2}}], - "address": test_spk_to_address(test_spk2), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block2, - "hex": "placeholder-test-txhex2" - } - - ###single confirmed tx in wallet belonging to us, address history built - rpc = TestJsonRpc([test_paying_in_tx1], [], - {test_containing_block1: 420000}) - txmonitor1 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor1.build_address_history([test_spk1]) - assert len(txmonitor1.address_history) == 1 - assert_address_history_tx(txmonitor1.address_history, spk=test_spk1, - height=420000, txid=test_paying_in_tx1["txid"], subscribed=False) - - ###two confirmed txes in wallet belonging to us, addr history built - rpc = TestJsonRpc([test_paying_in_tx1, test_paying_in_tx2], [], - {test_containing_block1: 1, test_containing_block2: 2}) - deterministic_wallets = [TestDeterministicWallet()] - txmonitor2 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor2.build_address_history([test_spk1, test_spk2]) - assert len(txmonitor2.address_history) == 2 - assert_address_history_tx(txmonitor2.address_history, spk=test_spk1, - height=1, txid=test_paying_in_tx1["txid"], subscribed=False) - assert_address_history_tx(txmonitor2.address_history, spk=test_spk2, - height=2, txid=test_paying_in_tx2["txid"], subscribed=False) - - ###one unconfirmed tx in wallet belonging to us, with confirmed inputs, - ### addr history built, then tx confirms, not subscribed to address - test_spk3 = "deadbeefdeadbeef" - test_containing_block3 = "blockhash-placeholder3" - input_utxo3 = {"txid": "placeholder-unknown-input-txid", "vout": 0, - "value": 1, "confirmations": 1} - test_paying_in_tx3 = { - "txid": "placeholder-test-txid3", - "vin": [input_utxo3], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk3}}], - "address": test_spk_to_address(test_spk3), - "category": "receive", - "confirmations": 0, - "blockhash": test_containing_block3, - "hex": "placeholder-test-txhex3" - } - rpc = TestJsonRpc([test_paying_in_tx3], [input_utxo3], - {test_containing_block3: 10}) - txmonitor3 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor3.build_address_history([test_spk3]) - assert len(txmonitor3.address_history) == 1 - assert_address_history_tx(txmonitor3.address_history, spk=test_spk3, - height=0, txid=test_paying_in_tx3["txid"], subscribed=False) - assert len(list(txmonitor3.check_for_updated_txes())) == 0 - test_paying_in_tx3["confirmations"] = 1 #tx confirms - #not subscribed so still only returns an empty list - assert len(list(txmonitor3.check_for_updated_txes())) == 0 - assert_address_history_tx(txmonitor3.address_history, spk=test_spk3, - height=10, txid=test_paying_in_tx3["txid"], subscribed=False) - - ###build empty address history, subscribe one address - ### an unconfirmed tx appears, then confirms - test_spk4 = "deadbeefdeadbeefaa" - test_containing_block4 = "blockhash-placeholder4" - input_utxo4 = {"txid": "placeholder-unknown-input-txid", "vout": 0, - "value": 1, "confirmations": 1} - test_paying_in_tx4 = { - "txid": "placeholder-test-txid4", - "vin": [input_utxo4], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk4}}], - "address": test_spk_to_address(test_spk4), - "category": "receive", - "confirmations": 0, - "blockhash": test_containing_block4, - "hex": "placeholder-test-txhex4" - } - rpc = TestJsonRpc([], [input_utxo4], {test_containing_block4: 10}) - txmonitor4 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor4.build_address_history([test_spk4]) - assert len(txmonitor4.address_history) == 1 - sh4 = hashes.script_to_scripthash(test_spk4) - assert len(txmonitor4.get_electrum_history(sh4)) == 0 - txmonitor4.subscribe_address(sh4) - # unconfirm transaction appears - assert len(list(txmonitor4.check_for_updated_txes())) == 0 - rpc.add_transaction(test_paying_in_tx4) - assert len(list(txmonitor4.check_for_updated_txes())) == 1 - assert_address_history_tx(txmonitor4.address_history, spk=test_spk4, - height=0, txid=test_paying_in_tx4["txid"], subscribed=True) - # transaction confirms - test_paying_in_tx4["confirmations"] = 1 - assert len(list(txmonitor4.check_for_updated_txes())) == 1 - assert_address_history_tx(txmonitor4.address_history, spk=test_spk4, - height=10, txid=test_paying_in_tx4["txid"], subscribed=True) - - ###transaction that has nothing to do with our wallet - test_spk5 = "deadbeefdeadbeefbb" - test_containing_block5 = "blockhash-placeholder5" - test_paying_in_tx5 = { - "txid": "placeholder-test-txid5", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk5}}], - "address": test_spk_to_address(test_spk5), - "category": "receive", - "confirmations": 0, - "blockhash": test_containing_block5, - "hex": "placeholder-test-txhex5" - } - test_spk5_1 = "deadbeefdeadbeefcc" - rpc = TestJsonRpc([test_paying_in_tx5], [], {test_containing_block4: 10}) - txmonitor5 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor5.build_address_history([test_spk5_1]) - assert len(txmonitor5.address_history) == 1 - assert len(txmonitor5.get_electrum_history(hashes.script_to_scripthash( - test_spk5_1))) == 0 - - ###transaction which arrives to an address which already has a tx on it - test_spk6 = "deadbeefdeadbeefdd" - test_containing_block6 = "blockhash-placeholder6" - test_paying_in_tx6 = { - "txid": "placeholder-test-txid6", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}], - "address": test_spk_to_address(test_spk6), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block6, - "hex": "placeholder-test-txhex6" - } - test_paying_in_tx6_1 = { - "txid": "placeholder-test-txid6_1", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk6}}], - "address": test_spk_to_address(test_spk6), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block6, - "hex": "placeholder-test-txhex6" - } - rpc = TestJsonRpc([test_paying_in_tx6], [], {test_containing_block6: 10}) - txmonitor6 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor6.build_address_history([test_spk6]) - sh = hashes.script_to_scripthash(test_spk6) - assert len(txmonitor6.get_electrum_history(sh)) == 1 - rpc.add_transaction(test_paying_in_tx6_1) - assert len(txmonitor6.get_electrum_history(sh)) == 1 - txmonitor6.check_for_updated_txes() - assert len(txmonitor6.get_electrum_history(sh)) == 2 - - ###transaction spending FROM one of our addresses - test_spk7 = "deadbeefdeadbeefee" - test_input_containing_block7 = "blockhash-input-placeholder7" - test_input_tx7 = { - "txid": "placeholder-input-test-txid7", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk7}}], - "address": test_spk_to_address(test_spk7), - "category": "send", - "confirmations": 2, - "blockhash": test_input_containing_block7, - "hex": "placeholder-input-test-txhex7" - } - test_containing_block7 = "blockhash-placeholder7" - test_paying_from_tx7 = { - "txid": "placeholder-test-txid7", - "vin": [{"txid": test_input_tx7["txid"], "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": "deadbeef"}}], - "address": test_spk_to_address(test_spk7), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block7, - "hex": "placeholder-test-txhex7" - } - rpc = TestJsonRpc([test_input_tx7, test_paying_from_tx7], [], - {test_containing_block7: 9, test_input_containing_block7: 8}) - txmonitor7 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor7.build_address_history([test_spk7]) - sh = hashes.script_to_scripthash(test_spk7) - assert len(txmonitor7.get_electrum_history(sh)) == 2 - - ###transaction from one address to the other, both addresses in wallet - test_spk8 = "deadbeefdeadbeefee" - test_spk8_1 = "deadbeefdeadbeefff" - test_input_containing_block8 = "blockhash-input-placeholder8" - test_input_tx8 = { - "txid": "placeholder-input-test-txid8", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8}}], - "address": test_spk_to_address(test_spk8), - "category": "send", - "confirmations": 2, - "blockhash": test_input_containing_block8, - "hex": "placeholder-input-test-txhex8" - } - test_containing_block8 = "blockhash-placeholder8" - test_paying_from_tx8 = { - "txid": "placeholder-test-txid8", - "vin": [{"txid": test_input_tx8["txid"], "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk8_1}}], - "address": test_spk_to_address(test_spk8), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block8, - "hex": "placeholder-test-txhex8" - } - rpc = TestJsonRpc([test_input_tx8, test_paying_from_tx8], [], - {test_containing_block8: 9, test_input_containing_block8: 8}) - txmonitor8 = TransactionMonitor(rpc, deterministic_wallets, debugf, logf) - assert txmonitor8.build_address_history([test_spk8, test_spk8_1]) - assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash( - test_spk8))) == 2 - assert len(txmonitor8.get_electrum_history(hashes.script_to_scripthash( - test_spk8_1))) == 1 - - ###overrun gap limit so import address is needed - test_spk9 = "deadbeefdeadbeef00" - test_containing_block9 = "blockhash-placeholder9" - test_paying_in_tx9 = { - "txid": "placeholder-test-txid9", - "vin": [{"txid": "placeholder-unknown-input-txid", "vout": 0}], - "vout": [{"value": 1, "scriptPubKey": {"hex": test_spk9}}], - "address": test_spk_to_address(test_spk9), - "category": "receive", - "confirmations": 1, - "blockhash": test_containing_block9, - "hex": "placeholder-test-txhex9" - } - test_spk9_imported = "deadbeefdeadbeef11" - class TestImportDeterministicWallet(DeterministicWallet): - def __init__(self): - pass - - def have_scriptpubkeys_overrun_gaplimit(self, scriptpubkeys): - return {0: 1} #overrun by one - - def get_new_scriptpubkeys(self, change, count): - return [test_spk9_imported] - - rpc = TestJsonRpc([], [], {test_containing_block9: 10}) - txmonitor9 = TransactionMonitor(rpc, [TestImportDeterministicWallet()], - debugf, logf) - assert txmonitor9.build_address_history([test_spk9]) - assert len(txmonitor9.address_history) == 1 - assert len(list(txmonitor9.check_for_updated_txes())) == 0 - assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( - test_spk9))) == 0 - rpc.add_transaction(test_paying_in_tx9) - assert len(list(txmonitor9.check_for_updated_txes())) == 0 - assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( - test_spk9))) == 1 - assert len(txmonitor9.get_electrum_history(hashes.script_to_scripthash( - test_spk9_imported))) == 0 - assert len(rpc.get_imported_addresses()) == 1 - assert rpc.get_imported_addresses()[0] == test_spk_to_address( - test_spk9_imported) - - #other possible stuff to test: - #finding confirmed and unconfirmed tx, in that order, then both confirm - #finding unconfirmed and confirmed tx, in that order, then both confirm - - print("\nAll tests passed") - -if __name__ == "__main__": - test() -