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