diff --git a/rein/cli.py b/rein/cli.py index b40fc5a..daa0dd3 100644 --- a/rein/cli.py +++ b/rein/cli.py @@ -27,7 +27,7 @@ from .lib.io import safe_get from .lib.script import build_2_of_3, build_mandatory_multisig, check_redeem_scripts from .lib.localization import init_localization -from .lib.transaction import partial_spend_p2sh, spend_p2sh, spend_p2sh_mediator, partial_spend_p2sh_mediator, partial_spend_p2sh_mediator_2 +from .lib.transaction import partial_spend_p2sh, spend_p2sh, spend_p2sh_mediator, partial_spend_p2sh_mediator, partial_spend_p2sh_mediator_2, unspent_txins, withdraw_from_wallet from .lib.rating import add_rating, get_user_jobs, get_average_user_rating, get_average_user_rating_display, get_all_user_ratings, calculate_trust_score # Import config @@ -38,6 +38,8 @@ # Import models from .lib.persistconfig import PersistConfig +from .lib.wallet import Wallet +from .lib.pubkeys import Pubkeys from .lib.user import User from .lib.bucket import Bucket from .lib.document import Document @@ -188,30 +190,32 @@ def post(multi, identity, defaults, dry_run): log.info('got user and key for post') job_guid = ''.join(random.SystemRandom().choice(string.ascii_lowercase + string.digits) for _ in range(20)) fields = [ - {'label': 'Job name', 'not_null': form, - 'help': 'Choose a brief but descriptive name for the job.'}, - {'label': 'Job ID', 'value': job_guid}, - {'label': 'Tags', 'validator': is_tags, 'not_null': form, - 'help': 'Each post can have a set of tags associated with it. Though not implemented yet,\n' - 'these tags may be used in searches and filters later. No spaces, dashes, or\n' - 'special characters are allowed. Please enter them as a comma-separated list.\n' - 'Example: software, 3dprinting'}, - {'label': 'Description', 'not_null': form}, - {'label': 'Block hash', 'value': block_hash}, - {'label': 'Time', 'value': str(block_time)}, - {'label': 'Expiration (days)', 'validator': is_int}, - {'label': 'Mediator', 'value': mediator['User']}, - {'label': 'Mediator contact', 'value': mediator['Contact']}, - {'label': 'Mediator fee', 'value': mediator['Mediator fee']}, - {'label': 'Mediator public key', 'value': mediator['Mediator public key']}, - {'label': 'Mediator master address', 'value': mediator['Master signing address']}, - {'label': 'Job creator', 'value': user.name}, - {'label': 'Job creator contact', 'value': user.contact}, - {'label': 'Job creator public key', 'value': key}, - {'label': 'Job creator master address', 'value': user.maddr}, - {'label': 'Job creator delegate address', 'value': user.daddr}, - ] + {'label': 'Job name', 'not_null': form, + 'help': 'Choose a brief but descriptive name for the job.'}, + {'label': 'Job ID', 'value': job_guid}, + {'label': 'Tags', 'validator': is_tags, 'not_null': form, + 'help': 'Each post can have a set of tags associated with it. Though not implemented yet,\n' + 'these tags may be used in searches and filters later. No spaces, dashes, or\n' + 'special characters are allowed. Please enter them as a comma-separated list.\n' + 'Example: software, 3dprinting'}, + {'label': 'Description', 'not_null': form}, + {'label': 'Block hash', 'value': block_hash}, + {'label': 'Time', 'value': str(block_time)}, + {'label': 'Expiration (days)', 'validator': is_int}, + {'label': 'Mediator', 'value': mediator['User']}, + {'label': 'Mediator contact', 'value': mediator['Contact']}, + {'label': 'Mediator fee', 'value': mediator['Mediator fee']}, + {'label': 'Mediator public key', 'value': mediator['Mediator public key']}, + {'label': 'Mediator master address', 'value': mediator['Master signing address']}, + {'label': 'Mediator delegate address', 'value': mediator['Delegate signing address']}, + {'label': 'Job creator', 'value': user.name}, + {'label': 'Job creator contact', 'value': user.contact}, + {'label': 'Job creator public key', 'value': key}, + {'label': 'Job creator master address', 'value': user.maddr}, + {'label': 'Job creator delegate address', 'value': user.daddr} + ] document_text = assemble_document('Job', fields) + print("document_text = "+document_text) if not rein.testnet: m = re.search('test', document_text, re.IGNORECASE) if m: @@ -286,11 +290,17 @@ def bid(multi, identity, defaults, dry_run): return log.info('got job for bid') - primary_redeem_script, primary_addr = build_2_of_3([job['Job creator public key'], - job['Mediator public key'], - key]) - mediator_redeem_script, mediator_escrow_addr = build_mandatory_multisig(job['Mediator public key'], - [job['Job creator public key'],key]) + next_addr_index_str = PersistConfig.get(rein, 'next_addr_index') + if not next_addr_index_str: + next_addr_index_str = '0' + next_addr_index = int(next_addr_index_str) + (pubkey_for_escrow,privkey_for_escrow) = bip32.generate_new_escrow_pubkey(bip32.from_xprv(user.dxprv),next_addr_index) + (primary_payment_address,primary_payment_privkey) = bip32.generate_new_payment_address(bip32.from_xprv(user.dxprv),next_addr_index) + next_addr_index += 1 + PersistConfig.set(rein,'next_addr_index',str(next_addr_index)) + Pubkeys.set(rein,pubkey_for_escrow,privkey_for_escrow,userid=rein.user.id) + Wallet.set(rein,primary_payment_address,primary_payment_privkey,ref=job['Job ID'],userid=rein.user.id) + fields = [ {'label': 'Job name', 'value_from': job}, {'label': 'Worker', 'value': user.name}, @@ -299,15 +309,13 @@ def bid(multi, identity, defaults, dry_run): {'label': 'Worker delegate address', 'value': user.daddr}, {'label': 'Description', 'not_null': form}, {'label': 'Bid amount (BTC)', 'not_null': form}, - {'label': 'Primary escrow address', 'value': primary_addr}, - {'label': 'Mediator escrow address', 'value': mediator_escrow_addr}, {'label': 'Job ID', 'value_from': job}, {'label': 'Job creator', 'value_from': job}, {'label': 'Job creator public key', 'value_from': job}, {'label': 'Mediator public key', 'value_from': job}, {'label': 'Worker public key', 'value': key}, - {'label': 'Primary escrow redeem script', 'value': primary_redeem_script}, - {'label': 'Mediator escrow redeem script', 'value': mediator_redeem_script}, + {'label': 'Primary payment address', 'value': primary_payment_address}, + {'label': 'Worker public key for escrow', 'value': pubkey_for_escrow} ] document = assemble_document('Bid', fields) res = sign_and_store_document(rein, 'bid', document, user.daddr, user.dkey, store) @@ -369,20 +377,38 @@ def offer(multi, identity, defaults, dry_run): return log.info('got bid to offer') + next_addr_index_str = PersistConfig.get(rein, 'next_addr_index') + if not next_addr_index_str: + next_addr_index_str = '0' + next_addr_index = int(next_addr_index_str) + (pubkey_for_escrow,privkey_for_escrow) = bip32.generate_new_escrow_pubkey(bip32.from_xprv(user.dxprv),next_addr_index) + (payment_address,payment_privkey) = bip32.generate_new_payment_address(bip32.from_xprv(user.dxprv),next_addr_index) + next_addr_index += 1 + PersistConfig.set(rein,'next_addr_index',str(next_addr_index)) + Pubkeys.set(rein,pubkey_for_escrow,privkey_for_escrow,userid=rein.user.id) + Wallet.set(rein,payment_address,payment_privkey,ref=bid['Job ID'],userid=rein.user.id) + worker_pubkey_for_escrow = bid['Worker public key for escrow'] + primary_redeem_script, primary_addr = build_2_of_3([pubkey_for_escrow,bid['Mediator public key'],worker_pubkey_for_escrow]) + mediator_redeem_script, mediator_escrow_addr = build_mandatory_multisig(bid['Mediator public key'],[pubkey_for_escrow,worker_pubkey_for_escrow]) fields = [ {'label': 'Job name', 'value_from': bid}, {'label': 'Worker', 'value_from': bid}, {'label': 'Description', 'value_from': bid}, {'label': 'Bid amount (BTC)', 'value_from': bid}, - {'label': 'Primary escrow address', 'value_from': bid}, - {'label': 'Mediator escrow address', 'value_from': bid}, + {'label': 'Primary escrow address', 'value': primary_addr}, + {'label': 'Mediator escrow address', 'value': mediator_escrow_addr}, {'label': 'Job ID', 'value_from': bid}, {'label': 'Job creator', 'value_from': bid}, {'label': 'Job creator public key', 'value_from': bid}, {'label': 'Mediator public key', 'value_from': bid}, {'label': 'Worker public key', 'value_from': bid}, - {'label': 'Primary escrow redeem script', 'value_from': bid}, - {'label': 'Mediator escrow redeem script', 'value_from': bid}, + {'label': 'Primary escrow redeem script', 'value': primary_redeem_script}, + {'label': 'Mediator escrow redeem script', 'value': mediator_redeem_script}, + {'label': 'Primary payment address', 'value': bid['Primary payment address']}, + {'label': 'Primary client payment address', 'value': payment_address}, + {'label': 'Worker public key for escrow', 'value': worker_pubkey_for_escrow}, + {'label': 'Job creator public key for escrow', 'value': pubkey_for_escrow} + ] document = assemble_document('Offer', fields) if not click.confirm('Are you sure you want to award this bid?'): @@ -438,7 +464,9 @@ def deliver(multi, identity, defaults, dry_run): redeemScript = doc['Primary escrow redeem script'] mediatorRedeemScript = doc['Mediator escrow redeem script'] mediator_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(doc['Mediator public key']))) - (payment_txins,payment_amount,payment_address,payment_sig) = partial_spend_p2sh(redeemScript,rein) + pubkey_for_escrow = doc['Worker public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) + (payment_txins,payment_amount,payment_address,payment_sig) = partial_spend_p2sh(redeemScript,rein,daddr=doc['Primary payment address'],privkey=privkey_for_escrow) (mediator_payment_txins,mediator_payment_amount,mediator_payment_address) = partial_spend_p2sh_mediator(mediatorRedeemScript,rein,mediator_daddr) fields = [ {'label': 'Job name', 'value_from': doc}, @@ -452,6 +480,8 @@ def deliver(multi, identity, defaults, dry_run): {'label': 'Worker public key', 'value_from': doc}, {'label': 'Mediator public key', 'value_from': doc}, {'label': 'Job creator public key', 'value_from': doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc}, {'label':'Primary payment inputs','value':payment_txins}, {'label':'Primary payment amount','value':payment_amount}, {'label':'Primary payment address','value':payment_address}, @@ -515,8 +545,10 @@ def accept(multi, identity, defaults, dry_run): txins_mediator = doc['Mediator payment inputs'] amount_mediator = doc['Mediator payment amount'] daddr_mediator = doc['Mediator payment address'] - (payment_txid,client_sig) = spend_p2sh(redeemScript,txins,[float(amount)],[daddr],worker_sig,rein) - tx_for_mediator = partial_spend_p2sh_mediator_2(redeemScript_mediator,txins_mediator,float(amount_mediator),daddr_mediator,rein) + pubkey_for_escrow = delivery['Job creator public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) + (payment_txid,client_sig) = spend_p2sh(redeemScript,txins,[float(amount)],[daddr],worker_sig,rein,privkey=privkey_for_escrow) + tx_for_mediator = partial_spend_p2sh_mediator_2(redeemScript_mediator,txins_mediator,float(amount_mediator),daddr_mediator,rein,privkey=privkey_for_escrow) fields = [ {'label': 'Job name', 'value_from': doc}, @@ -586,7 +618,11 @@ def creatordispute(multi, identity, defaults, dry_run): {'label': 'Mediator escrow redeem script', 'value_from': doc}, {'label': 'Job creator public key', 'value_from': doc}, {'label': 'Worker public key', 'value_from': doc}, - {'label': 'Mediator public key', 'value_from':doc} + {'label': 'Mediator public key', 'value_from':doc}, + {'label': 'Primary worker payment address', 'value':doc['Primary payment address']}, + {'label': 'Primary client payment address', 'value_from':doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc} ] document = assemble_document('Dispute Delivery', fields) res = sign_and_store_document(rein, 'creatordispute', document, user.daddr, user.dkey, store) @@ -637,7 +673,11 @@ def workerdispute(multi, identity, defaults, dry_run): {'label': 'Mediator escrow redeem script', 'value_from': doc}, {'label': 'Job creator public key', 'value_from': doc}, {'label': 'Worker public key','value_from': doc}, - {'label': 'Mediator public key','value_from':doc} + {'label': 'Mediator public key','value_from':doc}, + {'label': 'Primary worker payment address', 'value':doc['Primary payment address']}, + {'label': 'Primary client payment address', 'value_from':doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc} ] document = assemble_document('Dispute Offer', fields) res = sign_and_store_document(rein, 'workerdispute', document, user.daddr, user.dkey, store) @@ -706,8 +746,8 @@ def resolve(multi, identity, defaults, dry_run): redeemScript = doc['Primary escrow redeem script'] mediatorRedeemScript = doc['Mediator escrow redeem script'] mediator_daddr = rein.user.daddr - worker_payment_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(doc['Worker public key']))); - client_payment_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(doc['Job creator public key']))); + worker_payment_daddr = doc['Primary worker payment address']; + client_payment_daddr = doc['Primary client payment address']; client_payment_amount = float(click.prompt("Client payment amount")) (payment_txins,payment_amount_1,payment_address_1,payment_amount_2,payment_address_2,payment_sig) = partial_spend_p2sh(redeemScript,rein,worker_payment_daddr,client_payment_amount,client_payment_daddr) (mediator_payment_txins,mediator_payment_amount,mediator_payment_address,mediator_payment_sig) = partial_spend_p2sh_mediator(mediatorRedeemScript,rein,mediator_daddr,True) @@ -718,6 +758,8 @@ def resolve(multi, identity, defaults, dry_run): {'label': 'Job creator public key', 'value_from': doc}, {'label': 'Worker public key', 'value_from':doc}, {'label': 'Mediator public key', 'value_from':doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc}, {'label': 'Primary escrow redeem script', 'value_from': doc}, {'label': 'Mediator escrow redeem script', 'value_from': doc}, {'label':'Primary payment inputs','value':payment_txins}, @@ -800,10 +842,15 @@ def acceptresolution(multi, identity, defaults, dry_run): sig_mediator = doc['Mediator payment signature'] reverse_sigs = False - if key == doc['Worker public key']: + + if key == delivery['Worker public key']: reverse_sigs = True - (payment_txid,second_sig) = spend_p2sh(redeemScript,txins,[float(amount1),float(amount2)],[daddr1,daddr2],sig_primary,rein,reverse_sigs) - (payment_txid_mediator,second_sig_mediator) = spend_p2sh_mediator(redeemScript_mediator,txins_mediator,[float(amount_mediator)],[daddr_mediator],sig_mediator,rein) + pubkey_for_escrow = delivery['Worker public key for escrow'] + else: + pubkey_for_escrow = delivery['Job creator public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) + (payment_txid,second_sig) = spend_p2sh(redeemScript,txins,[float(amount1),float(amount2)],[daddr1,daddr2],sig_primary,rein,reverse_sigs,privkey=privkey_for_escrow) + (payment_txid_mediator,second_sig_mediator) = spend_p2sh_mediator(redeemScript_mediator,txins_mediator,[float(amount_mediator)],[daddr_mediator],sig_mediator,rein,privkey=privkey_for_escrow) fields = [ {'label': 'Job name', 'value_from': doc}, @@ -1457,14 +1504,12 @@ def register_user(): # Enroll user enrollment = build_enrollment_from_dict(user_data) - signed_enrollment = '-----BEGIN BITCOIN SIGNED MESSAGE-----\n' + \ - enrollment + \ - '\n-----BEGIN SIGNATURE-----\n' + \ - maddr + '\n' + \ - sign(mprv, enrollment) + \ - '\n-----END BITCOIN SIGNED MESSAGE-----\n' + signature = sign(mprv,json.dumps(enrollment,sort_keys=True)) + signed_enrollment = enrollment + signed_enrollment['signature'] = signature + signed_enrollment['signature_address'] = maddr User.set_enrolled(rein, new_identity) - document = Document(rein, 'enrollment', signed_enrollment, sig_verified=True, testnet=rein.testnet) + document = Document(rein, 'enrollment', json.dumps(signed_enrollment,sort_keys=True), sig_verified=True, testnet=rein.testnet) rein.session.add(document) rein.session.commit() return json.dumps({'enrolled': True}) @@ -1502,6 +1547,7 @@ def serve_static_file(path): if rein.has_no_account() or setup: webbrowser.open('http://'+host+':' + str(port) + '/setup') + #print('open '+'http://'+host+':' + str(port) + '/setup') app.run(host=host, port=port, debug=rein.debug) return else: @@ -1638,6 +1684,55 @@ def settings(): return render_template('settings.html', user=user, hidden_jobs=hidden_jobs, hidden_bids=hidden_bids, hidden_mediators=hidden_mediators, fee=fee, trust_score=trust_score) + @app.route("/wallet", methods=['GET']) + def wallet(): + fee = float(PersistConfig.get(rein, 'fee', 0.001)) + wallet_entries = rein.session.query(Wallet).filter(Wallet.userid == rein.user.id).all() + balance = 0. + txs = [] + for we in wallet_entries: + ref = we.ref + txs_we = None + new_job = False + for txj in txs: + if txj['ref'] == ref: + txs_we = txj + break + else: + new_job = True + txs_we = {} + txs_we['ref'] = we.ref + txs_we['txs'] = [] + (txins,value) = unspent_txins(rein,we.address,rein.testnet,txin_value=True) + for (txid,txvalue) in txins: + tx = {} + tx['txid'] = txid + tx['value'] = round(txvalue,8) + txs_we['txs'].append(tx) + balance += value + if new_job: + txs.append(txs_we) + + explorer = PersistConfig.get(rein, + 'explorer', + 'https://testnet.blockexplorer.com' if rein.testnet else 'https://blockexplorer.com' + ) + + return render_template('wallet.html', user=user, fee=fee, balance=round(balance,8), txs=txs, explorer=explorer) + + @app.route("/withdraw", methods=['GET','POST']) + def withdraw(): + try: + job_id = request.json['job'] + amount = request.json['amount'] + destaddr = request.json['addr'] + wallet_entries = rein.session.query(Wallet).filter(Wallet.ref == job_id).all() + withdraw_from_wallet(rein,wallet_entries,float(amount),destaddr) + except Exception as detail: + print "Unexpected error: ", detail + return 'false' + return 'true' + @app.route('/display-users', methods=['GET']) def display_users(): data = [] @@ -1850,19 +1945,36 @@ def job_offer(): if request.method == 'POST' and form.validate_on_submit(): bid_doc = Document.get(rein, form.bid_id.data) bid = parse_document(bid_doc.contents) + next_addr_index_str = PersistConfig.get(rein, 'next_addr_index') + if not next_addr_index_str: + next_addr_index_str = '0' + next_addr_index = int(next_addr_index_str) + (pubkey_for_escrow,privkey_for_escrow) = bip32.generate_new_escrow_pubkey(bip32.from_xprv(user.dxprv),next_addr_index) + (payment_address,payment_privkey) = bip32.generate_new_payment_address(bip32.from_xprv(user.dxprv),next_addr_index) + next_addr_index += 1 + PersistConfig.set(rein,'next_addr_index',str(next_addr_index)) + Pubkeys.set(rein,pubkey_for_escrow,privkey_for_escrow,userid=rein.user.id) + Wallet.set(rein,payment_address,payment_privkey,ref=bid['Job ID'],userid=rein.user.id) + worker_pubkey_for_escrow = bid['Worker public key for escrow'] + primary_redeem_script, primary_addr = build_2_of_3([pubkey_for_escrow,bid['Mediator public key'],worker_pubkey_for_escrow]) + mediator_redeem_script, mediator_escrow_addr = build_mandatory_multisig(bid['Mediator public key'],[pubkey_for_escrow,worker_pubkey_for_escrow]) fields = [ {'label': 'Job name', 'value': bid['Job name']}, {'label': 'Job ID', 'value': bid['Job ID']}, {'label': 'Description', 'value': bid['Description']}, {'label': 'Bid amount (BTC)', 'value': bid['Bid amount (BTC)']}, - {'label': 'Primary escrow address', 'value': bid['Primary escrow address']}, - {'label': 'Mediator escrow address', 'value': bid['Mediator escrow address']}, + {'label': 'Primary escrow address', 'value': primary_addr}, + {'label': 'Mediator escrow address', 'value': mediator_escrow_addr}, {'label': 'Job creator', 'value': bid['Job creator']}, {'label': 'Job creator public key', 'value': bid['Job creator public key']}, {'label': 'Mediator public key', 'value': bid['Mediator public key']}, {'label': 'Worker public key', 'value': bid['Worker public key']}, - {'label': 'Primary escrow redeem script', 'value': bid['Primary escrow redeem script']}, - {'label': 'Mediator escrow redeem script', 'value': bid['Mediator escrow redeem script']}, + {'label': 'Primary escrow redeem script', 'value': primary_redeem_script}, + {'label': 'Mediator escrow redeem script', 'value': mediator_redeem_script}, + {'label': 'Primary payment address', 'value': bid['Primary payment address']}, + {'label': 'Primary client payment address', 'value': payment_address}, + {'label': 'Worker public key for escrow', 'value': worker_pubkey_for_escrow}, + {'label': 'Job creator public key for escrow', 'value': pubkey_for_escrow} ] document_text = assemble_document('Offer', fields) store = True @@ -1935,8 +2047,10 @@ def job_accept(): txins_mediator = delivery['Mediator payment inputs'] amount_mediator = delivery['Mediator payment amount'] daddr_mediator = delivery['Mediator payment address'] - (payment_txid,client_sig) = spend_p2sh(redeemScript,txins,[float(amount)],[daddr],worker_sig,rein) - tx_for_mediator = partial_spend_p2sh_mediator_2(redeemScript_mediator,txins_mediator,float(amount_mediator),daddr_mediator,rein) + pubkey_for_escrow = delivery['Job creator public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) + (payment_txid,client_sig) = spend_p2sh(redeemScript,txins,[float(amount)],[daddr],worker_sig,rein,privkey=privkey_for_escrow) + tx_for_mediator = partial_spend_p2sh_mediator_2(redeemScript_mediator,txins_mediator,float(amount_mediator),daddr_mediator,rein,privkey=privkey_for_escrow) fields = [ {'label': 'Job name', 'value_from': delivery}, {'label': 'Job ID', 'value_from': delivery}, @@ -2041,11 +2155,16 @@ def job_acceptresolution(): daddr_mediator = delivery['Mediator payment address'] sig_mediator = delivery['Mediator payment signature'] + pubkey_for_escrow = '' reverse_sigs = False if key == delivery['Worker public key']: reverse_sigs = True - (payment_txid,second_sig) = spend_p2sh(redeemScript,txins,[float(amount1),float(amount2)],[daddr1,daddr2],sig_primary,rein,reverse_sigs) - (payment_txid_mediator,second_sig_mediator) = spend_p2sh_mediator(redeemScript_mediator,txins_mediator,[float(amount_mediator)],[daddr_mediator],sig_mediator,rein) + pubkey_for_escrow = delivery['Worker public key for escrow'] + else: + pubkey_for_escrow = delivery['Job creator public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) + (payment_txid,second_sig) = spend_p2sh(redeemScript,txins,[float(amount1),float(amount2)],[daddr1,daddr2],sig_primary,rein,reverse_sigs,privkey=privkey_for_escrow) + (payment_txid_mediator,second_sig_mediator) = spend_p2sh_mediator(redeemScript_mediator,txins_mediator,[float(amount_mediator)],[daddr_mediator],sig_mediator,rein,privkey=privkey_for_escrow) fields = [ {'label': 'Job name', 'value_from': delivery}, {'label': 'Job ID', 'value_from': delivery}, @@ -2158,8 +2277,8 @@ def job_resolve(): redeemScript = dispute['Primary escrow redeem script'] mediatorRedeemScript = dispute['Mediator escrow redeem script'] mediator_daddr = rein.user.daddr - worker_payment_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(dispute['Worker public key']))); - client_payment_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(dispute['Job creator public key']))); + worker_payment_daddr = dispute['Primary worker payment address']; + client_payment_daddr = dispute['Primary client payment address']; client_payment_amount = float(form.client_payment_amount.data) try: (payment_txins,payment_amount_1,payment_address_1,payment_amount_2,payment_address_2,payment_sig) = partial_spend_p2sh(redeemScript,rein,worker_payment_daddr,client_payment_amount,client_payment_daddr) @@ -2175,6 +2294,8 @@ def job_resolve(): {'label': 'Job creator public key', 'value_from': dispute}, {'label': 'Worker public key', 'value_from':dispute}, {'label': 'Mediator public key', 'value_from':dispute}, + {'label': 'Worker public key for escrow', 'value_from': dispute}, + {'label': 'Job creator public key for escrow', 'value_from': dispute}, {'label': 'Primary escrow redeem script', 'value_from': dispute}, {'label': 'Mediator escrow redeem script', 'value_from': dispute}, {'label':'Primary payment inputs','value':payment_txins}, @@ -2362,7 +2483,11 @@ def job_dispute(): {'label': 'Mediator escrow redeem script', 'value_from': doc}, {'label': 'Job creator public key', 'value_from': doc}, {'label': 'Worker public key', 'value_from':doc}, - {'label': 'Mediator public key', 'value_from':doc} + {'label': 'Mediator public key', 'value_from':doc}, + {'label': 'Primary worker payment address', 'value':doc['Primary payment address']}, + {'label': 'Primary client payment address', 'value_from':doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc} ] if key == doc['Job creator public key']: @@ -2463,13 +2588,17 @@ def job_bid(): flash('No matching Job ID found.') return redirect("/") - primary_redeem_script, primary_addr = \ - build_2_of_3([job['Job creator public key'], - job['Mediator public key'], - key]) - mediator_redeem_script, mediator_escrow_addr = \ - build_mandatory_multisig(job['Mediator public key'], - [job['Job creator public key'],key]) + next_addr_index_str = PersistConfig.get(rein, 'next_addr_index') + if not next_addr_index_str: + next_addr_index_str = '0' + next_addr_index = int(next_addr_index_str) + (pubkey_for_escrow,privkey_for_escrow) = bip32.generate_new_escrow_pubkey(bip32.from_xprv(user.dxprv),next_addr_index) + (primary_payment_address,primary_payment_privkey) = bip32.generate_new_payment_address(bip32.from_xprv(user.dxprv),next_addr_index) + next_addr_index += 1 + PersistConfig.set(rein,'next_addr_index',str(next_addr_index)) + Pubkeys.set(rein,pubkey_for_escrow,privkey_for_escrow,userid=rein.user.id) + Wallet.set(rein,primary_payment_address,primary_payment_privkey,ref=job['Job ID'],userid=rein.user.id) + fields = [ {'label': 'Job name', 'value_from': job}, {'label': 'Worker', 'value': user.name}, @@ -2479,15 +2608,13 @@ def job_bid(): {'label': 'Worker master address', 'value': user.maddr}, {'label': 'Description', 'value': form.description.data}, {'label': 'Bid amount (BTC)', 'value': form.bid_amount.data}, - {'label': 'Primary escrow address', 'value': primary_addr}, - {'label': 'Mediator escrow address', 'value': mediator_escrow_addr}, {'label': 'Job ID', 'value_from': job}, {'label': 'Job creator', 'value_from': job}, {'label': 'Job creator public key', 'value_from': job}, {'label': 'Mediator public key', 'value_from': job}, {'label': 'Worker public key', 'value': key}, - {'label': 'Primary escrow redeem script', 'value': primary_redeem_script}, - {'label': 'Mediator escrow redeem script', 'value': mediator_redeem_script}, + {'label': 'Primary payment address', 'value': primary_payment_address}, + {'label': 'Worker public key for escrow', 'value': pubkey_for_escrow} ] document_text = assemble_document('Bid', fields) store = True @@ -2560,8 +2687,10 @@ def job_deliver(): redeemScript = doc['Primary escrow redeem script'] mediatorRedeemScript = doc['Mediator escrow redeem script'] mediator_daddr = str(P2PKHBitcoinAddress.from_pubkey(x(doc['Mediator public key']))) + pubkey_for_escrow = doc['Worker public key for escrow'] + privkey_for_escrow = Pubkeys.get(rein,pubkey_for_escrow) try: - (payment_txins,payment_amount,payment_address,payment_sig) = partial_spend_p2sh(redeemScript,rein) + (payment_txins,payment_amount,payment_address,payment_sig) = partial_spend_p2sh(redeemScript,rein,daddr=doc['Primary payment address'],privkey=privkey_for_escrow) (mediator_payment_txins,mediator_payment_amount,mediator_payment_address) = partial_spend_p2sh_mediator(mediatorRedeemScript,rein,mediator_daddr) except ValueError as e: form.deliverable.errors.append(e.message) @@ -2579,6 +2708,8 @@ def job_deliver(): {'label': 'Worker public key', 'value_from': doc}, {'label': 'Mediator public key', 'value_from': doc}, {'label': 'Job creator public key', 'value_from': doc}, + {'label': 'Worker public key for escrow', 'value_from': doc}, + {'label': 'Job creator public key for escrow', 'value_from': doc}, {'label':'Primary payment inputs','value':payment_txins}, {'label':'Primary payment amount','value':payment_amount}, {'label':'Primary payment address','value':payment_address}, @@ -2638,7 +2769,7 @@ def serve_template_file(): orders=relevant_orders) webbrowser.open('http://'+host+':' + str(port)) - print("testnet = "+str(rein.testnet)) + #print('open '+'http://'+host+':' + str(port)) app.run(host=host, port=port, debug=rein.debug) # testing steps: Disable tor. Then turn on debug because debug doesn't work when socket is overriden diff --git a/rein/html/js/wallet.js b/rein/html/js/wallet.js new file mode 100644 index 0000000..c18fdca --- /dev/null +++ b/rein/html/js/wallet.js @@ -0,0 +1,25 @@ +function postError(data) { + if (data != 'true') { + alert('Error withdrawing.') + } else { + alert('Withdrawl succeeded. Please allow some time for it to confirm.') + } +} + +function withdraw_from_job(job_id) { + var amount = $('#withdraw_amount_'+job_id).val(); + var destaddr = $('#withdraw_address_'+job_id).val(); + $.ajax({ + method: "POST", + url: "/withdraw", + contentType: "application/json", + data: JSON.stringify({ + 'job': job_id, + 'amount': amount, + 'addr': destaddr + }), + success: function(data) { + postError(data); + } + }) +} diff --git a/rein/html/layout.html b/rein/html/layout.html index 769d5c9..f1aa8de 100644 --- a/rein/html/layout.html +++ b/rein/html/layout.html @@ -164,12 +164,18 @@ -
  • -
  • - - -
  • - +
  • +
  • + + +
  • + +
  • +
  • + + +
  • + diff --git a/rein/html/wallet.html b/rein/html/wallet.html new file mode 100644 index 0000000..fd487f9 --- /dev/null +++ b/rein/html/wallet.html @@ -0,0 +1,55 @@ +{% extends "layout.html" %} +{% from "_form_helpers.html" import render_error %} +{% block body %} + + + +
    +
    +

    Wallet

    +
    + Overall Balance: {{balance}} +
    + {% if txs %} + {% for txj in txs %} +

    +

    Unspent Payments for Job {{txj.ref}}

    + {% if txj.txs %} + + + + + + + {% for tx in txj.txs %} + + + + + {% endfor %} + +
    txidvalue
    {{tx.txid}}{{tx.value}}
    +
    +

     Withdraw from this job's payments

    +
    +   + +
    +
    +   + +
    +
    +   +
    +
    + {% else %} + None found. + {% endif %} + {% endfor %} + {% else %} +

    No transactions.

    + {% endif %} +
    + +{% endblock %} diff --git a/rein/lib/crypto/bip32.py b/rein/lib/crypto/bip32.py index 241f82e..0fe22ef 100644 --- a/rein/lib/crypto/bip32.py +++ b/rein/lib/crypto/bip32.py @@ -8,6 +8,8 @@ import hmac import os import rein.lib.config as config +from rein.lib.wallet import Wallet +from rein.lib.persistconfig import PersistConfig # TODO Make Python index this file automatically script_dir = os.path.dirname(__file__) @@ -86,3 +88,37 @@ def get_delegate_extended_key(mxprv): delegate_key = get_delegate_key(mxprv) return delegate_key.ExtendedKey() +def generate_new_payment_address(dxprv,i): + parent_key = get_child_key(dxprv, 0+BIP32_HARDEN) + subparent_key = get_child_key(parent_key,i) + target_key = get_child_key(subparent_key,0) + return (target_key.Address(),target_key.WalletImportFormat()) + +def generate_new_escrow_pubkey(dxprv,i): + parent_key = get_child_key(dxprv, 1+BIP32_HARDEN) + subparent_key = get_child_key(parent_key,i) + target_key = get_child_key(subparent_key,0) + return (hexlify(target_key.PublicKey()),target_key.WalletImportFormat()) + +def from_xprv(xkey): + return BIP32Key.fromExtendedKey(xkey) + +def get_new_change_address(dxprv,ref): + wallet_entries = rein.session.query(Wallet).filter(Wallet.ref == ref).all() + j = len(wallet_entries) + print("j = "+str(j)) + if j > 0: + parent_key = get_child_key(dxprv,0+BIP32_HARDEN) + next_addr_index = int(PersistConfig.get(rein, 'next_addr_index')) + for i in range(next_addr_index): + subparent_key = get_child_key(parent_key,i) + target_key_address = get_child_key(subparent_key,0).Address() + for we in wallet_entries: + wallet_address = we.address + if wallet_address == target_key_address: + change_key = get_child_key(subparent_key,j) + change_key_address = change_key.Address() + change_key_privkey = change_key.WalletImportFormat() + Wallet.set(rein,change_key_address,change_key_privkey,ref=ref) + return change_key_address + return None diff --git a/rein/lib/document.py b/rein/lib/document.py index 3de460a..670b908 100644 --- a/rein/lib/document.py +++ b/rein/lib/document.py @@ -6,6 +6,7 @@ from .order import Order from .io import safe_get from .util import document_to_dict +import json Base = declarative_base() @@ -106,11 +107,18 @@ def get_documents_by_job_id(rein, url, job_id): @staticmethod def get_job_id(text): - m = re.search('Job ID: (.+)\n', text) - if m: - return m.group(1) - else: - return None + try: + json_object = json.loads(text) + if "Job ID" in json_object: + return json_object["Job ID"] + else: + return None + except ValueError, e: + m = re.search('Job ID: (.+)\n', text) + if m: + return m.group(1) + else: + return None @staticmethod def get_document_type(document): diff --git a/rein/lib/market.py b/rein/lib/market.py index 6e8c688..01a3dfe 100644 --- a/rein/lib/market.py +++ b/rein/lib/market.py @@ -7,7 +7,7 @@ from sqlalchemy import and_ import os import click - +import json def assemble_document(title, fields): """ @@ -35,11 +35,12 @@ def assemble_document(title, fields): else: entry['value'] = click.prompt(prompt) data.append(entry) - document = "Rein %s\n" % title - for entry in data: - document = document + entry['label'] + ": " + entry['value'] + "\n" - return document[:-1] + document = {} + document['Title'] = "Rein %s" % title + for entry in data: + document[entry['label']] = entry['value'] + return json.dumps(document,sort_keys=True) def sign_and_store_document(rein, doc_type, document, signature_address=None, signature_key=None, store=True, overwrite_hash=None): """ @@ -67,10 +68,10 @@ def sign_and_store_document(rein, doc_type, document, signature_address=None, si if validated: # insert signed document into documents table - b = "-----BEGIN BITCOIN SIGNED MESSAGE-----" - c = "-----BEGIN SIGNATURE-----" - d = "-----END BITCOIN SIGNED MESSAGE-----" - signed = "%s\n%s\n%s\n%s\n%s\n%s" % (b, document, c, signature_address, signature, d) + document_json = json.loads(document) + document_json["signature_address"] = signature_address + document_json["signature"] = signature + signed = json.dumps(document_json,sort_keys=True) click.echo('\n' + signed + '\n') # If document doesn't already exist, create if store and not overwrite_hash: diff --git a/rein/lib/models.py b/rein/lib/models.py index ca86f73..1a1f6a0 100644 --- a/rein/lib/models.py +++ b/rein/lib/models.py @@ -41,4 +41,10 @@ from .hidden_content import Base Base.metadata.create_all(engine) +from .pubkeys import Base +Base.metadata.create_all(engine) + +from .wallet import Base +Base.metadata.create_all(engine) + log.info('database tables updated') diff --git a/rein/lib/pubkeys.py b/rein/lib/pubkeys.py new file mode 100644 index 0000000..53be647 --- /dev/null +++ b/rein/lib/pubkeys.py @@ -0,0 +1,38 @@ +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +import click + +Base = declarative_base() + +class Pubkeys(Base): + __tablename__ = 'pubkeys' + + pubkey = Column(String(34), primary_key=True) + privkey = Column(String(52)) + userid = Column(Integer) + + def __init__(self, session, pubkey, privkey, userid=None): + self.pubkey = pubkey + self.privkey = privkey + self.userid = userid + session.add(self) + session.commit() + + @classmethod + def get(self, rein, pubkey, default=False): + res = rein.session.query(Pubkeys).filter(Pubkeys.pubkey == pubkey).first() + if res: + return res.privkey + else: + return default + + @classmethod + def set(self, rein, pubkey, privkey='', userid=None): + res = rein.session.query(Pubkeys).filter(Pubkeys.pubkey == pubkey).first() + if res: + res.privkey = privkey + if userid is not None: + res.userid = userid + rein.session.commit() + else: + p = Pubkeys(rein.session, pubkey, privkey=privkey, userid=userid) diff --git a/rein/lib/rating.py b/rein/lib/rating.py index 7bf4324..a81a594 100644 --- a/rein/lib/rating.py +++ b/rein/lib/rating.py @@ -10,7 +10,7 @@ from .order import Order, STATE from .document import Document from .bitcoinaddress import generate_sin -from sqlalchemy import and_ +from sqlalchemy import and_, or_ import json def get_job_info(rein, order): @@ -78,30 +78,44 @@ def get_user_jobs(rein, return_dict=False): return json.dumps(job_list) def rating_identifier(fields): - """Generates a string that would be found in the contents of an existing rating document""" - identifier = '' - relevant_fields = ['User msin', 'Job id', 'Rater msin'] - for field in fields: - if field['label'] in relevant_fields: - identifier += field['label'] + ": " + field['value'] + "\n" - - return identifier + """Generates a string that would be found in the contents of an existing rating document""" + identifier = '' + relevant_fields = ['User msin', 'Job id', 'Rater msin'] + for field in fields: + if field['label'] in relevant_fields: + identifier += '"'+field['label']+'": "'+field['value']+'"%' + return identifier + +def rating_identifier_old(fields): + identifier = '' + relevant_fields = ['User msin', 'Job id', 'Rater msin'] + for field in fields: + if field['label'] in relevant_fields: + identifier += field['label'] + ": " + field['value'] + "\n" + return identifier def add_rating(rein, user, testnet, rating, user_msin, job_id, rated_by_msin, comments): """Adds a rating to the database or updates it if an already existing rating is adjusted""" fields = [ {'label': 'Rating', 'value': rating}, - {'label': 'User msin', 'value': user_msin}, {'label': 'Job id', 'value': job_id}, {'label': 'Rater msin', 'value': rated_by_msin}, - {'label': 'Comments', 'value': comments}, + {'label': 'User msin', 'value': user_msin}, + {'label': 'Comments', 'value': comments} ] + fields_old = [ + {'label': 'Rating', 'value': rating}, + {'label': 'User msin', 'value': user_msin}, + {'label': 'Job id', 'value': job_id}, + {'label': 'Rater msin', 'value': rated_by_msin}, + {'label': 'Comments', 'value': comments} + ] document_text = assemble_document('Rating', fields) - update_identifier = rating_identifier(fields) - look_for = '%\n{}%'.format(update_identifier) - update_rating = rein.session.query(Document).filter(and_(Document.testnet == testnet, Document.contents.like(look_for), Document.doc_type == 'rating')).first() + look_for = '%{}'.format(rating_identifier(fields)) + look_for_old = '%{}'.format(rating_itentifier_old(fields_old)) + update_rating = rein.session.query(Document).filter(and_(Document.testnet == testnet, or_(Document.contents.like(look_for),Document.contents.like(look_for_old)), Document.doc_type == 'rating')).first() store = True document = None @@ -203,7 +217,6 @@ def calculate_trust_score(dest_msin=None, source_msin=None, rein=None, test=Fals data = [rating['value'] for rating in raw_data] ratings_by_source = filter_and_parse_valid_sigs(rein, data) - else: ratings_by_source = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == 'SourceMsin'] @@ -234,7 +247,6 @@ def calculate_trust_score(dest_msin=None, source_msin=None, rein=None, test=Fals data = [rating['value'] for rating in raw_data] dest_ratings_by_vouched_user = filter_and_parse_valid_sigs(rein, data) - else: dest_ratings_by_vouched_user = [test_rating for test_rating in test_ratings if test_rating['Rater msin'] == vouched_user_msin and test_rating['User msin'] == 'DestMsin'] diff --git a/rein/lib/script.py b/rein/lib/script.py index 14a5869..5697e42 100644 --- a/rein/lib/script.py +++ b/rein/lib/script.py @@ -79,4 +79,4 @@ def check_redeem_scripts(document): ret['Mediator public key'], pubkeys): click.echo("2-of-3 check failed") return False - return True \ No newline at end of file + return True diff --git a/rein/lib/transaction.py b/rein/lib/transaction.py index 06328b6..6c53062 100644 --- a/rein/lib/transaction.py +++ b/rein/lib/transaction.py @@ -9,9 +9,9 @@ from .io import safe_get from .persistconfig import PersistConfig import click +from .crypto.bip32 import get_new_change_address, from_xprv - -def unspent_txins(rein, address, testnet): +def unspent_txins(rein, address, testnet, txin_value=False): api = PersistConfig.get(rein, 'api', 'blockr') if (api == "blockr"): @@ -30,7 +30,10 @@ def unspent_txins(rein, address, testnet): vout = tx['n']; value = float(tx['amount']); total_value += value; - txins.append((txid, vout)) + if txin_value: + txins.append((txid, value)) + else: + txins.append((txid, vout)) else: if testnet: url = "https://api.blocktrail.com/v1/tbtc/address/"+str(address)+"/unspent-outputs?api_key=1e1ebd7ae629e031310ae9d61fe8549c82d0c589" @@ -47,7 +50,10 @@ def unspent_txins(rein, address, testnet): vout = tx['index']; value = tx['value']/100000000.; total_value += value; - txins.append((txid, vout)) + if txin_value: + txins.append((txid, value)) + else: + txins.append((txid, vout)) return (txins,total_value) @@ -61,7 +67,7 @@ def broadcast_tx (tx_hex,rein): if data and 'txid' in data: return data['txid'] -def partial_spend_p2sh (redeemScript,rein,daddr=None,alt_amount=None,alt_daddr=None): +def partial_spend_p2sh (redeemScript,rein,daddr=None,alt_amount=None,alt_daddr=None,privkey=None): if daddr is None: daddr = rein.user.daddr txin_redeemScript = CScript(x(redeemScript)) @@ -90,7 +96,10 @@ def partial_spend_p2sh (redeemScript,rein,daddr=None,alt_amount=None,alt_daddr=N txouts.append(txout_alt) tx = CMutableTransaction(txins_obj, txouts) ntxins = len(txins_obj) - seckey = CBitcoinSecret(rein.user.dkey) + if privkey: + seckey = CBitcoinSecret(privkey) + else: + seckey = CBitcoinSecret(rein.user.dkey) sig = ""; for i in range(0,ntxins): sighash = SignatureHash(txin_redeemScript, tx, i, SIGHASH_ALL) @@ -127,7 +136,7 @@ def partial_spend_p2sh_mediator (redeemScript,rein,mediator_address,mediator_sig return (txins_str[1:],"{:.8f}".format(amount),str(mediator_address),sig[1:]) return (txins_str[1:],"{:.8f}".format(amount),str(mediator_address)) -def partial_spend_p2sh_mediator_2 (redeemScript,txins_str,amount,daddr,rein): +def partial_spend_p2sh_mediator_2 (redeemScript,txins_str,amount,daddr,rein,privkey=None): txin_redeemScript = CScript(x(redeemScript)) txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey() txins_obj = [] @@ -136,7 +145,10 @@ def partial_spend_p2sh_mediator_2 (redeemScript,txins_str,amount,daddr,rein): txins_obj.append(CMutableTxIn(COutPoint(lx(txin_list[0]),int(txin_list[1])))) txout = CMutableTxOut(amount*COIN,CBitcoinAddress(daddr).to_scriptPubKey()) tx = CMutableTransaction(txins_obj,[txout]) - seckey = CBitcoinSecret(rein.user.dkey) + if privkey: + seckey = CBitcoinSecret(privkey) + else: + seckey = CBitcoinSecret(rein.user.dkey) ntxins = len(txins_obj) for i in range(0,ntxins): sighash = SignatureHash(txin_redeemScript,tx,i,SIGHASH_ALL) @@ -146,7 +158,7 @@ def partial_spend_p2sh_mediator_2 (redeemScript,txins_str,amount,daddr,rein): tx_bytes = tx.serialize() return b2x(tx_bytes) -def spend_p2sh (redeemScript,txins_str,amounts,daddrs,sig,rein,reverse_sigs=False): +def spend_p2sh (redeemScript,txins_str,amounts,daddrs,sig,rein,reverse_sigs=False,privkey=None): txin_redeemScript = CScript(x(redeemScript)) txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey() txins_obj = [] @@ -158,7 +170,10 @@ def spend_p2sh (redeemScript,txins_str,amounts,daddrs,sig,rein,reverse_sigs=Fals for i in range(0,len_amounts): txouts.append(CMutableTxOut(round(amounts[i],8)*COIN,CBitcoinAddress(daddrs[i]).to_scriptPubKey())) tx = CMutableTransaction(txins_obj,txouts) - seckey = CBitcoinSecret(rein.user.dkey) + if privkey: + seckey = CBitcoinSecret(privkey) + else: + seckey = CBitcoinSecret(rein.user.dkey) ntxins = len(txins_obj) sig_list = [] for s in sig.split(): @@ -179,7 +194,7 @@ def spend_p2sh (redeemScript,txins_str,amounts,daddrs,sig,rein,reverse_sigs=Fals txid_causeway = broadcast_tx(b2x(tx_bytes),rein) return (txid,sig2_str[1:]) -def spend_p2sh_mediator (redeemScript,txins_str,amounts,daddrs,sig,rein): +def spend_p2sh_mediator (redeemScript,txins_str,amounts,daddrs,sig,rein,privkey=None): txin_redeemScript = CScript(x(redeemScript)) txin_scriptPubKey = txin_redeemScript.to_p2sh_scriptPubKey() txins_obj = [] @@ -191,7 +206,10 @@ def spend_p2sh_mediator (redeemScript,txins_str,amounts,daddrs,sig,rein): for i in range(0,len_amounts): txouts.append(CMutableTxOut(round(amounts[i],8)*COIN,CBitcoinAddress(daddrs[i]).to_scriptPubKey())) tx = CMutableTransaction(txins_obj,txouts) - seckey = CBitcoinSecret(rein.user.dkey) + if privkey: + seckey = CBitcoinSecret(privkey) + else: + seckey = CBitcoinSecret(rein.user.dkey) ntxins = len(txins_obj) sig_list = [] for s in sig.split(): @@ -209,5 +227,46 @@ def spend_p2sh_mediator (redeemScript,txins_str,amounts,daddrs,sig,rein): txid_causeway = broadcast_tx(b2x(tx_bytes),rein) return (txid,sig2_str[1:]) - - +def withdraw_from_wallet (rein, wallet_entries, amount, destaddr): + txins_obj = [] + scriptpubkeys = [] + seckeys = [] + total_value = 0. + for we in wallet_entries: + address = we.address + privkey = we.privkey + (txins,entry_value) = unspent_txins(rein, address, rein.testnet) + total_value += entry_value + for txid,vout in txins: + txins_obj.append(CMutableTxIn(COutPoint(lx(txid),vout))) + scriptpubkeys.append(CBitcoinAddress(address).to_scriptPubKey()) + seckeys.append(CBitcoinSecret(privkey)) + fee = float(PersistConfig.get(rein, 'fee', 0.001)) + total_amount = round(total_value-fee,8) + if total_amount <= 0: + raise ValueError('There is not enough funds available to withdraw') + amount_requested = round(amount,8) + if (amount_requested > total_amount+fee): + raise ValueError('The value of your funds for this job is less than what you requested') + change_address = None + if (amount_requested < total_amount): + change_address = get_new_change_address(from_xprv(rein.user.dxprv),wallet_entries[0].ref) + if change_address is None: + raise ValueError('Error generating a change address') + txouts = [] + txout = CMutableTxOut(amount_requested*COIN, CBitcoinAddress(destaddr).to_scriptPubKey()) + txouts.append(txout) + if change_address: + txout_change = CMutableTxOut(round(total_value-fee-amount_requested,8)*COIN, CBitcoinAddress(change_address).to_scriptPubKey()) + txouts.append(txout_change) + tx = CMutableTransaction(txins_obj, txouts) + ntxins = len(txins_obj) + for i in range(0,ntxins): + sighash = SignatureHash(scriptpubkeys[i],tx,i,SIGHASH_ALL) + sig = seckeys[i].sign(sighash)+x("01") + txins_obj[i].scriptSig = CScript([sig, seckeys[i].pub]) + VerifyScript(txins_obj[i].scriptSig,scriptpubkeys[i],tx,i,(SCRIPT_VERIFY_P2SH,)) + tx_bytes = tx.serialize() + hash = sha256(sha256(tx_bytes).digest()).digest() + txid = b2x(hash[::-1]) + txid_causeway = broadcast_tx(b2x(tx_bytes),rein) diff --git a/rein/lib/ui.py b/rein/lib/ui.py index 9e7c954..357668e 100644 --- a/rein/lib/ui.py +++ b/rein/lib/ui.py @@ -150,14 +150,12 @@ def create_account(rein): rein.user = new_identity enrollment = build_enrollment_from_dict(user_data) - signed_enrollment = '-----BEGIN BITCOIN SIGNED MESSAGE-----\n' + \ - enrollment + \ - '\n-----BEGIN SIGNATURE-----\n' + \ - maddr + '\n' + \ - sign(mprv, enrollment) + \ - '\n-----END BITCOIN SIGNED MESSAGE-----\n' + signature = sign(mprv,json.dumps(enrollment,sort_keys=True)) + signed_enrollment = enrollment + signed_enrollment['signature'] = signature + signed_enrollment['signature_address'] = maddr User.set_enrolled(rein, new_identity) - document = Document(rein, 'enrollment', signed_enrollment, sig_verified=True, testnet=rein.testnet) + document = Document(rein, 'enrollment', json.dumps(signed_enrollment,sort_keys=True), sig_verified=True, testnet=rein.testnet) rein.session.add(document) rein.session.commit() @@ -177,20 +175,23 @@ def create_account(rein): def build_enrollment_from_dict(user_data): - mediator_extras = '' + enrollment = {} + enrollment['Title'] = 'Rein User Enrollment' + enrollment['User'] = user_data['name'] + enrollment['Contact'] = user_data['contact'] + enrollment['Master signing address'] = user_data['maddr'] + enrollment['Secure Identity Number'] = user_data['msin'] + enrollment['Delegate signing address'] = user_data['daddr'] if user_data['will_mediate']: - mediator_extras = "\nMediator public key: %s\nMediator fee: %s%%" % \ - (pubkey(user_data['dkey']), user_data['mediator_fee']) - enrollment = "Rein User Enrollment\nUser: %s\nContact: %s\nMaster signing address: %s\n" \ - "Secure Identity Number: %s\nDelegate signing address: %s\n" \ - "Willing to mediate: %s%s" % \ - (user_data['name'], user_data['contact'], user_data['maddr'], user_data['msin'],\ - user_data['daddr'], user_data['will_mediate'], mediator_extras) + enrollment['Willing to mediate'] = 'True' + enrollment['Mediator public key'] = pubkey(user_data['dkey']) + enrollment['Mediator fee'] = user_data['mediator_fee'] + else: + enrollment['Willing to mediate'] = 'False' if user_data['testnet']: - enrollment += '\nTestnet: True' + enrollment['Testnet'] = 'True' return enrollment - def import_account(rein, mprv=None, mnemonic=None): Base.metadata.create_all(rein.engine) backup_filename = click.prompt("Enter backup file name", type=str, default=rein.backup_filename) @@ -222,40 +223,44 @@ def import_account(rein, mprv=None, mnemonic=None): if not privkey_to_address(mprv): raise Exception('Invalid master private key.') + enrollment = build_enrollment_from_dict(user_data) - signed_enrollment = '-----BEGIN BITCOIN SIGNED MESSAGE-----\n' + \ - enrollment + \ - '\n-----BEGIN SIGNATURE-----\n' + \ - user_data['maddr'] + '\n' + \ - sign(mprv, enrollment) + \ - '\n-----END BITCOIN SIGNED MESSAGE-----\n' + signature = sign(mprv,json.dumps(enrollment,sort_keys=True)) + signed_enrollment = enrollment + signed_enrollment['signature'] = signature + signed_enrollment['signature_address'] = user_data['maddr'] + User.set_enrolled(rein, new_identity) - document = Document(rein, 'enrollment', signed_enrollment, sig_verified=True, testnet=rein.testnet) + document = Document(rein, 'enrollment', json.dumps(signed_enrollment,sort_keys=True), sig_verified=True, testnet=rein.testnet) rein.session.add(document) rein.session.commit() return rein.user - def build_enrollment(rein): + + enrollment = {} user = rein.user - mediator_extras = '' + enrollment['Title'] = 'Rein User Enrollment' + enrollment['User'] = user.name + enrollment['Contact'] = user.contact + enrollment['Master signing address'] = user.maddr + enrollment['Delegate signing address'] = user.daddr if user.will_mediate: - mediator_extras = "\nMediator public key: %s\nMediator fee: %s%%" % \ - (pubkey(user.dkey), user.mediator_fee) - enrollment = "Rein User Enrollment\nUser: %s\nContact: %s\nMaster signing address: %s" \ - "\nDelegate signing address: %s\nWilling to mediate: %s%s" % \ - (user.name, user.contact, user.maddr, user.daddr, user.will_mediate, mediator_extras) + enrollment['Willing to mediate'] = 'True' + enrollment['Mediator public key'] = pubkey(user.dkey) + enrollment['Mediator fee'] = user.mediator_fee + else: + enrollment['Willing to mediate'] = 'False' if rein.testnet: - enrollment += '\nTestnet: True' + enrollment['Testnet'] = 'True' return enrollment - def enroll(rein): user = rein.user Base.metadata.create_all(rein.engine) enrollment = build_enrollment(rein) f = open(rein.enroll_filename, 'w') - f.write(enrollment) + f.write(json.dumps(enrollment,sort_keys=True)) f.close() click.echo("%s\n" % enrollment) done = False diff --git a/rein/lib/util.py b/rein/lib/util.py index 5c47b91..71342a5 100644 --- a/rein/lib/util.py +++ b/rein/lib/util.py @@ -1,23 +1,29 @@ +import json + def document_to_dict(content): """Turns a document's contents into a dict""" - doc = {} try: - # Grab part of the document containing information - content = content.split('\n-----BEGIN SIGNATURE-----')[0] - # Remove heading - content = content.split('\n')[2:] - for line in content: - key_value = line.split(': ') - if len(key_value) > 1: - key = key_value[0] - value = key_value[1] - doc[key] = value - - except: - return {'error': 'unspecified error'} - - return doc + json_object = json.loads(content) + return json_object + except ValueError, e: + doc = {} + try: + # Grab part of the document containing information + content = content.split('\n-----BEGIN SIGNATURE-----')[0] + # Remove heading + content = content.split('\n')[2:] + for line in content: + key_value = line.split(': ') + if len(key_value) > 1: + key = key_value[0] + value = key_value[1] + doc[key] = value + + except: + return {'error': 'unspecified error'} + + return doc def get_user_name(log, url, user, rein, msin): """Find a user's name by his msin""" diff --git a/rein/lib/validate.py b/rein/lib/validate.py index 44c0230..cba0601 100644 --- a/rein/lib/validate.py +++ b/rein/lib/validate.py @@ -7,6 +7,7 @@ from .io import safe_get from .util import unique from .block import Block +import json def filter_out_expired(rein, user, urls, jobs): live = [] @@ -66,29 +67,37 @@ def choose_best_block(blocks): def strip_armor(sig, dash_space=False): - '''Removes ASCII-armor from a signed message by default exlcudes 'dash-space' headers''' - sig = sig.replace('- ----', '-' * 5) if dash_space else sig - sig = re.sub("-{5}BEGIN BITCOIN SIGNED MESSAGE-{5}", "", sig) - sig = re.sub( - "\n+-{5}BEGIN SIGNATURE-{5}[\n\dA-z+=/]+-{5}END BITCOIN SIGNED MESSAGE-{5}\n*", - "", - sig - ) - sig = re.sub("^\n", "", sig) - sig = re.sub("\n\n", "", sig) - return sig - + try: + json_object = json.loads(sig) + json_object.pop("signature") + json_object.pop("signature_address") + return json.dumps(json_object,sort_keys=True) + except ValueError, e: + '''Removes ASCII-armor from a signed message by default exlcudes 'dash-space' headers''' + sig = sig.replace('- ----', '-' * 5) if dash_space else sig + sig = re.sub("-{5}BEGIN BITCOIN SIGNED MESSAGE-{5}", "", sig) + sig = re.sub( + "\n+-{5}BEGIN SIGNATURE-{5}[\n\dA-z+=/]+-{5}END BITCOIN SIGNED MESSAGE-{5}\n*", + "", + sig + ) + sig = re.sub("^\n", "", sig) + sig = re.sub("\n\n", "", sig) + return sig def parse_document(document): - ret = {} - m = re.search('(Rein .*)\n', document) - if m: - ret['Title'] = m.group(1) - matches = re.finditer("(.+?):\s(.+)(\n|$)", document) - for match in matches: - ret[match.group(1)] = match.group(2) - return ret - + try: + json_object = json.loads(document) + except ValueError, e: #for backwards compatibility + ret = {} + m = re.search('(Rein .*)\n', document) + if m: + ret['Title'] = m.group(1) + matches = re.finditer("(.+?):\s(.+)(\n|$)", document) + for match in matches: + ret[match.group(1)] = match.group(2) + return ret + return json_object def parse_sig(sig): ''' @@ -97,25 +106,28 @@ def parse_sig(sig): assigned within the message, for example: parse_sig(sig)['Name/handle'] === "David Sterry" ''' - ret = {} - m = re.search('\n(Rein .*)\n', sig) - if m: - ret['Title'] = m.group(1) - matches = re.finditer("(.+?):\s(.*)\n", sig) - for match in matches: - ret[match.group(1)] = match.group(2) - m = re.search( - "-{5}BEGIN SIGNATURE-{5}\n([A-z\d=+/]+)\n([A-z\d=+/]+)" - "\n-{5}END BITCOIN SIGNED MESSAGE-{5}", - sig - ) - if m: - ret['signature_address'] = m.group(1) - ret['signature'] = m.group(2) - else: - return False - return ret - + try: + json_object = json.loads(sig) + except ValueError, e: #for backwards compatibility + ret = {} + m = re.search('\n(Rein .*)\n', sig) + if m: + ret['Title'] = m.group(1) + matches = re.finditer("(.+?):\s(.*)\n", sig) + for match in matches: + ret[match.group(1)] = match.group(2) + m = re.search( + "-{5}BEGIN SIGNATURE-{5}\n([A-z\d=+/]+)\n([A-z\d=+/]+)" + "\n-{5}END BITCOIN SIGNED MESSAGE-{5}", + sig + ) + if m: + ret['signature_address'] = m.group(1) + ret['signature'] = m.group(2) + else: + return False + return ret + return json_object def filter_valid_sigs(rein, docs, expected_field=None): valid = [] @@ -206,7 +218,7 @@ def validate_review(reviewer_text): strip_armor(reviewer_text).replace('- ----', '-----') ] - +#is this function used? def validate_audit(auditor_text): a = verify_sig(auditor_text) txt = strip_armor(auditor_text) diff --git a/rein/lib/wallet.py b/rein/lib/wallet.py new file mode 100644 index 0000000..466ffeb --- /dev/null +++ b/rein/lib/wallet.py @@ -0,0 +1,42 @@ +from sqlalchemy import Column, Integer, String, DateTime +from sqlalchemy.ext.declarative import declarative_base +import click + +Base = declarative_base() + +class Wallet(Base): + __tablename__ = 'wallet' + + address = Column(String(34), primary_key=True) + privkey = Column(String(52)) + ref = Column(String(64)) + userid = Column(Integer) + + def __init__(self, session, address, privkey, ref=None, userid=None): + self.address = address + self.privkey = privkey + self.ref = ref + self.userid = userid + session.add(self) + session.commit() + + @classmethod + def get(self, rein, address, default=False): + res = rein.session.query(Wallet).filter(Wallet.address == address).first() + if res: + return res.privkey + else: + return default + + @classmethod + def set(self, rein, address, privkey='', ref=None, userid=None): + res = rein.session.query(Wallet).filter(Wallet.address == address).first() + if res: + res.privkey = privkey + if ref is not None: + res.ref = ref + if userid is not None: + res.userid = userid + rein.session.commit() + else: + p = Wallet(rein.session, address, privkey, ref=ref, userid=userid)