Support exchange rates.
authorWerner Koch <wk@gnupg.org>
Tue, 9 Sep 2014 18:52:24 +0000 (20:52 +0200)
committerWerner Koch <wk@gnupg.org>
Tue, 9 Sep 2014 18:59:52 +0000 (20:59 +0200)
* src/connection.c (currency_table, valid_currency_p): Move to ...
* src/currency.c, src/currency.h: new.
* src/journal.c (jrnl_store_sys_record): Print some more colons.
(jrnl_store_exchange_rate_record): New.
(jrnl_store_charge_record): Print Euro field.
* src/connection.c (cmd_checkamount): Return converted currency.
(cmd_getinfo): Use currency interface.
* src/payprocd.c (housekeeping_thread): Read exchange rates every
hour.
* src/util.h (AMOUNTBUF_SIZE): New.

* src/payproc-jrnl.c (jrnl_field_names): Add "euro".
(one_line): Increase size of field array.

* src/geteuroxref: New.

12 files changed:
README
doc/api-ref.org
src/Makefile.am
src/connection.c
src/currency.c [new file with mode: 0644]
src/currency.h [new file with mode: 0644]
src/geteuroxref [new file with mode: 0755]
src/journal.c
src/journal.h
src/payproc-jrnl.c
src/payprocd.c
src/util.h

diff --git a/README b/README
index 0e9e1f0..ac85549 100644 (file)
--- a/README
+++ b/README
@@ -12,5 +12,7 @@ root certificate in /etc/payproc/tls-ca.pem .  The given journal file
 name will be suffixed with the current date and ".log".  The time
 stamp in the journal is given in UTC.
 
+You should also add a cron job for the payproc user to run
+/usr/lib/payproc/geteuroxref each day at about 16:00 CET.
 
 See doc/api-ref.org for a description of supported commands.
index 46a005a..f4f6d5b 100644 (file)
@@ -22,7 +22,7 @@ Example:
 CARDTOKEN
 Number: 4242424242424242
 Exp-month: 8
-exp-year: 2014
+exp-year: 2016
 Cvc: 666
 Name: Juscelino Kubitschek
 
@@ -102,9 +102,15 @@ OK
 _amount: 1730
 Currency: Eur
 Amount: 17.3
+Euro: 17.30
 
 #+end_example
 
+The returned data set also includes a field "Euro" with the input
+amount converted to an Euro value.  This conversion is done using the
+reference data retrieved via cron job.  It may be different from the
+conversion done by the payment service provider.
+
 
 ** SESSION
 
index 146fe17..773d0d7 100644 (file)
@@ -21,6 +21,7 @@ EXTRA_DIST = cJSON.readme tls-ca.pem
 bin_PROGRAMS = payprocd payproc-jrnl ppipnhd
 noinst_PROGRAMS = $(module_tests) t-http
 noinst_LIBRARIES = libcommon.a libcommonpth.a
+dist_pkglibexec_SCRIPTS = geteuroxref
 
 TESTS = $(module_tests)
 
@@ -47,6 +48,7 @@ utility_sources = \
 payprocd_SOURCES = \
        payprocd.c payprocd.h \
        connection.c connection.h \
+       currency.c currency.h \
        stripe.c stripe.h \
        paypal-ipn.c paypal.h \
        tlssupport.c tlssupport.h \
@@ -83,6 +85,6 @@ t_http_CFLAGS  = $(t_common_cflags)
 t_http_LDADD   = $(t_common_ldadd)
 
 t_connection_SOURCES = t-connection.c stripe.c paypal-ipn.c \
-                       journal.c session.c $(t_common_sources)
+                       journal.c session.c currency.c $(t_common_sources)
 t_connection_CFLAGS  = $(t_common_cflags) $(LIBGCRYPT_CFLAGS)
 t_connection_LDADD   = $(t_common_ldadd) $(LIBGCRYPT_LIBS)
index 6de21f3..1996e4c 100644 (file)
@@ -32,6 +32,7 @@
 #include "paypal.h"
 #include "journal.h"
 #include "session.h"
+#include "currency.h"
 #include "connection.h"
 
 /* Maximum length of an input line.  */
