diff --git a/.gitmodules b/.gitmodules index 93d87cd42243b234ba4cfb821b64b21c932c25c8..0931976031587fb56e7c1a0e3686211263906bf3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -10,3 +10,9 @@ [submodule "static/chartjs"] path = static/chartjs url = git://github.com/nnnick/Chart.js.git +[submodule "static/typeahead"] + path = static/typeahead + url = https://github.com/twitter/typeahead.js +[submodule "static/hogan"] + path = static/hogan + url = https://github.com/twitter/hogan.js diff --git a/static/hogan b/static/hogan new file mode 160000 index 0000000000000000000000000000000000000000..5450e5ab61be562c843985409b04316a96840066 --- /dev/null +++ b/static/hogan @@ -0,0 +1 @@ +Subproject commit 5450e5ab61be562c843985409b04316a96840066 diff --git a/static/typeahead b/static/typeahead new file mode 160000 index 0000000000000000000000000000000000000000..331cff944d00f1d8e98694fc6f73ab80455bd3be --- /dev/null +++ b/static/typeahead @@ -0,0 +1 @@ +Subproject commit 331cff944d00f1d8e98694fc6f73ab80455bd3be diff --git a/static/wallets/typeahead.css b/static/wallets/typeahead.css new file mode 100644 index 0000000000000000000000000000000000000000..376e7caf2bf47afadb3e2977516bbf46fde8d8fb --- /dev/null +++ b/static/wallets/typeahead.css @@ -0,0 +1,63 @@ +.typeahead, +.tt-query, +.tt-hint { + width: 800px; + height: 30px; + padding: 8px 12px; + font-size: 24px; + line-height: 30px; + border: 2px solid #ccc; + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + outline: none; +} + +.typeahead { + background-color: #fff; +} + +.typeahead:focus { + border: 2px solid #0097cf; +} + +.tt-query { + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + -moz-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.075); +} + +.tt-hint { + color: #999 +} + +.tt-dropdown-menu { + width: 800px; + margin-top: 12px; + padding: 8px 0; + background-color: #fff; + border: 1px solid #ccc; + border: 1px solid rgba(0, 0, 0, 0.2); + -webkit-border-radius: 8px; + -moz-border-radius: 8px; + border-radius: 8px; + -webkit-box-shadow: 0 5px 10px rgba(0,0,0,.2); + -moz-box-shadow: 0 5px 10px rgba(0,0,0,.2); + box-shadow: 0 5px 10px rgba(0,0,0,.2); +} + +.tt-suggestion { + padding: 3px 20px; + font-size: 18px; + line-height: 24px; +} + +.tt-suggestion.tt-is-under-cursor { + color: #fff; + background-color: #0097cf; + +} + +.tt-suggestion p { + margin: 0; +} diff --git a/templates/base.html b/templates/base.html index 30c3661b8010356774e70b9852ee27d837a6f30c..c198fc771cc0e9eae8a5a78f7f8c4a6ed1ff04fc 100644 --- a/templates/base.html +++ b/templates/base.html @@ -13,7 +13,7 @@ var $SCRIPT_ROOT = {{ request.script_root|tojson|safe }}; </script> - {% block head %}{% endblock %} + {% block head -%}{% endblock -%} </head> <body> {% include "nav.html" %} diff --git a/templates/wallets/base.html b/templates/wallets/base.html index 964f888e82e9716e1823d8b335f2aadd333c7ed6..4afef8800068628a995da5e1988b5a029e678374 100644 --- a/templates/wallets/base.html +++ b/templates/wallets/base.html @@ -2,18 +2,26 @@ {% block section_title %}Wallets{% endblock %} +{% block head -%} + {% block subhead -%}{% endblock -%} +{% endblock -%} + {% block content %} <div class="row"> <div class="col-lg-3"> <div class="list-group" style="background-color: #f7f5fa;"> <a id="new" href="{{ url_for('new_wallet') }}" class="list-group-item"><i class="glyphicon glyphicon-plus-sign"></i> New</a> - <a id="list" href="{{ url_for('wallets') }}" class="list-group-item"><i class="glyphicon glyphicon-list-alt"></i> Wallet list</a> + <a id="list" href="{{ url_for('wallets') }}" class="list-group-item"><i class="glyphicon glyphicon-credit-card"></i> Wallets</a> </div> {% if key -%} - <div class="list-group" style="background-color: #f7f5fa;"> - <a id="history" href="{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint) }}" class="list-group-item"><i class="glyphicon glyphicon-calendar"></i> History</a> + <div class="panel panel-default"> + <div class="panel-heading">{{key.uids.0}}</div> + <div class="list-group" style="background-color: #f7f5fa;"> + <a id="history" href="{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint) }}" class="list-group-item"><i class="glyphicon glyphicon-calendar"></i> History</a> + <a id="transfer" href="{{ url_for('wallet_transfer', pgp_fingerprint=key.fingerprint) }}" class="list-group-item"><i class="glyphicon glyphicon-transfer"></i> Transfer</a> + </div> </div> {% endif -%} </div> @@ -35,6 +43,7 @@ {% if key -%} '{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint) }}': 'history', '{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint, type=type) }}': 'history', + '{{ url_for('wallet_transfer', pgp_fingerprint=key.fingerprint) }}': 'transfer', {% endif -%} }; diff --git a/templates/wallets/history.html b/templates/wallets/history.html index ca4365cc3564b9cb08287db9ab1b001878f689f4..aac5c318876ba2d8764ffe28b89a60fecb748ed0 100644 --- a/templates/wallets/history.html +++ b/templates/wallets/history.html @@ -3,11 +3,25 @@ {% block sub_content %} <h1><span class="label label-default">History</span> <span class="label label-primary">{{key.uids.0|truncate(50)}}</span></h1> -<ul class="nav nav-tabs pull-right"> - {% for name, color in [("all", "default"), ("transfer", "info"), ("issuance", "success"), ("fusion", "warning")] -%} - <li {% if type == name %}class="active"{% endif %}><a href="{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint, type=name) }}"><span class="label label-{{color}}">{{name|title}}</span></a></li> - {% endfor -%} -</ul> +<br/> + +<div class="row"> + {% with coins=clist.1|map(attribute="amount")|list|join(" + ") -%} + <div class="col-md-3"> + <div class="alert alert-info tooltip_link" title="{{coins}}">Account balance: <span class="badge alert-default">{{clist.0}}</span></div> + </div> + <div class="col-md-2"> + <div class="alert alert-info">{{coins}}</div> + </div> + {% endwith %} + <div class="col-md-7"> + <ul class="nav nav-tabs pull-right"> + {% for name, color in [("all", "default"), ("transfer", "info"), ("issuance", "success"), ("fusion", "warning")] -%} + <li {% if type == name %}class="active"{% endif %}><a href="{{ url_for('wallet_history', pgp_fingerprint=key.fingerprint, type=name) }}"><span class="label label-{{color}}">{{name|title}}</span></a></li> + {% endfor -%} + </ul> + </div> +</div> {% for label,data in [("Received", recipient), ("Sent", sender)] -%} <h3>{{label}} transactions</h3> @@ -30,7 +44,7 @@ <td> {% with reference=tx.sender if label == "Received" else tx.recipient -%} - {% with name=settings.list_keys[reference].uids.0 if settings.list_keys[reference] else "", keyid=reference[-8:] -%} + {% with name=settings.public_keys[reference].uids.0 if settings.public_keys[reference] else "", keyid=reference[-8:] -%} <a class="tooltip_link" title="{{name}} ({{keyid}})"> {% if reference == key.fingerprint -%} <span class="label label-default">me</span> diff --git a/templates/wallets/index.html b/templates/wallets/index.html index 21d5d4e251936f010edce7990d7aca6d37ef8bc9..0ac392164ee89f201048e846935e2f6088fdc33b 100644 --- a/templates/wallets/index.html +++ b/templates/wallets/index.html @@ -8,7 +8,7 @@ </div> <div class="list-group"> - {% for fp,k in settings.list_keys.items() %} + {% for fp,k in settings.secret_keys.items() %} <a href="{{ url_for('wallet_history', pgp_fingerprint=fp) }}" class="list-group-item {% if k.keyid == settings.keyid %}active{% endif %}"> <h4 class="list-group-item-heading">{{k.uids.0}}</h4> <p class="list-group-item-text">{{fp}}</p> diff --git a/templates/wallets/transfer.html b/templates/wallets/transfer.html new file mode 100644 index 0000000000000000000000000000000000000000..4789b701e4efd0c8405c978ca1b3a757c9025a7a --- /dev/null +++ b/templates/wallets/transfer.html @@ -0,0 +1,119 @@ +{% extends "wallets/base.html" %} + +{% block subhead -%} + <link rel="stylesheet" href="{{ url_for('static', filename='jqueryui/themes/base/jquery.ui.all.css') }}" /> + <link rel="stylesheet" href="{{ url_for('static', filename='wallets/typeahead.css') }}" /> +{% endblock -%} + +{% block sub_content %} +<h1><span class="label label-default">Transfer</span> <span class="label label-primary">{{key.uids.0|truncate(50)}}</span></h1> + +<br/> + +<div class="row"> + {% with coins=clist.1|map(attribute="amount")|list|join(" + ") -%} + <div class="col-md-3"> + <div class="alert alert-info tooltip_link" title="{{coins}}">Account balance: <span class="badge alert-default">{{clist.0}}</span></div> + </div> + <div class="col-md-2"> + <div class="alert alert-info">{{coins}}</div> + </div> + {% endwith -%} + <div class="col-md-7"> + </div> +</div> + +<form role="form" method="post" action="{{ url_for('wallet_transfer', pgp_fingerprint=key.fingerprint) }}"> + <div class="panel panel-success"> + <div class="panel-heading"><h4>Recipient</h4></div> + <div class="panel-body"> + <input class="typeahead" type="text" placeholder="Your recipient" name="recipient"/> + </div> + </div> + + <div class="panel panel-success"> + <div class="panel-heading"><h4>Amount</h4></div> + <div class="panel-body" style="padding: 20px 30px"> + <div class="one_slide"> + <div class="row"> + <div class="col-md-10" id="amounts-slide"></div> + <div class="col-md-2 input-group" style="padding-left: 20px"> + <input type="text" name="amount" id="amount" class="form-control" value="0"/> + <span class="input-group-addon">U</span> + </div> + </div> + </div> + </div> + <div class="panel-footer"> + Choose the amount you want to send. + </div> + </div> + + <div class="form-group"> + <button class="btn btn-lg btn-primary btn-block" type="submit">Transfer</button> + </div> +</form> +{% endblock %} + +{% block subfoot %} + <script src="{{ url_for('static', filename='jqueryui/ui/jquery.ui.core.js') }}"></script> + <script src="{{ url_for('static', filename='jqueryui/ui/jquery.ui.widget.js') }}"></script> + <script src="{{ url_for('static', filename='jqueryui/ui/jquery.ui.mouse.js') }}"></script> + <script src="{{ url_for('static', filename='jqueryui/ui/jquery.ui.slider.js') }}"></script> + <script src="{{ url_for('static', filename='typeahead/dist/typeahead.min.js') }}"></script> + <script src="{{ url_for('static', filename='hogan/web/builds/2.0.0/hogan-2.0.0.min.js') }}"></script> + + <script> + $(function() { + var amounts = [{{ sums|join(",") }}] + $('#amounts-slide').slider({ + range: 'max', + min: 0, + max: amounts.length-1, + value: 0, + slide: function( event, ui ) { + $('#amount')[0].value = amounts[parseInt(ui.value)]; + } + }); + $('#amount')[0].value = amounts[parseInt($('#amounts-slide').slider('value'))]; + + $('.payment_type_detail').hide(); + + $('.radio-select').each(function() { + if ($(this)[0].checked) { + $(this).parent().addClass('active'); + $('#payment_type_' + $(this).val()).show(); + } + if ($(this).hasClass('disabled')) { + $(this).parent().addClass('disabled'); + } + }); + + $('.radio-select').change(function() { + $('.payment_type_detail').hide(); + $('#payment_type_' + $(this).val()).show(); + }); + }); + </script> + + <style> + .ui-slider .ui-slider-handle { height: 2.7em; } + .ui-slider { height: 2.2em; } + </style> + + <script> + $(function() { + $('.typeahead').typeahead({ + name: 'recipient', + prefetch: '{{ url_for('wallet_public_keys') }}', + template: [ + {% raw -%} + '<p class="key-name">{{name}}</p>', + '<p class="key-fingerprint">{{fingerprint}}</p>', + {% endraw -%} + ].join(''), + engine: Hogan, + }); + }); + </script> +{% endblock %} diff --git a/webclient.py b/webclient.py index 8b99b8a2324b1f05a9c90b278cf7dbaa62761c77..dc736aa4a273c939cde2669ee0595741eb9e40ae 100755 --- a/webclient.py +++ b/webclient.py @@ -23,10 +23,12 @@ from collections import OrderedDict from merkle import Merkle from flask import Flask, request, render_template, jsonify from io import StringIO +from werkzeug.contrib.cache import SimpleCache +from itertools import combinations, chain logger = logging.getLogger("cli") - app = Flask(__name__) +cache = SimpleCache() @app.template_filter('split') def split_filter(s, sep=' '): @@ -76,28 +78,185 @@ VotersCount\t\t%(votersCount)s def wallets(): return render_template('wallets/index.html', settings=ucoin.settings) +@app.route('/wallets/new') +def new_wallet(): + return render_template('wallets/new.html', settings=ucoin.settings) + +@app.route('/wallets/new/create') +def new_wallet_create(): + __input = 'Key-Type: %(type)s\nName-Email: %(email)s\nName-Real: %(realm)s\nKey-Length: %(length)s\n%%commit\n' % request.args + newkey = ucoin.settings['gpg'].gen_key(__input) + return jsonify(result="Your new key (%s) has been successfully created." % newkey.fingerprint) + +def get_sender_transactions(pgp_fingerprint): + k = 'sender_transactions_%s' % pgp_fingerprint + rv = cache.get(k) + if rv is None: + rv = list(ucoin.hdc.transactions.Sender(pgp_fingerprint).get()) + cache.set(k, rv, timeout=5*60) + return rv + +def get_recipient_transactions(pgp_fingerprint): + k = 'sender_transactions_%s' % pgp_fingerprint + rv = cache.get(k) + if rv is None: + rv = list(ucoin.hdc.transactions.Recipient(pgp_fingerprint).get()) + cache.set(k, rv, timeout=5*60) + return rv + +def clist(pgp_fingerprint): + __list = ucoin.hdc.coins.List(pgp_fingerprint).get() + coins = [] + __sum = 0 + for c in __list['coins']: + for id in c['ids']: + n,b,p,t,i = id.split('-') + amount = int(b) * 10**int(p) + __dict = {'issuer': c['issuer'], 'number': int(n), 'base': int(b), 'power': int(p), 'type': t, 'type_number': int(i), 'amount': amount} + coins.append(__dict) + __sum += amount + return __sum, coins + @app.route('/wallets/<pgp_fingerprint>/history') @app.route('/wallets/<pgp_fingerprint>/history/<type>') def wallet_history(pgp_fingerprint, type='all'): - sender = ucoin.hdc.transactions.Sender(pgp_fingerprint).get() - recipient = ucoin.hdc.transactions.Recipient(pgp_fingerprint).get() + sender = get_sender_transactions(pgp_fingerprint) + recipient = get_recipient_transactions(pgp_fingerprint) return render_template('wallets/history.html', settings=ucoin.settings, - key=ucoin.settings['list_keys'].get(pgp_fingerprint), + key=ucoin.settings['secret_keys'].get(pgp_fingerprint), sender=sender, recipient=recipient, - type=type) + type=type, + clist=clist(pgp_fingerprint)) + +def powerset(iterable): + xs = list(iterable) + return chain.from_iterable( combinations(xs,n) for n in range(len(xs)+1) ) + +def cget(pgp_fingerprint, values): + __list = ucoin.hdc.coins.List(pgp_fingerprint).get() + coins = {} + for c in __list['coins']: + for id in c['ids']: + n,b,p,t,i = id.split('-') + amount = int(b) * 10**int(p) + coins[amount] = {'issuer': c['issuer'], 'number': int(n), 'base': int(b), 'power': int(p), 'type': t, 'type_number': int(i), 'amount': amount} + + issuers = {} + for v in values: + if v in coins: + c = coins[v] + issuers[c['issuer']] = issuers.get(c['issuer']) or [] + issuers[c['issuer']].append(c) + else: + raise ValueError('You do not have enough coins of value (%d)' % v) + + res = '' + for i, issuer in enumerate(issuers): + if i > 0: res += ',' + res += issuer + for c in issuers[issuer]: + res += ':%(number)d' % c -@app.route('/wallets/new') -def new_wallet(): - return render_template('wallets/new.html', settings=ucoin.settings) + return res -@app.route('/wallets/new/create') -def new_wallet_create(): - __input = 'Key-Type: %(type)s\nName-Email: %(email)s\nName-Real: %(realm)s\nKey-Length: %(length)s\n%%commit\n' % request.args - newkey = ucoin.settings['gpg'].gen_key(__input) - return jsonify(result="Your new key (%s) has been successfully created." % newkey.fingerprint) +def transfer(pgp_fingerprint, recipient, coins, message=''): + try: + last_tx = ucoin.hdc.transactions.sender.Last(pgp_fingerprint).get() + except ValueError: + last_tx = None + + __dict = {} + __dict.update(ucoin.settings) + __dict['version'] = 1 + __dict['number'] = 0 if not last_tx else last_tx['transaction']['number']+1 + __dict['previousHash'] = hashlib.sha1(("%(raw)s%(signature)s" % last_tx).encode('ascii')).hexdigest().upper() + __dict['type'] = 'TRANSFER' + __dict['recipient'] = recipient + __dict['message'] = message + + tx = """\ +Version: %(version)d +Currency: %(currency)s +Sender: %(fingerprint)s +Number: %(number)d +""" % __dict + + if last_tx: tx += "PreviousHash: %(previousHash)s\n" % __dict + + tx += """\ +Recipient: %(recipient)s +Type: %(type)s +Coins: +""" % __dict + + for coin in coins.split(','): + data = coin.split(':') + issuer = data[0] + for number in data[1:]: + __dict.update(ucoin.hdc.coins.View(issuer, int(number)).get()) + tx += '%(id)s, %(transaction)s\n' % __dict + + tx += """\ +Comment: +%(message)s +""" % __dict + + tx = tx.replace("\n", "\r\n") + txs = ucoin.settings['gpg'].sign(tx, detach=True) + + try: + ucoin.hdc.transactions.Process().post(transaction=tx, signature=txs) + except ValueError as e: + print(e) + else: + return True + + return False + +@app.route('/wallets/<pgp_fingerprint>/transfer', methods=['GET', 'POST',]) +def wallet_transfer(pgp_fingerprint): + balance, __clist = clist(pgp_fingerprint) + amounts = [x['amount'] for x in __clist] + __combinations = list(map(lambda x: (sum(x), x), powerset(amounts)))[1:] + __combinations.sort() + sums = [x[0] for x in __combinations] + + if request.method == 'GET': + return render_template('wallets/transfer.html', + settings=ucoin.settings, + key=ucoin.settings['secret_keys'].get(pgp_fingerprint), + clist=(balance,__clist), + sums=sums) + + amount = request.form.get('amount', type=int) + recipient = request.form.get('recipient') + message = request.form.get('message', '') + + try: + idx = sums.index(amount) + except ValueError as e: + raise ValueError(e) + + selected_combination = __combinations[idx][1] + + coins = cget(pgp_fingerprint, selected_combination) + + if not transfer(pgp_fingerprint, recipient, coins, message): + raise ValueError('transfer error') + + return '%s %s' % (str(selected_combination), recipient) + +@app.route('/wallets/public_keys') +def wallet_public_keys(): + keys = ucoin.settings['public_keys'] + for k,v in keys.items(): + v['value'] = v['fingerprint'] + v['tokens'] = v['uids'] + v['name'] = v['uids'][0] + return json.dumps(list(keys.values())) @app.route('/api') def api(): @@ -390,14 +549,19 @@ if __name__ == '__main__': logger.debug('selected keyid: %s' % ucoin.settings['user']) ucoin.settings['gpg'] = gpg = gnupg.GPG(options=['-u %s' % ucoin.settings['user']]) - keys = gpg.list_keys(True) - for idx, fp in enumerate(keys.fingerprints): + secret_keys = gpg.list_keys(True) + public_keys = gpg.list_keys() + + for idx, fp in enumerate(secret_keys.fingerprints): if fp[-8:] == ucoin.settings['user']: - ucoin.settings.update(keys[idx]) + ucoin.settings.update(secret_keys[idx]) break - ucoin.settings['list_keys'] = __list_keys = {} - for k in keys: __list_keys[k['fingerprint']] = k + ucoin.settings['secret_keys'] = __secret_keys = {} + ucoin.settings['public_keys'] = __public_keys = {} + + for k in secret_keys: __secret_keys[k['fingerprint']] = k + for k in public_keys: __public_keys[k['fingerprint']] = k else: ucoin.settings['gpg'] = gpg = gnupg.GPG()