gnunet-svn
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[GNUnet-SVN] [taler-bank] branch stable updated (4963d08 -> e3d5117)


From: gnunet
Subject: [GNUnet-SVN] [taler-bank] branch stable updated (4963d08 -> e3d5117)
Date: Mon, 11 Dec 2017 11:43:04 +0100

This is an automated email from the git hooks/post-receive script.

marcello pushed a change to branch stable
in repository bank.

    from 4963d08  including testcase for bad configs
     add e9dd349  version 0.4.0
     add c04007b  fix #5108
     add 034d177  define db custom type
     add f051ed2  glue new db column to models abstraction
     add 3738190  fix column type
     add fc6a56f  porting the bank to use the new Amount object
     add f129337  dead code
     add b466246  remove comment
     add c7dca16  bump amount.py version
     add 0acc0c3  logger call
     add debbb56  fix view returning None
     add bc8079d  fixing old fraction-free amount strings lying around
     add d888a3f  fix stringify arg
     add 5e8d822  fix other None-returning view
     add f80aae8  remove old use of 'currency' to instantiate bank accounts
     add a57e0fb  fix the amount format accepted by the wallet when withdrawing
     add 3a146da  django wants serializable values in the state, so amounts 
have to be stored in the state as dicts.
     add a80b2a3  passing data to 500 responses
     add 64a2d5d  fix currency repetition
     add 7b8b462  fix currency in profile page
     add b5a78db  modify configure to auto-detect --system on Debian
     add dbe8e1d  if then
     add 505cbc4  remove (dead) sample_donations and linting accounts generator
     add 5c876a2  pylint
     add f25b666  linting
     add c685201  linting
     add baa520a  linting
     add 1af891c  add indices where they belong (#5153)
     add 44f92cd  ignore generated files
     add e22f704   #5012
     add 62674ab  shortening pin/tan handler
     add a52cdc0  linting pin/tan verify
     add 5761de4  addressing obvious pylint warnings
     add bdb278f  still against the "too many return statement" warning; fetch 
login credentials from the headers via a dedicated decorator.
     add a75f50f  fix exception return value
     add 5b27b30  use auth_and_login as decorator
     add f4d73b4  validate /add/incoming body via validictory.
     add 55d9006  checking withdraw session via json validation and moving all 
the exception handlers for wire_transfer() into one decorator.
     add 0cf476e  grouping similar calls into loop
     add 67c89e1  instantiate objects directly into the args list
     add 1187972  make /history test more compact
     add 9c78889  test /pin/question
     add c68833b  document extra steps I had to take
     add 89a3315  withdraw testcase -- missing exchange interaction mocking
     add 477f3bc  check withdrawing POSTs the expected data to exchange
     add d45cd35  fix default debt string
     add 006beb0  fetch nested value
     add 75fbd51  instantiate and show the wire transfer form
     add 626e8a7  show informational bar about wire transfer outcome
     add 6e0b64b  tolerating non fractional amounts given
     add 840661b  enforcing minimum amount to transfer
     add 7d6e222  +details about wire transfer errors
     add 6810ea2  check money was actually transferred via testcase
     add d65529d  check status code after wire transfer
     add 592f212  fix Warning arg list
     add e0252ca  migration
     add 3c84eb2  'Warning' object used in checks MUST come from django.
     add dee0216  destroy test db by default
     add 1669a8b  readme
     add 4f80d3a  linting
     add 8ea1eda  linting talerconfig
     add 8efe6f9  linting talerconfig
     add c670a31  fix /history testcase
     add 8ce18c1  linting the settings
     add 7a9b2db  raw string regex
     add 2fce6d1  fix string newline
     add 6e6c43c  args list was indeed needed; not detected by testcases.
     add bf5715f  fix #5184
     add 27b6af1  exception objects stringification
     add f22dc61  no js in place
     add d05a5f0  note about how JS-free the bank is
     add 809a0a4  documenting SUGGESTED_EXCHANGE
     add dac7bc9  HTML5-friendly input tags
     add c8f9b7a  measuring time to query a user's history
     add 2cc5448  port to django2
     add 3611f61  provide __str__ to custom exceptions
     add 9dd4af1  linting
     add 711be52  remove fixme
     add da36d0b  done linting talerconfig
     add 5745f9e  linting done with views
     add 91da3bb  default log level = warning
     add d459d63  linting models. WARNING/NOTE: tests passed but this change 
altered - although did NOT break - the recommended way to use the model API.
     add 3d1d4bb  remove initial checks.  They pollute tests, and the way the 
bank is launched makes sure they always pass.  Moreover, they check for obvious 
conditions like: is a 'Bank' user present? Is a database accessible?
     add 19e9dac  pacifying PEP checkers
     add 4bacbbe  done with linting
     add 2f89247  shortening testcase where possible
     add f30b677  fix indent
     add a63ac6d  linting the launcher
     add e10115b  typing models
     add 4efff77  annotating types for config parser
     add b2ea43b  resolving conflict between mypy and Python itself
     add 1625873  wtid -> subject
     add 51fe2d3  make 'both' 'direction' exist
     add 45f614c  associate a 'cancelled' flag with transactions
     add 6e82367  make 'cancel+/-' /history args exist
     add b1a0491  pacify pep checkers
     add 611f6d4  fix function call to bool
     add 38d79c6  dummy /reject implementation
     add 14a3424  Allow PUT and POST for /reject
     add 8397962  return No Content instead of OK
     add 94b2977  implement /reject
     add 510f10c  remove duplicate /reject handlers + allowing slower db work 
of /history
     add 82cc638  check that the user who is rejecting a transaction was the 
credit party in that transaction.
     add 921e2d3  remove comment
     add ca310cf  roll our own mathcaptcha, the old one broke with django2
     add fe5757a  missing import random
     add 3a7e81c  hash answer
     add 826512e  typto
     add 3edebc5  add 'minus' in captcha
     add 35754af  styling
     add ec83b07  Undo styling experiment
     add e3d5117  don't autocomplete captcha

No new revisions were added by this update.

Summary of changes:
 .gitignore                                         |   5 +
 INCLUDE.APP                                        |   1 -
 Makefile.am                                        |  16 +-
 README                                             |  21 +-
 bank-check.conf                                    |   5 +-
 bank.conf                                          |   4 +-
 configure.ac                                       |  22 +-
 setup.py                                           |   1 -
 taler-bank-manage.in                               |  91 ++-
 talerbank/__init__.py                              |   8 +-
 talerbank/app/Makefile.am                          |   2 +-
 talerbank/app/__init__.py                          |   2 -
 talerbank/app/amount.py                            | 136 +++++
 talerbank/app/amounts.py                           | 101 ----
 talerbank/app/checks.py                            |  24 -
 talerbank/app/management/commands/Makefile.am      |   3 +-
 talerbank/app/management/commands/dump_talerdb.py  |  24 +-
 talerbank/app/management/commands/helpers.py       |  24 +
 .../app/management/commands/provide_accounts.py    |  32 +-
 .../app/management/commands/sample_donations.py    |  71 ---
 .../app/migrations/0002_bankaccount_amount.py      |  21 +
 .../app/migrations/0003_auto_20171030_1346.py      |  21 +
 .../app/migrations/0004_auto_20171030_1428.py      |  34 ++
 .../0005_remove_banktransaction_currency.py        |  19 +
 .../app/migrations/0006_auto_20171031_0823.py      |  21 +
 .../app/migrations/0007_auto_20171031_0906.py      |  21 +
 .../app/migrations/0008_auto_20171031_0938.py      |  31 +
 .../app/migrations/0009_auto_20171120_1642.py      |  20 +
 .../migrations/0010_banktransaction_cancelled.py   |  20 +
 talerbank/app/models.py                            |  81 +--
 talerbank/app/schemas.py                           | 117 +++-
 talerbank/app/static/chrome-store-link.js          |  11 -
 talerbank/app/templates/Makefile.am                |   1 -
 talerbank/app/templates/base.html                  |   7 +-
 talerbank/app/templates/javascript.html            |   9 -
 talerbank/app/templates/login.html                 |   6 +-
 talerbank/app/templates/pin_tan.html               |  14 +-
 talerbank/app/templates/profile_page.html          |  65 +-
 talerbank/app/templates/public_accounts.html       |   2 +-
 talerbank/app/templates/register.html              |   8 +-
 talerbank/app/tests.py                             | 585 ++++++++++++------
 talerbank/app/tests_alt.py                         |  37 +-
 talerbank/app/urls.py                              |  11 +-
 talerbank/app/views.py                             | 667 +++++++++++----------
 talerbank/jinja2.py                                |  47 +-
 talerbank/settings.py                              |  85 ++-
 talerbank/talerconfig.py                           | 273 +++++----
 47 files changed, 1659 insertions(+), 1168 deletions(-)
 create mode 100644 talerbank/app/amount.py
 delete mode 100644 talerbank/app/amounts.py
 delete mode 100644 talerbank/app/checks.py
 create mode 100644 talerbank/app/management/commands/helpers.py
 delete mode 100644 talerbank/app/management/commands/sample_donations.py
 create mode 100644 talerbank/app/migrations/0002_bankaccount_amount.py
 create mode 100644 talerbank/app/migrations/0003_auto_20171030_1346.py
 create mode 100644 talerbank/app/migrations/0004_auto_20171030_1428.py
 create mode 100644 
talerbank/app/migrations/0005_remove_banktransaction_currency.py
 create mode 100644 talerbank/app/migrations/0006_auto_20171031_0823.py
 create mode 100644 talerbank/app/migrations/0007_auto_20171031_0906.py
 create mode 100644 talerbank/app/migrations/0008_auto_20171031_0938.py
 create mode 100644 talerbank/app/migrations/0009_auto_20171120_1642.py
 create mode 100644 talerbank/app/migrations/0010_banktransaction_cancelled.py
 delete mode 100644 talerbank/app/static/chrome-store-link.js
 delete mode 100644 talerbank/app/templates/javascript.html

diff --git a/.gitignore b/.gitignore
index e217b27..6fe5135 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,3 +17,8 @@ bank.wsgi
 bank-admin.wsgi
 talerbank.egg-info
 talerbank/vassals-*/*.ini
+doc/mdate-sh
+doc/stamp-vti
+doc/taler-bank.info
+doc/version.texi
+doc/texinfo.tex
diff --git a/INCLUDE.APP b/INCLUDE.APP
index c35d472..11f4a11 100644
--- a/INCLUDE.APP
+++ b/INCLUDE.APP
@@ -5,7 +5,6 @@ talerbank/app/funds_mgmt.py
 talerbank/app/history.py
 talerbank/app/lib.py
 talerbank/app/management/commands/pre_accounts.py
-talerbank/app/management/commands/sample_donations.py
 talerbank/app/models.py
 talerbank/app/my-static/style.css
 talerbank/app/privileged_accounts.py
diff --git a/Makefile.am b/Makefile.am
index 63baa31..fdea99d 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -1,5 +1,9 @@
 # This script is in the public domain.
-SUBDIRS = . talerbank doc
+
+SUBDIRS = . talerbank
+if ENABLE_DOC
+SUBDIRS += doc
+endif
 
 EXTRA_DIST = \
  requirements.txt \
@@ -27,15 +31,15 @@ install-dev:
        @$(PYTHON) ./install-dev.py
 
 check:
-       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check.conf" && python3 -m 
django test talerbank.app.tests
+       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check.conf" && python3 -m 
django test --no-input talerbank.app.tests
        @printf -- 
"\n\n----------------------------------------------------------------------\nTesting
 against non existent config file\n\n"
-       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="non-existent.conf" && python3 -m 
django test talerbank.app.tests ; test 3 = $$?
+       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="non-existent.conf" && python3 -m 
django test --no-input talerbank.app.tests ; test 3 = $$?
        @printf -- 
"\n\n----------------------------------------------------------------------\nTesting
 against bad db string\n\n"
-       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-baddb.conf" && 
python3 -m django test talerbank.app.tests_alt.BadDatabaseStringTestCase ; test 
2 = $$?
+       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-baddb.conf" && 
python3 -m django test --no-input 
talerbank.app.tests_alt.BadDatabaseStringTestCase ; test 2 = $$?
        @printf -- 
"\n\n----------------------------------------------------------------------\nTesting
 against bad amount\n\n"
-       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-badamount.conf" && 
python3 -m django test talerbank.app.tests_alt.BadMaxDebtOptionTestCase
+       @export DJANGO_SETTINGS_MODULE="talerbank.settings" 
TALER_PREFIX="@prefix@" TALER_CONFIG_FILE="bank-check-alt-badamount.conf" && 
python3 -m django test --no-input 
talerbank.app.tests_alt.BadMaxDebtOptionTestCase
        @printf -- 
"\n\n----------------------------------------------------------------------\nTesting
 against no currency in config\n\n"
-       @export TALER_BASE_CONFIG="/tmp" 
DJANGO_SETTINGS_MODULE="talerbank.settings" TALER_PREFIX="@prefix@" 
TALER_CONFIG_FILE="bank-check-alt-nocurrency.conf" && python3 -m django test 
talerbank.app.tests_alt.NoCurrencyOptionTestCase ; test 3 = $$?
+       @export TALER_BASE_CONFIG="/tmp" 
DJANGO_SETTINGS_MODULE="talerbank.settings" TALER_PREFIX="@prefix@" 
TALER_CONFIG_FILE="bank-check-alt-nocurrency.conf" && python3 -m django test 
--no-input talerbank.app.tests_alt.NoCurrencyOptionTestCase ; test 3 = $$?
 
 # install into prefix
 install-exec-hook:
diff --git a/README b/README
index dbf4e8a..8fedf6a 100644
--- a/README
+++ b/README
@@ -17,6 +17,11 @@ Note that "make install" will re-download additional 
dependencies
 needed for "make check".  For the above, at the time of writing, you
 need Debian unstable, with older versions I get obscure errors.
 
+You may also have to install certain Python packages. Try:
+
+$ pip3 install validictory
+$ pip3 install django-simple-math-captcha
+
 ================== HOW TO INSTALL THE BANK =================
 
 From the repository's top directory, run
@@ -57,6 +62,7 @@ In order to properly run, the bank needs the following parts 
to be configured
 * Fractional length: how many digits after the floating point we want to be 
shown
   in HTML pages.
 * Debt thresholds
+* Suggested exchange
 
 # Mandatory section name
 [bank]
@@ -101,13 +107,16 @@ UWSGI_SERVE = unix
 UWSGI_UNIXPATH_MODE = 660
 
 # Maximum debt allowed for normal users.  The notation
-# used for amounts is: xy[.uv]:CURRENCY, with '.uv' being
-# optional.  NOTE that an amount of zero means there is no
-# maximum threshold.
-MAX_DEBT = 60:KUDOS
+# used for amounts is: CURRENCY:x.y.  Note, at least one
+# digit in the fractional part is required.
+MAX_DEBT = KUDOS:60.0
+
+# Maximum debt allowed for the bank itself.
+MAX_DEBT_BANK = KUDOS:0.0 # Infinite debt allowed.
 
-# Maximum debt allowed for the bank itself
-MAX_DEBT_BANK = 0:KUDOS # Infinite debt allowed.
+# The following option lets the bank suggest a default exchange
+# when the customer withdraws Taler coins.
+SUGGESTED_EXCHANGE = http://exchange.example.com/
 
 ================== HOW TO LAUNCH THE BANK =================
 
diff --git a/bank-check.conf b/bank-check.conf
index 547dce0..46658e2 100644
--- a/bank-check.conf
+++ b/bank-check.conf
@@ -7,9 +7,10 @@ CURRENCY = KUDOS
 
 # Which database should we use?
 DATABASE = postgres:///talercheck
+NDIGITS = 2
 
 # FIXME
-MAX_DEBT = KUDOS:50
+MAX_DEBT = KUDOS:50.0
 
 # FIXME
-MAX_DEBT_BANK = KUDOS:0
+MAX_DEBT_BANK = KUDOS:0.0
diff --git a/bank.conf b/bank.conf
index c99c2f9..9509f15 100644
--- a/bank.conf
+++ b/bank.conf
@@ -4,7 +4,7 @@
 DATABASE = postgres:///talerbank
 
 # FIXME
-MAX_DEBT = KUDOS:50
+MAX_DEBT = KUDOS:50.0
 
 # FIXME
-MAX_DEBT_BANK = KUDOS:0
\ No newline at end of file
+MAX_DEBT_BANK = KUDOS:0.0
diff --git a/configure.ac b/configure.ac
index eb50fd2..a22226f 100644
--- a/configure.ac
+++ b/configure.ac
@@ -1,6 +1,6 @@
 # This script is in the public domain.
 AC_PREREQ(2.61)
-AC_INIT([taler-bank], [0.3.0], address@hidden)
+AC_INIT([taler-bank], [0.4.0], address@hidden)
 
 AC_CONFIG_MACRO_DIR([m4])
 
@@ -8,6 +8,14 @@ AM_INIT_AUTOMAKE
 AC_PROG_AWK
 AC_PROG_SED
 
+
+AC_ARG_ENABLE([[doc]],
+  [AS_HELP_STRING([[--disable-doc]], [do not build any documentation])], ,
+    [enable_doc=yes])
+test "x$enable_doc" = "xno" || enable_doc=yes
+AM_CONDITIONAL([ENABLE_DOC], [test "x$enable_doc" = "xyes"])
+
+
 #
 # Check for Python
 #
@@ -35,13 +43,13 @@ AX_COMPARE_VERSION([$PIP_VERSION],[lt],[6.0], 
[AC_MSG_ERROR([Please install pip3
 
 # On Debian systems, we may need to pass "--system" to pip3 to get
 # to the desired installation target directory
-AC_ARG_ENABLE(debian-system,
-  AS_HELP_STRING(--enable-debian-system, pass --system option to pip3 to make 
Debian pip obey installation prefix),
-[if test x$enableval = xyes; then
-   DEBIAN_PIP3_SYSTEM="--system"
-else
+pip3 install --help | grep '\-\-system' >> /dev/null
+if test $? -ne 0;
+then
    DEBIAN_PIP3_SYSTEM=""
-fi])
+else
+   DEBIAN_PIP3_SYSTEM="--system"
+fi
 AC_SUBST(DEBIAN_PIP3_SYSTEM)
 
 #
diff --git a/setup.py b/setup.py
index 7a2a0f0..4bfdc5c 100755
--- a/setup.py
+++ b/setup.py
@@ -9,7 +9,6 @@ setup(name='talerbank',
       license='GPL',
       packages=find_packages(),
       install_requires=["django>=1.9",
-                        "django-simple-math-captcha",
                         "psycopg2",
                         "requests",
                         "uWSGI",
diff --git a/taler-bank-manage.in b/taler-bank-manage.in
index 75ba391..df83042 100644
--- a/taler-bank-manage.in
+++ b/taler-bank-manage.in
@@ -9,6 +9,8 @@ import argparse
 import sys
 import os
 import site
+import logging
+from talerbank.talerconfig import TalerConfig
 
 os.environ.setdefault("TALER_PREFIX", "@prefix@")
 site.addsitedir("%s/lib/python%d.%d/site-packages" % (
@@ -16,13 +18,11 @@ site.addsitedir("%s/lib/python%d.%d/site-packages" % (
     sys.version_info.major,
     sys.version_info.minor))
 
-from talerbank.talerconfig import TalerConfig
-import logging
-logger = logging.getLogger(__name__)
-
+LOGGER = logging.getLogger(__name__)
+TC = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
 
 # No perfect match to our logging format, but good enough ...
-uwsgi_logfmt = "%(ltime) %(proto) %(method) %(uri) %(proto) => %(status)"
+UWSGI_LOGFMT = "%(ltime) %(proto) %(method) %(uri) %(proto) => %(status)"
 
 def handle_django(args):
     import django
@@ -41,100 +41,95 @@ def handle_serve_http(args):
     call_command('migrate')
     call_command('provide_accounts')
     call_command('check')
-    tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
     port = args.port
     if port is None:
-        port = tc["bank"]["http_port"].value_int(required=True)
+        port = TC["bank"]["http_port"].value_int(required=True)
 
     httpspec = ":%d" % (port,)
     params = ["uwsgi", "uwsgi",
               "--master",
               "--die-on-term",
               "--http", httpspec,
-              "--log-format", uwsgi_logfmt,
+              "--log-format", UWSGI_LOGFMT,
               "--wsgi-file", "@prefix@/share/taler-bank/bank.wsgi"]
     os.execlp(*params)
 
 
 def handle_serve_uwsgi(args):
+    del args # pacify PEP checkers
     import django
     django.setup()
     from django.core.management import call_command
     call_command('migrate')
     call_command('provide_accounts')
     call_command('check')
-    tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
-    serve_uwsgi = tc["bank"]["uwsgi_serve"].value_string(required=True).lower()
+    serve_uwsgi = TC["bank"]["uwsgi_serve"].value_string(required=True).lower()
     params = ["uwsgi", "uwsgi",
               "--master",
               "--die-on-term",
-              "--log-format", uwsgi_logfmt,
+              "--log-format", UWSGI_LOGFMT,
               "--wsgi-file", "@prefix@/share/taler-bank/bank.wsgi"]
-    if "tcp" == serve_uwsgi:
-        port = tc["bank"]["uwsgi_port"].value_int(required=True)
+    if serve_uwsgi == "tcp":
+        port = TC["bank"]["uwsgi_port"].value_int(required=True)
         spec = ":%d" % (port,)
         params.extend(["--socket", spec])
     else:
-        spec = tc["bank"]["uwsgi_unixpath"].value_filename(required=True)
-        mode = tc["bank"]["uwsgi_unixpath_mode"].value_filename(required=True)
+        spec = TC["bank"]["uwsgi_unixpath"].value_filename(required=True)
+        mode = TC["bank"]["uwsgi_unixpath_mode"].value_filename(required=True)
         params.extend(["--socket", spec])
         params.extend(["--chmod-socket="+mode])
         os.makedirs(os.path.dirname(spec), exist_ok=True)
     logging.info("launching uwsgi with argv %s", params[1:])
     os.execlp(*params)
 
-def handle_sampledata(args):
+def handle_sampledata():
     import django
     django.setup()
     from django.core.management import call_command
     call_command('sample_donations')
 
 def handle_config(args):
-    from talerbank.talerconfig import TalerConfig
-    tc = TalerConfig.from_file(args.config)
-    tc.dump()
+    TC.from_file(args.config)
+    TC.dump()
 
 
-parser = argparse.ArgumentParser()
-parser.set_defaults(func=None)
-parser.add_argument('--config', '-c', help="configuration file to use",
+PARSER = argparse.ArgumentParser()
+PARSER.set_defaults(func=None)
+PARSER.add_argument('--config', '-c', help="configuration file to use",
                     metavar="CONFIG", type=str, dest="config", default=None)
-parser.add_argument('--with-db', help="use 'dbname' (currently only 
'dbtype'=='postgres' is supported)",
-                    type=str, metavar="dbtype:///dbname", dest="altdb")
-sub = parser.add_subparsers()
-
-p = sub.add_parser('django', help="Run django-admin command")
-p.add_argument("command", nargs=argparse.REMAINDER)
-p.set_defaults(func=handle_django)
+PARSER.add_argument('--with-db', type=str, metavar="dbtype:///dbname", 
dest="altdb",
+                    help="use 'dbname' (currently only 'dbtype'=='postgres' is 
supported)")
+SUB = PARSER.add_subparsers()
 
-# FIXME: adapt to newest wire_transfer()
-# p = sub.add_parser('sampledata', help="Put sample data into the db")
-# p.set_defaults(func=handle_sampledata)
+P = SUB.add_parser('django', help="Run django-admin command")
+P.add_argument("command", nargs=argparse.REMAINDER)
+P.set_defaults(func=handle_django)
 
-p = sub.add_parser('serve-http', help="Serve bank over HTTP")
-p.add_argument("--port", "-p", dest="port", type=int, default=None, 
metavar="PORT")
-p.set_defaults(func=handle_serve_http)
+P = SUB.add_parser('serve-http', help="Serve bank over HTTP")
+P.add_argument("--port", "-p", dest="port", type=int,
+               default=None, metavar="PORT")
+P.set_defaults(func=handle_serve_http)
 
-p = sub.add_parser('serve-uwsgi', help="Serve bank over UWSGI")
-p.set_defaults(func=handle_serve_uwsgi)
+P = SUB.add_parser('serve-uwsgi', help="Serve bank over UWSGI")
+P.set_defaults(func=handle_serve_uwsgi)
 
-p = sub.add_parser('config', help="show config")
-p.set_defaults(func=handle_config)
+P = SUB.add_parser('config', help="show config")
+P.set_defaults(func=handle_config)
 
 
-args = parser.parse_args()
+ARGS = PARSER.parse_args()
 
 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "talerbank.settings")
 
-if args.altdb:
-    logger.info("Setting alternate db: %s" % args.altdb)
-    os.environ.setdefault("TALER_BANK_ALTDB", args.altdb)
+if ARGS.altdb:
+    LOGGER.info("Setting alternate db: %s" % ARGS.altdb)
+    os.environ.setdefault("TALER_BANK_ALTDB", ARGS.altdb)
 
-if getattr(args, 'func', None) is None:
-    parser.print_help()
+if getattr(ARGS, 'func', None) is None:
+    PARSER.print_help()
     sys.exit(1)
 
-if args.config is not None:
-    os.environ["TALER_CONFIG_FILE"] = args.config
+if ARGS.config is not None:
+    os.environ["TALER_CONFIG_FILE"] = ARGS.config
 
-args.func(args)
+ARGS.func(ARGS)
diff --git a/talerbank/__init__.py b/talerbank/__init__.py
index 4efb025..6c4125c 100644
--- a/talerbank/__init__.py
+++ b/talerbank/__init__.py
@@ -1,8 +1,4 @@
 import logging
 
-log_conf = {
-    'format': '%(asctime)-15s %(module)s %(levelname)s %(message)s',
-    'level': logging.INFO
-}
-
-logging.basicConfig(**log_conf)
+FMT = '%(asctime)-15s %(module)s %(levelname)s %(message)s'
+logging.basicConfig(format=FMT, level=logging.WARNING)
diff --git a/talerbank/app/Makefile.am b/talerbank/app/Makefile.am
index 6376a68..b3efdd7 100644
--- a/talerbank/app/Makefile.am
+++ b/talerbank/app/Makefile.am
@@ -5,7 +5,7 @@ EXTRA_DIST = \
   schemas.py \
   urls.py \
   views.py \
-  amounts.py \
+  amount.py \
   checks.py  \
   __init__.py \
   models.py \
diff --git a/talerbank/app/__init__.py b/talerbank/app/__init__.py
index ecc3dff..e69de29 100644
--- a/talerbank/app/__init__.py
+++ b/talerbank/app/__init__.py
@@ -1,2 +0,0 @@
-# make sure that checks are registered
-from . import checks
diff --git a/talerbank/app/amount.py b/talerbank/app/amount.py
new file mode 100644
index 0000000..c3e2b93
--- /dev/null
+++ b/talerbank/app/amount.py
@@ -0,0 +1,136 @@
+#  This file is part of TALER
+#  (C) 2017 TALER SYSTEMS
+#
+#  This library is free software; you can redistribute it and/or
+#  modify it under the terms of the GNU Lesser General Public
+#  License as published by the Free Software Foundation; either
+#  version 2.1 of the License, or (at your option) any later version.
+#
+#  This library 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
+#  Lesser General Public License for more details.
+#
+#  You should have received a copy of the GNU Lesser General Public
+#  License along with this library; if not, write to the Free Software
+#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  
USA
+#
+#  @author Marcello Stanisci
+#  @version 0.1
+#  @repository https://git.taler.net/copylib.git/
+#  This code is "copylib", it is versioned under the Git repository
+#  mentioned above, and it is meant to be manually copied into any project
+#  which might need it.
+
+from typing import Type
+
+class CurrencyMismatch(Exception):
+    def __init__(self, curr1, curr2) -> None:
+        super(CurrencyMismatch, self).__init__(
+            "%s vs %s" % (curr1, curr2))
+
+class BadFormatAmount(Exception):
+    def __init__(self, faulty_str) -> None:
+        super(BadFormatAmount, self).__init__(
+            "Bad format amount: " + faulty_str)
+
+class Amount:
+    # How many "fraction" units make one "value" unit of currency
+    # (Taler requires 10^8).  Do not change this 'constant'.
+    @staticmethod
+    def _fraction() -> int:
+        return 10 ** 8
+
+    @staticmethod
+    def _max_value() -> int:
+        return (2 ** 53) - 1
+
+    def __init__(self, currency, value=0, fraction=0) -> None:
+        assert value >= 0 and fraction >= 0
+        self.value = value
+        self.fraction = fraction
+        self.currency = currency
+        self.__normalize()
+        assert self.value <= Amount._max_value()
+
+    # Normalize amount
+    def __normalize(self) -> None:
+        if self.fraction >= Amount._fraction():
+            self.value += int(self.fraction / Amount._fraction())
+            self.fraction = self.fraction % Amount._fraction()
+
+    # Parse a string matching the format "A:B.C"
+    # instantiating an amount object.
+    @classmethod
+    def parse(cls, amount_str: str):
+        exp = r'^\s*([-_*A-Za-z0-9]+):([0-9]+)\.([0-9]+)\s*$'
+        import re
+        parsed = re.search(exp, amount_str)
+        if not parsed:
+            raise BadFormatAmount(amount_str)
+        value = int(parsed.group(2))
+        fraction = 0
+        for i, digit in enumerate(parsed.group(3)):
+            fraction += int(int(digit) * (Amount._fraction() / 10 ** (i+1)))
+        return cls(parsed.group(1), value, fraction)
+
+    # Comare two amounts, return:
+    # -1 if a < b
+    # 0 if a == b
+    # 1 if a > b
+    @staticmethod
+    def cmp(am1, am2) -> int:
+        if am1.currency != am2.currency:
+            raise CurrencyMismatch(am1.currency, am2.currency)
+        if am1.value == am2.value:
+            if am1.fraction < am2.fraction:
+                return -1
+            if am1.fraction > am2.fraction:
+                return 1
+            return 0
+        if am1.value < am2.value:
+            return -1
+        return 1
+
+    def set(self, currency: str, value=0, fraction=0) -> None:
+        self.currency = currency
+        self.value = value
+        self.fraction = fraction
+
+    # Add the given amount to this one
+    def add(self, amount) -> None:
+        if self.currency != amount.currency:
+            raise CurrencyMismatch(self.currency, amount.currency)
+        self.value += amount.value
+        self.fraction += amount.fraction
+        self.__normalize()
+
+    # Subtract passed amount from this one
+    def subtract(self, amount) -> None:
+        if self.currency != amount.currency:
+            raise CurrencyMismatch(self.currency, amount.currency)
+        if self.fraction < amount.fraction:
+            self.fraction += Amount._fraction()
+            self.value -= 1
+        if self.value < amount.value:
+            raise ValueError('self is lesser than amount to be subtracted')
+        self.value -= amount.value
+        self.fraction -= amount.fraction
+
+    # Dump string from this amount, will put 'ndigits' numbers
+    # after the dot.
+    def stringify(self, ndigits: int) -> str:
+        assert ndigits > 0
+        ret = '%s:%s.' % (self.currency, str(self.value))
+        fraction = self.fraction
+        while ndigits > 0:
+            ret += str(int(fraction / (Amount._fraction() / 10)))
+            fraction = (fraction * 10) % (Amount._fraction())
+            ndigits -= 1
+        return ret
+
+    # Dump the Taler-compliant 'dict' amount
+    def dump(self) -> dict:
+        return dict(value=self.value,
+                    fraction=self.fraction,
+                    currency=self.currency)
diff --git a/talerbank/app/amounts.py b/talerbank/app/amounts.py
deleted file mode 100644
index f9bdd02..0000000
--- a/talerbank/app/amounts.py
+++ /dev/null
@@ -1,101 +0,0 @@
-#  This file is part of TALER
-#  (C) 2016 INRIA
-#
-#  TALER is free software; you can redistribute it and/or modify it under the
-#  terms of the GNU Affero General Public License as published by the Free 
Software
-#  Foundation; either version 3, or (at your option) any later version.
-#
-#  TALER 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
-#  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
-#
-#  @author Marcello Stanisci
-#  @author Florian Dold
-
-
-import re
-import math
-import logging
-from django.conf import settings
-
-logger = logging.getLogger(__name__)
-
-FRACTION = 100000000
-
-class CurrencyMismatchException(Exception):
-    def __init__(self, msg=None, status_code=0):
-        self.msg = msg
-        # HTTP status code to be returned as response for
-        # this exception
-        self.status_code = status_code
-
-class BadFormatAmount(Exception):
-    def __init__(self, msg=None, status_code=0):
-        self.msg = msg
-        # HTTP status code to be returned as response for
-        # this exception
-        self.status_code = status_code
-
-
-def check_currency(a1, a2):
-    if a1["currency"] != a2["currency"]:
-        logger.error("Different currencies given: %s vs %s" % (a1["currency"], 
a2["currency"]))
-        raise CurrencyMismatchException
-
-def get_zero():
-    return dict(value=0, fraction=0, currency=settings.TALER_CURRENCY)
-
-def amount_add(a1, a2):
-    check_currency(a1, a2)
-    a1_float = floatify(a1)
-    a2_float = floatify(a2)
-    return parse_amount("%s:%s" % (a2["currency"], str(a1_float + a2_float)))
-
-def amount_sub(a1, a2):
-    check_currency(a1, a2)
-    a1_float = floatify(a1)
-    a2_float = floatify(a2)
-    sub = a1_float - a2_float
-    fmt = "%s:%s" % (a2["currency"], str(sub))
-    return parse_amount(fmt)
-
-# Return -1 if a1 < a2, 0 if a1 == a2, 1 if a1 > a2
-def amount_cmp(a1, a2):
-    check_currency(a1, a2)
-    a1_float = floatify(a1)
-    a2_float = floatify(a2)
-
-    if a1_float < a2_float:
-        return -1
-    elif a1_float == a2_float:
-        return 0
-
-    return 1
-
-
-def floatify(amount_dict):
-    return amount_dict['value'] + (float(amount_dict['fraction']) / 
float(FRACTION))
-
-def stringify(amount_float, digits=2):
-    o = "".join(["%.", "%sf" % digits])
-    return o % amount_float
-
-def parse_amount(amount_str):
-    """
-    Parse amount of return None if not a
-    valid amount string
-    """
-    parsed = re.search("^\s*([-_*A-Za-z0-9]+):([0-9]+)(\.[0-9]+)?\s*$", 
amount_str)
-    if not parsed:
-        raise BadFormatAmount
-    value = int(parsed.group(2))
-    fraction = 0
-    if parsed.group(3) is not None:
-        for i, digit in enumerate(parsed.group(3)[1:]):
-            fraction += int(int(digit) * (FRACTION / 10 ** (i+1)))
-    return {'value': value,
-            'fraction': fraction,
-            'currency': parsed.group(1)}
diff --git a/talerbank/app/checks.py b/talerbank/app/checks.py
deleted file mode 100644
index 6734d3a..0000000
--- a/talerbank/app/checks.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from django.core.checks import register, Warning
-from django.db.utils import OperationalError
-
-
address@hidden()
-def example_check(app_configs, **kwargs):
-    errors = []
-    try:
-        from .models import User
-        User.objects.get(username='Bank')
-    except User.DoesNotExist:
-        errors.append(
-            Warning(
-                'The bank user does not exist',
-                hint="run the provide_accounts management command",
-                id='talerbank.E001'
-            ))
-    except OperationalError:
-            errors.append(Warning(
-                'Presumably non existent database',
-                hint="create a database for the application",
-                id='talerbank.E002'
-            ))
-    return errors
diff --git a/talerbank/app/management/commands/Makefile.am 
b/talerbank/app/management/commands/Makefile.am
index 3fc6f2d..cec82a7 100644
--- a/talerbank/app/management/commands/Makefile.am
+++ b/talerbank/app/management/commands/Makefile.am
@@ -3,5 +3,4 @@ SUBDIRS = .
 EXTRA_DIST = \
   dump_talerdb.py \
   __init__.py \
-  provide_accounts.py \
-  sample_donations.py
+  provide_accounts.py
diff --git a/talerbank/app/management/commands/dump_talerdb.py 
b/talerbank/app/management/commands/dump_talerdb.py
index ab587d9..ba81444 100644
--- a/talerbank/app/management/commands/dump_talerdb.py
+++ b/talerbank/app/management/commands/dump_talerdb.py
@@ -14,16 +14,14 @@
 #
 #  @author Marcello Stanisci
 
+import sys
+import logging
 from django.core.management.base import BaseCommand
-from ...models import BankAccount, BankTransaction
 from django.db.utils import OperationalError, ProgrammingError
-import logging
-import sys
-from ...amounts import floatify
-
-# Rewrite to match new BankTransaction layout.
+from ...models import BankAccount, BankTransaction
+from .helpers import hard_db_error_log
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
 def dump_accounts():
     try:
@@ -34,10 +32,7 @@ def dump_accounts():
         for acc in accounts:
             print(acc.user.username + " has account number " + 
str(acc.account_no))
     except (OperationalError, ProgrammingError):
-        logger.error("likely causes: non existent DB or unmigrated project\n"
-                     "(try 'taler-bank-manage django migrate' in the latter 
case)",
-                     stack_info=False,
-                     exc_info=True)
+        hard_db_error_log()
         sys.exit(1)
 
 
@@ -50,14 +45,11 @@ def dump_history():
             # as the first/last character on a line makes flake8 complain
             msg.append("+%s, " % item.credit_account.account_no)
             msg.append("-%s, " % item.debit_account.account_no)
-            msg.append("%.2f, " % floatify(item.amount_obj))
+            msg.append(item.amount.stringify(2))
             msg.append(item.subject)
             print(''.join(msg))
     except (OperationalError, ProgrammingError):
-        logger.error("likely causes: non existent DB or unmigrated project\n"
-                     "(try 'taler-bank-manage django migrate' in the latter 
case)",
-                     stack_info=False,
-                     exc_info=True)
+        hard_db_error_log()
         sys.exit(1)
 
 
diff --git a/talerbank/app/management/commands/helpers.py 
b/talerbank/app/management/commands/helpers.py
new file mode 100644
index 0000000..62137e9
--- /dev/null
+++ b/talerbank/app/management/commands/helpers.py
@@ -0,0 +1,24 @@
+#  This file is part of TALER
+#  (C) 2017 Taler Systems SA
+#
+#  TALER is free software; you can redistribute it and/or modify it under the
+#  terms of the GNU General Public License as published by the Free Software
+#  Foundation; either version 3, or (at your option) any later version.
+#
+#  TALER 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
+#  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
+#
+#  @author Marcello Stanisci
+
+import logging
+LOGGER = logging.getLogger(__name__)
+
+def hard_db_error_log():
+    LOGGER.error("likely causes: non existent DB or unmigrated project\n"
+                 "(try 'taler-bank-manage django migrate' in the latter case)",
+                 stack_info=False,
+                 exc_info=True)
diff --git a/talerbank/app/management/commands/provide_accounts.py 
b/talerbank/app/management/commands/provide_accounts.py
index cf3f0cd..c719296 100644
--- a/talerbank/app/management/commands/provide_accounts.py
+++ b/talerbank/app/management/commands/provide_accounts.py
@@ -18,12 +18,13 @@
 import sys
 import logging
 from django.contrib.auth.models import User
-from django.db.utils import ProgrammingError, DataError, OperationalError
+from django.db.utils import ProgrammingError, OperationalError
 from django.core.management.base import BaseCommand
-from ...models import BankAccount
 from django.conf import settings
+from ...models import BankAccount
+from .helpers import hard_db_error_log
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
 
 def demo_accounts():
@@ -31,38 +32,31 @@ def demo_accounts():
         try:
             User.objects.get(username=name)
         except User.DoesNotExist:
-            u = User.objects.create_user(username=name, password='x')
-            b = BankAccount(user=u,
-                            currency=settings.TALER_CURRENCY,
-                            is_public=True)
-            b.save()
-            logger.info("Creating account '%s' with number %s", name, 
b.account_no)
+            BankAccount(user=User.objects.create_user(username=name, 
password='x'),
+                        is_public=True).save()
+            LOGGER.info("Creating account for '%s'", name)
 
 
 def ensure_account(name):
-    logger.info("ensuring account '{}'".format(name))
+    LOGGER.info("ensuring account '%s'", name)
     user = None
     try:
         user = User.objects.get(username=name)
     except (OperationalError, ProgrammingError):
-        logger.error("likely causes: non existent DB or unmigrated project\n"
-                     "(try 'taler-bank-manage django migrate' in the latter 
case)",
-                     stack_info=False,
-                     exc_info=True)
+        hard_db_error_log()
         sys.exit(1)
     except User.DoesNotExist:
-        logger.info("Creating *user* account '{}'".format(name))
+        LOGGER.info("Creating *user* account '%s'", name)
         user = User.objects.create_user(username=name, password='x')
 
     try:
         BankAccount.objects.get(user=user)
 
     except BankAccount.DoesNotExist:
-        acc = BankAccount(user=user,
-                          currency=settings.TALER_CURRENCY,
-                          is_public=True)
+        acc = BankAccount(user=user, is_public=True)
         acc.save()
-        logger.info("Creating *bank* account number '{}' for user 
'{}'".format(acc.account_no, name))
+        LOGGER.info("Creating *bank* account number \
+                    '%s' for user '%s'", acc.account_no, name)
 
 
 def basic_accounts():
diff --git a/talerbank/app/management/commands/sample_donations.py 
b/talerbank/app/management/commands/sample_donations.py
deleted file mode 100644
index 35749a6..0000000
--- a/talerbank/app/management/commands/sample_donations.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#  This file is part of TALER
-#  (C) 2014, 2015, 2106 INRIA
-#
-#  TALER is free software; you can redistribute it and/or modify it under the
-#  terms of the GNU General Public License as published by the Free Software
-#  Foundation; either version 3, or (at your option) any later version.
-#
-#  TALER 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
-#  TALER; see the file COPYING.  If not, see <http://www.gnu.org/licenses/>
-#
-#  @author Marcello Stanisci
-
-from random import randint
-from django.core.management.base import BaseCommand
-from ...funds import wire_transfer_in_out
-from django.conf import settings
-from django.db.utils import OperationalError, ProgrammingError
-from ...models import BankAccount
-import logging
-import sys
-
-logger = logging.getLogger(__name__)
-
-public_accounts = []
-def sample_donations():
-    counter = -1
-    try:
-        public_accounts = BankAccount.objects.filter(is_public=True)
-        if len(public_accounts) is 0:
-            logger.warning("No public accounts still activated. Run 
'taler-bank-manage"
-                           " django provide_accounts' first")            
-        for account in public_accounts:
-            logger.debug("*")
-            for i in range(0, 9):
-                counter += 1
-                if account.user.username in settings.TALER_EXPECTS_DONATIONS:
-                    value = randint(1, 100)
-                    try:
-                        # 1st take money from bank and give to exchange
-                        wire_transfer_in_out({'value': value,
-                                              'fraction': 0,
-                                              'currency': 
settings.TALER_CURRENCY},
-                                             1,
-                                             2,
-                                             "Test-withdrawal-%d" % counter)
-    
-                        # 2nd take money from exchange and give to donation 
receiver
-                        wire_transfer_in_out({'value': value,
-                                              'fraction': 0,
-                                              'currency': 
settings.TALER_CURRENCY},
-                                             2,
-                                             account.account_no,
-                                             "Test-donation-%d" % counter)
-                    except BankAccount.DoesNotExist:
-                        logger.error("(At least) one account is not found. Run 
"
-                                     "'taler-bank-manage django 
provide_accounts' beforehand")
-                        sys.exit(1)
-    except (ProgrammingError, OperationalError):
-        logger.error("likely causes: non existent DB or unmigrated project\n"
-                     "(try 'taler-bank-manage django migrate' in the latter 
case)",
-                     stack_info=False,
-                     exc_info=True)
-        sys.exit(1)
-
-class Command(BaseCommand):
-    def handle(self, *args, **options):
-        sample_donations()
diff --git a/talerbank/app/migrations/0002_bankaccount_amount.py 
b/talerbank/app/migrations/0002_bankaccount_amount.py
new file mode 100644
index 0000000..beaa1d8
--- /dev/null
+++ b/talerbank/app/migrations/0002_bankaccount_amount.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 13:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0001_initial'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='bankaccount',
+            name='amount',
+            field=talerbank.app.models.AmountField(default=False),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0003_auto_20171030_1346.py 
b/talerbank/app/migrations/0003_auto_20171030_1346.py
new file mode 100644
index 0000000..91c6cb9
--- /dev/null
+++ b/talerbank/app/migrations/0003_auto_20171030_1346.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 13:46
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0002_bankaccount_amount'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='bankaccount',
+            name='amount',
+            field=talerbank.app.models.AmountField(),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0004_auto_20171030_1428.py 
b/talerbank/app/migrations/0004_auto_20171030_1428.py
new file mode 100644
index 0000000..b93ebd4
--- /dev/null
+++ b/talerbank/app/migrations/0004_auto_20171030_1428.py
@@ -0,0 +1,34 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 14:28
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0003_auto_20171030_1346'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='banktransaction',
+            name='amount_fraction',
+        ),
+        migrations.RemoveField(
+            model_name='banktransaction',
+            name='amount_value',
+        ),
+        migrations.AddField(
+            model_name='banktransaction',
+            name='amount',
+            field=talerbank.app.models.AmountField(default=False),
+        ),
+        migrations.AlterField(
+            model_name='bankaccount',
+            name='amount',
+            field=talerbank.app.models.AmountField(default=False),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0005_remove_banktransaction_currency.py 
b/talerbank/app/migrations/0005_remove_banktransaction_currency.py
new file mode 100644
index 0000000..9cd781f
--- /dev/null
+++ b/talerbank/app/migrations/0005_remove_banktransaction_currency.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-30 14:37
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0004_auto_20171030_1428'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='banktransaction',
+            name='currency',
+        ),
+    ]
diff --git a/talerbank/app/migrations/0006_auto_20171031_0823.py 
b/talerbank/app/migrations/0006_auto_20171031_0823.py
new file mode 100644
index 0000000..67c1a70
--- /dev/null
+++ b/talerbank/app/migrations/0006_auto_20171031_0823.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 08:23
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0005_remove_banktransaction_currency'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='bankaccount',
+            name='amount',
+            field=talerbank.app.models.AmountField(default=None),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0007_auto_20171031_0906.py 
b/talerbank/app/migrations/0007_auto_20171031_0906.py
new file mode 100644
index 0000000..923cff2
--- /dev/null
+++ b/talerbank/app/migrations/0007_auto_20171031_0906.py
@@ -0,0 +1,21 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 09:06
+from __future__ import unicode_literals
+
+from django.db import migrations
+import talerbank.app.models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0006_auto_20171031_0823'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='bankaccount',
+            name='amount',
+            
field=talerbank.app.models.AmountField(default=talerbank.app.models.get_zero_amount),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0008_auto_20171031_0938.py 
b/talerbank/app/migrations/0008_auto_20171031_0938.py
new file mode 100644
index 0000000..3b97829
--- /dev/null
+++ b/talerbank/app/migrations/0008_auto_20171031_0938.py
@@ -0,0 +1,31 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-10-31 09:38
+from __future__ import unicode_literals
+
+from django.db import migrations
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0007_auto_20171031_0906'),
+    ]
+
+    operations = [
+        migrations.RemoveField(
+            model_name='bankaccount',
+            name='balance',
+        ),
+        migrations.RemoveField(
+            model_name='bankaccount',
+            name='balance_fraction',
+        ),
+        migrations.RemoveField(
+            model_name='bankaccount',
+            name='balance_value',
+        ),
+        migrations.RemoveField(
+            model_name='bankaccount',
+            name='currency',
+        ),
+    ]
diff --git a/talerbank/app/migrations/0009_auto_20171120_1642.py 
b/talerbank/app/migrations/0009_auto_20171120_1642.py
new file mode 100644
index 0000000..590fb93
--- /dev/null
+++ b/talerbank/app/migrations/0009_auto_20171120_1642.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-11-20 16:42
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0008_auto_20171031_0938'),
+    ]
+
+    operations = [
+        migrations.AlterField(
+            model_name='banktransaction',
+            name='date',
+            field=models.DateTimeField(auto_now=True, db_index=True),
+        ),
+    ]
diff --git a/talerbank/app/migrations/0010_banktransaction_cancelled.py 
b/talerbank/app/migrations/0010_banktransaction_cancelled.py
new file mode 100644
index 0000000..ee46222
--- /dev/null
+++ b/talerbank/app/migrations/0010_banktransaction_cancelled.py
@@ -0,0 +1,20 @@
+# -*- coding: utf-8 -*-
+# Generated by Django 1.10.3 on 2017-12-07 16:11
+from __future__ import unicode_literals
+
+from django.db import migrations, models
+
+
+class Migration(migrations.Migration):
+
+    dependencies = [
+        ('app', '0009_auto_20171120_1642'),
+    ]
+
+    operations = [
+        migrations.AddField(
+            model_name='banktransaction',
+            name='cancelled',
+            field=models.BooleanField(default=False),
+        ),
+    ]
diff --git a/talerbank/app/models.py b/talerbank/app/models.py
index 868b0d6..f8c5c47 100644
--- a/talerbank/app/models.py
+++ b/talerbank/app/models.py
@@ -16,56 +16,67 @@
 #  @author Florian Dold
 
 from __future__ import unicode_literals
+from typing import Any, Tuple
 from django.contrib.auth.models import User
 from django.db import models
+from django.conf import settings
+from django.core.exceptions import ValidationError
+from .amount import Amount, BadFormatAmount
 
+class AmountField(models.Field):
+
+    description = 'Amount object in Taler style'
+
+    def deconstruct(self) -> Tuple[str, str, list, dict]:
+        name, path, args, kwargs = super(AmountField, self).deconstruct()
+        return name, path, args, kwargs
+
+    def db_type(self, connection: Any) -> str:
+        return "varchar"
+
+    # Pass stringified object to db connector
+    def get_prep_value(self, value: Amount) -> str:
+        if not value:
+            return "%s:0.0" % settings.TALER_CURRENCY
+        return value.stringify(settings.TALER_DIGITS)
+
+    @staticmethod
+    def from_db_value(value: str, *args) -> Amount:
+        del args # pacify PEP checkers
+        if value is None:
+            return Amount.parse(settings.TALER_CURRENCY)
+        return Amount.parse(value)
+
+    def to_python(self, value: Any) -> Amount:
+        if isinstance(value, Amount):
+            return value
+        try:
+            if value is None:
+                return Amount.parse(settings.TALER_CURRENCY)
+            return Amount.parse(value)
+        except BadFormatAmount:
+            raise ValidationError("Invalid input for an amount string: %s" % 
value)
+
+def get_zero_amount() -> Amount:
+    return Amount(settings.TALER_CURRENCY)
 
 class BankAccount(models.Model):
     is_public = models.BooleanField(default=False)
-    # Handier than keeping the amount signed, for two reasons:
-    # (1) checking if someone is in debt is less verbose: with signed
-    # amounts we have to check if the amount is less than zero; this
-    # way we only check if a boolean is true. (2) The bank logic is
-    # ready to welcome a data type for amounts which doesn't have any
-    # sign notion, like Taler amounts do.
     debit = models.BooleanField(default=False)
-    balance_value = models.IntegerField(default=0)
-    balance_fraction = models.IntegerField(default=0)
-    # From today's (16/10/2017) Mumble talk, it emerged that bank shouldn't
-    # store amounts as floats, but: normal banks should not care about
-    # Taler when representing values around in their databases..
-    balance = models.FloatField(default=0)
-    currency = models.CharField(max_length=12, default="")
     account_no = models.AutoField(primary_key=True)
     user = models.OneToOneField(User, on_delete=models.CASCADE)
-    def _get_balance(self):
-        return dict(value=self.balance_value,
-                    fraction=self.balance_fraction,
-                    currency=self.currency)
-    def _set_balance(self, amount):
-        self.balance_value = amount["value"]
-        self.balance_fraction = amount["fraction"]
-        self.currency = amount["currency"]
-    balance_obj = property(_get_balance, _set_balance)
+    amount = AmountField(default=get_zero_amount)
 
 class BankTransaction(models.Model):
-    amount_value = models.IntegerField(default=0)
-    amount_fraction = models.IntegerField(default=0)
-    currency = models.CharField(max_length=12)
+    amount = AmountField(default=False)
     debit_account = models.ForeignKey(BankAccount,
                                       on_delete=models.CASCADE,
+                                      db_index=True,
                                       related_name="debit_account")
     credit_account = models.ForeignKey(BankAccount,
                                        on_delete=models.CASCADE,
+                                       db_index=True,
                                        related_name="credit_account")
     subject = models.CharField(default="(no subject given)", max_length=200)
-    date = models.DateTimeField(auto_now=True)
-    def _get_amount(self):
-        return dict(value=self.amount_value,
-                    fraction=self.amount_fraction,
-                    currency=self.currency)
-    def _set_amount(self, amount):
-        self.amount_value = amount["value"]
-        self.amount_fraction = amount["fraction"]
-        self.currency = amount["currency"]
-    amount_obj = property(_get_amount, _set_amount)
+    date = models.DateTimeField(auto_now=True, db_index=True)
+    cancelled = models.BooleanField(default=False)
diff --git a/talerbank/app/schemas.py b/talerbank/app/schemas.py
index 91771d1..810f33f 100644
--- a/talerbank/app/schemas.py
+++ b/talerbank/app/schemas.py
@@ -19,10 +19,39 @@
 definitions of JSON schemas for validating data
 """
 
+import json
 import validictory
-from django.core.exceptions import ValidationError
+from django.conf import settings
 
-wiredetails_schema = {
+AMOUNT_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "value": {"type": "integer"},
+        "fraction": {"type": "integer"},
+        "currency": {"type": "string",
+                     "pattern": "^"+settings.TALER_CURRENCY+"$"}
+    }
+}
+
+WITHDRAW_SESSION_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "amount": {"type": AMOUNT_SCHEMA},
+        "exchange_url": {"type": "string"},
+        "reserve_pub": {"type": "string"},
+        "exchange_account_number": {"type": "integer"},
+        "sender_wiredetails": {
+            "type": "object",
+            "properties": {
+                "type": {"type": "string"},
+                "bank_uri": {"type": "string"},
+                "account_number": {"type": "integer"}
+            }
+        }
+    }
+}
+
+WIREDETAILS_SCHEMA = {
     "type": "object",
     "properties": {
         "test": {
@@ -37,42 +66,96 @@ wiredetails_schema = {
     }
 }
 
-auth_schema = {
+AUTH_SCHEMA = {
     "type": "object",
     "properties": {
-        "type": {"type": "string"},
+        "type": {"type": "string",
+                 "pattern": "^basic$"},
         "data": {"type": "object", "required": False}
     }
 }
 
-amount_schema = {
+REJECT_REQUEST_SCHEMA = {
     "type": "object",
     "properties": {
-        "value": {"type": "integer"},
-        "fraction": {"type": "integer"},
-        "currency": {"type": "string"}
+        "auth": AUTH_SCHEMA,
+        "row_id": {"type": "integer"},
+        "account_number": {"type": "integer"}
     }
 }
 
-incoming_request_schema = {
+HISTORY_REQUEST_SCHEMA = {
     "type": "object",
     "properties": {
-        "amount": {"type": amount_schema},
-        "wtid": {"type": "string"},
+        "auth": {"type": "string", "pattern": "^basic$"},
+        "delta": {"type": "string",
+                  "pattern": r"^([\+-])?([0-9])+$"},
+        "start": {"type": "string",
+                  "pattern": "^([0-9]+)$",
+                  "required": False},
+        "direction": {"type": "string",
+                      "pattern": "^(debit|credit|both|cancel\+|cancel-)$"},
+        "account_number": {"type": "string",
+                           "pattern": "^([0-9]+)$",
+                           "required": False}
+    }
+}
+
+INCOMING_REQUEST_SCHEMA = {
+    "type": "object",
+    "properties": {
+        "amount": {"type": AMOUNT_SCHEMA},
+        "subject": {"type": "string"},
         "exchange_url": {"type": "string"},
         "credit_account": {"type": "integer"},
-        "auth": auth_schema
+        "auth": AUTH_SCHEMA
     }
 }
 
+PIN_TAN_ARGS = {
+    "type": "object",
+    "properties": {
+        "amount_value": {"format": "str_to_int"},
+        "amount_fraction": {"format": "str_to_int"},
+        "amount_currency": {"type": "string"},
+        "exchange": {"type": "string"},
+        "reserve_pub": {"type": "string"},
+        "wire_details": {"format": "wiredetails_string"}
+    }
+}
+
+def validate_pintan_types(validator, fieldname, value, format_option):
+    del validator # pacify PEP checkers
+    try:
+        if format_option == "str_to_int":
+            int(value)
+        if format_option == "wiredetails_string":
+            data = json.loads(value)
+            validate_wiredetails(data)
+    except Exception:
+        raise validictory.FieldValidationError(
+            "Missing/malformed '%s'" % fieldname, fieldname, value)
+
+def validate_pin_tan_args(pin_tan_args):
+    format_dict = {
+        "str_to_int": validate_pintan_types,
+        "wiredetails_string": validate_pintan_types}
+    validictory.validate(pin_tan_args, PIN_TAN_ARGS, 
format_validators=format_dict)
+
+def validate_reject_request(reject_request):
+    validictory.validate(reject_request, REJECT_REQUEST_SCHEMA)
+
+def validate_history_request(history_request):
+    validictory.validate(history_request, HISTORY_REQUEST_SCHEMA)
+
 def validate_amount(amount):
-    validictory.validate(amount, amount_schema)
+    validictory.validate(amount, AMOUNT_SCHEMA)
 
 def validate_wiredetails(wiredetails):
-    validictory.validate(wiredetails, wiredetails_schema)
+    validictory.validate(wiredetails, WIREDETAILS_SCHEMA)
 
 def validate_incoming_request(incoming_request):
-    validictory.validate(incoming_request, incoming_request_schema)
+    validictory.validate(incoming_request, INCOMING_REQUEST_SCHEMA)
 
-def validate_auth_basic(auth_basic):
-    validictory.validate(auth_basic, auth_basic_schema)
+def check_withdraw_session(session):
+    validictory.validate(session, WITHDRAW_SESSION_SCHEMA)
diff --git a/talerbank/app/static/chrome-store-link.js 
b/talerbank/app/static/chrome-store-link.js
deleted file mode 100644
index e0d10cc..0000000
--- a/talerbank/app/static/chrome-store-link.js
+++ /dev/null
@@ -1,11 +0,0 @@
-function onSuccess() {
-  console.log("installation successful");
-}
-
-function onFailure(detail) {
-  console.log("installation failed", detail);
-}
-
-function installWallet() {
-  
chrome.webstore.install("https://chrome.google.com/webstore/detail/millncjiddlpgdmkklmhfadpacifaonc";,
 onSuccess, onFailure);
-}
diff --git a/talerbank/app/templates/Makefile.am 
b/talerbank/app/templates/Makefile.am
index 15aa5a8..15f8023 100644
--- a/talerbank/app/templates/Makefile.am
+++ b/talerbank/app/templates/Makefile.am
@@ -8,5 +8,4 @@ EXTRA_DIST = \
   pin_tan.html \
   register.html \
   login.html \
-  javascript.html \
   error_exchange.html
diff --git a/talerbank/app/templates/base.html 
b/talerbank/app/templates/base.html
index 9a6cda5..a604561 100644
--- a/talerbank/app/templates/base.html
+++ b/talerbank/app/templates/base.html
@@ -25,7 +25,6 @@
     <link rel="stylesheet" type="text/css" href="{{ static('bank.css') }}" />
     <link rel="stylesheet" type="text/css" href="{{ 
static('web-common/demo.css') }}" />
     <link rel="stylesheet" type="text/css" href="{{ 
static('web-common/taler-fallback.css') }}" id="taler-presence-stylesheet" />
-    <script src="{{ static('web-common/taler-wallet-lib.js') }}" 
type="application/javascript"></script>
     {% block head %} {% endblock %}
   </head>
   <body>
@@ -46,10 +45,8 @@
     <div class="content">
       {% block headermsg %} {% endblock %}
       {% block content %} {% endblock %}
-      <div class="copyright">
-        <hr />
-        <p>Copyright &copy; 2014&mdash;2017 INRIA</p>
-        <a href="{{ url('javascript') }}" data-jslicense="1" 
class="jslicenseinfo">JavaScript license information</a>
+      <div>
+        <p>NOTE: the bank runs completely JavaScript-free.</p>
       </div>
     </div>
   </body>
diff --git a/talerbank/app/templates/javascript.html 
b/talerbank/app/templates/javascript.html
deleted file mode 100644
index 7c1f866..0000000
--- a/talerbank/app/templates/javascript.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<!-- This file is in the public domain -->
-<table id="jslicense-labels1">
-<tr>
-  <td><a 
href="/static/web-common/taler-wallet-lib.js">taler-wallet-lib.js</a></td>
-  <td><a href="http://www.gnu.org/copyleft/lesser.html";>LGPL</a></td>
-  <td><a 
href="/static/web-common/taler-wallet-lib.js.tar.gz">taler-wallet-lib.js.tar.gz</a></td>
-  <td>
-</tr>
-</table>
diff --git a/talerbank/app/templates/login.html 
b/talerbank/app/templates/login.html
index c86d4f0..5a45aad 100644
--- a/talerbank/app/templates/login.html
+++ b/talerbank/app/templates/login.html
@@ -40,7 +40,7 @@
         {% endif %}
 
         {% if next %}
-            {% if user.is_authenticated() %}
+            {% if user.is_authenticated %}
             <p class="informational informational-fail">Your account doesn't 
have access to this page. To proceed,
             please login with an account that has access.</p>
             {% else %}
@@ -48,9 +48,9 @@
             {% endif %}
         {% endif %}
         <form method="post" class="pure-form" action="{{ url('login') }}">
-          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}">
+          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}" />
           {{ form.username }}
-          <input type="password" name="password" 
placeholder="password"></input>
+          <input type="password" name="password" placeholder="password" />
           <input type="submit" value="login" class="pure-button 
pure-button-primary" />
           <input type="hidden" name="next" value="{{ next }}" />
         </form>
diff --git a/talerbank/app/templates/pin_tan.html 
b/talerbank/app/templates/pin_tan.html
index b545524..f8f8188 100644
--- a/talerbank/app/templates/pin_tan.html
+++ b/talerbank/app/templates/pin_tan.html
@@ -31,16 +31,20 @@
   {% endif %}
   <p>
     {{ settings_value("TALER_CURRENCY") }} Bank needs to verify that you
-    intend to withdraw <b>{{ amount }} {{ settings_value("TALER_CURRENCY") 
}}</b> from
+    intend to withdraw <b>{{ amount }}</b> from
     <b>{{ exchange }}</b>.
     To prove that you are the account owner, please answer the
     following &quot;security question&quot; (*):
   </p>
+  <p>
+    What is {{ question }} ?
+  </p>
   <form method="post" action="{{ url('pin-verify') }}">
-    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
-    {{ form.pin }}
-    <input type="hidden" name="question_url" value="{{ request.get_full_path() 
}}"></input>
-    <input type="submit" value="Ok"></input>
+    <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}" />
+    <input type="text" name="pin_0" value="" autocomplete="off" />
+    <input type="hidden" name="pin_1" value="{{ hashed_answer }}" />
+    <input type="hidden" name="question_url" value="{{ request.get_full_path() 
}}" />
+    <input type="submit" value="Ok" />
   </form>
   <small style="margin: 40px 0px">(*) A real bank should ask for
     a PIN/TAN instead of a simple calculation. For example by sending
diff --git a/talerbank/app/templates/profile_page.html 
b/talerbank/app/templates/profile_page.html
index c64f215..ef53437 100644
--- a/talerbank/app/templates/profile_page.html
+++ b/talerbank/app/templates/profile_page.html
@@ -28,7 +28,6 @@
     <meta name="suggested-exchange" value="{{ suggested_exchange }}">
   {% endif %}
   <link rel="stylesheet" type="text/css" href="{{ 
static('disabled-button.css') }}">
-  <script src="{{ static('chrome-store-link.js') }}" 
type="application/javascript"></script>
 {% endblock head %}
 {% block headermsg %}
   <div>
@@ -39,25 +38,40 @@
 {% block content %}
   <section id="menu">
     <p>Account: # {{ account_no }}</p>
-    <p>Current balance: <b>{{ sign }} {{ balance }} {{ currency }}</b></p>
+    <p>Current balance: <b>{{ sign }} {{ balance }}</b></p>
   </section>
   <section id="main">
     <article>
       <div class="notification">
+        {% if wire_transfer_error %}
+          <p class="informational informational-fail">
+            {% if info_bar %}
+              {{ info_bar }}
+            {% else %}
+              Could not perform wire transfer, check all fields are correctly
+              entered.
+            {% endif %}
+          </p>
+        {% endif %}
+        {% if just_wire_transferred %}
+          <p class="informational informational-ok">
+            Wire transfer done!
+          </p>
+        {% endif %}
         {% if no_initial_bonus %}
-        <p class="informational informational-fail">
-          No initial bonus given, poor bank!
-        </p>
+          <p class="informational informational-fail">
+            No initial bonus given, poor bank!
+          </p>
         {% endif %}
         {% if just_withdrawn %}
-        <p class="informational informational-ok">
-          Withdrawal approved!
-        </p>
+          <p class="informational informational-ok">
+            Withdrawal approved!
+          </p>
         {% endif %}
         {% if just_registered %}
-        <p class="informational informational-ok">
-          Registration successful!
-        </p>
+          <p class="informational informational-ok">
+            Registration successful!
+          </p>
         {% endif %}
         </div>
     </article>
@@ -86,22 +100,35 @@
               action="{{ url('withdraw-nojs') }}"
               method="post"
               name="tform">
-          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}">
+          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}" />
           Amount to withdraw:
           <select id="reserve-amount" name="kudos_amount" autofocus>
-              <option value="{{ currency }}:5">5.00 {{ currency }}</option>
-              <option value="{{ currency }}:10">10.00 {{ currency }}</option>
-              <option value="{{ currency }}:15">15.00 {{ currency }}</option>
-              <option value="{{ currency }}:20">20.00 {{ currency }}</option>
+              <option value="{{ currency }}:5.00">5.00 {{ currency }}</option>
+              <option value="{{ currency }}:10.00">10.00 {{ currency 
}}</option>
+              <option value="{{ currency }}:15.00">15.00 {{ currency 
}}</option>
+              <option value="{{ currency }}:20.00">20.00 {{ currency 
}}</option>
           </select>
           <input id="select-exchange"
                  class="taler-installed-show pure-button pure-button-primary"
                  type="submit"
-                 value="Select exchange provider"></input>
+                 value="Select exchange provider" />
           <input class="taler-installed-hide pure-button pure-button-primary"
                  type="button"
                  disabled
-                 value="Select exchange provider"></input>
+                 value="Select exchange provider" />
+        </form>
+        <h2>Wire transfer</h2>
+        <form id="wt-form"
+              class="pure-form"
+              action="{{ url('profile') }}"
+              method="post"
+              name="tform">
+
+          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}" />
+          {{ wt_form }}
+          <input class="pure-button pure-button-primary"
+                 type="submit"
+                 value="Submit" />
         </form>
       </div>
       <p>
@@ -125,7 +152,7 @@
           <tr>
             <td style="text-align:right">{{ item.date }}</td>
            <td style="text-align:right">
-              {{ item.float_amount }} {{ item.float_currency }}
+              {{ item.sign }} {{ item.amount }}
             </td>
            <td class="text-align:left">{% if item.counterpart_username %} {{ 
item.counterpart_username }} {% endif %} (account #{{ item.counterpart }})</td>
            <td class="text-align:left">{{ item.subject }}</td>
diff --git a/talerbank/app/templates/public_accounts.html 
b/talerbank/app/templates/public_accounts.html
index 2f38489..90d5e59 100644
--- a/talerbank/app/templates/public_accounts.html
+++ b/talerbank/app/templates/public_accounts.html
@@ -54,7 +54,7 @@
           <tr>
             <td>{{entry.date}}</td>
             <td>
-              {{ entry.float_amount }} {{ entry.float_currency }}
+              {{ entry.sign }} {{ entry.amount }}
             </td>
             <td>{% if entry.counterpart_username %} {{ 
entry.counterpart_username }} {% endif %} (account #{{ entry.counterpart 
}})</td>
             <td>
diff --git a/talerbank/app/templates/register.html 
b/talerbank/app/templates/register.html
index 509c689..b7423e7 100644
--- a/talerbank/app/templates/register.html
+++ b/talerbank/app/templates/register.html
@@ -48,10 +48,10 @@
       <div class="register-form">
         <h1>Registration form</h1>
         <form class="pure-form" method="post" action="{{ url('register') }}">
-          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}">
-          <input type="text" name="username" placeholder="username" 
autofocus></input>
-          <input type="password" name="password" 
placeholder="password"></input>
-          <input type="submit" value="Ok" class="pure-button 
pure-button-primary"></input>
+          <input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token 
}}" />
+          <input type="text" name="username" placeholder="username" autofocus 
/>
+          <input type="password" name="password" placeholder="password" />
+          <input type="submit" value="Ok" class="pure-button 
pure-button-primary" />
         </form>
       </div>
     </article>
diff --git a/talerbank/app/tests.py b/talerbank/app/tests.py
index feff213..35374c6 100644
--- a/talerbank/app/tests.py
+++ b/talerbank/app/tests.py
@@ -14,44 +14,142 @@
 #
 #  @author Marcello Stanisci
 
+import json
+import timeit
+import logging
 from django.test import TestCase, Client
-from django.core.urlresolvers import reverse
+from django.urls import reverse
 from django.conf import settings
 from django.contrib.auth.models import User
+from mock import patch, MagicMock
 from .models import BankAccount, BankTransaction
 from . import urls
-from . import amounts
 from .views import wire_transfer
-import json
-
-import logging
+from .amount import Amount, CurrencyMismatch, BadFormatAmount
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger()
+LOGGER.setLevel(logging.WARNING)
 
-def clearDb():
+def clear_db():
+    # FIXME: this way we do not reset autoincrement
+    # fields.
     User.objects.all().delete()
     BankAccount.objects.all().delete()
     BankTransaction.objects.all().delete()
 
+class WithdrawTestCase(TestCase):
+    def setUp(self):
+        BankAccount(
+            user=User.objects.create_user(
+                username="test_user",
+                password="test_password"),
+            account_no=100).save()
+
+        BankAccount(
+            user=User.objects.create_user(
+                username="test_exchange",
+                password=""),
+            account_no=99).save()
+        self.client = Client()
+
+    def tearDown(self):
+        clear_db()
+
+    @patch('hashlib.new')
+    @patch('requests.post')
+    @patch('time.time')
+    def test_withdraw(self, mocked_time, mocked_post, mocked_hashlib):
+        wire_details = '''{
+            "test": {
+                "type":"test",
+                "account_number":99,  
+                "bank_uri":"http://bank.example/";,
+                "name":"example"
+            }
+        }'''
+        params = {
+            "amount_value": "0",
+            "amount_fraction": "1",
+            "amount_currency": settings.TALER_CURRENCY,
+            "exchange": "http://exchange.example/";,
+            "reserve_pub": "UVZ789",
+            "wire_details": wire_details.replace("\n", "").replace(" ", "")
+        }
+        self.client.login(username="test_user", password="test_password")
+
+        self.client.get(reverse("pin-question", urlconf=urls),
+                        params)
+        # We mock hashlib in order to fake the CAPTCHA.
+        hasher = MagicMock()
+        hasher.hexdigest = MagicMock()
+        hasher.hexdigest.return_value = "0"
+        mocked_hashlib.return_value = hasher
+        post = MagicMock()
+        post.status_code = 200
+        mocked_post.return_value = post
+        mocked_time.return_value = 0
+        self.client.post(reverse("pin-verify", urlconf=urls),
+                         {"pin_1": "0"})
+        expected_json = {
+            "reserve_pub": "UVZ789",
+            "execution_date": "/Date(0)/",
+            "sender_account_details": {
+                "type": "test",
+                "bank_uri": "http://testserver/";,
+                "account_number": 100
+                },
+            "transfer_details": {"timestamp": 0},
+            "amount": {
+                "value": 0,
+                "fraction": 1,
+                "currency": settings.TALER_CURRENCY}
+        }
+        
mocked_post.assert_called_with("http://exchange.example/admin/add/incoming";,
+                                       json=expected_json)
+
+    def tearDown(self):
+        clear_db()
+
+class InternalWireTransferTestCase(TestCase):
+
+    def setUp(self):
+        BankAccount(user=User.objects.create_user(username='give_money',
+                                                  password="gm")).save()
+        BankAccount(user=User.objects.create_user(username='take_money'),
+                    account_no=88).save()
+
+    def tearDown(self):
+        clear_db()
+
+    def test_internal_wire_transfer(self):
+        client = Client()
+        client.login(username="give_money", password="gm")
+        response = client.post(reverse("profile", urlconf=urls),
+                               {"amount": 3.0,
+                                "counterpart": 88,
+                                "subject": "charity"})
+        self.assertEqual(0, Amount.cmp(Amount(settings.TALER_CURRENCY, 3),
+                                       
BankAccount.objects.get(account_no=88).amount))
+        self.assertEqual(200, response.status_code)
+
 
 class RegisterTestCase(TestCase):
     """User registration"""
 
     def setUp(self):
-        bank = User.objects.create_user(username='Bank')
-        ba = BankAccount(user=bank, currency=settings.TALER_CURRENCY)
-        ba.account_no = 1
-        ba.save() 
+        BankAccount(
+            user=User.objects.create_user(username='Bank'),
+            account_no=1).save()
 
     def tearDown(self):
-        clearDb()
+        clear_db()
 
     def test_register(self):
-        c = Client()
-        response = c.post(reverse("register", urlconf=urls),
-                          {"username": "test_register",
-                           "password": "test_register"},
-                           follow=True)
+        client = Client()
+        response = client.post(reverse("register", urlconf=urls),
+                               {"username": "test_register",
+                                "password": "test_register"},
+                               follow=True)
         self.assertIn(("/profile", 302), response.redirect_chain)
         # this assertion tests "/profile""s view
         self.assertEqual(200, response.status_code)
@@ -60,272 +158,363 @@ class RegisterTestCase(TestCase):
 class RegisterWrongCurrencyTestCase(TestCase):
     """User registration"""
 
-    # Activating this user with a faulty currency.
     def setUp(self):
-        bank = User.objects.create_user(username='Bank')
-        ba = BankAccount(user=bank, currency="XYZ")
-        ba.account_no = 1
-        ba.save() 
+        # Note, config has KUDOS as currency.
+        BankAccount(
+            user=User.objects.create_user(username='Bank'),
+            amount=Amount('WRONGCURRENCY'),
+            account_no=1).save()
 
     def tearDown(self):
-        clearDb()
+        clear_db()
 
     def test_register(self):
-        c = Client()
-        response = c.post(reverse("register", urlconf=urls),
-                          {"username": "test_register",
-                           "password": "test_register"},
-                           follow=True)
-        # A currency mismatch is expected when the 100 KUDOS will be
-        # attempted to be given to the new user.
-        # This scenario expects a 500 Internal server error response to
-        # be returned.
+        client = Client()
+        response = client.post(reverse("register", urlconf=urls),
+                               {"username": "test_register",
+                                "password": "test_register"},
+                               follow=True)
         self.assertEqual(500, response.status_code)
 
 class LoginTestCase(TestCase):
     """User login"""
 
     def setUp(self):
-        user = User.objects.create_user(username="test_user",
-                                        password="test_password")
-        user_account = BankAccount(user=user,
-                                   currency=settings.TALER_CURRENCY)
-        user_account.save()
+        BankAccount(
+            user=User.objects.create_user(
+                username="test_user",
+                password="test_password")).save()
 
     def tearDown(self):
-        clearDb()
-    
-    def test_login(self):
-        c = Client()
-        response = c.post(reverse("login", urlconf=urls),
-                          {"username": "test_user",
-                           "password": "test_password"},
-                           follow=True)
-        self.assertIn(("/profile", 302), response.redirect_chain)
+        clear_db()
 
-        # Send wrong password
-        response = c.post(reverse("login", urlconf=urls),
-                          {"username": "test_user",
-                           "password": "test_passwordoo"},
-                           follow=True)
-        # The current logic -- django's default -- returns 200 OK
-        # even when the login didn't succeed.
-        # So the test here ensures that the bank just doesn't crash.
-        self.assertEqual(200, response.status_code)
+    def test_login(self):
+        client = Client()
+        self.assertTrue(client.login(username="test_user",
+                                     password="test_password"))
+        self.assertFalse(client.login(username="test_user",
+                                      password="test_passwordii"))
 
 class AmountTestCase(TestCase):
-    
+
     def test_cmp(self):
-        a1 = dict(value=1, fraction=0, currency="X")
-        _a1 = dict(value=1, fraction=0, currency="X")
-        a2 = dict(value=2, fraction=0, currency="X")
-        self.assertEqual(-1, amounts.amount_cmp(a1, a2))
-        self.assertEqual(1, amounts.amount_cmp(a2, a1))
-        self.assertEqual(0, amounts.amount_cmp(a1, _a1))
+        amount1 = Amount("X", 1)
+        _amount1 = Amount("X", 1)
+        amount2 = Amount("X", 2)
+
+        self.assertEqual(-1, Amount.cmp(amount1, amount2))
+        self.assertEqual(1, Amount.cmp(amount2, amount1))
+        self.assertEqual(0, Amount.cmp(amount1, _amount1))
 
     # Trying to compare amount of different currencies
     def test_cmp_diff_curr(self):
-        a1 = dict(value=1, fraction=0, currency="X")
-        a2 = dict(value=2, fraction=0, currency="Y")
-        try:
-            amounts.amount_cmp(a1, a2)
-        except amounts.CurrencyMismatchException:
-            self.assertTrue(True)
-            return
-        # Should never get here
-        self.assertTrue(False)
+        amount1 = Amount("X", 1)
+        amount2 = Amount("Y", 2)
+        with self.assertRaises(CurrencyMismatch):
+            Amount.cmp(amount1, amount2)
+
+class RejectTestCase(TestCase):
+
+    def setUp(self):
+        BankAccount(
+            user=User.objects.create_user(
+                username="rejected_user",
+                password="rejected_password")).save()
+        BankAccount(
+            user=User.objects.create_user(
+                username="rejecting_user",
+                password="rejecting_password")).save()
+
+    def tearDown(self):
+        clear_db()
+
+    def test_reject(self):
+        client = Client()
+        rejecting = User.objects.get(username="rejecting_user")
+        data = '{"auth": {"type": "basic"}, \
+                 "credit_account": %d, \
+                 "subject": "TESTWTID", \
+                 "exchange_url": "https://exchange.test";, \
+                 "amount": \
+                   {"value": 5, \
+                    "fraction": 0, \
+                    "currency": "%s"}}' \
+               % (rejecting.bankaccount.account_no,
+                  settings.TALER_CURRENCY)
+        response = client.post(
+            reverse("add-incoming", urlconf=urls),
+            data=data,
+            content_type="application/json",
+            follow=True, **{
+                "HTTP_X_TALER_BANK_USERNAME": "rejected_user",
+                "HTTP_X_TALER_BANK_PASSWORD": "rejected_password"})
+
+        data = response.content.decode("utf-8")
+        jdata = json.loads(data)
+        rejected = User.objects.get(username="rejected_user")
+        response = client.put(
+            reverse("reject", urlconf=urls),
+            data='{"row_id": %d, \
+                   "auth": {"type": "basic"}, \
+                   "account_number": %d}' \
+                  % (jdata["row_id"], rejected.bankaccount.account_no),
+            content_type="application/json",
+            **{"HTTP_X_TALER_BANK_USERNAME": "rejecting_user",
+               "HTTP_X_TALER_BANK_PASSWORD": "rejecting_password"})
+        self.assertEqual(response.status_code, 204)
 
 
 class AddIncomingTestCase(TestCase):
     """Test money transfer's API"""
 
     def setUp(self):
-        bank = User.objects.create_user(username="bank_user",
-                                        password="bank_password")
-        bank_account = BankAccount(user=bank,
-                                   currency=settings.TALER_CURRENCY)
-        user = User.objects.create_user(username="user_user",
-                                        password="user_password")
-        user_account = BankAccount(user=user,
-                                   currency=settings.TALER_CURRENCY)
-        bank_account.save()
-        user_account.save()
+        BankAccount(user=User.objects.create_user(
+            username="bank_user",
+            password="bank_password")).save()
+        BankAccount(user=User.objects.create_user(
+            username="user_user",
+            password="user_password")).save()
 
     def tearDown(self):
-        clearDb()
+        clear_db()
 
     def test_add_incoming(self):
-        c = Client()
+        client = Client()
         data = '{"auth": {"type": "basic"}, \
                  "credit_account": 1, \
-                 "wtid": "TESTWTID", \
+                 "subject": "TESTWTID", \
                  "exchange_url": "https://exchange.test";, \
                  "amount": \
                    {"value": 1, \
                     "fraction": 0, \
                     "currency": "%s"}}' \
                % settings.TALER_CURRENCY
-        response = c.post(reverse("add-incoming", urlconf=urls),
-                          data=data,
-                          content_type="application/json",
-                          follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+        response = client.post(reverse("add-incoming", urlconf=urls),
+                               data=data,
+                               content_type="application/json",
+                               follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user",
+                                               "HTTP_X_TALER_BANK_PASSWORD": 
"user_password"})
         self.assertEqual(200, response.status_code)
         data = '{"auth": {"type": "basic"}, \
                  "credit_account": 1, \
-                 "wtid": "TESTWTID", \
+                 "subject": "TESTWTID", \
                  "exchange_url": "https://exchange.test";, \
                  "amount": \
                    {"value": 1, \
                     "fraction": 0, \
                     "currency": "%s"}}' \
                % "WRONGCURRENCY"
-        response = c.post(reverse("add-incoming", urlconf=urls),
-                          data=data,
-                          content_type="application/json",
-                          follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+        response = client.post(reverse("add-incoming", urlconf=urls),
+                               data=data,
+                               content_type="application/json",
+                               follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user",
+                                               "HTTP_X_TALER_BANK_PASSWORD": 
"user_password"})
         self.assertEqual(406, response.status_code)
-
         # Try to go debit
         data = '{"auth": {"type": "basic"}, \
                  "credit_account": 1, \
-                 "wtid": "TESTWTID", \
+                 "subject": "TESTWTID", \
                  "exchange_url": "https://exchange.test";, \
                  "amount": \
                    {"value": 50, \
                     "fraction": 1, \
                     "currency": "%s"}}' \
                % settings.TALER_CURRENCY
-        response = c.post(reverse("add-incoming", urlconf=urls),
-                          data=data,
-                          content_type="application/json",
-                          follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user", "HTTP_X_TALER_BANK_PASSWORD": "user_password"})
+        response = client.post(reverse("add-incoming", urlconf=urls),
+                               data=data,
+                               content_type="application/json",
+                               follow=True, **{"HTTP_X_TALER_BANK_USERNAME": 
"user_user",
+                                               "HTTP_X_TALER_BANK_PASSWORD": 
"user_password"})
         self.assertEqual(403, response.status_code)
 
+class HistoryContext:
+    def __init__(self, expected_resp, **kwargs):
+        self.expected_resp = expected_resp
+        self.urlargs = kwargs
+        self.urlargs.update({"auth": "basic"})
+    def dump(self):
+        return self.urlargs
 
 class HistoryTestCase(TestCase):
 
     def setUp(self):
-        user = User.objects.create_user(username='User', password="Password")
-        ub = BankAccount(user=user, currency=settings.TALER_CURRENCY)
-        ub.account_no = 1
-        ub.balance_obj = dict(value=100, fraction=0, 
currency=settings.TALER_CURRENCY)
-        ub.save() 
-        user_passive = User.objects.create_user(username='UserP', 
password="PasswordP")
-        ub_p = BankAccount(user=user_passive, currency=settings.TALER_CURRENCY)
-        ub_p.account_no = 2
-        ub_p.save() 
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="a")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="b")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="c")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="d")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="e")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="f")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="g")
-        wire_transfer(dict(value=1, fraction=0, 
currency=settings.TALER_CURRENCY), ub, ub_p, subject="h")
+        debit_account = BankAccount(
+            user=User.objects.create_user(
+                username='User',
+                password="Password"),
+            amount=Amount(settings.TALER_CURRENCY, 100),
+            account_no=1)
+        debit_account.save()
+        credit_account = BankAccount(
+            user=User.objects.create_user(
+                username='User0',
+                password="Password0"),
+            account_no=2)
+        credit_account.save()
+        for subject in ("a", "b", "c", "d", "e", "f", "g", "h"):
+            wire_transfer(Amount(settings.TALER_CURRENCY, 1),
+                          debit_account,
+                          credit_account, subject)
 
     def tearDown(self):
-        clearDb()
+        clear_db()
 
     def test_history(self):
-        c = Client()
+        client = Client()
+        for ctx in (HistoryContext(expected_resp={"status": 200},
+                                   delta="4", direction="both"),
+                    HistoryContext(expected_resp={
+                        "field": "row_id", "value": 6,
+                        "status": 200}, delta="+1", start="5", 
direction="both"),
+                    HistoryContext(expected_resp={
+                        "field": "wt_subject", "value": "h",
+                        "status": 200}, delta="-1", direction="both"),
+                    HistoryContext(expected_resp={"status": 204},
+                                   delta="1", start="11", direction="both"),
+                    HistoryContext(expected_resp={"status": 204},
+                                   delta="+1", direction="cancel+"),
+                    HistoryContext(expected_resp={"status": 204},
+                                   delta="+1", direction="credit"),
+                    HistoryContext(expected_resp={"status": 200},
+                                   delta="+1", direction="debit")):
+            response = client.get(reverse("history", urlconf=urls), 
ctx.urlargs,
+                                  **{"HTTP_X_TALER_BANK_USERNAME": "User",
+                                     "HTTP_X_TALER_BANK_PASSWORD": "Password"})
+            data = response.content.decode("utf-8")
+            try:
+                data = json.loads(data)["data"][0]
+            except (json.JSONDecodeError, KeyError):
+                data = {}
+
+            self.assertEqual(data.get(ctx.expected_resp.get("field")),
+                             ctx.expected_resp.get("value"))
+            self.assertEqual(ctx.expected_resp.get("status"),
+                             response.status_code)
+
+class DBAmountSubtraction(TestCase):
+    def setUp(self):
+        BankAccount(
+            user=User.objects.create_user(username='U'),
+            amount=Amount(settings.TALER_CURRENCY, 3)).save()
 
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "+4"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        self.assertEqual(200, response.status_code)
+    def tearDown(self):
+        clear_db()
 
-        # Get a delta=+1 record in the middle of the list
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "+1", "start": "5"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        data = response.content.decode("utf-8")
-        data = json.loads(data)
-        self.assertEqual(data["data"][0]["row_id"], 6)
-        # Get latest record
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "-1"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        data = response.content.decode("utf-8")
-        data = json.loads(data)
-        self.assertEqual(data["data"][0]["wt_subject"], "h")
-        # Get non-existent record: the latest plus one in the future: 
transaction "h" takes row_id 11
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "1", "start": "11"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        response_txt = response.content.decode("utf-8")
-        self.assertEqual(204, response.status_code)
-        # Get credit records
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "+1", "direction": "credit"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        self.assertEqual(204, response.status_code)
-        # Get debit records
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "+1", "direction": "debit"},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        self.assertNotEqual(204, response.status_code)
-        # Query about non-owned account
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "+1", "account_number": 2},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        self.assertEqual(403, response.status_code)
-        # Query about non-existent account
-        response = c.get(reverse("history", urlconf=urls), {"auth": "basic", 
"delta": "-1", "account_number": 9},
-                         **{"HTTP_X_TALER_BANK_USERNAME": "User", 
"HTTP_X_TALER_BANK_PASSWORD": "Password"})
-        self.assertEqual(404, response.status_code)
+    def test_subtraction(self):
+        user_bankaccount = BankAccount.objects.get(
+            user=User.objects.get(username='U'))
+        user_bankaccount.amount.subtract(
+            Amount(settings.TALER_CURRENCY, 2))
+        self.assertEqual(
+            Amount.cmp(Amount(settings.TALER_CURRENCY, 1),
+                       user_bankaccount.amount),
+            0)
 
 
+class DBCustomColumnTestCase(TestCase):
+
+    def setUp(self):
+        BankAccount(
+            user=User.objects.create_user(username='U')).save()
+
+    def tearDown(self):
+        clear_db()
+
+    def test_exists(self):
+        user_bankaccount = BankAccount.objects.get(
+            user=User.objects.get(username='U'))
+        self.assertTrue(isinstance(user_bankaccount.amount, Amount))
+
 # This tests whether a bank account goes debit and then goes >=0  again
 class DebitTestCase(TestCase):
 
     def setUp(self):
-        u = User.objects.create_user(username='U')
-        u0 = User.objects.create_user(username='U0')
-        ua = BankAccount(user=u, currency=settings.TALER_CURRENCY)
-        u0a = BankAccount(user=u0, currency=settings.TALER_CURRENCY)
+        BankAccount(
+            user=User.objects.create_user(username='U')).save()
+        BankAccount(
+            user=User.objects.create_user(username='U0')).save()
 
-        ua.save()
-        u0a.save()
+    def tearDown(self):
+        clear_db()
 
     def test_green(self):
-        u = User.objects.get(username='U')
-        ub = BankAccount.objects.get(user=u)
-        self.assertEqual(False, ub.debit)
+        user_bankaccount = BankAccount.objects.get(
+            user=User.objects.get(username='U'))
+        self.assertEqual(False, user_bankaccount.debit)
 
     def test_red(self):
-        u = User.objects.get(username='U')
-        u0 = User.objects.get(username='U0')
-
-        ub = BankAccount.objects.get(user=u)
-        ub0 = BankAccount.objects.get(user=u0)
-
-        wire_transfer(dict(value=10, fraction=0, 
currency=settings.TALER_CURRENCY),
-                      ub0,
-                      ub,
+        user_bankaccount = BankAccount.objects.get(
+            user=User.objects.get(username='U'))
+        user_bankaccount0 = BankAccount.objects.get(
+            user=User.objects.get(username='U0'))
+
+        wire_transfer(Amount(settings.TALER_CURRENCY, 10, 0),
+                      user_bankaccount0,
+                      user_bankaccount,
                       "Go green")
-        tmp = amounts.get_zero()
-        tmp["value"] = 10
+        tmp = Amount(settings.TALER_CURRENCY, 10)
+        self.assertEqual(0, Amount.cmp(user_bankaccount.amount, tmp))
+        self.assertEqual(0, Amount.cmp(user_bankaccount0.amount, tmp))
+        self.assertFalse(user_bankaccount.debit)
 
-        self.assertEqual(0, amounts.amount_cmp(ub.balance_obj, tmp))
-        self.assertEqual(False, ub.debit)
-        self.assertEqual(True, ub0.debit)
+        self.assertTrue(user_bankaccount0.debit)
 
-        wire_transfer(dict(value=11, fraction=0, 
currency=settings.TALER_CURRENCY),
-                      ub,
-                      ub0,
+        wire_transfer(Amount(settings.TALER_CURRENCY, 11),
+                      user_bankaccount,
+                      user_bankaccount0,
                       "Go red")
 
-        self.assertEqual(True, ub.debit)
-        self.assertEqual(False, ub0.debit)
+        tmp.value = 1
+        self.assertTrue(user_bankaccount.debit)
+        self.assertFalse(user_bankaccount0.debit)
+        self.assertEqual(0, Amount.cmp(user_bankaccount.amount, tmp))
+        self.assertEqual(0, Amount.cmp(user_bankaccount0.amount, tmp))
 
-        tmp["value"] = 1
+class ParseAmountTestCase(TestCase):
+    def test_parse_amount(self):
+        ret = Amount.parse("KUDOS:4.0")
+        self.assertJSONEqual('{"value": 4, "fraction": 0, "currency": 
"KUDOS"}', ret.dump())
+        ret = Amount.parse("KUDOS:4.3")
+        self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency": 
"KUDOS"}', ret.dump())
+        try:
+            Amount.parse("Buggy")
+        except BadFormatAmount:
+            return
+        # make sure the control doesn't get here
+        self.assertEqual(True, False)
 
-        self.assertEqual(0, amounts.amount_cmp(ub0.balance_obj, tmp))
+class MeasureHistory(TestCase):
 
-class ParseAmountTestCase(TestCase):
-     def test_parse_amount(self):
-         ret = amounts.parse_amount("KUDOS:4")
-         self.assertJSONEqual('{"value": 4, "fraction": 0, "currency": 
"KUDOS"}', json.dumps(ret))
-         ret = amounts.parse_amount("KUDOS:4.00")
-         self.assertJSONEqual('{"value": 4, "fraction": 0, "currency": 
"KUDOS"}', json.dumps(ret))
-         ret = amounts.parse_amount("KUDOS:4.3")
-         self.assertJSONEqual('{"value": 4, "fraction": 30000000, "currency": 
"KUDOS"}', json.dumps(ret))
-         try:
-             amounts.parse_amount("Buggy")
-         except amounts.BadFormatAmount:
-             return
-         # make sure the control doesn't get here
-         self.assertEqual(True, False)
+    def setUp(self):
+        self.user_bankaccount0 = BankAccount(
+            user=User.objects.create_user(username='U0'),
+            amount=Amount(settings.TALER_CURRENCY, 3000))
+        self.user_bankaccount0.save()
+
+        user_bankaccount = BankAccount(
+            user=User.objects.create_user(username='U'))
+        user_bankaccount.save()
+
+        self.ntransfers = 1000
+
+        # Make sure logging level is WARNING, otherwise the loop
+        # will overwhelm the console.
+        for i in range(self.ntransfers):
+            del i # to pacify PEP checkers
+            wire_transfer(Amount(settings.TALER_CURRENCY, 1),
+                          self.user_bankaccount0,
+                          user_bankaccount,
+                          "bulk")
+    def tearDown(self):
+        clear_db()
+
+    def test_extract_history(self):
+
+        # Measure the time extract_history() needs to retrieve
+        # ~ntransfers records.
+        timer = timeit.Timer(stmt="extract_history(self.user_bankaccount0)",
+                             setup="from talerbank.app.views import 
extract_history",
+                             globals=locals())
+        total_time = timer.timeit(number=1)
+        allowed_time_per_record = 0.003
+        self.assertLess(total_time, self.ntransfers*allowed_time_per_record)
diff --git a/talerbank/app/tests_alt.py b/talerbank/app/tests_alt.py
index cf782d1..423ebb4 100644
--- a/talerbank/app/tests_alt.py
+++ b/talerbank/app/tests_alt.py
@@ -14,38 +14,25 @@
 #
 #  @author Marcello Stanisci
 
-from django.test import TestCase, Client
-from django.core.urlresolvers import reverse
+import logging
+from django.test import TestCase
 from django.conf import settings
-from django.contrib.auth.models import User
-from .models import BankAccount, BankTransaction
-from . import urls
-from . import amounts
-from .views import wire_transfer
-import json
+from .amount import Amount, BadFormatAmount
 
-import logging
+LOGGER = logging.getLogger()
+LOGGER.setLevel(logging.WARNING)
 
-logger = logging.getLogger(__name__)
+class BadMaxDebtOptionTestCase(TestCase):
+    def test_badmaxdebtoption(self):
+        with self.assertRaises(BadFormatAmount):
+            Amount.parse(settings.TALER_MAX_DEBT)
+            Amount.parse(settings.TALER_MAX_DEBT_BANK)
 
+# Note, the two following classes are used to check a faulty
+# _config_ file, so they are not supposed to have any logic.
 class BadDatabaseStringTestCase(TestCase):
     def test_baddbstring(self):
         pass
 
-class BadMaxDebtOptionTestCase(TestCase):
-    def test_badmaxdebtoption(self):
-        try:
-            amounts.parse_amount(settings.TALER_MAX_DEBT)
-        except amounts.BadFormatAmount:
-            self.assertTrue(True)
-            return
-        try:
-            amounts.parse_amount(settings.TALER_MAX_DEBT_BANK)
-        except amounts.BadFormatAmount:
-            self.assertTrue(True)
-            return
-        # Force to have at least one bad amount in config
-        self.assertTrue(False)
-
 class NoCurrencyOptionTestCase(TestCase):
     pass
diff --git a/talerbank/app/urls.py b/talerbank/app/urls.py
index 667366c..c44c7c3 100644
--- a/talerbank/app/urls.py
+++ b/talerbank/app/urls.py
@@ -23,16 +23,17 @@ urlpatterns = [
     url(r'^$', RedirectView.as_view(pattern_name="profile"), name="index"),
     url(r'^favicon\.ico$', views.ignore),
     url(r'^admin/add/incoming$', views.add_incoming, name="add-incoming"),
-    url(r'^javascript(?:.html)?/$', views.javascript_licensing, 
name="javascript"),
     url(r'^login/$', views.login_view, name="login"),
     url(r'^logout/$', views.logout_view, name="logout"),
     url(r'^accounts/register/$', views.register, name="register"),
     url(r'^profile$', views.profile_page, name="profile"),
-    url(r'^history$', views.history, name="history"),
+    url(r'^history$', views.serve_history, name="history"),
+    url(r'^reject$', views.reject, name="reject"),
     url(r'^withdraw$', views.withdraw_nojs, name="withdraw-nojs"),
-    url(r'^public-accounts$', views.public_accounts, name="public-accounts"),
-    url(r'^public-accounts/(?P<name>[a-zA-Z0-9 ]+)$', views.public_accounts, 
name="public-accounts"),
+    url(r'^public-accounts$', views.serve_public_accounts, 
name="public-accounts"),
+    url(r'^public-accounts/(?P<name>[a-zA-Z0-9 ]+)$',
+        views.serve_public_accounts,
+        name="public-accounts"),
     url(r'^pin/question$', views.pin_tan_question, name="pin-question"),
     url(r'^pin/verify$', views.pin_tan_verify, name="pin-verify"),
-    url(r'^javascript$', views.javascript_licensing)
     ]
diff --git a/talerbank/app/views.py b/talerbank/app/views.py
index 45328f8..39d35cf 100644
--- a/talerbank/app/views.py
+++ b/talerbank/app/views.py
@@ -15,7 +15,15 @@
 #  @author Marcello Stanisci
 #  @author Florian Dold
 
+from urllib.parse import urljoin
+from functools import wraps
+import json
+import logging
+import time
+import hashlib
+import random
 import re
+import requests
 import django.contrib.auth
 import django.contrib.auth.views
 import django.contrib.auth.forms
@@ -23,28 +31,29 @@ from django.db import transaction
 from django import forms
 from django.conf import settings
 from django.contrib.auth.decorators import login_required
-from django.http import JsonResponse, HttpResponse, HttpResponseBadRequest, 
HttpResponseServerError
-from django.shortcuts import render, redirect
 from django.views.decorators.csrf import csrf_exempt
 from django.views.decorators.http import require_POST, require_GET
-from simplemathcaptcha.fields import MathCaptchaField, MathCaptchaWidget
-from django.core.urlresolvers import reverse
+from django.views.decorators.http import require_http_methods
+from django.urls import reverse
 from django.contrib.auth.models import User
 from django.db.models import Q
-import json
-import logging
-import time
-import hashlib
-import requests
-from urllib.parse import urljoin
-from . import amounts
-from . import schemas
+from django.http import (JsonResponse, HttpResponse,
+                         HttpResponseBadRequest as HRBR)
+from django.shortcuts import render, redirect
+from validictory.validator import (RequiredFieldValidationError as RFVE,
+                                   FieldValidationError as FVE)
 from .models import BankAccount, BankTransaction
+from .amount import Amount, CurrencyMismatch, BadFormatAmount
+from .schemas import (validate_pin_tan_args, check_withdraw_session,
+                      validate_history_request, validate_incoming_request,
+                      validate_reject_request)
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
 class DebtLimitExceededException(Exception):
-    pass
+    def __init__(self) -> None:
+        super().__init__("Debt limit exceeded")
+
 class SameAccountException(Exception):
     pass
 
@@ -55,20 +64,17 @@ class 
MyAuthenticationForm(django.contrib.auth.forms.AuthenticationForm):
         self.fields["username"].widget.attrs["placeholder"] = "Username"
         self.fields["password"].widget.attrs["placeholder"] = "Password"
 
-
 def ignore(request):
+    del request
     return HttpResponse()
 
-def javascript_licensing(request):
-    return render(request, "javascript.html")
-
 def login_view(request):
     just_logged_out = get_session_flag(request, "just_logged_out")
     response = django.contrib.auth.views.login(
-            request,
-            authentication_form=MyAuthenticationForm,
-            template_name="login.html",
-            extra_context={"user": request.user})
+        request,
+        authentication_form=MyAuthenticationForm,
+        template_name="login.html",
+        extra_context={"user": request.user})
     # sometimes the response is a redirect and not a template response
     if hasattr(response, "context_data"):
         response.context_data["just_logged_out"] = just_logged_out
@@ -84,11 +90,43 @@ def get_session_flag(request, name):
     return False
 
 
+class WTForm(forms.Form):
+    '''Form used to wire transfer funds internally in the bank.'''
+    amount = forms.FloatField(label=settings.TALER_CURRENCY, min_value=0.1)
+    counterpart = forms.IntegerField()
+    subject = forms.CharField()
+
 # Check if user's logged in.  Check if he/she has withdrawn or
 # registered; render profile page.
 
 @login_required
 def profile_page(request):
+    info_bar = None
+    if request.method == "POST":
+        wtf = WTForm(request.POST)
+        if wtf.is_valid():
+            amount_parts = (settings.TALER_CURRENCY,
+                            wtf.cleaned_data.get("amount") + 0.0)
+            try:
+                wire_transfer(Amount.parse("%s:%s" % amount_parts),
+                              BankAccount.objects.get(
+                                  user=request.user),
+                              BankAccount.objects.get(
+                                  
account_no=wtf.cleaned_data.get("counterpart")),
+                              wtf.cleaned_data.get("subject"))
+                request.session["just_wire_transferred"] = True
+            except BankAccount.DoesNotExist:
+                request.session["wire_transfer_error"] = True
+                info_bar = "Specified account for receiver does not exist"
+            except WireTransferException as exc:
+                request.session["wire_transfer_error"] = True
+                info_bar = "Internal server error, sorry!"
+                if isinstance(exc.exc, SameAccountException):
+                    info_bar = "Operation not possible: debit and credit 
account are the same!"
+    wtf = WTForm()
+
+    just_wire_transferred = get_session_flag(request, "just_wire_transferred")
+    wire_transfer_error = get_session_flag(request, "wire_transfer_error")
     just_withdrawn = get_session_flag(request, "just_withdrawn")
     just_registered = get_session_flag(request, "just_registered")
     no_initial_bonus = get_session_flag(request, "no_initial_bonus")
@@ -98,74 +136,69 @@ def profile_page(request):
 
     context = dict(
         name=user_account.user.username,
-        balance=amounts.stringify(amounts.floatify(user_account.balance_obj)),
-        sign = "-" if user_account.debit else "",
-        currency=user_account.currency,
+        balance=user_account.amount.stringify(settings.TALER_DIGITS),
+        sign="-" if user_account.debit else "",
         precision=settings.TALER_DIGITS,
+        currency=user_account.amount.currency,
         account_no=user_account.account_no,
+        wt_form=wtf,
         history=history,
         just_withdrawn=just_withdrawn,
         just_registered=just_registered,
         no_initial_bonus=no_initial_bonus,
+        just_wire_transferred=just_wire_transferred,
+        wire_transfer_error=wire_transfer_error,
+        info_bar=info_bar
     )
     if settings.TALER_SUGGESTED_EXCHANGE:
         context["suggested_exchange"] = settings.TALER_SUGGESTED_EXCHANGE
 
     response = render(request, "profile_page.html", context)
     if just_withdrawn:
-       response["X-Taler-Operation"] = "confirm-reserve"
-       response["X-Taler-Reserve-Pub"] = reserve_pub
-       response.status_code = 202
+        response["X-Taler-Operation"] = "confirm-reserve"
+        response["X-Taler-Reserve-Pub"] = reserve_pub
+        response.status_code = 202
     return response
 
 
-class Pin(forms.Form):
-    pin = MathCaptchaField(
-        widget=MathCaptchaWidget(
-            attrs=dict(autocomplete="off", autofocus=True),
-            question_tmpl="<div lang=\"en\">What is %(num1)i %(operator)s 
%(num2)i ?</div>"))
+def hash_answer(ans):
+    hasher = hashlib.new("sha1")
+    hasher.update(settings.SECRET_KEY.encode("utf-8"))
+    hasher.update(ans.encode("utf-8"))
+    return hasher.hexdigest()
+
+def make_question():
+    num1 = random.randint(1, 10)
+    op = random.choice(("*", "+", "-"))
+    num2 = random.randint(1, 10)
+    if op == "*":
+        answer = str(num1 * num2)
+    elif op == "-":
+        # ensure result is positive
+        num1, num2 = max(num1, num2), min(num1, num2)
+        answer = str(num1 - num2)
+    else:
+        answer = str(num1 + num2)
+    question = "{} {} {}".format(num1, op, num2)
+    return question, hash_answer(answer)
 
 
 @require_GET
 @login_required
 def pin_tan_question(request):
-    for param in ("amount_value",
-                  "amount_fraction",
-                  "amount_currency",
-                  "exchange",
-                  "reserve_pub",
-                  "wire_details"):
-        if param not in request.GET:
-            return HttpResponseBadRequest("parameter {} missing".format(param))
-    try:
-        value = int(request.GET.get("amount_value", None))
-    except ValueError:
-        return HttpResponseBadRequest("invalid parameters: \"amount_value\" 
not given or NaN")
-    try:
-        fraction = int(request.GET.get("amount_fraction", None))
-    except ValueError:
-        return HttpResponseBadRequest("invalid parameters: \"amount_fraction\" 
not given or NaN")
     try:
-        currency = request.GET.get("amount_currency", None)
-    except ValueError:
-        return HttpResponseBadRequest("invalid parameters: \"amount_currency\" 
not given")
-    amount = {"value": value,
-              "fraction": fraction,
-              "currency": currency}
+        validate_pin_tan_args(request.GET.dict())
+        # Currency is not checked, as any mismatches will be
+        # detected afterwards
+    except (FVE, RFVE) as err:
+        return HRBR("invalid '%s'" % err.fieldname)
     user_account = BankAccount.objects.get(user=request.user)
-    wiredetails = json.loads(request.GET["wire_details"])
-    if not isinstance(wiredetails, dict) or "test" not in wiredetails:
-        return HttpResponseBadRequest(
-                "This bank only supports the test wire transfer method. "
-                "The exchange does not seem to support it.")
-    try:
-        schemas.validate_wiredetails(wiredetails)
-        schemas.validate_amount(amount)
-    except ValueError as error:
-        return HttpResponseBadRequest("invalid parameters (%s)" % error)
-    # parameters we store in the session are (more or less) validated
-    request.session["exchange_account_number"] = 
wiredetails["test"]["account_number"]
-    request.session["amount"] = amount
+    request.session["exchange_account_number"] = \
+        json.loads(request.GET["wire_details"])["test"]["account_number"]
+    amount = Amount(request.GET["amount_currency"],
+                    int(request.GET["amount_value"]),
+                    int(request.GET["amount_fraction"]))
+    request.session["amount"] = amount.dump()
     request.session["exchange_url"] = request.GET["exchange"]
     request.session["reserve_pub"] = request.GET["reserve_pub"]
     request.session["sender_wiredetails"] = dict(
@@ -174,85 +207,70 @@ def pin_tan_question(request):
         account_number=user_account.account_no
     )
     previous_failed = get_session_flag(request, "captcha_failed")
+    question, hashed_answer = make_question()
     context = dict(
-        form=Pin(auto_id=False),
-        amount=amounts.floatify(amount),
+        question=question,
+        hashed_answer=hashed_answer,
+        amount=amount.stringify(settings.TALER_DIGITS),
         previous_failed=previous_failed,
-        exchange=request.GET["exchange"],
-    )
+        exchange=request.GET["exchange"])
     return render(request, "pin_tan.html", context)
 
 
 @require_POST
 @login_required
 def pin_tan_verify(request):
-    try:
-        given = request.POST["pin_0"]
-        hashed_result = request.POST["pin_1"]
-        question_url = request.POST["question_url"]
-    except Exception:  # FIXME narrow the Exception type
-        return redirect("profile")
-    hasher = hashlib.new("sha1")
-    hasher.update(settings.SECRET_KEY.encode("utf-8"))
-    hasher.update(given.encode("utf-8"))
-    hashed_attempt = hasher.hexdigest()
-    if hashed_attempt != hashed_result:
+    hashed_attempt = hash_answer(request.POST.get("pin_0", ""))
+    hashed_solution = request.POST.get("pin_1", "")
+    if hashed_attempt != hashed_solution:
+        LOGGER.warning("Wrong CAPTCHA answer: %s vs %s",
+                       type(hashed_attempt),
+                       type(request.POST.get("pin_1")))
         request.session["captcha_failed"] = True
-        return redirect(question_url)
-    # We recover the info about reserve creation from the session (and
-    # not from POST parameters), since we don't what the user to
-    # change it after we've verified it.
+        return redirect(request.POST.get("question_url", "profile"))
+    # Check the session is a "pin tan" one
     try:
-        amount = request.session["amount"]
-        exchange_url = request.session["exchange_url"]
-        reserve_pub = request.session["reserve_pub"]
-        exchange_account_number = request.session["exchange_account_number"]
-        sender_wiredetails = request.session["sender_wiredetails"]
-    except KeyError:
-        # This is not a withdraw session, we redirect the user to the
-        # profile page.
+        check_withdraw_session(request.session)
+        amount = Amount(**request.session["amount"])
+        exchange_bank_account = BankAccount.objects.get(
+            account_no=request.session["exchange_account_number"])
+        wire_transfer(amount,
+                      BankAccount.objects.get(user=request.user),
+                      exchange_bank_account,
+                      request.session["reserve_pub"],
+                      request=request,
+                      session_expand=dict(debt_limit=True))
+    except (FVE, RFVE) as exc:
+        LOGGER.warning("Not a withdrawing session")
         return redirect("profile")
-    try:
-        BankAccount.objects.get(account_no=exchange_account_number)
-    except BankAccount.DoesNotExist:
-        raise HttpResponseBadRequest("The bank account #{} of exchange {} does 
not exist".format(exchange_account_no, exchange_url))
-    logging.info("asking exchange {} to create reserve 
{}".format(exchange_url, reserve_pub))
-    json_body = dict(
-            reserve_pub=reserve_pub,
-            execution_date="/Date(" + str(int(time.time())) + ")/",
-            sender_account_details=sender_wiredetails,
-             # just something unique
-            transfer_details=dict(timestamp=int(time.time() * 1000)),
-            amount=amount,
-    )
-    user_account = BankAccount.objects.get(user=request.user)
-    exchange_account = 
BankAccount.objects.get(account_no=exchange_account_number)
-    try:
-        wire_transfer(amount, user_account, exchange_account, reserve_pub)
-    except DebtLimitExceededException:
-        logger.warning("Withdrawal impossible due to debt limit exceeded")
-        request.session["debt_limit"] = True
-        return redirect("profile")
-    except amounts.BadFormatAmount as e:
-        return HttpResponse(e.msg, status=e.status_code) 
-    except amounts.CurrencyMismatchException as e:
-        return HttpResponse(e.msg, status=e.status_code) 
-    except SameAccountException:
-        logger.error("Odd situation: SameAccountException should NOT occur in 
this function")
-        return HttpResponse("internal server error", status=500)
-
-    request_url = urljoin(exchange_url, "admin/add/incoming")
-    res = requests.post(request_url, json=json_body)
+
+    except BankAccount.DoesNotExist as exc:
+        return JsonResponse({"error": "That exchange is unknown to this bank"},
+                            status=404)
+    except WireTransferException as exc:
+        return exc.response
+    res = requests.post(
+        urljoin(request.session["exchange_url"],
+                "admin/add/incoming"),
+        json={"reserve_pub": request.session["reserve_pub"],
+              "execution_date":
+                  "/Date(" + str(int(time.time())) + ")/",
+              "sender_account_details":
+                  request.session["sender_wiredetails"],
+              "transfer_details":
+                  {"timestamp": int(time.time() * 1000)},
+              "amount": amount.dump()})
     if res.status_code != 200:
-        return render(request, "error_exchange.html", dict(
-            message="Could not transfer funds to the exchange.  The exchange 
({}) gave a bad response.".format(exchange_url),
-            response_text=res.text,
-            response_status=res.status_code,
-        ))
+        return render(request,
+                      "error_exchange.html",
+                      {"message": "Could not transfer funds to the exchange. \
+                                   The exchange (%s) gave a bad response.\
+                                   " % request.session["exchange_url"],
+                       "response_text": res.text,
+                       "response_status": res.status_code})
     request.session["just_withdrawn"] = True
     return redirect("profile")
 
-
 class UserReg(forms.Form):
     username = forms.CharField()
     password = forms.CharField(widget=forms.PasswordInput())
@@ -273,23 +291,18 @@ def register(request):
         return render(request, "register.html", dict(not_available=True))
     with transaction.atomic():
         user = User.objects.create_user(username=username, password=password)
-        user_account = BankAccount(user=user, currency=settings.TALER_CURRENCY)
+        user_account = BankAccount(user=user)
         user_account.save()
     bank_internal_account = BankAccount.objects.get(account_no=1)
-    amount = dict(value=100, fraction=0, currency=settings.TALER_CURRENCY)
     try:
-        wire_transfer(amount, bank_internal_account, user_account, "Joining 
bonus")
-    except DebtLimitExceededException:
-        logger.info("Debt situation encountered")
-        request.session["no_initial_bonus"] = True
-    except amounts.CurrencyMismatchException as e:
-        return HttpResponse(e.msg, status=e.status_code)
-    except amounts.BadFormatAmount as e:
-        return HttpResponse(e.msg, status=e.status_code)
-    except SameAccountException:
-        logger.error("Odd situation: SameAccountException should NOT occur in 
this function")
-        return HttpResponse("internal server error", status=500)
-        
+        wire_transfer(Amount(settings.TALER_CURRENCY, 100, 0),
+                      bank_internal_account,
+                      user_account,
+                      "Joining bonus",
+                      request=request,
+                      session_expand=dict(no_initial_bobus=True))
+    except WireTransferException as exc:
+        return exc.response
     request.session["just_registered"] = True
     user = django.contrib.auth.authenticate(username=username, 
password=password)
     django.contrib.auth.login(request, user)
@@ -308,17 +321,17 @@ def logout_view(request):
 def extract_history(account):
     history = []
     related_transactions = BankTransaction.objects.filter(
-            Q(debit_account=account) | Q(credit_account=account))
+        Q(debit_account=account) | Q(credit_account=account))
     for item in related_transactions:
         if item.credit_account == account:
             counterpart = item.debit_account
-            sign = 1
+            sign = ""
         else:
             counterpart = item.credit_account
-            sign = -1
+            sign = "-"
         entry = dict(
-            float_amount=amounts.stringify(amounts.floatify(item.amount_obj) * 
sign),
-            float_currency=item.currency,
+            sign=sign,
+            amount=item.amount.stringify(settings.TALER_DIGITS),
             counterpart=counterpart.account_no,
             counterpart_username=counterpart.user.username,
             subject=item.subject,
@@ -328,15 +341,13 @@ def extract_history(account):
     return history
 
 
-def public_accounts(request, name=None):
+def serve_public_accounts(request, name=None):
     if not name:
         name = settings.TALER_PREDEFINED_ACCOUNTS[0]
     try:
         user = User.objects.get(username=name)
         account = BankAccount.objects.get(user=user, is_public=True)
-    except User.DoesNotExist:
-        return HttpResponse("account '{}' not found".format(name), status=404)
-    except BankAccount.DoesNotExist:
+    except (User.DoesNotExist, BankAccount.DoesNotExist):
         return HttpResponse("account '{}' not found".format(name), status=404)
     public_accounts = BankAccount.objects.filter(is_public=True)
     history = extract_history(account)
@@ -350,69 +361,64 @@ def public_accounts(request, name=None):
     )
     return render(request, "public_accounts.html", context)
 
+def login_via_headers(view_func):
+    def _decorator(request, *args, **kwargs):
+        user_account = auth_and_login(request)
+        if not user_account:
+            LOGGER.error("authentication failed")
+            return JsonResponse(dict(error="authentication failed"),
+                                status=401)
+        return view_func(request, user_account, *args, **kwargs)
+    return wraps(view_func)(_decorator)
+
 @require_GET
-def history(request):
address@hidden
+def serve_history(request, user_account):
     """
     This API is used to get a list of transactions related to one user.
     """
-    # login caller
-    user_account = auth_and_login(request)
-    if not user_account:
-        return JsonResponse(dict(error="authentication failed: bad credentials 
OR auth method"),
-                            status=401)
-    # delta
-    delta = request.GET.get("delta")
-    if not delta:
-        return HttpResponseBadRequest()
-    #FIXME: make the '+' sign optional
-    parsed_delta = re.search("([\+-])?([0-9]+)", delta)
     try:
-        parsed_delta.group(0)
-    except AttributeError:
-        return JsonResponse(dict(error="Bad 'delta' parameter"), status=400)
-    delta = int(parsed_delta.group(2))
+        # Note, this does check the currency.
+        validate_history_request(request.GET.dict())
+    except (FVE, RFVE) as exc:
+        LOGGER.error("/history, bad '%s' arg" % exc.fieldname)
+        return JsonResponse({"error": "invalid '%s'" % exc.fieldname},
+                            status=400)
+
+    # delta
+    parsed_delta = re.search(r"([\+-])?([0-9]+)",
+                             request.GET.get("delta"))
     # start
-    start = request.GET.get("start")
-    if start:
-        start = int(start)
+    start = int(request.GET.get("start", -1))
 
     sign = parsed_delta.group(1)
 
-    if ("+" == sign) or (not sign):
-        sign = ""
     # Assuming Q() means 'true'
     sign_filter = Q()
-    if "-" == sign and start:
-        sign_filter = Q(id__lt=start)
-    elif "" == sign and start:
+    if start >= 0:
         sign_filter = Q(id__gt=start)
-    # direction (debit/credit)
-    direction = request.GET.get("direction")
-
-    # target account
-    target_account = request.GET.get("account_number")
-    if not target_account:
-        target_account = user_account.bankaccount
-    else:
-        try:
-            target_account = BankAccount.objects.get(account_no=target_account)
-        except BankAccount.DoesNotExist:
-            logger.error("Attempted /history about non existent account")
-            return JsonResponse(dict(error="Queried account does not exist"), 
status=404)
-
-    if target_account != user_account.bankaccount:
-        return JsonResponse(dict(error="Querying unowned accounts not 
allowed"), status=403)
-
-    query_string = Q(debit_account=target_account) | 
Q(credit_account=target_account)
+        if sign == "-":
+            sign_filter = Q(id__lt=start)
+
+    direction_switch = {
+        "both": Q(debit_account=user_account.bankaccount) \
+                | Q(credit_account=user_account.bankaccount),
+        "credit": Q(credit_account=user_account.bankaccount),
+        "debit": Q(debit_account=user_account.bankaccount),
+        "cancel+": Q(credit_account=user_account.bankaccount) \
+                      & Q(cancelled=True),
+        "cancel-": Q(debit_account=user_account.bankaccount) \
+                      & Q(cancelled=True)
+    }
+    # Sanity checks are done at the beginning, so 'direction' key
+    # (and its value as switch's key) does exist here.
+    query_string = direction_switch[request.GET["direction"]]
     history = []
 
-    if "credit" == direction:
-        query_string = Q(credit_account=target_account)
-    if "debit" == direction:
-        query_string = Q(debit_account=target_account)
-
-    qs = BankTransaction.objects.filter(query_string, 
sign_filter).order_by("%sid" % sign)[:delta]
-    if 0 == qs.count():
+    qs = BankTransaction.objects.filter(
+        query_string, sign_filter).order_by(
+            "-id" if sign == "-" else "id")[:int(parsed_delta.group(2))]
+    if qs.count() == 0:
         return HttpResponse(status=204)
     for entry in qs:
         counterpart = entry.credit_account.account_no
@@ -421,7 +427,7 @@ def history(request):
             counterpart = entry.debit_account.account_no
             sign_ = "+"
         history.append(dict(counterpart=counterpart,
-                            amount=entry.amount_obj,
+                            amount=entry.amount.dump(),
                             sign=sign_,
                             wt_subject=entry.subject,
                             row_id=entry.id,
@@ -434,29 +440,58 @@ def auth_and_login(request):
        credentials, False if errors occur"""
 
     auth_type = None
-    if "POST" == request.method:
+    if request.method in ["POST", "PUT"]:
         data = json.loads(request.body.decode("utf-8"))
         auth_type = data["auth"]["type"]
-    if "GET" == request.method:
+    if request.method == "GET":
         auth_type = request.GET.get("auth")
-
-    if "basic" != auth_type:
-        logger.error("auth method not supported")
+    if auth_type != "basic":
+        LOGGER.error("auth method not supported")
         return False
 
     username = request.META.get("HTTP_X_TALER_BANK_USERNAME")
     password = request.META.get("HTTP_X_TALER_BANK_PASSWORD")
-    logger.info("Trying to log '%s/%s' in" % (username, password))
+    LOGGER.info("Trying to log '%s/%s' in" % (username, password))
     if not username or not password:
-        logger.error("user or password not given")
+        LOGGER.error("user or password not given")
         return False
     return django.contrib.auth.authenticate(username=username,
                                             password=password)
 
address@hidden
address@hidden(["PUT", "POST"])
address@hidden
+def reject(request, user_account):
+    data = json.loads(request.body.decode("utf-8"))
+    try:
+        validate_reject_request(data)
+    except (FVE, RFVE) as exc:
+        LOGGER.error("invalid %s" % exc.fieldname)
+        return JsonResponse({"error": "invalid '%s'" % exc.fieldname}, 
status=400)
+    try:
+        trans = BankTransaction.objects.get(id=data["row_id"])
+    except BankTransaction.DoesNotExist:
+        return JsonResponse({"error": "unknown transaction"}, status=404)
+
+    if trans.credit_account.account_no != user_account.bankaccount.account_no:
+        LOGGER.error("you can only reject a transaction where you _got_ money")
+        return JsonResponse({"error": "you can only reject a transaction where 
you _got_ money"},
+                            status=401) # Unauthorized
+    try:
+        wire_transfer(trans.amount, user_account.bankaccount,
+                      trans.debit_account, "/reject: reimbursement",
+                      cancelled=True)
+    except WireTransferException as exc:
+        # Logging the error is taken care of wire_transfer()
+        return exc.response
+
+    return HttpResponse(status=204)
+
 
 @csrf_exempt
 @require_POST
-def add_incoming(request):
address@hidden
+def add_incoming(request, user_account):
     """
     Internal API used by exchanges to notify the bank
     of incoming payments.
@@ -465,50 +500,40 @@ def add_incoming(request):
     within the browser, and only over the private admin interface.
     """
     data = json.loads(request.body.decode("utf-8"))
-    subject = "%s %s" % (data["wtid"], data["exchange_url"])
     try:
-        schemas.validate_incoming_request(data)
-    except ValueError as error:
-        logger.error("Bad data POSTed: %s" % error)
-        return JsonResponse(dict(error="invalid data POSTed: %s" % error), 
status=400)
-
-    user_account = auth_and_login(request)
+        # Note, this does check the currency.
+        validate_incoming_request(data)
+    except (FVE, RFVE) as exc:
+        return JsonResponse({"error": "invalid '%s'" % exc.fieldname},
+                            status=406 if exc.fieldname == "currency" else 400)
 
-    if not user_account:
-        logger.error("authentication failed")
-        return JsonResponse(dict(error="authentication failed"),
-                            status=401)
 
+    subject = "%s %s" % (data["subject"], data["exchange_url"])
     try:
-        credit_account = BankAccount.objects.get(user=data["credit_account"])
+        credit_account = 
BankAccount.objects.get(account_no=data["credit_account"])
+        wtrans = wire_transfer(Amount(**data["amount"]),
+                               user_account.bankaccount,
+                               credit_account,
+                               subject)
     except BankAccount.DoesNotExist:
-        return HttpResponse(status=404)
-    try:
-        transaction = wire_transfer(data["amount"],
-                                    user_account.bankaccount,
-                                    credit_account,
-                                    subject)
-        return JsonResponse(dict(serial_id=transaction.id, 
timestamp="/Date(%s)/" % int(transaction.date.timestamp())))
-    except amounts.BadFormatAmount as e:
-        return JsonResponse(dict(error=e.msg), status=e.status_code)
-    except SameAccountException:
-        return JsonResponse(dict(error="debit and credit account are the 
same"), status=422)
-    except DebtLimitExceededException:
-        logger.info("Prevenetd transfer, debit account would go beyond debt 
threshold")
-        return JsonResponse(dict(error="debit count has reached its debt 
limit", status=403 ),
-                             status=403)
-    except amounts.CurrencyMismatchException as e:
-        return JsonResponse(dict(error=e.msg), status=e.status_code)
+        return JsonResponse({"error": "credit_account (%d) not found" % 
data["credit_account"]},
+                            status=404)
+    except WireTransferException as exc:
+        return exc.response
+    return JsonResponse({"row_id": wtrans.id,
+                         "timestamp":
+                             "/Date(%s)/" % int(wtrans.date.timestamp())})
+
 
 @login_required
 @require_POST
 def withdraw_nojs(request):
 
     try:
-        amount = amounts.parse_amount(request.POST.get("kudos_amount", ""))
-    except amounts.BadFormatAmount:
-        logger.error("Amount did not pass parsing")
-        return HttpResponseBadRequest()
+        amount = Amount.parse(request.POST.get("kudos_amount", ""))
+    except BadFormatAmount:
+        LOGGER.error("Amount did not pass parsing")
+        return HRBR()
 
     user_account = BankAccount.objects.get(user=request.user)
 
@@ -516,7 +541,7 @@ def withdraw_nojs(request):
     response["X-Taler-Operation"] = "create-reserve"
     response["X-Taler-Callback-Url"] = reverse("pin-question")
     response["X-Taler-Wt-Types"] = '["test"]'
-    response["X-Taler-Amount"] = json.dumps(amount)
+    response["X-Taler-Amount"] = json.dumps(amount.dump())
     response["X-Taler-Sender-Wire"] = json.dumps(dict(
         type="test",
         bank_uri=request.build_absolute_uri(reverse("index")),
@@ -526,86 +551,90 @@ def withdraw_nojs(request):
         response["X-Taler-Suggested-Exchange"] = 
settings.TALER_SUGGESTED_EXCHANGE
     return response
 
-
-def wire_transfer(amount,
-                  debit_account,
-                  credit_account,
-                  subject):
-    if debit_account.pk == credit_account.pk:
-        logger.error("Debit and credit account are the same!")
-        raise SameAccountException()
-
-    transaction_item = BankTransaction(amount_value=amount["value"],
-                                       amount_fraction=amount["fraction"],
-                                       currency=amount["currency"],
-                                       credit_account=credit_account,
-                                       debit_account=debit_account,
-                                       subject=subject)
-
-    try:
+class WireTransferException(Exception):
+    def __init__(self, exc, response):
+        self.exc = exc
+        self.response = response
+        super().__init__()
+
+def wire_transfer(amount, debit_account, credit_account, subject, **kwargs):
+
+    def err_cb(exc, resp):
+        LOGGER.error(str(exc))
+        raise WireTransferException(exc, resp)
+
+    def wire_transfer_internal(amount, debit_account, credit_account, subject):
+        LOGGER.info("%s => %s, %s, %s" %
+                    (debit_account.account_no,
+                     credit_account.account_no,
+                     amount.stringify(2),
+                     subject))
+        if debit_account.pk == credit_account.pk:
+            LOGGER.error("Debit and credit account are the same!")
+            raise SameAccountException()
+
+        transaction_item = BankTransaction(amount=amount,
+                                           credit_account=credit_account,
+                                           debit_account=debit_account,
+                                           subject=subject,
+                                           cancelled=kwargs.get("cancelled", 
False))
         if debit_account.debit:
-            debit_account.balance_obj = 
amounts.amount_add(debit_account.balance_obj,
-                                                           amount)
-    
-        elif -1 == amounts.amount_cmp(debit_account.balance_obj, amount):
+            debit_account.amount.add(amount)
+
+        elif -1 == Amount.cmp(debit_account.amount, amount):
             debit_account.debit = True
-            debit_account.balance_obj = amounts.amount_sub(amount,
-                                                           
debit_account.balance_obj)
+            tmp = Amount(**amount.dump())
+            tmp.subtract(debit_account.amount)
+            debit_account.amount.set(**tmp.dump())
         else:
-            debit_account.balance_obj = 
amounts.amount_sub(debit_account.balance_obj,
-                                                           amount)
-
-        if False == credit_account.debit:
-            credit_account.balance_obj = 
amounts.amount_add(credit_account.balance_obj,
-                                                            amount)
-    
-        elif 1 == amounts.amount_cmp(amount, credit_account.balance_obj):
+            debit_account.amount.subtract(amount)
+
+        if not credit_account.debit:
+            credit_account.amount.add(amount)
+        elif Amount.cmp(amount, credit_account.amount) == 1:
             credit_account.debit = False
-            credit_account.balance_obj = amounts.amount_sub(amount,
-                                                            
credit_account.balance_obj)
+            tmp = Amount(**amount.dump())
+            tmp.subtract(credit_account.amount)
+            credit_account.amount.set(**tmp.dump())
         else:
-            credit_account.balance_obj = 
amounts.amount_sub(credit_account.balance_obj,
-                                                            amount)
-    except amounts.CurrencyMismatchException:
-        msg = "The amount to be transferred (%s) doesn't match the bank's 
currency (%s)" % (amount["currency"], settings.TALER_CURRENCY)
-        status_code = 406
-        if settings.TALER_CURRENCY != credit_account.balance_obj["currency"]:
-            logger.error("Internal inconsistency: credit account's currency 
(%s) differs from bank's (%s)" % (credit_account.balance_obj["currency"], 
settings.TALER_CURRENCY))
-            msg = "Internal server error"
-            status_code = 500
-        elif settings.TALER_CURRENCY != debit_account.balance_obj["currency"]:
-            logger.error("Internal inconsistency: debit account's currency 
(%s) differs from bank's (%s)" % (debit_account.balance_obj["currency"], 
settings.TALER_CURRENCY))
-            msg = "Internal server error"
-            status_code = 500
-        logger.error(msg)
-        raise amounts.CurrencyMismatchException(msg=msg, 
status_code=status_code)
-
-    # Check here if any account went beyond the allowed
-    # debit threshold.
+            credit_account.amount.subtract(amount)
 
-    try:
-        threshold = amounts.parse_amount(settings.TALER_MAX_DEBT)
+        # Check here if any account went beyond the allowed
+        # debit threshold.
 
+        threshold = Amount.parse(settings.TALER_MAX_DEBT)
         if debit_account.user.username == "Bank":
-            threshold = amounts.parse_amount(settings.TALER_MAX_DEBT_BANK)
-    except amounts.BadFormatAmount:
-        logger.error("MAX_DEBT|MAX_DEBT_BANK had the wrong format")
-        raise amounts.BadFormatAmount(msg="internal server error", 
status_code=500)
-
-    try:
-        if 1 == amounts.amount_cmp(debit_account.balance_obj, threshold) \
-           and 0 != amounts.amount_cmp(amounts.get_zero(), threshold) \
-           and debit_account.debit:
-            logger.info("Negative balance '%s' not allowed." % 
json.dumps(debit_account.balance_obj))
-            logger.info("%s's threshold is: '%s'." % 
(debit_account.user.username, json.dumps(threshold)))
+            threshold = Amount.parse(settings.TALER_MAX_DEBT_BANK)
+        if Amount.cmp(debit_account.amount, threshold) == 1 \
+            and Amount.cmp(Amount(settings.TALER_CURRENCY), threshold) != 0 \
+            and debit_account.debit:
+            LOGGER.info("Negative balance '%s' not allowed.\
+                        " % json.dumps(debit_account.amount.dump()))
+            LOGGER.info("%s's threshold is: '%s'.\
+                        " % (debit_account.user.username, 
json.dumps(threshold.dump())))
             raise DebtLimitExceededException()
-    except amounts.CurrencyMismatchException:
-        logger.error("(Internal) currency mismatch between debt threshold and 
debit account")
-        raise amounts.CurrencyMismatchException(msg="internal server error", 
status_code=500)
 
-    with transaction.atomic():
-        debit_account.save()
-        credit_account.save()
-        transaction_item.save()
+        with transaction.atomic():
+            debit_account.save()
+            credit_account.save()
+            transaction_item.save()
+
+        return transaction_item
 
-    return transaction_item
+    try:
+        return wire_transfer_internal(amount, debit_account, credit_account, 
subject)
+    except (CurrencyMismatch, BadFormatAmount) as exc:
+        err_cb(exc, JsonResponse({"error": "internal server error"},
+                                 status=500))
+    except DebtLimitExceededException as exc:
+        if kwargs.get("request"):
+            if kwargs.get("session_expand"):
+                kwargs["request"].session.update(kwargs["session_expand"])
+            if kwargs["request"].request.path == "/pin/verify":
+                err_cb(exc, redirect("profile"))
+        else:
+            err_cb(exc, JsonResponse({"error": "Unallowed debit"},
+                                     status=403))
+    except SameAccountException as exc:
+        err_cb(exc, JsonResponse({"error": "sender account == receiver 
account"},
+                                 status=422))
diff --git a/talerbank/jinja2.py b/talerbank/jinja2.py
index 9169a93..7b29976 100644
--- a/talerbank/jinja2.py
+++ b/talerbank/jinja2.py
@@ -14,48 +14,46 @@
 #
 #  @author Florian Dold
 
-from django.contrib.staticfiles.storage import staticfiles_storage
-from django.core.urlresolvers import reverse
-from django.conf import settings
-from django.core.urlresolvers import get_script_prefix
+import os
 from urllib.parse import urlparse
+from django.urls import reverse, get_script_prefix
+from django.conf import settings
 from jinja2 import Environment
-import os
 
 
-def is_absolute(url):
-    return bool(urlparse(url).netloc)
+def is_absolute(urloc):
+    return bool(urlparse(urloc).netloc)
 
 
 def join_urlparts(*parts):
-    s = ""
-    i = 0
-    while i < len(parts):
-        n = parts[i]
-        i += 1
-        if s.endswith("/"):
-            n = n.lstrip("/")
-        elif s and  not n.startswith("/"):
-            n = "/" + n
-        s += n
-    return s
+    ret = ""
+    part = 0
+    while part < len(parts):
+        buf = parts[part]
+        part += 1
+        if ret.endswith("/"):
+            buf = buf.lstrip("/")
+        elif ret and not buf.startswith("/"):
+            buf = "/" + buf
+        ret += buf
+    return ret
 
 
-def static(url):
-    if is_absolute(url):
-        return url
-    return join_urlparts(get_script_prefix(), settings.STATIC_URL, url)
+def static(urloc):
+    if is_absolute(urloc):
+        return urloc
+    return join_urlparts(get_script_prefix(), settings.STATIC_URL, urloc)
 
 
 def settings_value(name):
     return getattr(settings, name, "")
 
 
-def url(*args, **kwargs):
+def url(url_name, **kwargs):
     # strangely, Django's 'reverse' function
     # takes a named parameter 'kwargs' instead
     # of real kwargs.
-    return reverse(*args, kwargs=kwargs)
+    return reverse(url_name, kwargs=kwargs)
 
 
 def env_get(name, default=None):
@@ -71,4 +69,3 @@ def environment(**options):
         'env': env_get,
     })
     return env
-
diff --git a/talerbank/settings.py b/talerbank/settings.py
index b553a3b..6380937 100644
--- a/talerbank/settings.py
+++ b/talerbank/settings.py
@@ -8,19 +8,19 @@ For the full list of settings and their values, see
 https://docs.djangoproject.com/en/1.9/ref/settings/
 """
 
-import os
-import logging
 import base64
-from .talerconfig import TalerConfig, ConfigurationError
+import logging
+import os
+import re
 import sys
 import urllib.parse
-import re
+from .talerconfig import TalerConfig, ConfigurationError
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
-logger.info("DJANGO_SETTINGS_MODULE: %s" % 
os.environ.get("DJANGO_SETTINGS_MODULE"))
+LOGGER.info("DJANGO_SETTINGS_MODULE: %s" % 
os.environ.get("DJANGO_SETTINGS_MODULE"))
 
-tc = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
+TC = TalerConfig.from_file(os.environ.get("TALER_CONFIG_FILE"))
 
 # Build paths inside the project like this: os.path.join(BASE_DIR, ...)
 BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@@ -32,7 +32,8 @@ BASE_DIR = 
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
 SECRET_KEY = os.environ.get("TALER_BANK_SECRET_KEY", None)
 
 if not SECRET_KEY:
-    logging.info("secret key not configured in TALER_BANK_SECRET_KEY env 
variable, generating random secret")
+    logging.info("secret key not configured in TALER_BANK_SECRET_KEY " \
+                 + "env variable, generating random secret")
     SECRET_KEY = base64.b64encode(os.urandom(32)).decode('utf-8')
 
 # SECURITY WARNING: don't run with debug turned on in production!
@@ -57,13 +58,13 @@ INSTALLED_APPS = [
     'talerbank.app'
 ]
 
-MIDDLEWARE_CLASSES = [
+MIDDLEWARE = [
     'django.middleware.security.SecurityMiddleware',
     'django.contrib.sessions.middleware.SessionMiddleware',
     'django.middleware.common.CommonMiddleware',
     'django.middleware.csrf.CsrfViewMiddleware',
     'django.contrib.auth.middleware.AuthenticationMiddleware',
-    'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
+    'django.contrib.sessions.middleware.SessionMiddleware',
     'django.contrib.messages.middleware.MessageMiddleware',
     'django.middleware.clickjacking.XFrameOptionsMiddleware',
 ]
@@ -91,44 +92,42 @@ WSGI_APPLICATION = 'talerbank.wsgi.application'
 
 DATABASES = {}
 
-dbname = tc.value_string("bank", "database", required=True)
-# db given in cli argument takes precedence over config
-dbname = os.environ.get("TALER_BANK_ALTDB", dbname)
+DBNAME = TC.value_string("bank", "database", required=True)
+DBNAME = os.environ.get("TALER_BANK_ALTDB", DBNAME)
 
-if not dbname:
+if not DBNAME:
     raise Exception("DB not specified (neither in config or as cli argument)")
 
-logger.info("dbname: %s" % dbname)
+LOGGER.info("dbname: %s" % DBNAME)
 
-check_dbstring_format = re.search("[a-z]+:///[a-z]+", dbname)
-if not check_dbstring_format:
-    logger.error("Bad db string given, respect the format 'dbtype:///dbname'")
+CHECK_DBSTRING_FORMAT = re.search("[a-z]+:///[a-z]+", DBNAME)
+if not CHECK_DBSTRING_FORMAT:
+    LOGGER.error("Bad db string given, respect the format 'dbtype:///dbname'")
     sys.exit(2)
 
-dbconfig = {}
-db_url = urllib.parse.urlparse(dbname)
-
+DBCONFIG = {}
+DB_URL = urllib.parse.urlparse(DBNAME)
 
-if ((db_url.scheme not in ("postgres")) or ("" == db_url.scheme)):
-    logger.error("DB '%s' is not supported" % db_url.scheme)
+if DB_URL.scheme not in ("postgres") or DB_URL.scheme == "":
+    LOGGER.error("DB '%s' is not supported" % DB_URL.scheme)
     sys.exit(1)
-if db_url.scheme == "postgres":
-    dbconfig["ENGINE"] = 'django.db.backends.postgresql_psycopg2'
-    dbconfig["NAME"] = db_url.path.lstrip("/")
-
-if not db_url.netloc:
-    p = urllib.parse.parse_qs(db_url.query)
-    if ("host" not in p) or len(p["host"]) == 0:
-        host = None
+if DB_URL.scheme == "postgres":
+    DBCONFIG["ENGINE"] = 'django.db.backends.postgresql_psycopg2'
+    DBCONFIG["NAME"] = DB_URL.path.lstrip("/")
+
+if not DB_URL.netloc:
+    P = urllib.parse.parse_qs(DB_URL.query)
+    if ("host" not in P) or P["host"] == "":
+        HOST = None
     else:
-        host = p["host"][0]
+        HOST = P["host"][0]
 else:
-    host = db_url.netloc
+    HOST = DB_URL.netloc
 
-if host:
-    dbconfig["HOST"] = host
+if HOST:
+    DBCONFIG["HOST"] = HOST
 
-DATABASES["default"] = dbconfig
+DATABASES["default"] = DBCONFIG
 
 # Password validation
 # https://docs.djangoproject.com/en/1.9/ref/settings/#auth-password-validators
@@ -182,15 +181,15 @@ STATIC_ROOT = '/tmp/talerbankstatic/'
 ROOT_URLCONF = "talerbank.app.urls"
 
 try:
-    TALER_CURRENCY = tc.value_string("taler", "currency", required=True)
-except ConfigurationError as e:
-    logger.error(e)
+    TALER_CURRENCY = TC.value_string("taler", "currency", required=True)
+except ConfigurationError as exc:
+    LOGGER.error(exc)
     sys.exit(3)
 
-TALER_MAX_DEBT = tc.value_string("bank", "MAX_DEBT", default="%s:50" % 
TALER_CURRENCY)
-TALER_MAX_DEBT_BANK = tc.value_string("bank", "MAX_DEBT_BANK", default="%s:0" 
% TALER_CURRENCY)
+TALER_MAX_DEBT = TC.value_string("bank", "MAX_DEBT", default="%s:50.0" % 
TALER_CURRENCY)
+TALER_MAX_DEBT_BANK = TC.value_string("bank", "MAX_DEBT_BANK", 
default="%s:0.0" % TALER_CURRENCY)
 
-TALER_DIGITS = 2
+TALER_DIGITS = TC.value_int("bank", "NDIGITS", default=2)
 TALER_PREDEFINED_ACCOUNTS = ['Tor', 'GNUnet', 'Taler', 'FSF', 'Tutorial']
 TALER_EXPECTS_DONATIONS = ['Tor', 'GNUnet', 'Taler', 'FSF']
-TALER_SUGGESTED_EXCHANGE = tc.value_string("bank", "suggested_exchange")
+TALER_SUGGESTED_EXCHANGE = TC.value_string("bank", "suggested_exchange")
diff --git a/talerbank/talerconfig.py b/talerbank/talerconfig.py
index b686240..5d42adc 100644
--- a/talerbank/talerconfig.py
+++ b/talerbank/talerconfig.py
@@ -18,24 +18,23 @@
 Parse GNUnet-style configurations in pure Python
 """
 
-# FIXME: make sure that autovivification of config entries
-# does not leave garbage behind (use weakrefs!)
-
 import logging
 import collections
 import os
 import weakref
 import sys
+import re
+from typing import Callable, Any
 
-logger = logging.getLogger(__name__)
+LOGGER = logging.getLogger(__name__)
 
 __all__ = ["TalerConfig"]
 
-taler_datadir = None
+TALER_DATADIR = None
 try:
     # not clear if this is a good idea ...
-    from talerpaths import taler_datadir as t
-    taler_datadir = t
+    from talerpaths import TALER_DATADIR as t
+    TALER_DATADIR = t
 except ImportError:
     pass
 
@@ -46,7 +45,7 @@ class ExpansionSyntaxError(Exception):
     pass
 
 
-def expand(s, getter):
+def expand(var: str, getter: Callable[[str], str]) -> str:
     """
     Do shell-style parameter expansion.
     Supported syntax:
@@ -57,18 +56,18 @@ def expand(s, getter):
     pos = 0
     result = ""
     while pos != -1:
-        start = s.find("$", pos)
+        start = var.find("$", pos)
         if start == -1:
             break
-        if s[start:].startswith("${"):
+        if var[start:].startswith("${"):
             balance = 1
             end = start + 2
-            while balance > 0 and end < len(s):
-                balance += {"{": 1, "}": -1}.get(s[end], 0)
+            while balance > 0 and end < len(var):
+                balance += {"{": 1, "}": -1}.get(var[end], 0)
                 end += 1
             if balance != 0:
                 raise ExpansionSyntaxError("unbalanced parentheses")
-            piece = s[start+2:end-1]
+            piece = var[start+2:end-1]
             if piece.find(":-") > 0:
                 varname, alt = piece.split(":-", 1)
                 replace = getter(varname)
@@ -78,119 +77,123 @@ def expand(s, getter):
                 varname = piece
                 replace = getter(varname)
                 if replace is None:
-                    replace = s[start:end]
+                    replace = var[start:end]
         else:
             end = start + 2
-            while end < len(s) and s[start+1:end+1].isalnum():
+            while end < len(var) and var[start+1:end+1].isalnum():
                 end += 1
-            varname = s[start+1:end]
+            varname = var[start+1:end]
             replace = getter(varname)
             if replace is None:
-                replace = s[start:end]
+                replace = var[start:end]
         result = result + replace
         pos = end
 
 
-    return result + s[pos:]
-
-
-class OptionDict(collections.defaultdict):
-    def __init__(self, config, section_name):
-        self.config = weakref.ref(config)
-        self.section_name = section_name
-        super().__init__()
-    def __missing__(self, key):
-        e = Entry(self.config(), self.section_name, key)
-        self[key] = e
-        return e
-    def __getitem__(self, slice):
-        return super().__getitem__(slice.lower())
-    def __setitem__(self, slice, value):
-        super().__setitem__(slice.lower(), value)
-
-
-class SectionDict(collections.defaultdict):
-    def __init__(self):
-        super().__init__()
-    def __missing__(self, key):
-        v = OptionDict(self, key)
-        self[key] = v
-        return v
-    def __getitem__(self, slice):
-        return super().__getitem__(slice.lower())
-    def __setitem__(self, slice, value):
-        super().__setitem__(slice.lower(), value)
+    return result + var[pos:]
 
 
 class Entry:
-    def __init__(self, config, section, option, value=None, filename=None, 
lineno=None):
-        self.value = value
-        self.filename = filename
-        self.lineno = lineno
+    def __init__(self, config, section: str, option: str, **kwargs) -> None:
+        self.value = kwargs.get("value")
+        self.filename = kwargs.get("filename")
+        self.lineno = kwargs.get("lineno")
         self.section = section
         self.option = option
         self.config = weakref.ref(config)
 
-    def __repr__(self):
-        return "<Entry section=%s, option=%s, value=%s>" % (self.section, 
self.option, repr(self.value),)
+    def __repr__(self) -> str:
+        return "<Entry section=%s, option=%s, value=%s>" \
+               % (self.section, self.option, repr(self.value),)
 
-    def __str__(self):
+    def __str__(self) -> Any:
         return self.value
 
-    def value_string(self, default=None, required=False, warn=False):
+    def value_string(self, default=None, required=False, warn=False) -> str:
         if required and self.value is None:
-            raise ConfigurationError("Missing required option '%s' in section 
'%s'" % (self.option.upper(), self.section.upper()))
+            raise ConfigurationError("Missing required option '%s' in section 
'%s'" \
+                                     % (self.option.upper(), 
self.section.upper()))
         if self.value is None:
             if warn:
                 if default is not None:
-                    logger.warn("Configuration is missing option '%s' in 
section '%s', falling back to '%s'",
-                            self.option, self.section, default)
+                    LOGGER.warning("Configuration is missing option '%s' in 
section '%s',\
+                                   falling back to '%s'", self.option, 
self.section, default)
                 else:
-                    logger.warn("Configuration ** is missing option '%s' in 
section '%s'", self.option.upper(), self.section.upper())
+                    LOGGER.warning("Configuration ** is missing option '%s' in 
section '%s'",
+                                   self.option.upper(), self.section.upper())
             return default
         return self.value
 
-    def value_int(self, default=None, required=False, warn=False):
-        v = self.value_string(default, warn, required)
-        if v is None:
+    def value_int(self, default=None, required=False, warn=False) -> int:
+        value = self.value_string(default, warn, required)
+        if value is None:
             return None
         try:
-            return int(v)
+            return int(value)
         except ValueError:
-            raise ConfigurationError("Expected number for option '%s' in 
section '%s'" % (self.option.upper(), self.section.upper()))
-
-    def _getsubst(self, key):
-        x = self.config()["paths"][key].value
-        if x is not None:
-            return x
-        x = os.environ.get(key)
-        if x is not None:
-            return x
+            raise ConfigurationError("Expected number for option '%s' in 
section '%s'" \
+                                     % (self.option.upper(), 
self.section.upper()))
+
+    def _getsubst(self, key: str) -> Any:
+        value = self.config()["paths"][key].value
+        if value is not None:
+            return value
+        value = os.environ.get(key)
+        if value is not None:
+            return value
         return None
 
-    def value_filename(self, default=None, required=False, warn=False):
-        v = self.value_string(default, warn, required)
-        if v is None:
+    def value_filename(self, default=None, required=False, warn=False) -> str:
+        value = self.value_string(default, required, warn)
+        if value is None:
             return None
-        return expand(v, lambda x: self._getsubst(x))
+        return expand(value, self._getsubst)
 
-    def location(self):
+    def location(self) -> str:
         if self.filename is None or self.lineno is None:
             return "<unknown>"
         return "%s:%s" % (self.filename, self.lineno)
 
 
+class OptionDict(collections.defaultdict):
+    def __init__(self, config, section_name: str) -> None:
+        self.config = weakref.ref(config)
+        self.section_name = section_name
+        super().__init__()
+    def __missing__(self, key: str) -> Entry:
+        entry = Entry(self.config(), self.section_name, key)
+        self[key] = entry
+        return entry
+    def __getitem__(self, chunk: str) -> Entry:
+        return super().__getitem__(chunk.lower())
+    def __setitem__(self, chunk: str, value: Entry) -> None:
+        super().__setitem__(chunk.lower(), value)
+
+
+class SectionDict(collections.defaultdict):
+    def __missing__(self, key):
+        value = OptionDict(self, key)
+        self[key] = value
+        return value
+    def __getitem__(self, chunk: str) -> OptionDict:
+        return super().__getitem__(chunk.lower())
+    def __setitem__(self, chunk: str, value: OptionDict) -> None:
+        super().__setitem__(chunk.lower(), value)
+
+
 class TalerConfig:
     """
     One loaded taler configuration, including base configuration
     files and included files.
     """
-    def __init__(self):
+    def __init__(self) -> None:
         """
         Initialize an empty configuration
         """
-        self.sections = SectionDict()
+        self.sections = SectionDict() # just plain dict
 
+    # defaults != config file: the first is the 'base'
+    # whereas the second overrides things from the first.
     @staticmethod
     def from_file(filename=None, load_defaults=True):
         cfg = TalerConfig()
@@ -205,28 +208,34 @@ class TalerConfig:
         cfg.load_file(filename)
         return cfg
 
-    def value_string(self, section, option, default=None, required=None, 
warn=False):
-        return self.sections[section][option].value_string(default, required, 
warn)
+    def value_string(self, section, option, **kwargs) -> str:
+        return self.sections[section][option].value_string(
+            kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
 
-    def value_filename(self, section, option, default=None, required=None, 
warn=False):
-        return self.sections[section][option].value_filename(default, 
required, warn)
+    def value_filename(self, section, option, **kwargs) -> str:
+        return self.sections[section][option].value_filename(
+            kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
 
-    def value_int(self, section, option, default=None, required=None, 
warn=False):
-        return self.sections[section][option].value_int(default, required, 
warn)
+    def value_int(self, section, option, **kwargs) -> int:
+        return self.sections[section][option].value_int(
+            kwargs.get("default"), kwargs.get("required"), kwargs.get("warn"))
 
-    def load_defaults(self):
+    def load_defaults(self) -> None:
         base_dir = os.environ.get("TALER_BASE_CONFIG")
         if base_dir:
             self.load_dir(base_dir)
             return
         prefix = os.environ.get("TALER_PREFIX")
         if prefix:
+            tmp = os.path.split(os.path.normpath(prefix))
+            if re.match("lib", tmp[1]):
+                prefix = tmp[0]
             self.load_dir(os.path.join(prefix, "share/taler/config.d"))
             return
-        if taler_datadir:
-            self.load_dir(os.path.join(taler_datadir, "share/taler/config.d"))
+        if TALER_DATADIR:
+            self.load_dir(os.path.join(TALER_DATADIR, "share/taler/config.d"))
             return
-        logger.warn("no base directory found")
+        LOGGER.warning("no base directory found")
 
     @staticmethod
     def from_env(*args, **kwargs):
@@ -237,18 +246,18 @@ class TalerConfig:
         filename = os.environ.get("TALER_CONFIG_FILE")
         return TalerConfig.from_file(filename, *args, **kwargs)
 
-    def load_dir(self, dirname):
+    def load_dir(self, dirname) -> None:
         try:
             files = os.listdir(dirname)
         except FileNotFoundError:
-            logger.warn("can't read config directory '%s'", dirname)
+            LOGGER.warning("can't read config directory '%s'", dirname)
             return
         for file in files:
             if not file.endswith(".conf"):
                 continue
             self.load_file(os.path.join(dirname, file))
 
-    def load_file(self, filename):
+    def load_file(self, filename) -> None:
         sections = self.sections
         try:
             with open(filename, "r") as file:
@@ -257,7 +266,7 @@ class TalerConfig:
                 for line in file:
                     lineno += 1
                     line = line.strip()
-                    if len(line) == 0:
+                    if line == "":
                         # empty line
                         continue
                     if line.startswith("#"):
@@ -265,62 +274,70 @@ class TalerConfig:
                         continue
                     if line.startswith("["):
                         if not line.endswith("]"):
-                            logger.error("invalid section header in line %s: 
%s", lineno, repr(line))
+                            LOGGER.error("invalid section header in line %s: 
%s",
+                                         lineno, repr(line))
                         section_name = line.strip("[]").strip().strip('"')
                         current_section = section_name
                         continue
                     if current_section is None:
-                        logger.error("option outside of section in line %s: 
%s", lineno, repr(line))
+                        LOGGER.error("option outside of section in line %s: 
%s", lineno, repr(line))
                         continue
-                    kv = line.split("=", 1)
-                    if len(kv) != 2:
-                        logger.error("invalid option in line %s: %s", lineno, 
repr(line))
-                    key = kv[0].strip()
-                    value = kv[1].strip()
+                    pair = line.split("=", 1)
+                    if len(pair) != 2:
+                        LOGGER.error("invalid option in line %s: %s", lineno, 
repr(line))
+                    key = pair[0].strip()
+                    value = pair[1].strip()
                     if value.startswith('"'):
                         value = value[1:]
                         if not value.endswith('"'):
-                            logger.error("mismatched quotes in line %s: %s", 
lineno, repr(line))
+                            LOGGER.error("mismatched quotes in line %s: %s", 
lineno, repr(line))
                         else:
                             value = value[:-1]
-                    e = Entry(self.sections, current_section, key, value, 
filename, lineno)
-                    sections[current_section][key] = e
+                    entry = Entry(self.sections, current_section, key,
+                                  value=value, filename=filename, 
lineno=lineno)
+                    sections[current_section][key] = entry
         except FileNotFoundError:
-            logger.error("Configuration file (%s) not found", filename)
+            LOGGER.error("Configuration file (%s) not found", filename)
             sys.exit(3)
 
 
-    def dump(self):
-        for section_name, section in self.sections.items():
-            print("[%s]" % (section.section_name,))
-            for option_name, e in section.items():
-                print("%s = %s # %s" % (e.option, e.value, e.location()))
+    def dump(self) -> None:
+        for kv_section in self.sections.items():
+            print("[%s]" % (kv_section[1].section_name,))
+            for kv_option in kv_section[1].items():
+                print("%s = %s # %s" % \
+                      (kv_option[1].option,
+                       kv_option[1].value,
+                       kv_option[1].location()))
 
-    def __getitem__(self, slice):
-        if isinstance(slice, str):
-            return self.sections[slice]
+    def __getitem__(self, chunk: str) -> OptionDict:
+        if isinstance(chunk, str):
+            return self.sections[chunk]
         raise TypeError("index must be string")
 
 
 if __name__ == "__main__":
-    import sys
     import argparse
 
-    parser = argparse.ArgumentParser()
-    parser.add_argument("--section", "-s", dest="section", default=None, 
metavar="SECTION")
-    parser.add_argument("--option", "-o", dest="option", default=None, 
metavar="OPTION")
-    parser.add_argument("--config", "-c", dest="config", default=None, 
metavar="FILE")
-    parser.add_argument("--filename", "-f", dest="expand_filename", 
default=False, action='store_true')
-    args = parser.parse_args()
-
-    tc = TalerConfig.from_file(args.config)
-
-    if args.section is not None and args.option is not None:
-        if args.expand_filename:
-            x = tc.value_filename(args.section, args.option)
+    PARSER = argparse.ArgumentParser()
+    PARSER.add_argument("--section", "-s", dest="section",
+                        default=None, metavar="SECTION")
+    PARSER.add_argument("--option", "-o", dest="option",
+                        default=None, metavar="OPTION")
+    PARSER.add_argument("--config", "-c", dest="config",
+                        default=None, metavar="FILE")
+    PARSER.add_argument("--filename", "-f", dest="expand_filename",
+                        default=False, action='store_true')
+    ARGS = PARSER.parse_args()
+
+    TC = TalerConfig.from_file(ARGS.config)
+
+    if ARGS.section is not None and ARGS.option is not None:
+        if ARGS.expand_filename:
+            X = TC.value_filename(ARGS.section, ARGS.option)
         else:
-            x = tc.value_string(args.section, args.option)
-        if x is not None:
-            print(x)
+            X = TC.value_string(ARGS.section, ARGS.option)
+        if X is not None:
+            print(X)
     else:
-        tc.dump()
+        TC.dump()

-- 
To stop receiving notification emails like this one, please contact
address@hidden



reply via email to

[Prev in Thread] Current Thread [Next in Thread]