@@ -59,21 +60,6 @@ struct conn_s
 };
 
 
-/* The list of supported currencies  */
-static struct
-{
-  const char *name;
-  unsigned char decdigits;
-  const char *desc;
-} currency_table[] = {
-  { "USD", 2, "US Dollar" },
-  { "EUR", 2, "Euro" },
-  { "GBP", 2, "British Pound" },
-  { "JPY", 0, "Yen" },
-  { NULL }
-};
-
-
 \f
 /* Allocate a new conenction object and return it.  Returns
    NULL on error and sets ERRNO.  */
@@ -366,24 +352,6 @@ write_data_line (keyvalue_t kv, estream_t fp)
  * Helper functions.
  */
 
-/* Check that the currency described by STRING is valid.  Returns true
-   if so.  The number of of digits after the decimal point for that
-   currency is stored at R_DECDIGITS.  */
-static int
-valid_currency_p (const char *string, int *r_decdigits)
-{
-  int i;
-
-  for (i=0; currency_table[i].name; i++)
-    if (!strcasecmp (string, currency_table[i].name))
-      {
-        *r_decdigits = currency_table[i].decdigits;
-        return 1;
-      }
-  return 0;
-}
-
-
 /* Check the amount given in STRING and convert it to the smallest
    currency unit.  DECDIGITS gives the number of allowed post decimal
    positions.  Return 0 on error or the converted amount.  */
@@ -397,7 +365,7 @@ convert_amount (const char *string, int decdigits)
   unsigned int v;
 
   if (*string == '+')
-    string++; /* Skip an optioanl leading plsu sign.  */
+    string++; /* Skip an optioanl leading plus sign.  */
   for (s = string; *s; s++)
     {
       if (*s == '.')
@@ -432,7 +400,7 @@ convert_amount (const char *string, int decdigits)
 }
 
 
-/* Retrun a string with the amount computed from CENTS.  DECDIGITS
+/* Return a string with the amount computed from CENTS.  DECDIGITS
    gives the number of post decimal positions in CENTS.  Return NULL
    on error.  */
 static char *
@@ -774,6 +742,7 @@ cmd_chargecard (conn_t conn, char *args)
    _amount:    The amount converted to an integer (i.e. 10.42 EUR -> 1042)
    Amount:     The amount as above.
    Limit:      If given, the maximum amount acceptable
+   Euro:       If returned, Amount converted to Euro.
 
  */
 static gpg_error_t
@@ -782,9 +751,11 @@ cmd_checkamount (conn_t conn, char *args)
   gpg_error_t err;
   keyvalue_t dict = conn->dataitems;
   keyvalue_t kv;
+  const char *curr;
   const char *s;
   unsigned int cents;
   int decdigs;
+  char amountbuf[AMOUNTBUF_SIZE];
 
   (void)args;
 
@@ -792,8 +763,8 @@ cmd_checkamount (conn_t conn, char *args)
   keyvalue_del (conn->dataitems, "Limit");
 
   /* Get currency and amount.  */
-  s = keyvalue_get_string (dict, "Currency");
-  if (!valid_currency_p (s, &decdigs))
+  curr = keyvalue_get_string (dict, "Currency");
+  if (!valid_currency_p (curr, &decdigs))
     {
       set_error (MISSING_VALUE, "Currency missing or not supported");
       goto leave;
@@ -805,6 +776,12 @@ cmd_checkamount (conn_t conn, char *args)
       set_error (MISSING_VALUE, "Amount missing or invalid");
       goto leave;
     }
+
+  if (*convert_currency (amountbuf, sizeof amountbuf, curr, s))
+    err = keyvalue_put (&conn->dataitems, "Euro", amountbuf);
+  else
+    err = 0;
+
   err = keyvalue_putf (&conn->dataitems, "_amount", "%u", cents);
   dict = conn->dataitems;
   if (err)
