mirror of
https://github.com/JoinMarket-Org/joinmarket-clientserver.git
synced 2026-08-18 13:17:51 +02:00
Replaces core transaction, address, serialization and sign functionality for Bitcoin with python-bitcointx backend. Removes bech32 and btscript modules from jmbitcoin. Removes all string, hex, binary conversion routines. A generic hex/binary conversion now is added to jmbase. Removes all transaction serialization and deserialization routines. Removes the now irrelevant test modules. Remaining functions in jmbitcoin remove any parsing of hex format, requiring callers to use binary only. One additional test added, testing the remaining function in secp256k1_transaction.py: the signing of transactions. Deserialized form is now bitcointx.CMutableTransaction. For jmbase, in addition to the above, generic conversions for utxos to and from strings is added, and a dynamic conversion for AMP messages to binary-only. Within the code, utxos are now only in (binarytxid, int) form, except where converted for communcation. Tthe largest part of the changes are the modifications to jmbitcoin calls in jmclient; as well as different encapsulation with CMutableTransaction, there is also a removal of some but not all hex parsing; it remains for rpc calls to Core and for AMP message parsing. Backwards compatibility must be ensured so some joinmarket protocol messages still use hex, and it is also preserved in persistence of PoDLE data. As part of this, some significant simplification of certain legacy functions within the wallet has been done. jmdaemon is entirely unaltered (save for one test which simulates jmclient code).
165 lines
5.1 KiB
Python
Executable file
165 lines
5.1 KiB
Python
Executable file
#!/usr/bin/env python3
|
|
import argparse
|
|
import json
|
|
import os.path
|
|
from hashlib import sha256
|
|
from binascii import hexlify, unhexlify
|
|
from collections import defaultdict
|
|
from pyaes import AESModeOfOperationCBC, Decrypter
|
|
from jmbase import JM_APP_NAME
|
|
from jmclient import Storage, load_program_config, BTCEngine
|
|
from jmclient.wallet_utils import get_password, get_wallet_cls,\
|
|
cli_get_wallet_passphrase_check, get_wallet_path
|
|
|
|
|
|
class ConvertException(Exception):
|
|
pass
|
|
|
|
|
|
def get_max_mixdepth(data):
|
|
return max(1, len(data.get('index_cache', [1])) - 1,
|
|
*data.get('imported', {}).keys())
|
|
|
|
|
|
def is_encrypted(wallet_data):
|
|
return 'encrypted_seed' in wallet_data or 'encrypted_entropy' in wallet_data
|
|
|
|
|
|
def double_sha256(plaintext):
|
|
return sha256(sha256(plaintext).digest()).digest()
|
|
|
|
|
|
def decrypt_data(key, data):
|
|
decrypter = Decrypter(AESModeOfOperationCBC(key, iv=data[:16]))
|
|
plain = decrypter.feed(data[16:])
|
|
plain += decrypter.feed()
|
|
return plain
|
|
|
|
|
|
def decrypt_entropy_extension(enc_data, key):
|
|
data = decrypt_data(key, unhexlify(enc_data))
|
|
if data[-9] != b'\xff':
|
|
raise ConvertException("Wrong password.")
|
|
chunks = data.split(b'\xff')
|
|
if len(chunks) < 3 or data[-8:] != hexlify(double_sha256(chunks[1]).decode('ascii')[:4]):
|
|
raise ConvertException("Wrong password.")
|
|
return chunks[1]
|
|
|
|
|
|
def decrypt_wallet_data(data, password):
|
|
key = double_sha256(password)
|
|
|
|
enc_entropy = data.get('encrypted_seed') or data.get('encrypted_entropy')
|
|
enc_entropy_ext = data.get('encrypted_mnemonic_extension')
|
|
enc_imported = data.get('imported_keys')
|
|
|
|
entropy = decrypt_data(key, unhexlify(enc_entropy))
|
|
data['entropy'] = entropy
|
|
if enc_entropy_ext:
|
|
data['entropy_ext'] = decrypt_entropy_extension(enc_entropy_ext, key)
|
|
|
|
if enc_imported:
|
|
imported_keys = defaultdict(list)
|
|
for e in enc_imported:
|
|
md = int(e['mixdepth'])
|
|
imported_enc_key = unhexlify(e['encrypted_privkey'])
|
|
imported_key = decrypt_data(key, imported_enc_key)
|
|
imported_keys[md].append(imported_key)
|
|
data['imported'] = imported_keys
|
|
|
|
|
|
def new_wallet_from_data(data, file_name):
|
|
print("Creating new wallet file.")
|
|
new_pw = cli_get_wallet_passphrase_check()
|
|
if new_pw is False:
|
|
return False
|
|
|
|
storage = Storage(file_name, create=True, password=new_pw)
|
|
wallet_cls = get_wallet_cls()
|
|
|
|
kwdata = {
|
|
'entropy': data['entropy'],
|
|
'timestamp': data.get('creation_time'),
|
|
'max_mixdepth': get_max_mixdepth(data)
|
|
}
|
|
|
|
if 'entropy_ext' in data:
|
|
kwdata['entropy_extension'] = data['entropy_ext']
|
|
|
|
wallet_cls.initialize(storage, data['network'], **kwdata)
|
|
wallet = wallet_cls(storage)
|
|
|
|
if 'index_cache' in data:
|
|
for md, indices in enumerate(data['index_cache']):
|
|
wallet.set_next_index(md, 0, indices[0], force=True)
|
|
wallet.set_next_index(md, 1, indices[1], force=True)
|
|
|
|
if 'imported' in data:
|
|
for md in data['imported']:
|
|
for privkey in data['imported'][md]:
|
|
privkey += b'\x01'
|
|
wif = BTCEngine.privkey_to_wif(privkey)
|
|
wallet.import_private_key(md, wif)
|
|
|
|
wallet.save()
|
|
wallet.close()
|
|
return True
|
|
|
|
|
|
def parse_old_wallet(fh):
|
|
file_data = json.load(fh)
|
|
|
|
if is_encrypted(file_data):
|
|
pw = get_password("Enter password for old wallet file: ")
|
|
try:
|
|
decrypt_wallet_data(file_data, pw)
|
|
except ValueError:
|
|
print("Failed to open wallet: bad password")
|
|
return
|
|
except Exception as e:
|
|
print("Error: {}".format(e))
|
|
print("Failed to open wallet. Wrong password?")
|
|
return
|
|
|
|
return file_data
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="Convert old joinmarket json wallet format to new jmdat "
|
|
"format")
|
|
parser.add_argument('old_wallet_file', type=open)
|
|
parser.add_argument('--name', '-n', required=False, dest='name',
|
|
help="Name of the new wallet file. Default: [old wallet name].jmdat")
|
|
# hack; not using jmclient.add_base_options because ArgumentParser not OptionParser:
|
|
parser.add_argument(
|
|
'--datadir',
|
|
dest='datadir',
|
|
default="",
|
|
help='Specify the path to a directory you want to use to store your user'
|
|
'data - wallets, logs and commitment files - and your joinmarket.cfg. '
|
|
'By default, the directory .' + JM_APP_NAME + ' is used.'
|
|
)
|
|
|
|
try:
|
|
args = parser.parse_args()
|
|
except Exception as e:
|
|
print("Error: {}".format(e))
|
|
return
|
|
load_program_config(config_path=args.datadir)
|
|
data = parse_old_wallet(args.old_wallet_file)
|
|
|
|
if not data:
|
|
return
|
|
|
|
file_name = args.name or\
|
|
os.path.split(args.old_wallet_file.name)[-1].rsplit('.', 1)[0] + '.jmdat'
|
|
wallet_path = get_wallet_path(file_name, None)
|
|
if new_wallet_from_data(data, wallet_path):
|
|
print("New wallet file created at {}".format(wallet_path))
|
|
else:
|
|
print("Failed to convert wallet.")
|
|
|
|
|
|
if __name__ == '__main__':
|
|
main()
|