From 333112a2a0ea51229d9f2bdd30431a3dd6777722 Mon Sep 17 00:00:00 2001 From: Caner Candan <candan@info.univ-angers.fr> Date: Sun, 26 Jan 2014 20:20:15 +0100 Subject: [PATCH] * added issuance section in order to issue UD * fixed a complexity issue in transfer section --- static/wallets/slider.js | 164 +++++++++++++++++++ templates/wallets/base.html | 2 + templates/wallets/history.html | 2 +- templates/wallets/issuance.html | 67 ++++++++ templates/wallets/no_issuance.html | 12 ++ templates/wallets/transfer.html | 63 +------- ucoin.py | 48 +++--- webclient.py | 244 ++++++++++++++++++++++++++--- 8 files changed, 498 insertions(+), 104 deletions(-) create mode 100644 static/wallets/slider.js create mode 100644 templates/wallets/issuance.html create mode 100644 templates/wallets/no_issuance.html diff --git a/static/wallets/slider.js b/static/wallets/slider.js new file mode 100644 index 00000000..3d58ef00 --- /dev/null +++ b/static/wallets/slider.js @@ -0,0 +1,164 @@ +// +// This program is free software; you can redistribute it and/or +// modify it under the terms of the GNU Affero General Public License +// version 3 as published by the Free Software Foundation. +// +// This program is distributed in the hope that it will be useful, but +// WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +// General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program; if not, write to the Free Software +// Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +// USA +// +// Authors: +// Caner Candan <caner@candan.fr>, http://caner.candan.fr +// Geraldine Starke <geraldine@starke.fr>, http://www.vegeclic.fr +// + +$(function() { + var sliders = $("#sliders .slider"); + var availableTotal = parseInt($('#available_total').val()); + + sliders.each(function() { + var s = $(this); + var coin_value = parseInt(s.parent().find('.coin_value').val()); + var max_value = parseInt(s.parent().find('.max_value').val()); + + s.empty().slider({ + value: 0, + min: 0, + max: max_value, + range: "max", + step: 1, + coin: coin_value, + animate: false, + + stop: function(event, ui) { + // Get current total + var total = 0; + sliders.not(this).each(function() { + total += $(this).slider('option', 'value') * $(this).slider('option', 'coin'); + }); + + var value = $(this).slider('option', 'value'); + + if ((total + (value*coin_value)) > (availableTotal)) { + value = parseInt((availableTotal-total)/coin_value); + } + + total += value * coin_value; + + $(this).slider('option', 'value', value); + $(this).siblings().find('.quantity').text(value); + $(this).siblings().find('.equal').text(value*coin_value); + $(this).siblings('.input_value').val(value); + + $('#sliders_total').text(total); + $('#remains').text(availableTotal-total); + }, + + slide: function(event, ui) { + // Get current total + var total = 0; + sliders.not(this).each(function() { + total += $(this).slider('option', 'value') * $(this).slider('option', 'coin'); + }); + + if ((total + (ui.value*coin_value)) > (availableTotal)) { + ui.value = parseInt((availableTotal-total)/coin_value); + } + + // Need to do this because apparently jQ UI + // does not update value until this event completes + total += ui.value * coin_value; + + // Update display to current value + $(this).siblings().find('.quantity').text(ui.value); + $(this).siblings().find('.equal').text(ui.value*coin_value); + $(this).siblings('.input_value').val(ui.value); + + $('#sliders_total').text(total); + $('#remains').text(availableTotal-total); + } + }); + }); + + var initialize = function() { + var total = 0; + sliders.each(function() { + $(this).slider('value', 0); + $(this).siblings().find('.quantity').text(0); + $(this).siblings().find('.equal').text(0); + $(this).siblings('.input_value').val(0); + }); + $('#sliders_total').text(0); + $('#remains').text(availableTotal); + }; + + var randomize = function() { + initialize(); + + var total = 0 + sliders.each(function() { + if (total >= availableTotal) { return; } + + var value = $(this).slider('option', 'value'); + var max = $(this).slider('option', 'max'); + var coin = $(this).slider('option', 'coin'); + + max = (availableTotal-total)/coin; + + var sol = (coin > 1) ? parseInt(Math.random()*max) : max; + total += sol*coin; + + $(this).slider('value', sol); + $(this).siblings().find('.quantity').text(sol); + $(this).siblings().find('.equal').text(sol); + $(this).siblings('.input_value').val(sol); + }); + + $('#sliders_total').text(total); + $('#remains').text(availableTotal-total); + }; + + $('#init').click(initialize); + $('#random').click(randomize); + + randomize(); +}); + +function create_one_slider(choices, id, default_value, disabled) { + $('#' + id + '-slide').slider({ + range: 'max', + min: 0, + max: choices.length-1, + value: default_value, + disabled: disabled, + slide: function( event, ui ) { + var i = parseInt(ui.value); + var v = choices[i]; + $('#id_' + id)[0].selectedIndex = i; + $('#id_' + id).change(); + $('#' + id + '-slide-text').text( v[1] ); + } + }); + + var i = parseInt($('#' + id + '-slide').slider('value')); + var v = choices[i]; + $('#id_' + id)[0].selectedIndex = i; + $('#id_' + id).change(); + $('#' + id + '-slide-text').text( v[1] ); +} + +$(function() { + $('.slidebar-select').each(function() { + var s = $(this)[0]; + var options = []; + $(this).find('option').each(function() { options.push([$(this).val(), $(this).text()]); }); + create_one_slider(options, s.name, s.selectedIndex, $(this).hasClass('disabled')); + $(this).hide(); + }); +}); diff --git a/templates/wallets/base.html b/templates/wallets/base.html index 4afef880..632e5d17 100644 --- a/templates/wallets/base.html +++ b/templates/wallets/base.html @@ -21,6 +21,7 @@ <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> + <a id="issuance" href="{{ url_for('wallet_issuance', pgp_fingerprint=key.fingerprint) }}" class="list-group-item"><i class="glyphicon glyphicon-cloud-download"></i> Issuance</a> </div> </div> {% endif -%} @@ -44,6 +45,7 @@ '{{ 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', + '{{ url_for('wallet_issuance', pgp_fingerprint=key.fingerprint) }}': 'issuance', {% endif -%} }; diff --git a/templates/wallets/history.html b/templates/wallets/history.html index 78122b95..3ebafc0c 100644 --- a/templates/wallets/history.html +++ b/templates/wallets/history.html @@ -11,7 +11,7 @@ <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 class="alert alert-info">{{coins}}</div> #} </div> {% endwith %} <div class="col-md-7"> diff --git a/templates/wallets/issuance.html b/templates/wallets/issuance.html new file mode 100644 index 00000000..c3511a5d --- /dev/null +++ b/templates/wallets/issuance.html @@ -0,0 +1,67 @@ +{% extends "wallets/base.html" %} + +{% block subhead -%} + <link rel="stylesheet" href="{{ url_for('static', filename='jqueryui/themes/base/jquery.ui.all.css') }}" /> +{% endblock -%} + +{% block sub_content %} + <h1><span class="label label-default">Issuance</span> <span class="label label-primary">{{key.uids.0|truncate(50)}}</span></h1> + + <br/> + + <div class="form-group"> + <a class="btn btn-primary btn-lg btn-block tooltip_link" id="random" title="Click here in order to balance the set of coins values"><strong>Suggest</strong></a> + </div> + + <form role="form" method="post" action="{{ url_for('wallet_issuance', pgp_fingerprint=key.fingerprint) }}"> + + <div id="sliders"> + <input id="available_total" type="hidden" value="{{remainder}}" /> + + {% for coin, count in coins|reverse %} + <div class="form-group well"> + <div class="row"> + <div class="col-md-2"> + <span class="badge alert-info">{{coin}} coins</span> + </div> + <div class="col-md-8 slider"></div> + <div class="col-md-2 value_content"> + <span class="badge">{{coin}} x <span class="quantity">0</span> = <span class="equal">0</span></span> + </div> + <input class="input_value" id="coin_{{coin}}" name="coin_{{coin}}" type="hidden" value="0" /> + <input class="coin_value" type="hidden" value="{{coin}}" /> + <input class="max_value" type="hidden" value="{{count}}" /> + </div> + </div> + {% endfor %} + + </div> + + <div class="form-group well text-center"> + <h4> + <span class="label label-info">Total</span> + <span id="sliders_total" class="label label-info">0</span> + + <span class="label label-success">Remains</span> + <span class="label label-success">+ <span id="remains">{{remainder}}</span></span> + </h4> + </div> + + <div class="form-group"> + <button class="btn btn-lg btn-primary btn-block" type="submit">Ready ? Issue</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='wallets/slider.js') }}"></script> + + <style type="text/css"> + .ui-slider { height: 1.9em; } + .ui-slider .ui-slider-handle { height: 2.3em; } + </style> +{% endblock %} diff --git a/templates/wallets/no_issuance.html b/templates/wallets/no_issuance.html new file mode 100644 index 00000000..ba077837 --- /dev/null +++ b/templates/wallets/no_issuance.html @@ -0,0 +1,12 @@ +{% extends "wallets/base.html" %} + +{% block sub_content %} +<h1><span class="label label-default">Issue</span> <span class="label label-primary">{{key.uids.0|truncate(50)}}</span></h1> + +<br/> + +<div class="alert alert-warning"> + Currently, there is no dividend to issue. Try out this feature later on. +</div> + +{% endblock %} diff --git a/templates/wallets/transfer.html b/templates/wallets/transfer.html index 06cbd34d..fc59240d 100644 --- a/templates/wallets/transfer.html +++ b/templates/wallets/transfer.html @@ -1,7 +1,6 @@ {% 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 -%} @@ -16,7 +15,7 @@ <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-9"> - <div class="alert alert-info">{{coins}}</div> + {# <div class="alert alert-info">{{coins}}</div> #} </div> {% endwith -%} </div> @@ -27,24 +26,15 @@ <div class="panel-body"> <input class="typeahead form-control" type="text" placeholder="Your recipient" name="recipient"/> </div> + <div class="panel-footer">Type the name, email or fingerprint of your 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 class="panel-body"> + <input class="form-control" type="text" placeholder="0" name="amount" autocomplete="off"/> </div> + <div class="panel-footer">Choose the amount you want to send.</div> </div> <div class="panel panel-success"> @@ -61,51 +51,9 @@ {% 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({ @@ -115,6 +63,7 @@ {% raw -%} '<p class="key-name">{{name}}</p>', '<p class="key-fingerprint">{{fingerprint}}</p>', + '<hr/>' {% endraw -%} ].join(''), engine: Hogan, diff --git a/ucoin.py b/ucoin.py index dd303d2b..2c7290ea 100755 --- a/ucoin.py +++ b/ucoin.py @@ -606,7 +606,9 @@ def vote(): print('Posted vote for Amendment #%d' % r['amendment']['number']) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='uCoin client.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + common_options = {'formatter_class': argparse.ArgumentDefaultsHelpFormatter} + + parser = argparse.ArgumentParser(description='uCoin client.', **common_options) levels = OrderedDict([('debug', logging.DEBUG), ('info', logging.INFO), @@ -631,57 +633,57 @@ if __name__ == '__main__': subparsers = parser.add_subparsers(help='sub-command help') - subparsers.add_parser('current', help='Show current amendment of the contract').set_defaults(func=current) - subparsers.add_parser('contract', help='List all amendments constituting the contract').set_defaults(func=contract) + subparsers.add_parser('current', help='Show current amendment of the contract', **common_options).set_defaults(func=current) + subparsers.add_parser('contract', help='List all amendments constituting the contract', **common_options).set_defaults(func=contract) - sp = subparsers.add_parser('lookup', help='Search for a public key') + sp = subparsers.add_parser('lookup', help='Search for a public key', **common_options) sp.add_argument('search', help='A value for searching in PGP certificates database. May start with \'0x\' for direct search on PGP fingerprint.') sp.set_defaults(func=lookup) - subparsers.add_parser('peering', help='Show peering informations').set_defaults(func=peering) - subparsers.add_parser('pubkey', help='Show pubkey of remote node').set_defaults(func=pubkey) - subparsers.add_parser('index', help='List reiceved votes count for each amendment').set_defaults(func=index) + subparsers.add_parser('peering', help='Show peering informations', **common_options).set_defaults(func=peering) + subparsers.add_parser('pubkey', help='Show pubkey of remote node', **common_options).set_defaults(func=pubkey) + subparsers.add_parser('index', help='List reiceved votes count for each amendment', **common_options).set_defaults(func=index) - sp = subparsers.add_parser('issue', help='Issue new coins') + sp = subparsers.add_parser('issue', help='Issue new coins', **common_options) sp.add_argument('amendment', type=int, help='amendment number') sp.add_argument('coins', nargs='+', help='coins will respect this format [coin_value,number_of_zero_behind]') sp.add_argument('--message', '-m', help='write a comment', default='') sp.set_defaults(func=issue) - sp = subparsers.add_parser('transfer', help='Transfers property of coins (coins a read from STDIN)') + sp = subparsers.add_parser('transfer', help='Transfers property of coins (coins a read from STDIN)', **common_options) sp.add_argument('recipient', help='recipient address') sp.add_argument('coins', nargs='?', help='coins to send [coin,...]. If no value has passed, it will be read from STDIN.') sp.add_argument('--message', '-m', help='write a comment', default='') sp.set_defaults(func=transfer) - sp = subparsers.add_parser('fusion', help='Fusion coins to make a bigger coin (coins a read from STDIN)') + sp = subparsers.add_parser('fusion', help='Fusion coins to make a bigger coin (coins a read from STDIN)', **common_options) sp.add_argument('coins', nargs='?', help='coins to fusion [coin,...]. If no value has passed, it will be read from STDIN.') sp.add_argument('--message', '-m', help='write a comment', default='') sp.set_defaults(func=fusion) - sp = subparsers.add_parser('host-add', help='Add given key fingerprint to hosts managing transactions of key -u') + sp = subparsers.add_parser('host-add', help='Add given key fingerprint to hosts managing transactions of key -u', **common_options) sp.add_argument('key', help='key fingerprint') sp.set_defaults(func=host_add) - sp = subparsers.add_parser('host-rm', help='Same as \'host-add\', but remove host instead') + sp = subparsers.add_parser('host-rm', help='Same as \'host-add\', but remove host instead', **common_options) sp.add_argument('key', help='key fingerprint') sp.set_defaults(func=host_rm) - subparsers.add_parser('host-list', help='Show the list of keys').set_defaults(func=host_list) + subparsers.add_parser('host-list', help='Show the list of keys', **common_options).set_defaults(func=host_list) - sp = subparsers.add_parser('trust-add', help='Add given key fingerprint to hosts key -u trust for receiving transactions') + sp = subparsers.add_parser('trust-add', help='Add given key fingerprint to hosts key -u trust for receiving transactions', **common_options) sp.add_argument('key', help='key fingerprint') sp.set_defaults(func=trust_add) - sp = subparsers.add_parser('trust-rm', help='Same as \'trust-add\', but remove host instead') + sp = subparsers.add_parser('trust-rm', help='Same as \'trust-add\', but remove host instead', **common_options) sp.add_argument('key', help='key fingerprint') sp.set_defaults(func=trust_rm) - subparsers.add_parser('trust-list', help='Show the list of keys').set_defaults(func=trust_list) - subparsers.add_parser('tht', help='Show THT entry resulting of host-* and trust-* commands').set_defaults(func=tht) - subparsers.add_parser('pub-tht', help='Publish THT entry according to data returned by \'trust-list\' and \'host-list\'').set_defaults(func=pub_tht) + subparsers.add_parser('trust-list', help='Show the list of keys', **common_options).set_defaults(func=trust_list) + subparsers.add_parser('tht', help='Show THT entry resulting of host-* and trust-* commands', **common_options).set_defaults(func=tht) + subparsers.add_parser('pub-tht', help='Publish THT entry according to data returned by \'trust-list\' and \'host-list\'', **common_options).set_defaults(func=pub_tht) - sp = subparsers.add_parser('forge-am', help='Forge an amendment, following currently promoted of given node.') + sp = subparsers.add_parser('forge-am', help='Forge an amendment, following currently promoted of given node.', **common_options) sp.add_argument('changes', nargs='?', help='[members;voters] changes') sp.add_argument('--dividend', '-d', type=int, help='Universal Dividend value') sp.add_argument('--power10', '-m', type=int, help='Minimal coin 10 power') @@ -690,19 +692,19 @@ if __name__ == '__main__': sp.add_argument('--stdin', '-C', action='store_true', default=False, help='forge-am will read community changes from STDIN') sp.set_defaults(func=forge_am) - sp = subparsers.add_parser('clist', help='List coins of given user. May be limited by upper amount.') + sp = subparsers.add_parser('clist', help='List coins of given user. May be limited by upper amount.', **common_options) sp.add_argument('limit', nargs='?', type=int, help='limit value') sp.set_defaults(func=clist) - sp = subparsers.add_parser('cget', help='Get coins for given values in user account.') + sp = subparsers.add_parser('cget', help='Get coins for given values in user account.', **common_options) sp.add_argument('value', nargs='+', type=int, help='value of the coin you want to select') sp.set_defaults(func=cget) - sp = subparsers.add_parser('send-pubkey', help='Send signed public key [file] to a uCoin server. If -u option is provided, [file] is ommited. If [file] is not provided, it is read from STDIN. Note: [file] may be forged using \'forge-*\' commands.') + sp = subparsers.add_parser('send-pubkey', help='Send signed public key [file] to a uCoin server. If -u option is provided, [file] is ommited. If [file] is not provided, it is read from STDIN. Note: [file] may be forged using \'forge-*\' commands.', **common_options) sp.add_argument('file', nargs='?', help='signed public key to send') sp.set_defaults(func=send_pubkey) - sp = subparsers.add_parser('vote', help='Signs given amendment [file] and sends it to a uCoin server. If [file] is not provided, it is read from STDIN.') + sp = subparsers.add_parser('vote', help='Signs given amendment [file] and sends it to a uCoin server. If [file] is not provided, it is read from STDIN.', **common_options) sp.add_argument('file', nargs='?', help='amendment file') sp.set_defaults(func=vote) diff --git a/webclient.py b/webclient.py index 8be2df7c..ff27b23a 100755 --- a/webclient.py +++ b/webclient.py @@ -20,7 +20,8 @@ from pprint import pprint import\ ucoin, json, logging, argparse, sys,\ - gnupg, hashlib, re, datetime as dt + gnupg, hashlib, re, datetime as dt,\ + webbrowser, math from collections import OrderedDict from merkle import Merkle from flask import\ @@ -151,10 +152,6 @@ def wallet_history_refresh(pgp_fingerprint, type='all'): flash(u'History refreshed', 'info') return redirect(url_for('wallet_history', pgp_fingerprint=pgp_fingerprint, type=type)) -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 = {} @@ -192,7 +189,7 @@ def transfer(pgp_fingerprint, recipient, coins, message=''): __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['previousHash'] = hashlib.sha1(("%(raw)s%(signature)s" % last_tx).encode('ascii')).hexdigest().upper() if last_tx else None __dict['type'] = 'TRANSFER' __dict['recipient'] = recipient __dict['message'] = message @@ -236,38 +233,71 @@ Comment: return False -@app.route('/wallets/<pgp_fingerprint>/transfer', methods=['GET', 'POST',]) +def powerset(iterable): + xs = list(iterable) + return chain.from_iterable( combinations(xs,n) for n in range(len(xs)+1) ) + +def powerset_bis(iterable, low=0, step=1): + xs = list(iterable) + + combins = [] + sums = [] + for n in range(low, len(xs)+1, step): + for c in combinations(xs,n): + __sum = sum(c) + if __sum not in sums: + combins.append(c) + sums.append(__sum) + + res = list(zip(sums, combins)) + res.sort() + return res + # return sums, combins + +@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) + clist=(balance,__clist)) + + amounts = [x['amount'] for x in __clist] + amounts.sort() + amounts.reverse() - amount = request.form.get('amount', type=int) recipient = request.form.get('recipient') + amount = request.form.get('amount', type=int) message = request.form.get('message', '') - try: - idx = sums.index(amount) - except ValueError as e: - raise ValueError(e) + if not recipient or not amount: + flash('recipient or amount field is missing.', 'error') + return redirect(url_for('wallet_transfer', pgp_fingerprint=pgp_fingerprint)) + + if amount > balance: + flash('amount is higher than available balance (%d > %d).' % (amount, balance), 'error') + return redirect(url_for('wallet_transfer', pgp_fingerprint=pgp_fingerprint)) + + coins = [] + total = 0 + for coin in amounts: + if total >= amount: break + if total+coin <= amount: + coins.append(coin) + total += coin - selected_combination = __combinations[idx][1] + if sum(coins) != amount: + flash('this amount cannot be reached with existing coins in your wallet.', 'error') + return redirect(url_for('wallet_transfer', pgp_fingerprint=pgp_fingerprint)) - coins = cget(pgp_fingerprint, selected_combination) + coins = cget(pgp_fingerprint, coins) if not transfer(pgp_fingerprint, recipient, coins, message): flash(u'Transfer error', 'error') else: - flash(u'Transfer success (%s %s)' % (str(selected_combination), recipient), 'success') + flash(u'Transfer succed', 'success') return redirect(url_for('wallet_transfer', pgp_fingerprint=pgp_fingerprint)) @@ -280,6 +310,169 @@ def wallet_public_keys(): v['name'] = v['uids'][0] return json.dumps(list(keys.values())) +def issue(pgp_fingerprint, amendment, coins, message=''): + try: + last_tx = ucoin.hdc.transactions.sender.Last(pgp_fingerprint).get() + except ValueError: + last_tx = None + + try: + last_issuance = ucoin.hdc.transactions.sender.issuance.Last(pgp_fingerprint).get() + except ValueError: + last_issuance = 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() if last_tx else None + __dict['type'] = 'ISSUANCE' + __dict['message'] = message + __dict['amendment'] = amendment + + 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: %(fingerprint)s +Type: %(type)s +Coins: +""" % __dict + + def get_next_coin_number(coins): + number = 0 + for c in coins: + candidate = int(c['id'].split('-')[1]) + if candidate > number: number = candidate + return number+1 + + previous_idx = 0 if not last_issuance else get_next_coin_number(last_issuance['transaction']['coins']) + + for idx, coin in enumerate(coins): + __dict['idx'] = idx+previous_idx + __dict['base'], __dict['power'] = [int(x) for x in coin.split(',')] + tx += '%(fingerprint)s-%(idx)d-%(base)d-%(power)d-A-%(amendment)d\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 + +def compute_dividend_remainders(pgp_fingerprint): + remainders = {} + for am in ucoin.hdc.amendments.List().get(): + if not am['dividend']: continue + dividend_sum = 0 + for x in ucoin.hdc.transactions.sender.issuance.Dividend(pgp_fingerprint, am['number']).get(): + __sum = 0 + for coin in x['value']['transaction']['coins']: + base, power = coin['id'].split('-')[2:4] + __sum += int(base) * 10**int(power) + dividend_sum += __sum + + if am['dividend'] > dividend_sum: + remainders[int(am['number'])] = am['dividend'] - dividend_sum + return remainders + +@app.route('/wallets/<pgp_fingerprint>/issuance', methods=['GET', 'POST']) +def wallet_issuance(pgp_fingerprint): + k = 'remainders_%s' % pgp_fingerprint + remainders = cache.get(k) + if remainders is None: + remainders = compute_dividend_remainders(pgp_fingerprint) + cache.set(k, remainders, timeout=5*60) + + if not remainders: + return render_template('wallets/no_issuance.html', + settings=ucoin.settings, + key=ucoin.settings['secret_keys'].get(pgp_fingerprint)) + + # remainders = {1:10, 3:80} + + remainder = sum(remainders.values()) + max_remainder = max(remainders.values()) if remainders.values() else 0 + + # flash(remainders, 'info') + + def count_coins(coin): + count = 0 + for r in remainders.values(): + if r >= coin: count += int(r/coin) + return count + + coins = [] + for power in range(10): + for base in [1,2,5]: + coin = base*(10**power) + if coin > max_remainder: break + coins.append((coin,count_coins(coin))) + + if request.method == 'GET': + return render_template('wallets/issuance.html', + settings=ucoin.settings, + key=ucoin.settings['secret_keys'].get(pgp_fingerprint), + remainders=remainders, remainder=remainder, + max_remainder=max_remainder, coins=coins) + + quantities = [] + for coin, count in reversed(coins): + qte = request.form.get('coin_%d' % coin, type=int) + if qte: quantities.append((coin, qte)) + + # flash(str(quantities), 'info') + + issuances = {} + + for am in remainders: + # flash('%s %s' % (am, remainders[am]), 'info') + issuances[am] = issuance = [] + + for i in range(len(quantities)): + coin, qte = quantities[i] + if not qte: continue + + if coin <= remainders[am]: + new_qte = int(remainders[am]/coin) + if new_qte > qte: new_qte = qte + remainders[am] -= coin*new_qte + quantities[i] = (coin,qte-new_qte) + + # flash('%s x %s, remainder: %s' % (coin, new_qte, remainders[am])) + + for i in range(new_qte): + power = int(math.log10(coin)) + issuance.append('%d,%d' % (coin/10**power, power)) + + # flash(issuances) + + for am, coins in issuances.items(): + if not issue(pgp_fingerprint, am, coins): + flash(u'Issuance error', 'error') + break + + flash('The issuance was completed.', 'success') + cache.set(k, None) + + return redirect(url_for('wallet_issuance', pgp_fingerprint=pgp_fingerprint)) + @app.route('/api') def api(): return render_template('api/index.html') @@ -508,7 +701,9 @@ def hdc_transactions_view_tx(transaction_id): return render_prettyprint('api/result.html', ucoin.hdc.transactions.View(transaction_id).get()) if __name__ == '__main__': - parser = argparse.ArgumentParser(description='uCoin webclient.', formatter_class=argparse.ArgumentDefaultsHelpFormatter) + common_options = {'formatter_class': argparse.ArgumentDefaultsHelpFormatter} + + parser = argparse.ArgumentParser(description='uCoin webclient.', **common_options) levels = OrderedDict([('debug', logging.DEBUG), ('info', logging.INFO), @@ -536,10 +731,13 @@ if __name__ == '__main__': def run(): print('Running...') app.secret_key = ucoin.settings['secret_key'] + if ucoin.settings['browser']: + webbrowser.open('http://localhost:5000/') app.run(debug=True) - sp = subparsers.add_parser('run', help='Run the webclient') + sp = subparsers.add_parser('run', help='Run the webclient', **common_options) sp.add_argument('--secret_key', '-s', help='Pass a secret key used by the server for sessions', default='some_secret') + sp.add_argument('--browser', '-b', action='store_true', help='Open it into your favorite browser', default=False) sp.set_defaults(func=run) args = parser.parse_args() -- GitLab