@@ -841,10 +818,13 @@ cmd_getinfo (conn_t conn, char *args)
 
   if (has_leading_keyword (args, "list-currencies"))
     {
+      const char *name, *desc;
+      double rate;
+
       es_fputs ("OK\n", conn->stream);
-      for (i=0; currency_table[i].name; i++)
-        es_fprintf (conn->stream, "# %s - %s\n",
-                    currency_table[i].name, currency_table[i].desc);
+      for (i=0; (name = get_currency_info (i, &desc, &rate)); i++)
+        es_fprintf (conn->stream, "# %s %11.4f - %s\n",
+                    name, rate, desc);
     }
   else if (has_leading_keyword (args, "version"))
     {
diff --git a/src/currency.c b/src/currency.c
new file mode 100644 (file)
index 0000000..0d04dd1
--- /dev/null
@@ -0,0 +1,258 @@
+/* currency.c - Currency management functions
+ * Copyright (C) 2014 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include <config.h>
+
+#include <stdlib.h>
+#include <string.h>
+#include <stdio.h>
+#include <errno.h>
+
+#include "payprocd.h"
+#include "util.h"
+#include "estream.h"
+#include "estream-printf.h"
+#include "logging.h"
+#include "journal.h"
+#include "currency.h"
+
+/* The file with the exchange rates.  This is expected to be created
+   by a cron job and the geteuroxref script.  */
+static const char euroxref_fname[] = "/var/lib/payproc/euroxref.dat";
+
+
+/* The list of supported currencies  */
+static struct
+{
+  const char *name;
+  unsigned char decdigits;
+  const char *desc;
+  double rate;     /* Exchange rate to Euro.  */
+} currency_table[] = {
+  { "EUR", 2, "Euro", 1.0 },  /* Must be the first entry! */
+  { "USD", 2, "US Dollar" },
+  { "GBP", 2, "British Pound" },
+  { "JPY", 0, "Yen" }
+};
+
+
+void
+read_exchange_rates (void)
+{
+  gpg_error_t err = 0;
+  estream_t fp;
+  int lnr = 0;
+  int n, c, idx;
+  char line[256];
+  char *p, *pend;
+  double rate;
+
+  fp = es_fopen (euroxref_fname, "r");
+  if (!fp)
+    {
+      err = gpg_error_from_syserror ();
+      log_error ("error opening '%s': %s\n",
+                 euroxref_fname, gpg_strerror (err));
+      return;
+    }
+
+  while (es_fgets (line, DIM(line)-1, fp))
+    {
+      lnr++;
+
+      n = strlen (line);
+      if (!n || line[n-1] != '\n')
+        {
+          /* Eat until end of line. */
+          while ((c=es_getc (fp)) != EOF && c != '\n')
+            ;
+          err = gpg_error (*line? GPG_ERR_LINE_TOO_LONG
+                           : GPG_ERR_INCOMPLETE_LINE);
+          log_error ("error reading '%s', line %d: %s\n",
+                     euroxref_fname, lnr, gpg_strerror (err));
+          continue;
+        }
+      line[--n] = 0; /* Chop the LF. */
+      if (n && line[n-1] == '\r')
+        line[--n] = 0; /* Chop an optional CR. */
+
+      /* Allow leading spaces and skip empty and comment lines. */
+      for (p=line; spacep (p); p++)
+        ;
+      if (!*p || *p == '#')
+        continue;
+
+      /* Parse the currency name. */
+      pend = strchr (p, '=');
+      if (!pend)
+        {
+          log_error ("error parsing '%s', line %d: %s\n",
+                     euroxref_fname, lnr, "missing '='");
+          continue;
+        }
+      *pend++ = 0;
+      trim_spaces (p);
+      if (!*p)
+        {
+          log_error ("error parsing '%s', line %d: %s\n",
+                     euroxref_fname, lnr, "currency name missing");
+          continue;
+        }
+
+      /* Note that we start at 1 to skip the first entry which is
+         EUR.  */
+      for (idx=1; idx < DIM(currency_table); idx++)
+        if (!strcasecmp (currency_table[idx].name, p))
+          break;
+      if (!(idx < DIM(currency_table)))
+        continue; /* Currency not supported.  */
+
+      /* Parse the rate. */
+      p = pend;
+      errno = 0;
+      rate = strtod (p, &pend);
+      if ((!rate && p == pend) || errno || rate <= 0.0 || rate > 10000.0)
+        {
+          log_error ("error parsing '%s', line %d: %s\n",
+                     euroxref_fname, lnr, "invalid exchange rate");
+          continue;
+        }
+      p = pend;
+      trim_spaces (p);
+      if (*p)
+        {
+          log_error ("error parsing '%s', line %d: %s\n",
+                     euroxref_fname, lnr, "garbage after exchange rate");
+          continue;
+        }
+
+      /* Update the tbale.  */
+      if (currency_table[idx].rate != rate)
+        {
+          if (!currency_table[idx].rate)
+            log_info ("setting exchange rate for %s to %.4f\n",
+                      currency_table[idx].name, rate);
+          else
+            log_info ("changing exchange rate for %s from %.4f to %.4f\n",
+                      currency_table[idx].name, currency_table[idx].rate, rate);
+
+          currency_table[idx].rate = rate;
+          jrnl_store_exchange_rate_record (currency_table[idx].name, rate);
+        }
+    }
+
+  es_fclose (fp);
+}
+
+
+/* Return the exchange rate for CURRENCY or 0.0 is not known.  */
+static double
+get_exchange_rate (const char *currency)
+{
+  int i;
+
+  for (i=0; i < DIM(currency_table); i++)
+    if (!strcasecmp (currency, currency_table[i].name))
+      return currency_table[i].rate;
+  return 0.0;
+}
+
+
+/* Check that the currency described by STRING is valid.  Returns true
+   if so.  The number of of digits after the decimal point for that
+   currency is stored at R_DECDIGITS.  */
+int
+valid_currency_p (const char *string, int *r_decdigits)
+{
+  int i;
+
+  for (i=0; i < DIM(currency_table); i++)
+    if (!strcasecmp (string, currency_table[i].name))
+      {
+        *r_decdigits = currency_table[i].decdigits;
+        return 1;
+      }
+  return 0;
+}
+
+
+/* Return information for currencies.  SEQ needs to be iterated from 0
+   upwards until the function returns NULL.  If not NULL a description
+   of the currency is stored at R_DESC.  if not NULL, the latest known
+   exchange rate is stored at R_RATE.  */
+const char *
+get_currency_info (int seq, char const **r_desc, double *r_rate)
+{
+  if (seq < 0 || seq >= DIM (currency_table))
+    return NULL;
+  if (r_desc)
+    *r_desc = currency_table[seq].desc;
+  if (r_rate)
+    *r_rate = currency_table[seq].rate;
+  return currency_table[seq].name;
+}
+
+
+/* Convert (AMOUNT, CURRENCY) to an Euro amount and store it in BUFFER
+   up to a length of BUFSIZE-1.  Returns BUFFER.  If a conversion is
+   not possible an empty string is returned. */
+char *
+convert_currency (char *buffer, size_t bufsize,
+                  const char *currency, const char *amount)
+{
+  double value, rate;
+  char *pend;
+
+  if (!bufsize)
+    log_bug ("buffer too short in convert_currency\n");
+
+  *buffer = 0;
+  errno = 0;
+  value = strtod (amount, &pend);
+  if ((!value && amount == pend) || errno)
+    {
+      log_error ("error converting %s %s to Euro: %s\n",
+                 amount, currency, strerror (errno));
+      return buffer;
+    }
+
+  rate = get_exchange_rate (currency);
+  if (!rate)
+    {
+      if (opt.verbose)
+        log_info ("error converting %s %s to Euro: %s\n",
+                  amount, currency, "no exchange rate available");
+      return buffer;
+    }
+  if (rate != 1.0)
+    {
+      value /= rate;
+      value += 0.005; /* So that snprintf round tyhe value. */
+    }
+
+  if (estream_snprintf (buffer, bufsize, "%.2f", value) < 0)
+    {
+      log_error ("error converting %s %s to Euro: %s\n",
+                 amount, currency, strerror (errno));
+      *buffer = 0;
+      return buffer;
+    }
+
+  return buffer;
+}
diff --git a/src/currency.h b/src/currency.h
new file mode 100644 (file)
index 0000000..8a6dc24
--- /dev/null
@@ -0,0 +1,31 @@
+/* currency.h - Definitions for currency management functions
+ * Copyright (C) 2014 g10 Code GmbH
+ *
+ * This file is part of Payproc.
+ *
+ * Payproc 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 of the License, or
+ * (at your option) any later version.
+ *
+ * Payproc is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifndef CURRENCY_H
+#define CURRENCY_H
+
+void read_exchange_rates (void);
+
+int valid_currency_p (const char *string, int *r_decdigits);
+const char *get_currency_info (int seq, char const **r_desc, double *r_rate);
+char *convert_currency (char *buffer, size_t bufsize,
+                        const char *currency, const char *amount);
+
+
+#endif /*CURRENCY_H*/
diff --git a/src/geteuroxref b/src/geteuroxref
new file mode 100755 (executable)
index 0000000..5bbb47e
--- /dev/null
@@ -0,0 +1,27 @@
+#!/bin/sh
+
+name=euroxref.dat
+dir=/var/lib/payproc
+file="$dir/$name"
+tfile="$dir/.#$name"
+
+if [ ! -d "$dir" ]; then
+    echo "geteuroxref: directory '$dir' does not exist" >&2
+    exit 1
+fi
+
+wget -qO- https://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml \
+    | awk '
+/<Cube time/          { split($0, a, /'\''/); print "# " a[2] }
+/<Cube currency/      { split($0, a, /'\''/); print a[2] "=" a[4];}
+/<\/gesmes:Envelope/  { print "# eof" }
+' >$tfile
+
+if ! grep -q '^# eof' $tfile; then
+    echo "geteuroxref: error retrieving data (check $tfile)" >&2
+    exit 1
+fi
+if ! mv $tfile $file ; then
+    echo "geteuroxref: error storing data" >&2
+    exit 1
+fi
index ba26460..62ab27c 100644 (file)
@@ -50,6 +50,7 @@
    | 12 | chargeid | Charge id                                      |
    | 13 | txid     | Transaction id                                 |
    | 14 | rtxid    | Reference txid (e.g. for refunds)              |
+   | 15 | euro     | amount converted to Euro                       |
    |----+----------+------------------------------------------------|
 
    Because of the multithreaded operation it may happen that records
 #include "estream.h"
 #include "payprocd.h"
 #include "http.h"
+#include "currency.h"
 #include "journal.h"
 
 /* The size of our standard timestamp.  */
 #define TIMESTAMP_SIZE 15
 
 
-/* Infor about an open log file.  */
+/* Info about an open log file.  */
 struct logfile_s
 {
   char *basename;  /* The base name of the file.  */
@@ -321,10 +323,24 @@ jrnl_store_sys_record (const char *text)
   fp = start_record ('$', NULL);
   es_fputs (":::", fp);
   write_escaped (text, fp);
-  es_fputs ("::::::", fp);
+  es_fputs (":::::::::", fp);
   write_and_close_fp (fp);
 }
 
+
+/* Store a currency exchange record in the journal. */
+void
+jrnl_store_exchange_rate_record (const char *currency, double rate)
+{
+  estream_t fp;
+
+  fp = start_record ('$', NULL);  /* System record.  */
+  es_fprintf (fp,"1:%s:%f:new exchange rate:", currency, rate);
+  es_fputs ("::::::::1.0:", fp);
+  write_and_close_fp (fp);
+}
+
+
 /* Create a new record and spool it.  There is no error return because
    the actual transaction has already happened and we want to make
    sure to write that to the journal.  If we can't do that, we better
@@ -337,13 +353,15 @@ jrnl_store_charge_record (keyvalue_t *dictp)
   estream_t fp;
   char timestamp[TIMESTAMP_SIZE + 1];
   keyvalue_t dict;
+  const char *curr, *amnt;
+  char amountbuf[AMOUNTBUF_SIZE];
 
   fp = start_record ('C', timestamp);
   keyvalue_put (dictp, "_timestamp", timestamp);
   dict = *dictp;
   es_fprintf (fp, "%d:", (*keyvalue_get_string (dict, "Live") == 't'));
-  write_escaped (keyvalue_get_string (dict, "Currency"), fp);
-  write_escaped (keyvalue_get_string (dict, "Amount"), fp);
+  write_escaped ((curr=keyvalue_get_string (dict, "Currency")), fp);
+  write_escaped ((amnt=keyvalue_get_string (dict, "Amount")), fp);
   write_escaped (keyvalue_get_string (dict, "Desc"), fp);
   write_escaped (keyvalue_get_string (dict, "Email"), fp);
   write_meta (dict, fp);
@@ -353,6 +371,8 @@ jrnl_store_charge_record (keyvalue_t *dictp)
   write_escaped (keyvalue_get_string (dict, "Charge-Id"), fp);
   write_escaped (keyvalue_get_string (dict, "balance-transaction"), fp);
   es_fputs (":", fp);   /* rtxid */
+  es_fputs (convert_currency (amountbuf, sizeof amountbuf, curr, amnt), fp);
+  es_fputs (":", fp);   /* euro */
 
   write_and_close_fp (fp);
 }
index 6241707..23e609d 100644 (file)
@@ -22,6 +22,7 @@
 
 void jrnl_set_file (const char *fname);
 void jrnl_store_sys_record (const char *text);
+void jrnl_store_exchange_rate_record (const char *currency, double rate);
 void jrnl_store_charge_record (keyvalue_t *dictp);
 
 
index 1824103..413e219 100644 (file)
@@ -73,7 +73,7 @@ static char *jrnl_field_names[] =
     "_lnr", /* virtual field.  */
     "date", "type", "live", "currency", "amount",
     "desc", "mail", "meta", "last4", "service", "account",
-    "chargeid", "txid", "rtxid"
+    "chargeid", "txid", "rtxid", "euro"
   };
 
 
@@ -737,7 +737,7 @@ print_meta (char *buffer, const char *name)
 static int
 one_line (const char *fname, unsigned int lnr, char *line)
 {
-  char *field[12];
+  char *field[15];
   int nfields = 0;
 
   /* Parse into fields.  */
@@ -748,7 +748,7 @@ one_line (const char *fname, unsigned int lnr, char *line)
       if (line)
        *(line++) = '\0';
     }
-  if (nfields < DIM(field))
+  if (nfields < 12)  /* Early versions had only 12 fields.  */
     {
       log_error ("%s:%u: not enough fields - not a Payproc journal?\n",
                  fname, lnr);
index 9179913..9c72c30 100644 (file)
@@ -41,6 +41,7 @@
 #include "cred.h"
 #include "journal.h"
 #include "session.h"
+#include "currency.h"
 #include "payprocd.h"
 
 
@@ -503,6 +504,7 @@ launch_server (const char *logfile)
 
   log_info ("payprocd %s started\n", PACKAGE_VERSION);
   jrnl_store_sys_record ("payprocd "PACKAGE_VERSION" started");
+  read_exchange_rates ();
   server_loop (fd);
   close (fd);
 }
@@ -669,9 +671,12 @@ static void *
 housekeeping_thread (void *arg)
 {
   static int sentinel;
+  static int count;
 
   (void)arg;
 
+  count++;
+
   if (sentinel)
     {
       log_info ("only one cleaning person at a time please\n");
@@ -683,6 +688,13 @@ housekeeping_thread (void *arg)
 
   session_housekeeping ();
 
+  /* Stuff we do only every hour:  */
+  if (count >= 3600 / HOUSEKEEPING_INTERVAL)
+    {
+      count = 0;
+      read_exchange_rates ();
+    }
+
   if (opt.verbose > 1)
     log_info ("finished with housekeeping\n");
   sentinel--;
index 390b99a..3acb991 100644 (file)
@@ -98,6 +98,9 @@
    source file and thus is usable in code shared by applications.  */
 extern gpg_err_source_t default_errsource;
 
+/* The size of a buffer suitable to hold a string with an amount.  */
+#define AMOUNTBUF_SIZE 48
+
 
 /*-- util.c --*/
 void *xmalloc (size_t n);