com.truledger.client.Client.java Source code

Java tutorial

Introduction

Here is the source code for com.truledger.client.Client.java

Source

package com.truledger.client;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.KeyPair;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.List;
import java.util.Set;
import java.util.Vector;

import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.message.BasicNameValuePair;

import android.content.Context;
import android.net.http.AndroidHttpClient;

import com.truledger.client.LispList.Keyword;

/**
 * A Truledger client API. Talks the protocol of truledger.com
 * @author billstclair
 */
public class Client {
    Context ctx;
    ClientDB db;
    Parser parser;
    ClientDB.PubkeyDB pubkeydb;

    // Initialized by login() and newuser()
    String id;
    KeyPair privkey;
    String pubkeystr;

    // Initialized by setserver() and addserver()
    ServerProxy server;
    String serverid;

    // Set true by getreq()
    boolean isSyncedreq;

    // The last coupon generaget by a spend:
    // (<serverid>,couponenvelope,<id>,<encrypted-coupon>)
    String coupon;

    // The last outbox time generated by a spend
    String lastSpendTime;

    // Set true to keep history of spend and processinbox
    boolean keepHistory;

    // Set during call of process() when a T.REQ is included.
    // Updated by updateMsgReqNumbers() when a new crypto session
    // is created, invalidating the original T.REQ value.
    // Called *msg* in the lisp code
    String processMsg;

    BCMath bcm = new BCMath();

    /**
     * Constructor
     * @param ctx The context from the main activity. Needed for the database.
     */
    public Client(Context ctx) {
        this.ctx = ctx;
        db = new ClientDB(ctx);
        pubkeydb = db.getPubkeyDB();
        parser = new Parser(pubkeydb);
        parser.setAlwaysVerifySigs(true);
    }

    // API methods

    /**
     * Close the server connection and the databases
     */
    public void close() {
        this.logout();
        if (db != null) {
            db.close();
            db = null;
        }
    }

    public Parser getParser() {
        return parser;
    }

    public ClientDB getDB() {
        return db;
    }

    // API methods 

    /**
     * Create a new user with the given passphrase and private key
     * @param passphrase The passphrase
     * @param privkey The private key
     * @throws ClientException if the passphrase already has an associated private key
     */
    public void newuser(String passphrase, KeyPair privkey) throws ClientException {
        this.newuser(passphrase, privkey, true);
    }

    /**
     * Create a new user with the given passphrase and a newly generated private key
     * @param passphrase The passphrase
     * @param keysize The size in bits of the new key
     * @throws ClientException If the passphrase already has an associated private key
     */
    public void newuser(String passphrase, int keysize) throws ClientException {
        if (db.getPrivkeyDB().get(passphrase) != null) {
            throw new ClientException("Passphrase already has an associated private key");
        }
        KeyPair privkey = Crypto.RSAGenerateKey(keysize);
        newuser(passphrase, privkey, false);
    }

    /**
     * Create a new user with the given passphrase and private key
     * @param passphrase The passphrase
     * @param privkey The prvate key
     * @param checkPassphrase If true, error if the passphrase already has an associated private key
     * @throws ClientException If there is an erro encoding the public key for the new private key (unlikely)
     */
    public void newuser(String passphrase, KeyPair privkey, boolean checkPassphrase) throws ClientException {
        String hash = passphraseHash(passphrase);
        this.logout();
        ClientDB.PrivkeyDB privkeydb = db.getPrivkeyDB();

        if (checkPassphrase && privkeydb.get(passphrase) != null) {
            throw new ClientException("Passphrase already has an associated private key");
        }

        String pubstr, id, privstr;
        try {
            pubstr = Crypto.encodeRSAPublicKey(privkey);
            id = Crypto.getKeyID(pubstr);
            privstr = Crypto.encodeRSAPrivateKey(privkey, passphrase);
        } catch (IOException e) {
            throw new ClientException("While encoding public key for new private key", e);
        }

        privkeydb.put(hash, privstr);
        this.id = id;
        this.privkey = privkey;
        try {
            this.pubkeystr = Crypto.encodeRSAPublicKey(privkey);
        } catch (IOException e) {
            throw new ClientException(e);
        }
    }

    /**
     * Look up the private key for a passphrase
     * @param passphrase
     * @return The private key
     * @throws ClientException If no private key is known for passphrase or if we fail to decrypt the string for it
     */
    public KeyPair getPrivkey(String passphrase) throws ClientException {
        String hash = passphraseHash(passphrase);
        String privstr = db.getPrivkeyDB().get(hash);
        if (privstr == null) {
            throw new ClientException("No account for passphrase in database");
        }
        try {
            return Crypto.decodeRSAPrivateKey(privstr, passphrase);
        } catch (IOException e) {
            throw new ClientException(null, e);
        }
    }

    /**
     * Log in locally
     * @param passphrase
     * @throws ClientException if there is no user associated with passphrase,
     *         or if we somehow fail to encode the public key to a string.
     */
    public void login(String passphrase) throws ClientException {
        this.logout();
        KeyPair privkey = this.getPrivkey(passphrase);
        String pubkeystr;
        try {
            pubkeystr = Crypto.encodeRSAPublicKey(privkey);
        } catch (IOException e) {
            throw new ClientException(e);
        }
        String id = Crypto.getKeyID(pubkeystr);
        this.id = id;
        this.privkey = privkey;
        this.pubkeystr = pubkeystr;
    }

    /**
     * Log in using a sessionid to get the user's passphrase
     * @param sessionid
     * @throws ClientException
     */
    public void loginWithSessionid(String sessionid) throws ClientException {
        String passphrase = this.sessionPassphrase(sessionid);
        this.login(passphrase);
        this.isSyncedreq = true; // Don't need a server sync after a session login
    }

    public void loginNewSession(String passphrase) throws ClientException {
        this.login(passphrase);
        this.makeSession(passphrase);
    }

    public void logout() {
        if (id != null) {
            this.removeSession();
            id = null;
        }
        privkey = null;
        serverid = null;
        ServerProxy s = server;
        if (s != null) {
            server = null;
            s.close();
        }
    }

    /**
     * Return the ID of the logged-in users
     * @return The logged-in user ID or null if there is none.
     */
    public String currentUser() {
        return (privkey != null) ? id : null;
    }

    // All the API methods below require the user to be logged in.
    // id and privkey must be set.

    /**
     * Ensure that the user is logged in
     * @throws ClientException if no user is logged in, i.e. currentUser() returns null.
     */
    public String requireCurrentUser() throws ClientException {
        String id = this.currentUser();
        if (id == null)
            throw new ClientException("Not logged in");
        return id;
    }

    /**
     * For returning information about servers
     * @author billstclair
     */
    public static class ServerInfo implements Comparable<ServerInfo> {
        public String id;
        public String name;
        public String url;

        public ServerInfo(String id, String name, String url) {
            this.id = id;
            this.name = name;
            this.url = url;
        }

        public int compareTo(ServerInfo info) {
            return name.compareTo(info.name);
        }
    }

    /**
     * Return information about a server
     * @param serverid The ID of the server
     * @param all true to return non-null even if the current logged-in user isn't known to the server
     * @return
     */
    public ServerInfo getServer(String serverid, boolean all) {
        if (!all && this.getUserReq(serverid) == null)
            return null;
        return new ServerInfo(serverid, getServerProp(T.NAME, serverid), getServerProp(T.URL, serverid));
    }

    /**
     * Return information about a server
     * @param serverid The ID of the server
     * @return null if the current logged-in user isn't known to the server
     */
    public ServerInfo getServer(String serverid) {
        return this.getServer(serverid, false);
    }

    /**
     * Return information about the current logged-in server
     * @return shouldn't be null
     */
    public ServerInfo getServerInfo() {
        return this.getServer(serverid, false);
    }

    /**
     * Return info about the servers known to the logged-in user, sorted by name
     * @return
     * @throws ClientException if no user is logged in
     */
    public ServerInfo[] getServers() throws ClientException {
        String id = this.requireCurrentUser();
        String[] servers = db.getAccountDB().contents(id + '/' + T.SERVER);
        ServerInfo[] res = new ServerInfo[servers.length];
        for (int i = 0; i < servers.length; i++) {
            String serverid = servers[i];
            res[i] = this.getServer(serverid, true);
        }
        Arrays.sort(res);
        return res;
    }

    /**
     * @param url
     * @return true if url is properly formed
     */
    public static boolean isUrl(String url) {
        if (Utility.isBlank(url))
            return false;
        try {
            new URL(url);
        } catch (MalformedURLException e) {
            return false;
        }
        return true;
    }

    /**
     * @param url
     * @param number
     * @return [<url> <number>]
     */
    public String encodeCoupon(String url, String number) {
        return '[' + url + ' ' + number + ']';
    }

    /**
     * Parses a string of the form [<url> <coupon>] into a 2-element array.
     * Allows the starting or ending square bracket to be missing.
     * Allows a comma instead of a space between url and coupon.
     * Does no validation of url or coupon.
     * @param coupon The string to parse
     * @return new String[] {<url>, <coupon>}
     * @throws ClientException
     */
    public static String[] decodeCoupon(String coupon) throws ClientException {
        coupon = coupon.trim();
        int len = coupon.length();
        if (len == 0)
            throw new ClientException("Blank coupon");
        int start = (coupon.charAt(0) == '[') ? 1 : 0;
        int end = (coupon.charAt(len - 1) == ']') ? len - 1 : len;
        if (start > 0 || end < len) {
            coupon = coupon.substring(start, end);
        }
        coupon = coupon.replace(',', ' ');
        int pos = coupon.indexOf(' ');
        if (pos < 0)
            throw new ClientException("Malformed coupon");
        return new String[] { coupon.substring(0, pos), coupon.substring(pos + 1).trim() };
    }

    /**
     * Parse a [<url> <coupon>] string and validate the syntax of <url> and <coupon>
     * @param coupon The string to parse
     * @return new String[] {<url>, <coupon>}
     * @throws ClientException If the parse fails, or <url> or <coupon> are malformed.
     */
    public static String[] parseCoupon(String coupon) throws ClientException {
        String[] res = decodeCoupon(coupon);
        String url = res[0];
        String number = res[1];
        if (!isUrl(url))
            throw new ClientException("Coupon url isn't a url: " + url);
        if (!Utility.isCouponNumber(number))
            throw new ClientException("Coupon number malformed: " + number);
        return res;
    }

    /**
     * Verify that a message is a valid coupon.
     * Check that it is actually signed by the server that it claims
     * to be from.
     * Ask the server whether a coupon of that number is still valid.
     * @param coupon The coupon: "[<url> <number>]
     * @param serverid The ID of the server at URL
     * @param url The web address of the server
     * @return The matched coupon msg from the server
     * @throws ClientException if the coupon won't parse, or it isn't valid at the server.
     */
    public Parser.Dict verifyCoupon(String coupon, String serverid, String url) throws ClientException {
        String couponNumber = parseCoupon(coupon)[1];
        this.verifyServer(url, serverid);
        String msg = "(0," + T.SERVERID + ",0," + couponNumber + "):0";
        ServerProxy server = new ServerProxy(url);
        Parser.DictList reqs;
        try {
            msg = server.process(msg);
            reqs = parser.parse(msg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        } finally {
            server.close();
        }
        this.matchServerReq(reqs.get(0), T.REGISTER, serverid);
        if (reqs.size() != 2)
            throw new ClientException("verifycoupon: expected 2 messages from server");
        return this.matchServerReq(reqs.get(1), T.COUPONNUMBERHASH, serverid);
    }

    /**
     * Simple class to return details about a server from its response to a "serverid" message.
     * @author billstclair
     */
    public static class ServerDetails {
        String id;
        String pubkeystr;
        String name;

        public ServerDetails(String id, String pubkeystr, String name) {
            this.id = id;
            this.pubkeystr = pubkeystr;
            this.name = name;
        }
    }

    /**
     * Query server at URL and return information about it
     * @param url the web address of the server
     * @param serverid the serverid, or null if not known
     * @return info about the server
     * @throws ClientException if the communication fails, the return message can't be parsed,
     *         the server returns a "failed" message, the server doesn't return a "register" message,
     *         the customer & serverid fields of the register message don't match,
     *         or the returned public key string doesn't hash to the server's ID
     */
    public ServerDetails serveridForUrl(String url, String serverid) throws ClientException {
        String msg = "(0," + T.SERVERID + ",0):0";
        ServerProxy server = new ServerProxy(url);
        String savedid = this.serverid;
        Parser.Dict args;
        try {
            msg = server.process(msg);
            this.serverid = serverid;
            args = parser.matchMessage(msg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        } finally {
            this.serverid = savedid;
            server.close();
        }
        String request = args.stringGet(T.REQUEST);
        serverid = args.stringGet(T.CUSTOMER);
        String pubkeystr = args.stringGet(T.PUBKEY);
        String name = args.stringGet(T.NAME);
        if (T.FAILED.equals(request)) {
            String errmsg = args.stringGet(T.ERRMSG);
            if (errmsg == null)
                errmsg = msg;
            throw new ClientException("Failed to get ID from server: " + errmsg);
        }
        if (!T.REGISTER.equals(request) || !(serverid != null && serverid.equals(args.stringGet(T.SERVERID)))) {
            throw new ClientException("Server's register message malformed");
        }
        if (!serverid.equals(Crypto.getKeyID(pubkeystr))) {
            throw new ClientException("Server's ID doesn't match its public key");
        }
        return new ServerDetails(serverid, pubkeystr, name);
    }

    /**
     * Verify that a server matches its URL.
      * Add the server to our database if it's not there already.
        * Error if ID is non-null and doesn't match serverid at URL.
        * Return serverid
     * @param url The web address of the server
     * @param id The server id or null
     * @return The server id
     * @throws ClientException if the URL is malformed or the server at URL
     *         doesn't exist or its id doesn't match the arg.
     */
    public String verifyServer(String url, String id) throws ClientException {
        if (!isUrl(url))
            throw new ClientException("Not a URL: " + url);
        if (Utility.isBlank(url))
            url = null;
        String urlhash = Crypto.sha1(url);
        ClientDB.ServeridDB serveridDB = db.getServeridDB();
        String serverid = serveridDB.get(urlhash);
        if (serverid != null) {
            if (id != null && !id.equals(serverid))
                throw new ClientException("verifyServer: id !> serverid");
            return serverid;
        }
        ServerDetails details = this.serveridForUrl(url, id);
        serverid = details.id;
        if (id != null && !id.equals(serverid))
            throw new ClientException("Serverid not as expected");
        if (this.getServerProp(T.URL, serverid) == null) {
            // Initialize the server in the database
            serveridDB.put(urlhash, serverid);
            ClientDB.ServerDB serverDB = db.getServerDB();
            serverDB.put(serverid, T.URL, url);
            serverDB.put(serverid, T.NAME, details.name);
            db.getPubkeyDB().put(serverid, details.pubkeystr.trim() + "\n");
        }
        return serverid;
    }

    /**
     * Verify that a server matches its URL.
      * Add the server to our database if it's not there already.
     * @param url The web address of the server
     * @return The server id
     * @throws ClientException if the URL is malformed or the server at URL
     *         doesn't exist.
     */
    public String verifyServer(String url) throws ClientException {
        return this.verifyServer(url, null);
    }

    public void addServer(String url, String name) throws ClientException {
        this.addServer(url, name, false);
    }

    /**
     * Add a server with the given URL to the database.
      * URL can be a coupon to redeem that with registration.
      * No error, but does nothing, if the server is already there.
      * If the server is NOT already there, registers with the given NAME and coupon.
      * If registration fails, removes the server and you'll have to add it again
      * after getting enough usage tokens at the server to register.
      * Sets the client instance to use this server until addServer() or setServer()
      * is called to change it.
     * @param url The server's web address
     * @param name The name to use for registration with the server
     * @param couponok true to skip verification of the coupon before registerting
     * @throws ClientException
     */
    public void addServer(String url, String name, boolean couponok) throws ClientException {
        String serverid;
        String realurl;
        String coupon = null;
        this.requireCurrentUser();
        if (isUrl(url)) {
            realurl = url;
            serverid = this.verifyServer(realurl);
        } else {
            String[] couponInfo = parseCoupon(url);
            realurl = couponInfo[0];
            coupon = couponInfo[1];
            serverid = this.verifyServer(realurl);
            if (!couponok)
                this.verifyCoupon(url, serverid, realurl);
        }
        boolean isAlreadyRegistered = true;
        try {
            this.setServer(serverid, false);
        } catch (ClientException e) {
            isAlreadyRegistered = false;
        }
        if (isAlreadyRegistered) {
            if (coupon != null)
                this.redeem(coupon);
        } else {
            String oldserverid = this.serverid;
            ServerProxy oldserver = this.server;
            boolean ok = false;
            this.serverid = serverid;
            try {
                url = this.getServerProp(T.URL, serverid);
                if (url == null)
                    throw new ClientException("URL not stored for verififed server: " + serverid);
                this.server = new ServerProxy(url);
                this.register(name, coupon);
                this.forceinit();
                ok = true;
            } finally {
                if (ok)
                    oldserver.close();
                else {
                    this.setUserReq(serverid, null);
                    this.serverid = oldserverid;
                    this.server.close();
                    this.server = oldserver;
                }
            }
        }
    }

    /**
     * clear the serverid and server
     */
    public void clearServer() {
        ServerProxy server = this.server;
        this.serverid = null;
        this.server = null;
        if (server != null)
            server.close();
    }

    /**
     * Set the server to the given id.
      * Sets the client instance to use this server until addserver() or setserver()
      * is called to change it, by setting this.serverid and this.server"
     * Ensure that the server knows our ID and that its ID is serverid
     * @param serverid The ID of the server to set. The current user must have registered there before.
     * @throws ClientException If server unknown, not registered there before, server returns an error
     *         to the T.SERVERID message signed by the current user, or returned server ID not as expected.
     */
    public void setServer(String serverid) throws ClientException {
        this.setServer(serverid, true);
    }

    /**
     * Set the server to the given id.
      * Sets the client instance to use this server until addserver() or setserver()
      * is called to change it, by setting this.serverid and this.server"
     * @param serverid The ID of the server to set. The current user must have registered there before.
     * @param check if true, ensure that the server knows our ID and that its ID is serverid
     * @throws ClientException If server unknown, not registered there before, server returns an error
     *         to the T.SERVERID message signed by the current user, or returned server ID not as expected.
     */
    public void setServer(String serverid, boolean check) throws ClientException {
        String url = this.getServerProp(T.URL, serverid);
        if (url == null)
            throw new ClientException("Server not known: " + serverid);
        this.requireCurrentUser();
        if (this.userServerProp(T.REQ) == null) {
            throw new ClientException("User not registered at server");
        }
        this.clearServer();
        this.serverid = serverid;
        this.server = new ServerProxy(url);
        String msg;
        Parser.Dict args;
        if (check) {
            try {
                msg = this.sendmsg(T.SERVERID, this.pubkeystr);
                args = parser.matchMessage(msg);
            } catch (Exception e) {
                this.clearServer();
                throw new ClientException("Server's serverid response error", e);
            }
            if (serverid != args.stringGet(T.CUSTOMER)) {
                this.clearServer();
                throw new ClientException("Serverid changed since we last contacted this server, old: " + serverid
                        + ", new: " + args.stringGet(T.CUSTOMER));
            }
            if (!T.REGISTER.equals(args.get(T.REQUEST)) || !serverid.equals(args.get(T.SERVERID))) {
                this.clearServer();
                throw new ClientException("Server's serverid response wrong: " + msg);
            }

        }
    }

    /**
     * Get the current server 
     * @return The server ID, if the user is logged in and there is a server;
     */
    public String currentServer() {
        return (this.currentUser() != null && this.server != null) ? this.serverid : null;
    }

    /**
     * Error if this.currentServer() is null
     * @throws ClientException
     */
    public String requireCurrentServer() throws ClientException {
        return this.requireCurrentServer(null);
    }

    /**
     * Error with the given message if this.currentServer() is null;
     * @param msg
     * @throws ClientException
     */
    public String requireCurrentServer(String msg) throws ClientException {
        String res = this.currentServer();
        if (res == null) {
            throw new ClientException(msg == null ? "Server not set" : msg);
        }
        return res;
    }

    //  All the API methods below require the user to be logged in and the server to be set.
    //  Do this by calling newuser() or login(), and addserver() or setserver().
    //  id, privkey, serverid, & server must all be set.

    /**
     * Register at the current server.
     * No error if already registered
     * If not registered, and COUPON is  non-null, encrypts and signs it,
       and sends it to the server with the registration request."
     * @param name The name to register with. null for none.
     * @param coupon A coupon to register with or null.
     * @throws ClientException
     */
    public void register(String name, String coupon) throws ClientException {
        String id = this.requireCurrentUser();
        this.requireCurrentServer("In register: Server not set");

        // If already registered, and we know it, nothing to do
        ClientDB.AccountDB accountDB = db.getAccountDB();
        String pubkeysigPath = this.userServerKey(T.PUBKEYSIG);
        if (accountDB.get(pubkeysigPath, id) != null)
            return;

        // See if server already knows us.
        // Resist the urge to change this to a call to getPubkeyFromServer(). Trust me.
        String msg = this.sendmsg(T.ID, serverid, id);
        Parser.Dict args;
        try {
            args = this.unpackServermsg(msg, T.REGISTER);
        } catch (ClientException e) {
            // Server doesn't know us. Register with server.
            msg = this.custmsg(T.REGISTER, serverid, pubkeystr, name);
            if (coupon != null) {
                String serverkey = db.getPubkeyDB().get(serverid);
                if (serverkey == null)
                    throw new ClientException("Can't get server public key");
                try {
                    String couponCrypt = Crypto.RSAPubkeyEncrypt(coupon, serverkey);
                    msg += '.' + this.custmsg(T.COUPONENVELOPE, serverid, couponCrypt);
                } catch (IOException e2) {
                    throw new ClientException(e2);
                }
            }
            msg = server.process(msg);
            args = this.unpackServermsg(msg, T.ATREGISTER);
        }

        // Registration succeeded. Record in database.
        args = (Parser.Dict) args.get(T.MSG);
        if (args == null || !id.equals(args.get(T.CUSTOMER)) || !T.REGISTER.equals(args.get(T.REQUEST))
                || !serverid.equals(args.get(T.SERVERID))) {
            throw new ClientException("Malformed registration message");
        }
        String keyid = Crypto.getKeyID(args.stringGet(T.PUBKEY));
        if (!id.equals(keyid))
            throw new ClientException("Server's pubkey wrong");
        accountDB.put(pubkeysigPath, id, msg);
        accountDB.put(this.userServerKey(), T.REQ, "-1");
    }

    private static String $PRIVKEY_CACHE_SALT = "privkey-cache-salt";

    public boolean isPrivkeyCached(String serverid) throws ClientException {
        if (serverid == null) {
            serverid = this.requireCurrentServer();
        }
        return this.userServerProp(T.PRIVKEYCACHEDP, serverid) == "cached";
    }

    public boolean isPrivkeyCached() throws ClientException {
        return this.isPrivkeyCached(null);
    }

    public void setPrivkeyCached(boolean value, String serverid) throws ClientException {
        if (serverid == null) {
            serverid = this.requireCurrentServer();
        }
        this.setUserServerProp(T.PRIVKEYCACHEDP, value ? "cached" : null);
    }

    public boolean needPrivkeyCache(String serverid) throws ClientException {
        if (serverid == null) {
            serverid = this.requireCurrentServer();
        }
        return this.userServerProp(T.NEEDPRIVKEYCACHE, serverid) == T.NEEDPRIVKEYCACHE;
    }

    public boolean needPrivkeyCached() throws ClientException {
        return this.needPrivkeyCache(null);
    }

    public void setNeedPrivkeyCached(boolean value, String serverid) throws ClientException {
        if (serverid == null) {
            serverid = this.requireCurrentServer();
        }
        this.setUserServerProp(T.NEEDPRIVKEYCACHE, value ? T.NEEDPRIVKEYCACHE : null);
    }

    /**
     * Cache or uncache the user's private key on the server.
     * We could encrypt the private key again, so it doesn't look like a
     * private key, but that's really not any more secure, since it will
      * only use the passphrase a second time. We could require yet
      * another passphrase, but users will forget that, since they'll
      * hardly ever use it.
     * @param sessionid The ID of the current session
     * @param uncache True to uncache, false to cache
     * @throws ClientException
     */
    public void cachePrivkey(String sessionid, boolean uncache) throws ClientException {
        String serverid = this.requireCurrentServer();
        if (!uncache && id == serverid)
            throw new ClientException("You may not cache the server private key");
        String passphrase = this.sessionPassphrase(sessionid);
        String data = uncache ? "" : db.getPrivkeyDB().get(passphraseHash(passphrase));
        String key = passphraseHash(passphrase, $PRIVKEY_CACHE_SALT);
        this.writeData(key, data, true);
        this.setPrivkeyCached(!uncache, serverid);
    }

    /**
     * Get a saved private key from a server
     * @param serverurl The web address of the server
     * @param passphrase The passphrase of the private key
     * @return The encrypted password string
     * @throws ClientException
     */
    public String fetchPrivkey(String serverurl, String passphrase) throws ClientException {
        String key = passphraseHash(passphrase, $PRIVKEY_CACHE_SALT);
        return this.readData(key, true, serverurl)[0];
    }

    public static class Contact {
        public String id;
        public String name;
        public String nickname;
        public String note;
        public String[] servers;
        public Client client;

        public Contact() {
        }

        public Contact(String id, String name, String nickname, String note, String[] servers, Client client) {
            this.id = id;
            this.name = name;
            this.nickname = nickname;
            this.note = note;
            this.servers = servers;
            this.client = client;
        }

        /**
         * @return true if this contact is known at the current logged in server
         */
        public boolean isClientContact() {
            if (client == null)
                return false;
            String serverid = client.serverid;
            if (serverid == null)
                return false;
            for (String sid : servers) {
                if (serverid.equals(sid))
                    return true;
            }
            return false;
        }

        /**
         * @param c2 Contact with which to compare
         * @return -1, 0, or 1 according to whether c2 is <, ==, or > this.
         *         Comparison is by nickname, name, then id
         */
        public int compareTo(Contact c2) {
            int res = Utility.compareStrings(nickname, c2.nickname);
            if (res != 0)
                return res;
            res = Utility.compareStrings(name, c2.name);
            if (res != 0)
                return res;
            return Utility.compareStrings(id, c2.id);
        }
    }

    /**
     * Get contacts for the current server.
      * Contacts are sorted by nickname, name, id
      * Signals an error or returns a list of CONTACT instances.
     * @param all
     * @return contacts for the current server.
     */
    public Contact[] getContacts() throws ClientException {
        return this.getContacts(false);
    }

    /**
     * Get contacts for the current server.
      * Contacts are sorted by nickname, name, id
      * Signals an error or returns a list of CONTACT instances.
      * If ALL is true, return all contacts.
      * Otherwise, return only contacts for the current server.
     * @param all
     * @return
     */
    public Contact[] getContacts(boolean all) throws ClientException {
        this.requireCurrentServer();
        ClientDB.AccountDB acctDB = db.getAccountDB();
        String[] ids = acctDB.contents(this.contactkey());
        ArrayList<Contact> res = new ArrayList<Contact>(ids.length);
        for (String otherid : ids) {
            Contact contact = this.getContact(otherid, false, false);
            if (all || Utility.position(serverid, contact.servers) >= 0) {
                res.add(contact);
            }
        }
        return res.toArray(new Contact[res.size()]);
    }

    /**
     * Get a contact from the database or the server
     * @param otherid The id of the desired contact
     * @param add True to add the contact to the database
     * @param probeserver True to probe the server for the contact (implied if ADD is true)
     * @return The Contact, with all the fields we know populated
     * @throws ClientException If there's an error talking to the server or the database
     */
    public Contact getContact(String otherid, boolean add, boolean probeserver) throws ClientException {
        if (this.currentServer() == null)
            return null;
        String pubkeysig = this.getContactProp(otherid, T.PUBKEYSIG);
        if (pubkeysig == null) {
            if (add) {
                this.addContact(otherid);
                pubkeysig = this.getContactProp(otherid, T.PUBKEYSIG);
            } else if (probeserver) {
                String[] sig_name = this.getID(otherid);
                pubkeysig = sig_name[0];
                String name = sig_name[1];
                if (pubkeysig == null)
                    return null;
                return new Contact(otherid, name, null, null, new String[] { serverid }, this);
            }
        }
        if (pubkeysig != null) {
            String name = this.getContactProp(otherid, T.NAME);
            String nickname = this.getContactProp(otherid, T.NICKNAME);
            String note = this.getContactProp(otherid, T.NOTE);
            String[] servers = Utility.explode(' ', this.getContactProp(otherid, T.SERVERS));
            return new Contact(otherid, name, nickname, note, servers, this);
        }
        return null;
    }

    /**
     * Get a contact from the database or the server
     * @param otherid The id of the desired contact
     * @param add True to add the contact to the database, after getting it from the server
     * @return The Contact, with all the fields we know populated
     * @throws ClientException If there's an error talking to the server or the database
     */
    public Contact getContact(String otherid, boolean add) throws ClientException {
        return this.getContact(otherid, add, false);
    }

    /**
     * Get a contact from the database
     * @param otherid The id of the desired contact
     * @return The Contact, with all the fields we know populated
     * @throws ClientException If there's an error talking to the server or the database
     */
    public Contact getContact(String otherid) throws ClientException {
        return this.getContact(otherid, false, false);
    }

    /**
     * Add a contact to the database, or change its nickname and/or note
     * Fetch the contact from the server if unknown on the client.
     * @param otherid The id of the contact
     * @param nickname The contact's nickname, or null if none
     * @param note A note about the contact, or null if none
     * @return The pubkeysig message for the contact
     * @throws ClientException
     */
    public String addContact(String otherid, String nickname, String note) throws ClientException {
        this.requireCurrentServer();
        String[] servers = Utility.explode(' ', this.getContactProp(otherid, T.SERVERS));
        if (servers == null || Utility.position(serverid, servers) < 0) {
            this.setContactProp(otherid, T.SERVERS, Utility.implode(' ', serverid, servers));
        }
        String pubkeysig = this.getContactProp(otherid, T.PUBKEYSIG);
        if (pubkeysig != null) {
            if (nickname != null)
                this.setContactProp(otherid, T.NICKNAME, nickname);
            if (note != null)
                this.setContactProp(otherid, T.NOTE, note);
        } else {
            String[] sig_name = this.getID(otherid);
            if (sig_name == null)
                throw new ClientException("Can't find id at server: " + otherid);
            pubkeysig = sig_name[0];
            String name = sig_name[1];
            if (nickname == null)
                nickname = Utility.isBlank(name) ? "anonymous" : name;
            this.setContactProp(otherid, T.NICKNAME, nickname);
            this.setContactProp(otherid, T.NOTE, note);
            this.setContactProp(otherid, T.NAME, name);
            this.setContactProp(otherid, T.PUBKEYSIG, pubkeysig);
        }
        return pubkeysig;
    }

    /**
     * Add contact to database if it isn't yet there by fetching info from the server
     * @param otherid The id to add
     * @return The pubkeysig message for the contact
     * @throws ClientException If there is an error talking to the server
     */
    public String addContact(String otherid) throws ClientException {
        return this.addContact(otherid, null, null);
    }

    /**
     * Delete a contact from the current user's 
     * @param otherid The contact to delete
     * @throws ClientException if no user is logged in
     */
    public void deleteContact(String otherid) throws ClientException {
        this.requireCurrentUser();
        String key = this.contactkey(otherid);
        ClientDB.AccountDB acctDB = db.getAccountDB();
        String[] props = acctDB.contents(key);
        for (String prop : props) {
            acctDB.put(key, prop, null);
        }
    }

    private static String $SERVER_CONTACTS_SALT = "server-contacts-salt";

    /**
     * @return The string to use as a key for storing contacts on the server
     */
    private String serverContactsKey() {
        return Crypto.sha1(Utility.xorSalt(id, $SERVER_CONTACTS_SALT));
    }

    /**
     * Get the encoded contacts string from the server
     * @return "((:id <id> :name <name> :nickname <nickname> :note <note> :servers <servers>) ...)"
     */
    private String getServerContactsString() throws ClientException {
        return this.readData(this.serverContactsKey())[0];
    }

    private void setServerContactsString(String value) throws ClientException {
        this.writeData(this.serverContactsKey(), value == null ? "" : value);
    }

    private static Keyword ID = Keyword.intern("id");
    private static Keyword NAME = Keyword.intern("name");
    private static Keyword NICKNAME = Keyword.intern("nickname");
    private static Keyword NOTE = Keyword.intern("note");
    private static Keyword SERVERS = Keyword.intern("servers");

    /**
     * Pack a contact into a LispList for printing
     * @param contact
     * @return
     */
    public LispList packContact(Contact contact) {
        LispList res = new LispList(10);
        if (contact.id != null) {
            res.add(ID);
            res.add(contact.id);
        }
        if (contact.name != null) {
            res.add(NAME);
            res.add(contact.name);
        }
        if (contact.nickname != null) {
            res.add(NICKNAME);
            res.add(contact.nickname);
        }
        if (contact.note != null) {
            res.add(NOTE);
            res.add(contact.note);
        }
        if (contact.servers != null) {
            res.add(SERVERS);
            res.add(LispList.valueOf(contact.servers));
        }
        return res;
    }

    /**
     * Unpack a contact list created by packContact()
     * @param list
     * @return
     */
    public Contact unpackContact(LispList list) {
        Contact res = new Contact();
        String id = list.getString(ID);
        String name = list.getString(NAME);
        String nickname = list.getString(NICKNAME);
        String note = list.getString(NOTE);
        LispList servers = (LispList) list.getprop(SERVERS);
        if (id != null)
            res.id = id;
        if (name != null)
            res.name = name;
        if (nickname != null)
            res.nickname = name;
        if (note != null)
            res.note = note;
        if (servers != null) {
            int size = servers.size();
            String[] a = new String[size];
            for (int i = 0; i < size; i++) {
                a[i] = (String) servers.get(i);
            }
            res.servers = a;
        }
        return res;
    }

    /**
     * Return the printed representation of an array of Contacts
     * @param contacts
     * @return
     * @throws ClientException
     */
    public String packContacts(Contact[] contacts) throws ClientException {
        LispList list = new LispList(contacts.length);
        for (Contact contact : contacts) {
            list.add(packContact(contact));
        }
        try {
            return list.prin1ToString();
        } catch (Exception e) {
            throw new ClientException(e);
        }
    }

    /**
     * Turn a printed representation into an array of Contacts
     * @param string
     * @return
     * @throws ClientException
     */
    public Contact[] unpackContacts(String string) throws ClientException {
        try {
            LispList packedContacts = LispList.parse(string);
            Contact[] res = new Contact[packedContacts.size()];
            int i = 0;
            for (Object list : packedContacts) {
                res[i++] = this.unpackContact((LispList) list);
            }
            return res;
        } catch (Exception e) {
            throw new ClientException(e);
        }
    }

    /**
     * Read, decrypt, and return the saved Contacts from the server
     * @return
     * @throws ClientException
     */
    public Contact[] getServerContacts() throws ClientException {
        String string = this.getServerContactsString();
        if (string == null)
            return null;
        return this.unpackContacts(Crypto.RSAPrivkeyDecrypt(string, this.privkey));
    }

    /**
     * Save an array of Contacts on the server as a string
     * @param contacts
     * @throws ClientException
     */
    public void setServerContacts(Contact[] contacts) throws ClientException {
        this.setServerContactsString(
                Crypto.RSAPubkeyEncrypt(this.packContacts(contacts), this.privkey.getPublic()));
    }

    /**
     * Find an ID in an array of Contacts
     * @param id
     * @param contacts
     * @return
     */
    public static Contact findContact(String id, Contact[] contacts) {
        for (Contact contact : contacts) {
            if (id.equals(contact.id))
                return contact;
        }
        return null;
    }

    /**
     * Synchronize the local contacts with the saved contacts on the server
     * @return the synced, and now saved locally, contacts
     * @throws ClientException
     */
    public Contact[] syncContacts() throws ClientException {
        Contact[] contacts = this.getContacts(true);
        Contact[] serverContacts = this.getServerContacts();
        if (serverContacts == null)
            return contacts;
        ArrayList<Contact> newContacts = new ArrayList<Contact>();
        for (Contact sc : serverContacts) {
            String otherid = sc.id;
            Contact c = findContact(otherid, contacts);
            if (c != null) {
                // Contact in db and on server. Merge new server information.
                String newnick = sc.nickname;
                if (Utility.isBlank(c.nickname) && !Utility.isBlank(newnick)) {
                    c.nickname = newnick;
                    this.setContactProp(otherid, T.NICKNAME, newnick);
                }
                String newnote = sc.note;
                if (Utility.isBlank(sc.note) && !Utility.isBlank(newnote)) {
                    c.note = newnote;
                    this.setContactProp(otherid, T.NOTE, newnote);
                }
                String[] servers = sc.servers;
                Set<String> set = new HashSet<String>(servers.length);
                for (String server : servers)
                    set.add(server);
                for (String server : c.servers)
                    set.add(server);
                if (set.size() != servers.length) {
                    servers = new String[set.size()];
                    int i = 0;
                    for (String server : set)
                        servers[i++] = server;
                    c.servers = servers;
                    this.setContactProp(otherid, T.SERVERS, Utility.implode(' ', servers));
                }
            } else {
                // Contact only on server. Write new contact to database.
                newContacts.add(sc);
                if (this.getContactProp(otherid, T.PUBKEYSIG) == null) {
                    try {
                        String pubkeysig = this.getID(otherid)[0];
                        if (pubkeysig != null) {
                            this.setContactProp(otherid, T.PUBKEYSIG, pubkeysig);
                        }
                    } catch (ClientException e) {
                    }
                    this.setContactProp(otherid, T.NICKNAME, sc.nickname);
                    this.setContactProp(otherid, T.NOTE, sc.note);
                    this.setContactProp(otherid, T.NAME, sc.name);
                    this.setContactProp(otherid, T.SERVERS, Utility.implode(' ', sc.servers));
                }
            }
        }
        if (newContacts.size() > 0) {
            int size = contacts.length + newContacts.size();
            Contact[] res = new Contact[size];
            int i = 0;
            for (Contact c : contacts)
                res[i++] = c;
            for (Contact c : newContacts)
                res[i++] = c;
            contacts = res;
        }
        return contacts;
    }

    /**
     * Check for an ID in the database and on the server
     * @param id The ID to check for
     * @return [pubkeysig, name] or null if not found
     * @throws ClientException If there is an error talking to the server or the database
     */
    public String[] getID(String id) throws ClientException {
        if (serverid == null)
            return null;
        String key = this.userServerKey(T.PUBKEYSIG);
        ClientDB.AccountDB acctDB = db.getAccountDB();
        String pubkeysig = acctDB.get(key, id);
        boolean needstore = false;
        if (pubkeysig == null) {
            pubkeysig = this.sendmsg(T.ID, serverid, id);
            needstore = true;
        }
        Parser.Dict args = this.unpackServermsg(pubkeysig, T.ATREGISTER);
        args = (Parser.Dict) args.get(T.MSG);
        String pubkey = args.stringGet(T.PUBKEY);
        String name = args.stringGet(T.NAME);
        if (!id.equals(Crypto.getKeyID(pubkey)))
            return null;
        if (needstore)
            acctDB.put(key, id, pubkeysig);
        return new String[] { pubkeysig, name };
    }

    /**
     * Compare acct strings
     * @param a1
     * @param a2
     * @return -1, 0, 1 according to a1 <, =, or > a2
     */
    public static int acctCompare(String a1, String a2) {
        int res = Utility.compareStrings(a1, a2);
        if (res == 0)
            return 0;
        if (a1.equals(T.MAIN))
            return -1;
        if (a2.equals(T.MAIN))
            return 1;
        return res;
    }

    /**
     * Return true if a1 < a2, according to acctCompare()
     * @param a1
     * @param a2
     * @return
     */
    public static boolean acctLessp(String a1, String a2) {
        return acctCompare(a1, a2) < 0;
    }

    /**
     * Get the sub-account names, sorted
     * @return
     * @throws ClientException
     */
    public String[] getAccts() throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        String[] accts = db.getAccountDB().contents(this.userBalanceKey());
        Arrays.sort(accts, new Comparator<String>() {
            public int compare(String a1, String a2) {
                return acctCompare(a1, a2);
            };
        });
        return accts;
    }

    /**
     * Package up information about an asset
     * @author billstclair
     */
    public static class Asset {
        public String id;
        public String assetid;
        public String scale;
        public String precision;
        public String name;
        public String issuer;
        public String percent;

        public Asset() {
        }

        public Asset(String id, String assetid, String scale, String precision, String name, String issuer,
                String percent) {
            this.id = id;
            this.assetid = assetid;
            this.scale = scale;
            this.precision = precision;
            this.name = name;
            this.issuer = issuer;
            this.percent = percent;
        }
    }

    /**
     * Compare two assets on name and assetid
     * @param a1
     * @param a2
     * @return -1, 0, 1 according to a1 <, =, or > a2
     */
    public static int assetCompare(Asset a1, Asset a2) {
        int res = Utility.compareStrings(a1.name, a2.name);
        if (res != 0)
            return res;
        return Utility.compareStrings(a1.assetid, a2.assetid);
    }

    /**
     * True if a1 < a2 according to assetCompare
     * @param a1
     * @param a2
     * @return
     */
    public static boolean assetLessp(Asset a1, Asset a2) {
        return assetCompare(a1, a2) < 0;
    }

    /**
     * Get a sorted list of all the assets in balances for the current account
     * @return
     * @throws ClientException
     */
    public Asset[] getAssets() throws ClientException {
        if (serverid == null)
            return null;
        String key = this.userBalanceKey();
        ClientDB.AccountDB acctDB = db.getAccountDB();
        String[] accts = acctDB.contents(key);
        HashMap<String, Asset> assets = new HashMap<String, Asset>(accts.length);
        for (String acct : accts) {
            for (String assetid : acctDB.contents(key, acct)) {
                if (assets.get(assetid) == null) {
                    Asset asset = this.getAsset(assetid);
                    if (asset != null)
                        assets.put(assetid, asset);
                }
            }
        }
        Asset[] res = new Asset[assets.size()];
        int i = 0;
        for (Asset asset : assets.values())
            res[i++] = asset;
        Arrays.sort(res, new Comparator<Asset>() {
            public int compare(Asset a1, Asset a2) {
                return assetCompare(a1, a2);
            }
        });
        return res;
    }

    /**
     * Get an asset.
     * @param assetid The ID of the asset
     * @param forceserver true to force asking the server
     * @return
     * @throws ClientException
     */
    public Asset getAsset(String assetid, boolean forceserver) throws ClientException {
        this.requireCurrentServer();
        ClientDB.AccountDB acctDB = db.getAccountDB();
        String key = this.assetKey();
        Parser.Dict args;
        if (forceserver) {
            args = this.getAssetInternal(assetid, acctDB, key);
        } else {
            String msg = acctDB.get(key, assetid);
            args = this.unpackServermsg(msg);
        }
        Parser.DictList reqs = (Parser.DictList) args.get(T.UNPACK_REQS_KEY);
        args = (Parser.Dict) args.get(T.MSG);
        String percent = null;
        String issuer = null;
        if (reqs.size() > 1) {
            Parser.Dict req = (Parser.Dict) reqs.get(1);
            Parser.Dict args1 = (Parser.Dict) this.matchServerReq(req, T.ATSTORAGE).get(T.MSG);
            issuer = args1.stringGet(T.CUSTOMER);
            percent = args1.stringGet(T.PERCENT);
        }
        return new Asset(args.stringGet(T.CUSTOMER), assetid, args.stringGet(T.SCALE), args.stringGet(T.PRECISION),
                args.stringGet(T.ASSETNAME), issuer, percent);
    }

    /**
     * Get an Asset from the local client database
     * @param assetid The ID of the asset
     * @return
     * @throws ClientException
     */
    public Asset getAsset(String assetid) throws ClientException {
        return getAsset(assetid, false);
    }

    /**
     * Get an asset from the server, store it in the database, and return its parsed dictionary
     * @param assetid The ID of the asset to get
     * @param acctDB The account database
     * @param key The key to the asset directory in the account database
     * @return
     * @throws ClientException
     */
    public Parser.Dict getAssetInternal(String assetid, ClientDB.AccountDB acctDB, String key)
            throws ClientException {
        String req = this.getreq();
        final String msg = this.sendmsg(serverid, req, assetid);
        Parser.Dict args;
        boolean verifySigs = parser.getVerifySigs();
        parser.setVerifySigs(true);
        try {
            args = this.unpackServermsg(msg, T.ATASSET);
        } finally {
            parser.setVerifySigs(verifySigs);
        }
        Parser.Dict msgargs = (Parser.Dict) args.get(T.MSG);
        if (!(msgargs.stringGet(T.REQUEST).equals(T.ASSET) && msgargs.stringGet(T.SERVERID).equals(serverid)
                && msgargs.stringGet(T.ASSET).equals(assetid))) {
            throw new ClientException("Server wrapped wrong object with @asset");
        }
        acctDB.put(key, assetid, msg);
        return args;
    }

    public Asset addAsset(String scale, String precision, String assetname) throws ClientException {
        return this.addAsset(scale, precision, assetname, null);
    }

    /**
     * A HashMap that maps String keys to String values
     * @author billstclair
     */
    public static class StringMap extends HashMap<String, String> {
        private static final long serialVersionUID = -7751573715299890864L;

        /**
         * Default constructor
         */
        public StringMap() {
            super();
        }

        /**
         * Constructor that initializes with a list of keys and values
         * @param keysAndValues
         */
        public StringMap(String... keysAndValues) {
            super();
            int len = keysAndValues.length;
            for (int i = 0; i < len; i++) {
                this.put(keysAndValues[i], keysAndValues[i + 1]);
            }
        }
    }

    /**
     * A Hashmap that maps String keys to StringMap values
     * @author billstclair
     */
    public static class StringMapMap extends HashMap<String, StringMap> {
        private static final long serialVersionUID = 1249880246491576097L;

        /**
         * Default constructor
         */
        public StringMapMap() {
            super();
        }

        /**
         * Constructor that initializes wiht a key/value pair
         * @param key
         * @param stringMap
         */
        public StringMapMap(String key, StringMap value) {
            super();
            this.put(key, value);
        }

        /**
         * Get an element, creating it if it's not already there
         */
        public StringMap getInited(String key) {
            StringMap res = this.get(key);
            if (res == null) {
                res = new StringMap();
                this.put(key, res);
            }
            return res;
        }
    }

    public Asset addAsset(String scale, String precision, String assetname, String percent) throws ClientException {
        this.requireCurrentServer();
        String assetid = Utility.assetid(id, scale, precision, assetname);
        String time = this.getTime();
        Fee tranfee = this.getFees().tranfee;
        String tokenid = tranfee.assetid;
        String balancehash = null;
        String msg = this.custmsg(T.ASSET, serverid, assetid, scale, precision, assetname);
        String storage = null;
        boolean nonserverp = !id.equals(serverid);
        String bal1 = null;
        if (nonserverp) {
            Balance b1 = this.getBalance(tokenid);
            if (b1 == null)
                throw new ClientException("No token balance");
            bal1 = b1.amount;
        }
        Asset oldasset = null;
        try {
            oldasset = this.getAsset(assetid, true);
        } catch (Exception e) {
        }
        String bal2 = null;
        StringMap mainbals = new StringMap();
        StringMapMap acctbals = new StringMapMap(T.MAIN, mainbals);
        ClientDB.AccountDB accountDB = db.getAccountDB();

        if (oldasset != null && (Utility.isBlank(percent) ? Utility.isBlank(oldasset.percent)
                : (id.equals(oldasset.issuer) && percent.equals(oldasset.percent)))) {
            // Init unless we have a balance in this asset
            boolean needinit = true;
            for (String acct : accountDB.contents(this.userBalanceKey())) {
                if (accountDB.get(this.userBalanceKey(acct), assetid) != null) {
                    needinit = false;
                    break;
                }
            }
            if (needinit)
                this.forceinit();
            return oldasset;
        }
        if (nonserverp) {
            String tokens = oldasset != null ? "1" : "2";
            boolean ispos = bcm.compare(bal1, "0") >= 0;
            bal1 = bcm.subtract(bal1, tokens);
            if (ispos && bcm.compare(bal1, "0") < 0) {
                throw new ClientException(oldasset == null ? "You need 2 usage tokens to create a new asset"
                        : "You need 1 usage token to update an asset");
            }
            bal1 = this.custmsg(T.BALANCE, serverid, time, tokenid, bal1);
        }
        if (oldasset == null)
            bal2 = this.custmsg(T.BALANCE, serverid, time, assetid, "-1");
        if (bal1 != null)
            mainbals.put(tokenid, bal1);
        if (bal2 != null)
            mainbals.put(assetid, bal2);
        if (nonserverp)
            balancehash = this.balancehashmsg(time, acctbals);

        if (!Utility.isBlank(percent)) {
            if (!Utility.isNumeric(percent))
                throw new ClientException("Percent must be numeric");
            storage = this.custmsg(T.STORAGE, serverid, time, assetid, percent);
            msg += "." + storage;
        }
        if (bal1 != null)
            msg += "." + bal1;
        if (bal2 != null)
            msg += "." + bal2;
        if (balancehash != null)
            msg += "." + balancehash;

        msg = server.process(msg);

        Parser.DictList reqs;
        try {
            reqs = parser.parse(msg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }

        String gotbal1 = null;
        String gotbal2 = null;
        String gotstorage = null;
        for (Parser.Dict req : reqs) {
            Parser.Dict args = this.matchServerReq(req);
            String reqmsg = Parser.getParseMsg(req);
            String m = Parser.getParseMsg((Parser.Dict) args.get(T.MSG)).trim();
            if (m.equals(bal1))
                gotbal1 = reqmsg;
            if (m.equals(bal2))
                gotbal2 = reqmsg;
            if (m.equals(storage))
                gotstorage = reqmsg;
        }
        if ((bal1 != null && gotbal1 == null) || (bal2 != null && gotbal2 == null)) {
            throw new ClientException("While adding asset: missing returned balance from server");
        }
        if (!Utility.isBlank(percent) && gotstorage == null) {
            throw new ClientException("While adding asset: storage fee not returned from server");
        }

        // All is well. Commit the balance changes
        String key = this.userBalanceKey(T.MAIN);
        if (bal1 != null)
            accountDB.put(key, tokenid, gotbal1);
        if (bal2 != null)
            accountDB.put(key, assetid, gotbal2);

        return this.getAsset(assetid, true);
    }

    public static class Fee {
        String type;
        String assetid;
        String assetname;
        String amount;
        String formattedAmount;

        public Fee() {
        }

        public Fee(String type, String assetid, String assetname, String amount, String formattedAmount) {
            this.type = type;
            this.assetid = assetid;
            this.assetname = assetname;
            this.amount = amount;
            this.formattedAmount = formattedAmount;
        }
    }

    /**
     * Compare two fees on assetname and type
     * @param f1
     * @param f2
     * @return -1, 0, 1 according to f1 <, =, or > f2
     */
    public static int feeCompare(Fee f1, Fee f2) {
        int res = Utility.compareStrings(f1.assetname, f2.assetname);
        if (res != 0)
            return res;
        return Utility.compareStrings(f1.type, f2.type);
    }

    /**
     * True if f1 < f2 according to assetCompare
     * @param f1
     * @param f2
     * @return
     */
    public static boolean feeLessp(Fee f1, Fee f2) {
        return feeCompare(f1, f2) < 0;
    }

    /**
     * Package up the return values from getFees()
     * @author billstclair
     */
    public static class Fees {
        public Fee tranfee;
        public Fee regfee;
        public Fee[] others;

        public Fees() {
            super();
        }

        public Fees(Fee tranfee, Fee regfee, Fee[] others) {
            this.tranfee = tranfee;
            this.regfee = regfee;
            this.others = others;
        }
    }

    /**
     * Return the fees for the logged-in server.
     * @return At least two elements. First element is tranfee, second is regfee, rest are other fees
     */
    public Fees getFees() throws ClientException {
        return this.getFees(false);
    }

    /**
     * Turn a fee message into a Fee instance
     * @param msg
     * @param type
     * @return
     * @throws ClientException
     */
    public Fee decodeFee(String msg, String type) throws ClientException {
        Parser.Dict args = this.unpackServermsg(msg, type);
        String assetid = args.stringGet(T.ASSET);
        Asset asset = this.getAsset(assetid);
        String amount = args.stringGet(T.AMOUNT);
        return new Fee(type.equals(T.FEE) ? args.stringGet(T.OPERATION) : type, assetid, asset.name, amount,
                formatAssetValue(amount, asset));
    }

    /**
     * Return the fees for the logged-in server.
     * @param reload true to reload fees from server
     * @return
     */
    public Fees getFees(boolean reload) throws ClientException {
        this.requireCurrentServer();
        String msg;
        if (!reload)
            msg = this.tranfee();
        else
            msg = this.getFeesInternal();
        Fee tranfee = this.decodeFee(msg, T.TRANFEE);
        msg = this.regfee();
        Fee regfee = null;
        if (!Utility.isBlank(msg))
            regfee = this.decodeFee(msg, T.REGFEE);
        Parser.DictList reqs;
        try {
            reqs = parser.parse(this.otherfees());
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        Fee[] others = new Fee[reqs.size()];
        int i = 0;
        for (Parser.Dict req : reqs) {
            Parser.Dict args = this.matchServerReq(req);
            String assetid = args.stringGet(T.ASSET);
            Asset asset = this.getAsset(assetid, true);
            String amount = args.stringGet(T.AMOUNT);
            others[i++] = new Fee(args.stringGet(T.OPERATION), assetid, asset.name, amount,
                    this.formatAssetValue(amount, asset));
        }

        return new Fees(tranfee, regfee, others);
    }

    private String getFeesInternal() throws ClientException {
        String req = this.getreq();
        String msg = this.sendmsg(T.GETFEES, serverid, req);
        Parser.DictList reqs;
        try {
            reqs = parser.parse(msg, true);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        String tranmsg = null;
        String regmsg = null;
        StringMap feemap = new StringMap();

        for (Parser.Dict feereq : reqs) {
            Parser.Dict args = this.matchServerReq(feereq);
            String request = args.stringGet(T.REQUEST);
            String feemsg = Parser.getParseMsg(feereq);
            if (T.TRANFEE.equals(request))
                tranmsg = feemsg;
            else if (T.REGFEE.equals(request))
                regmsg = feemsg;
            else if (T.FEE.equals(request)) {
                String operation = args.stringGet(T.OPERATION);
                String fees = feemap.get(operation);
                if (fees != null)
                    fees += ".'" + feemsg;
                else
                    fees = feemsg;
                feemap.put(operation, feemsg);
            }
        }

        if (tranmsg == null)
            throw new ClientException("No tranfee from getfees request");
        ClientDB.ServerDB serverDB = db.getServerDB();
        serverDB.put(serverid, T.TRANFEE, tranmsg);
        serverDB.put(serverid, T.REGFEE, regmsg);
        String dir = serverid + '.' + T.FEE;
        String[] operations = serverDB.contents(dir);
        StringMap opmap = new StringMap();
        for (String op : operations)
            opmap.put(op, op);
        Set<String> keys = feemap.keySet();
        for (String key : keys) {
            String value = feemap.get(key);
            opmap.remove(key);
            serverDB.put(dir, key, value);
        }
        keys = opmap.keySet();
        for (String op : keys)
            serverDB.put(dir, op, null);
        return tranmsg;
    }

    /**
     * Set the server transaction fees. If none of the Fee instances is of type
     * T.TRANFEE or T.REGFEE, the server defaults will be used for those fees.
     * The tranfee and regfee instances must use tokenid as assetid. null will
     * be interpreted as tokenid. 
     * @param fees
     * @return result of getfees(true) after setting the fees
     * @throws ClientException
     */
    public Fees setFees(Fee... fees) throws ClientException {
        this.requireCurrentServer();
        if (!id.equals(serverid)) {
            throw new ClientException("Only the server can set fees");
        }
        String time = this.getTime();
        String tokenid = this.getFees().tranfee.assetid;
        String tranmsg = null;
        String regmsg = null;
        String othersMsg = null;
        int count = 0;
        for (Fee fee : fees) {
            count++;
            String type = fee.type;
            boolean isTranfee = type.equals(T.TRANFEE);
            boolean isRegfee = type.equals(T.REGFEE);
            String amount = fee.amount;
            String assetid = fee.assetid;
            if (assetid == null)
                assetid = tokenid;
            Asset asset = this.getAsset(assetid);
            if (amount == null) {
                String formattedAmount = fee.formattedAmount;
                if (formattedAmount != null)
                    amount = this.unformatAssetValue(formattedAmount, asset);
            }
            if (amount == null || (Integer.parseInt(amount) < ((isTranfee || isRegfee) ? 0 : 1))) {
                throw new ClientException("Fee amount not a positive integer: " + amount);
            }
            if (isTranfee) {
                if (tranmsg != null)
                    throw new ClientException("Only one tranfee allowed");
                if (!assetid.equals(tokenid))
                    throw new ClientException("tranfee must be in tokens");
                tranmsg = this.custmsg(T.TRANFEE, serverid, time, assetid, amount);
            } else if (isRegfee) {
                if (regmsg != null)
                    throw new ClientException("Only one regfee allowed");
                if (!assetid.equals(tokenid))
                    throw new ClientException("regfee must be in tokens");
                regmsg = this.custmsg(T.REGFEE, serverid, time, assetid, amount);
            } else {
                String msg = this.custmsg(T.FEE, serverid, time, type, assetid, amount);
                if (othersMsg == null)
                    othersMsg = msg;
                else
                    othersMsg += "." + msg;
            }
        }
        String setfeesmsg = this.custmsg(T.SETFEES, time, String.valueOf(count));
        String msg = setfeesmsg;
        if (tranmsg != null)
            msg += "." + tranmsg;
        if (regmsg != null)
            msg += "." + regmsg;
        if (othersMsg != null)
            msg += "." + othersMsg;

        String servermsg = server.process(msg);
        Parser.Dict args = this.unpackServermsg(servermsg);
        if (!setfeesmsg.equals(Parser.getParseMsg((Parser.Dict) args.get(T.MSG)))) {
            throw new ClientException("Returned message wasn't sent");
        }
        // All is well. Clear the database and reload
        db.getAccountDB().put(this.getServerProp(T.TRANFEE), null);
        return this.getFees(true);
    }

    public static class Balance {
        public String acct;
        public String assetid;
        public String assetname;
        public String amount;
        public String time;
        public String formattedAmount;

        public Balance() {
            super();
        }

        public Balance(String acct, String assetid, String assetname, String amount, String time,
                String formattedAmount) {
            this.acct = acct;
            this.assetid = assetid;
            this.amount = amount;
            this.time = time;
            this.formattedAmount = formattedAmount;
        }

        public int compareTo(Balance b) {
            int c = acctCompare(this.acct, b.acct);
            if (c != 0)
                return c;
            return this.assetname.compareTo(b.assetname);
        }
    }

    public static Comparator<Balance> balanceComparator = new Comparator<Balance>() {
        public int compare(Balance b1, Balance b2) {
            return b1.compareTo(b2);
        }
    };

    public static class BalanceMap extends HashMap<Balance, String> {
        private static final long serialVersionUID = 5854035155868986274L;

        public BalanceMap() {
            super();
        }
    }

    /**
     * Get the balance for a particular assetid in a particular account
     * @param assetid The assetid
     * @param acct The acct, null means the default account: T.MAIN
     * @param rawmap If non-null, store the raw balance message in rawmap.get(<balance>), where
     *        <balance> is the returned Balance instance.
     * @return
     */
    public Balance getBalance(String assetid, String acct, BalanceMap rawmap) throws ClientException {
        this.initServerAccts();
        return this.getBalanceInternal(assetid, acct, rawmap);
    }

    protected Balance getBalanceInternal(String assetid, String acct, BalanceMap rawmap) throws ClientException {
        if (acct == null)
            acct = T.MAIN;
        String[] btr = this.userBalanceAndTime(acct, assetid);
        if (btr == null)
            return null;
        String amount = btr[0];
        if (!Utility.isNumeric(amount, true)) {
            throw new ClientException("Non-numeric balance amount: " + amount);
        }
        Asset asset = this.getAsset(assetid);
        String formattedAmount = this.formatAssetValue(amount, asset);
        String assetname = asset.name;
        Balance res = new Balance(acct, assetid, assetname, amount, btr[1], formattedAmount);
        if (rawmap != null)
            rawmap.put(res, btr[2]);
        return res;
    }

    /**
     * Returns the balance for assetid in the main acct.
     * @param assetid
     * @return
     * @throws ClientException
     */
    public Balance getBalance(String assetid) throws ClientException {
        return this.getBalance(assetid, null, null);
    }

    /**
     * Return an array of arrays of Balance instances.
     * Each array of Balance instances is for one acct, in acct order, sorted by asset name.
     * @param assetids
     * @param accts null means [T.MAIN], zero-length means all accts
     * @param rawmap
     * @return
     * @throws ClientException
     */
    public Balance[][] getBalances(String[] assetids, String[] accts, BalanceMap rawmap) throws ClientException {
        ClientDB.AccountDB accountDB = db.getAccountDB();
        String key = this.userBalanceKey();
        if (accts == null)
            accts = new String[] { T.MAIN };
        if (accts.length == 0)
            accts = accountDB.contents(key);
        Vector<Balance[]> resv = new Vector<Balance[]>(accts.length);
        for (String acct : accts) {
            String[] ids = assetids;
            if (ids == null)
                ids = accountDB.contents(key, acct);
            if (ids == null)
                continue;
            Vector<Balance> balsv = new Vector<Balance>(ids.length);
            for (String id : ids) {
                Balance bal = this.getBalanceInternal(id, acct, rawmap);
                if (bal != null)
                    balsv.add(bal);
            }
            if (balsv.size() > 0) {
                Balance[] bals = balsv.toArray(new Balance[balsv.size()]);
                Arrays.sort(bals, balanceComparator);
                resv.add(bals);
            }
        }
        if (resv.size() == 0)
            return null;
        return resv.toArray(new Balance[resv.size()][]);
    }

    /**
     * Return all the balances for assetid for all accts
     * @param assetid
     * @return
     * @throws ClientException
     */
    public Balance[] getBalances(String assetid) throws ClientException {
        Balance[][] bals = this.getBalances(new String[] { assetid }, new String[0], null);
        int len = bals.length;
        Balance[] res = new Balance[len];
        for (int i = 0; i < len; i++) {
            res[i] = bals[i][0];
        }
        return res;
    }

    public static class Fraction {
        public String assetid;
        public String assetname;
        public String amount;
        public String scale;

        public Fraction() {
            super();
        }

        public Fraction(String assetid, String assetname, String amount, String scale) {
            this.assetid = assetid;
            this.assetname = assetname;
            this.amount = amount;
            this.scale = scale;
        }
    }

    /**
     * 
     * Return the Fraction record for the given assetid, or null if there is none
     * @param assetid
     * @return
     * @throws ClientException
     */
    public Fraction getFraction(String assetid) throws ClientException {
        return this.getFraction(assetid, null);
    }

    /**
     * Return the Fraction record for the given assetid, or null if there is none
     * If rawmap is non-null, store the raw fraction message string in rawmap.get(assetid)
     * @param assetid
     * @param rawmap
     * @return
     */
    public Fraction getFraction(String assetid, StringMap rawmap) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        return getFractionInternal(assetid, rawmap);
    }

    protected Fraction getFractionInternal(String assetid, StringMap rawmap) throws ClientException {
        String key = this.userFractionKey();
        String msg = db.getAccountDB().get(key, assetid);
        if (msg == null)
            return null;
        Parser.Dict args = (Parser.Dict) this.unpackServermsg(msg, T.ATFRACTION).get(T.MSG);
        String fraction = args.stringGet(T.AMOUNT);
        Asset asset = this.getAsset(assetid);
        String scale = asset.scale;
        String assetname = asset.name;
        Fraction res = new Fraction(assetid, assetname, fraction, scale);
        if (rawmap != null)
            rawmap.put(assetid, msg);
        return res;
    }

    /**
     * Returns all the fractional balances
     * @return
     * @throws ClientException
     */
    public Fraction[] getFractions() throws ClientException {
        return this.getFractions(null);
    }

    /**
     * Returns all the fractions balances. If rawmap is non-null, stores the raw message strings there,
     * indexed by their assetids.
     * @param rawmap
     * @return
     * @throws ClientException
     */
    public Fraction[] getFractions(StringMap rawmap) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        String[] assetids = db.getAccountDB().contents(this.userFractionKey());
        int len = assetids.length;
        Fraction[] res = new Fraction[len];
        for (int i = 0; i < len; i++) {
            res[i] = this.getFractionInternal(assetids[i], rawmap);
        }
        return res;
    }

    public static class BalanceAndFraction extends Balance {
        public String fraction;

        public BalanceAndFraction() {
            super();
        }

        public BalanceAndFraction(String time, String assetid, String assetname, String amount,
                String formattedAmount, String fraction) {
            this.time = time;
            this.assetid = assetid;
            this.assetname = assetname;
            this.amount = amount;
            this.formattedAmount = formattedAmount;
            this.fraction = fraction;
        }
    }

    /**
     * Get the storagefee balance for a particular assetid.
     * Return null if there is no balance for that asset.
     * @param assetid
     * @throws ClientException
     */
    public BalanceAndFraction getStorageFee(String assetid) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        return this.getStorageFeeInternal(assetid);
    }

    public BalanceAndFraction getStorageFeeInternal(String assetid) throws ClientException {
        String key = this.userStorageFeeKey();
        String msg = db.getAccountDB().get(key, assetid);
        if (msg == null)
            return null;
        Parser.Dict args = this.unpackServermsg(msg, T.STORAGEFEE);
        String time = args.stringGet(T.TIME);
        if (!assetid.equals(args.stringGet(T.ASSET))) {
            throw new ClientException("Storage fee record has wrong assetid");
        }
        String amount = args.stringGet(T.AMOUNT);
        String[] fractionBuf = new String[] { "0" };
        Asset asset = this.getAsset(assetid);
        String percent = asset.percent;
        amount = Utility.normalizeBalance(amount, fractionBuf, Utility.fractionDigits(percent));
        String fraction = fractionBuf[0];
        if (bcm.compare(amount, "0") == 0)
            return null;
        String formattedAmount = this.formatAssetValue(amount, asset);
        return new BalanceAndFraction(time, assetid, asset.name, amount, formattedAmount, fraction);
    }

    /**
     * Return storage fees for all assetids
     * @return
     * @throws ClientException
     */
    public BalanceAndFraction[] getStorageFees() throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        String key = this.userStorageFeeKey();
        String[] assetids = db.getAccountDB().contents(key);
        Vector<BalanceAndFraction> res = new Vector<BalanceAndFraction>();
        for (String assetid : assetids) {
            BalanceAndFraction baf = this.getStorageFee(assetid);
            if (baf != null)
                res.add(baf);
        }
        return res.toArray(new BalanceAndFraction[res.size()]);
    }

    public static class ValidationError extends ClientException {
        private static final long serialVersionUID = 2195331726119216616L;

        public ValidationError(String message) {
            super(message);
        }

        public ValidationError(Exception e) {
            super(e);
        }
    }

    /**
     * Throw a ValidationError
     * @param message
     * @throws ValidationError
     */
    public static void validationError(String message) throws ValidationError {
        throw new ValidationError(message);
    }

    public static class SpendFees {
        public String transactionFee;
        public String storageFee;

        public SpendFees(String transactionFee, String storageFee) {
            this.transactionFee = transactionFee;
            this.storageFee = storageFee;
        }
    }

    /**
     * Initiate a spend.
     * @param toid The ID of the recipient of the spend. May be T.COUPON to generate a coupon.
     *             In that case, the coupon itself can be fetched with getCoupon()
     * @param assetid The id of the asset to spend
     * @param formattedAmount The formatted amount to spend
     * @param acct The source sub-account. null means T.MAIN.
     * @param note A note to attach to the spend. May be null. 
     * @param toacct The destination sub-account. If non-null, toid should be the logged in user id, and the
     *               spend will be a transfer from one sub-account to another.            
     * @return
     * @throws ClientException
     */
    public SpendFees spend(String toid, String assetid, String formattedAmount, String acct, String note,
            String toacct) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        try {
            return this.spendInternal(toid, assetid, formattedAmount, acct, note, toacct);
        } catch (ValidationError e) {
            throw e;
        } catch (ClientException e) {
            this.reloadAsset(assetid);
            this.forceinit();
            return this.spendInternal(toid, assetid, formattedAmount, acct, note, toacct);
        }
    }

    /**
     * Send a commit message to the server and check and return the result;
     * @param time
     * @return
     * @throws ClientException
     */
    public String sendCommitMsg(String time) throws ClientException {
        String msg = this.custmsg(T.COMMIT, serverid, time);
        String servermsg = server.process(msg);
        Parser.DictList reqs;
        try {
            reqs = parser.parse(servermsg, true);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        Parser.Dict req = reqs.get(0);
        Parser.Dict args;
        try {
            args = this.matchServerReq(req, T.ATCOMMIT);
        } catch (ClientException e) {
            args = this.matchServerReq(req);
            String request = args.stringGet(T.REQUEST);
            throw new ClientException("Spend commit request returned unknown message type: " + request);
        }
        if (!msg.equals(Parser.getParseMsg((Parser.Dict) args.get(T.MSG)))) {
            throw new ClientException("Returned commit message doesn't match");
        }
        return msg;
    }

    /**
     * Do the work for spend()
     * @param toid
     * @param assetid
     * @param formattedAmount
     * @param acct
     * @param note
     * @param toacct
     * @return
     * @throws ClientException
     */
    protected SpendFees spendInternal(String toid, String assetid, String formattedAmount, String acct, String note,
            String toacct) throws ClientException {
        if (toid == null || assetid == null || formattedAmount == null) {
            validationError("toid, acct, assetid, and formattedAmount must all be non-null");
        }
        if (acct == null)
            acct = T.MAIN;
        if (toacct == null)
            toacct = T.MAIN;

        Asset asset = this.getAsset(assetid);
        String amount = this.unformatAssetValue(formattedAmount, asset);
        boolean useTwoPhaseCommit = !id.equals(serverid) && this.useTwoPhaseCommit();
        String lastTransaction = null;
        String oldamount, oldtime;
        String storagefee = "0";
        int digits = 0;
        BCMath pctbcm = null;
        String percent = null;
        String fraction = "0";
        String fractime, fracfee;
        String baseoldamount = "0";
        String newamount;
        String newtoamount = null;
        String oldtoamount = null;
        Fee tranfee = null;
        String tranfeeAsset = null;
        String tranfeeAmt = null;
        String feeBalance = null;
        boolean needFeeBalance = false;
        String operation = null;
        Fees fees = null;
        String[] buf = new String[1];
        StringMap feeAmounts = new StringMap();

        if (id.equals(toid) && acct.equals(toacct))
            validationError("Transfer from and to the same acct");
        if (serverid.equals(toid))
            validationError("Spends to the server are not allowed.");

        // Must get time before accessing balances, since getTime() may forceinit();
        String time = this.getTime();

        if (bcm.compare(amount, "0") < 0) {
            String bal = this.userBalance(acct, assetid);
            if (bcm.compare(bal, amount) != 0)
                validationError("Negative spends must be for the whole issuer balance");
        }

        String[] bt = this.userBalanceAndTime(acct, assetid);
        oldamount = bt[0];
        oldtime = bt[1];
        if (oldamount == null)
            oldamount = "0";
        else {
            if (!Utility.isNumeric(oldamount, true)) {
                validationError("Error getting balance for asset in acct " + acct + ": " + oldamount);
            }
            StorageInfo info = this.getClientStorageInfo(assetid);
            percent = info.percent;
            fraction = info.fraction;
            fractime = info.fractime;
            if (percent != null) {
                digits = Utility.fractionDigits(percent);
                pctbcm = new BCMath(digits);
                buf[0] = fraction;
                fracfee = Utility.storageFee(buf, fractime, time, percent, digits);
                fraction = buf[0];
                buf[0] = oldamount;
                storagefee = Utility.storageFee(buf, oldtime, time, percent, digits);
                oldamount = buf[0];
                storagefee = pctbcm.add(storagefee, fracfee);
                baseoldamount = oldamount;
                buf[0] = fraction;
                oldamount = Utility.normalizeBalance(oldamount, buf, digits);
                fraction = buf[0];
            }
        }

        newamount = bcm.subtract(oldamount, amount);
        if (bcm.compare(oldamount, "0") >= 0 && bcm.compare(newamount, "0") < 0) {
            if (id.equals(toid) && percent != null && bcm.compare(amount, baseoldamount) <= 0) {
                // User asked to transfer less than the whole amount, but the storage fee put it over.
                // Reduce amount to leave 0 in acct
                amount = oldamount;
                newamount = "0";
            }
        } else {
            validationError("Insufficient balance");
        }

        if (id.equals(toid)) {
            String totime, tofee;
            bt = this.userBalanceAndTime(toacct, assetid);
            oldtoamount = bt[0];
            totime = bt[1];
            if (percent != null && oldtoamount != null) {
                buf[0] = oldtoamount;
                tofee = Utility.storageFee(buf, totime, time, percent, digits);
                oldtoamount = buf[0];
                storagefee = pctbcm.add(storagefee, tofee);
            }
            newtoamount = bcm.add(oldtoamount == null ? "0" : oldtoamount, amount);
            if (percent != null) {
                buf[0] = fraction;
                newtoamount = Utility.normalizeBalance(newtoamount, buf, digits);
                fraction = buf[0];
            }
            if (oldtoamount != null && bcm.compare(oldtoamount, "0") < 0 && bcm.compare(newtoamount, "0") >= 0) {
                // This shouldn't happen
                validationError("asset out of balance on self-spend");
            }
        }

        if (!id.equals(serverid)) {
            fees = this.getFees();
            tranfee = fees.tranfee;
            tranfeeAsset = tranfee.assetid;
            operation = id.equals(toid) ? T.TRANSFER : T.SPEND;
            Vector<Fee> otherFees = new Vector<Fee>();
            for (Fee fee : fees.others) {
                if (!operation.equals(fee.type))
                    continue;
                if (!assetid.equals(fee.assetid))
                    continue;
                String issuer = asset.issuer;
                if (issuer == null)
                    issuer = asset.id;
                if (id.equals(issuer))
                    continue;
                otherFees.add(fee);
            }
            tranfeeAmt = id.equals(toid) ? (oldtoamount != null ? "0" : "1") : tranfee.amount;
            if (tranfeeAsset.equals(assetid) && acct.equals(T.MAIN)) {
                newamount = bcm.subtract(newamount, tranfeeAmt);
                for (Fee fee : otherFees) {
                    String feeamt = fee.amount;
                    newamount = bcm.subtract(newamount, feeamt);
                    feeAmounts.put(assetid, feeamt);
                }
                if (bcm.compare(oldamount, "0") >= 0 && bcm.compare(newamount, "0") < 0) {
                    validationError("Insufficient balance for transaction fees");
                }
            } else if (id.equals(toid) && tranfeeAsset.equals(assetid) && toacct.equals(T.MAIN)) {
                newtoamount = bcm.subtract(newtoamount, tranfeeAmt);
                for (Fee fee : otherFees) {
                    String feeamt = fee.amount;
                    newtoamount = bcm.subtract(newtoamount, feeamt);
                    feeAmounts.put(assetid, feeamt);
                }
                if (bcm.compare(newtoamount, oldtoamount) == 0) {
                    validationError("Transferring transaction fee to a new acct is silly");
                }
                if (bcm.compare(oldtoamount, "0") >= 0 && bcm.compare(newtoamount, "0") < 0) {
                    validationError("Insufficient balance for transaction fee");
                }
            } else {
                String oldFeeBalance = this.userBalance(T.MAIN, tranfeeAsset);
                String feeamt = "0";
                for (Fee fee : otherFees) {
                    feeamt = bcm.add(feeamt, fee.amount);
                }
                feeAmounts.put(tranfeeAsset, feeamt);
                if (!BCMath.isZero(tranfeeAmt) || !BCMath.isZero(feeamt)) {
                    feeBalance = bcm.subtract(oldFeeBalance, tranfeeAmt, feeamt);
                    needFeeBalance = true;
                    if (bcm.compare(oldFeeBalance, "0") >= 0 && bcm.compare(feeBalance, "0") < 0) {
                        validationError("Insufficient tokens for transaction fee");
                    }
                }
            }
            // Compute non-refundable fee amounts for other than the token asset
            if (!assetid.equals(tranfeeAsset)) {
                for (Fee fee : otherFees) {
                    String feeamt = fee.amount;
                    if (bcm.compare(newamount, feeamt) > 0) {
                        newamount = bcm.subtract(newamount, feeamt);
                    } else if (id.equals(toid) && bcm.compare(newtoamount, feeamt) > 0) {
                        newtoamount = bcm.subtract(newtoamount, feeamt);
                    } else {
                        validationError("Insufficient balance for nonrefundable fee");
                    }
                    feeAmounts.put(assetid, feeamt);
                }
            }
        }

        // Numbers are computed and validated. Create messages for the server. 
        String feeandbal = null;
        String feebal = null;
        String feemsg = null;
        String[] feeMsgs = null;
        String balance;
        String tobalance = null;
        String outboxhash = null;
        String balancehash = null;
        String storagefeemsg = null;
        String fracmsg = null;

        if (!Utility.isBlank(note) && !toid.equals(T.COUPON)) {
            String[] ids = id.equals(toid) ? new String[] { id } : new String[] { id, toid };
            note = Crypto.encryptNote(db.getPubkeyDB(), note, ids);
        }

        String spend = Utility.isBlank(note) ? this.custmsg(T.SPEND, serverid, time, toid, assetid, amount)
                : this.custmsg(T.SPEND, serverid, time, toid, assetid, amount, note);
        if (tranfeeAmt != null && !id.equals(toid)) {
            feemsg = this.custmsg(T.TRANFEE, serverid, time, tranfeeAsset, tranfeeAmt);
            feeandbal = feemsg;
        }
        if (needFeeBalance) {
            feebal = this.custmsg(T.BALANCE, serverid, time, tranfeeAsset, feeBalance);
            if (feeandbal != null)
                feeandbal += "." + feebal;
            else
                feeandbal = feebal;
        }
        balance = this.custmsg(T.BALANCE, serverid, time, assetid, newamount, acct);
        if (id.equals(toid)) {
            tobalance = this.custmsg(T.BALANCE, serverid, time, assetid, newtoamount, toacct);
        }
        if (!id.equals(serverid) && !id.equals(toid)) {
            outboxhash = this.outboxhashmsg(time, new String[] { spend }, null, useTwoPhaseCommit);
        }

        // Create fees messages
        Set<String> assetids = feeAmounts.keySet();
        int i = 0;
        for (String feeAssetid : assetids) {
            if (feeMsgs == null)
                feeMsgs = new String[assetids.size()];
            String feeAmount = feeAmounts.get(assetid);
            feeMsgs[i++] = this.custmsg(T.FEE, serverid, time, operation, feeAssetid, feeAmount);
        }

        // Compute balancehash
        if (!id.equals(serverid)) {
            StringMapMap acctbals = new StringMapMap();
            acctbals.getInited(acct).put(assetid, balance);
            if (feebal != null) {
                acctbals.getInited(T.MAIN).put(tranfeeAsset, feebal);
            }
            if (tobalance != null) {
                acctbals.getInited(toacct).put(assetid, tobalance);
            }
            balancehash = this.balancehashmsg(time, acctbals, useTwoPhaseCommit);
        }

        // Prepare storage fee related message components
        if (percent != null) {
            storagefeemsg = this.custmsg(T.STORAGEFEE, serverid, time, assetid, storagefee);
            fracmsg = this.custmsg(T.FRACTION, serverid, time, assetid, fraction);
        }

        // Prepare request to send to server
        String msg = spend;
        if (feeandbal != null)
            msg += '.' + feeandbal;
        msg += '.' + balance;
        if (tobalance != null)
            msg += '.' + tobalance;
        if (outboxhash != null)
            msg += '.' + outboxhash;
        if (balancehash != null)
            msg += '.' + balancehash;
        if (percent != null)
            msg += '.' + storagefeemsg + '.' + fracmsg;
        for (String fee : feeMsgs)
            msg += '.' + fee;

        // Send request to server and get response
        String servermsg = server.process(msg); // *** Here's the server call ***
        Parser.DictList reqs;
        try {
            reqs = parser.parse(servermsg, true);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }

        // Store all the sent messages as keys in a map
        StringMap msgs = new StringMap(spend, null, balance, null);
        String coupon = null;
        String encryptedCoupon = null;
        try {
            this.matchServerReq(reqs.get(0), T.ATSPEND);
        } catch (ClientException e) {
            Parser.Dict args = this.matchServerReq(reqs.get(0));
            String request = args.stringGet(T.REQUEST);
            throw new ClientException("Spend request returned unknown message type: " + request);
        }
        if (tobalance != null)
            msgs.put(tobalance, null);
        if (outboxhash != null)
            msgs.put(outboxhash, null);
        if (balancehash != null)
            msgs.put(balancehash, null);
        if (feeandbal != null) {
            if (feemsg != null)
                msgs.put(feemsg, null);
            if (feebal != null)
                msgs.put(feebal, null);
        }
        if (percent != null) {
            msgs.put(storagefeemsg, null);
            msgs.put(fracmsg, null);
        }
        for (String fee : feeMsgs)
            msgs.put(fee, null);

        // Validate the returned messages, saving the server-signed messages in the map
        Set<String> msgkeys = msgs.keySet();
        for (Parser.Dict req : reqs) {
            String onemsg = Parser.getParseMsg(req);
            Parser.Dict oneargs = this.matchServerReq(req);
            if (T.COUPONENVELOPE.equals(oneargs.stringGet(T.REQUEST))) {
                if (coupon != null)
                    throw new ClientException("Multiple coupons returned from server");
                coupon = onemsg;
                encryptedCoupon = oneargs.stringGet(T.ENCRYPTEDCOUPON);
            } else {
                String m = oneargs.stringGet(T.MSG).trim();
                if (!msgkeys.contains(m))
                    throw new ClientException("Returned message wasn't sent: " + m);
                if (msgs.get(m) != null)
                    throw new ClientException("Duplicated returned message: " + m);
                msgs.put(msg, onemsg);
            }
        }
        // Ensure that the server signed all messages
        for (String m : msgkeys) {
            msg = msgs.get(m);
            if (msg == null)
                throw new ClientException("Message not returned from spend: " + m);
        }

        // Do the second phase of the commit
        if (useTwoPhaseCommit)
            lastTransaction = this.sendCommitMsg(time);

        // All is well. Commit this baby
        ClientDB.AccountDB accountDB = db.getAccountDB();
        accountDB.put(this.userBalanceKey(acct), assetid, msgs.get(balance));
        if (tobalance != null)
            accountDB.put(this.userBalanceKey(toacct), assetid, msgs.get(tobalance));
        String key = this.userServerKey();
        if (outboxhash != null)
            accountDB.put(key, T.OUTBOXHASH, msgs.get(outboxhash));
        if (balancehash != null)
            accountDB.put(key, T.BALANCEHASH, msgs.get(balancehash));
        spend = msgs.get(spend);
        if (feeandbal != null) {
            spend += '.' + msgs.get(feemsg);
            if (feebal != null)
                accountDB.put(this.userBalanceKey(T.MAIN), tranfeeAsset, msgs.get(feebal));
        }
        if (coupon != null) {
            spend += '.' + coupon;
            this.coupon = encryptedCoupon;
        }
        if (!id.equals(toid) && !(id.equals(serverid)))
            accountDB.put(this.userOutboxKey(), time, spend);
        this.lastSpendTime = time;
        if (percent != null)
            accountDB.put(this.userFractionKey(), assetid, msgs.get(fracmsg));
        if (lastTransaction != null)
            accountDB.put(key, T.LASTTRANSACTION, lastTransaction);
        if (this.keepHistory)
            accountDB.put(this.userHistoryKey(), time, spend);

        // All done. Package up the fees and return them
        String feeamt = feeAmounts.get(assetid);
        return new SpendFees(feeamt == null ? null : this.formatAssetValue(feeamt, asset),
                storagefee == null ? null : this.formatAssetValue(storagefee, asset));
    }

    /**
     * Reload an asset from the server. Return true if the storage percent changed
     * @param assetid
     * @return
     */
    public boolean reloadAsset(String assetid) throws ClientException {
        Asset asset = this.getAsset(assetid);
        String percent = asset.percent;
        asset = this.getAsset(assetid, true);
        return !percent.equals(asset.percent);
    }

    /**
     * Reject a spend.
     * @param time The time for the spend in the outbox
     * @param note may be null to put no note in the reject message
     * @throws ClientException
     */
    public void spendReject(String time, String note) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        boolean needInit = true;
        try {
            this.spendRejectInternal(time, note);
            needInit = false;
        } finally {
            if (needInit)
                this.forceinit();
        }
    }

    protected void spendRejectInternal(String time, String note) throws ClientException {
        String toid = null;
        ClientDB.AccountDB accountDB = db.getAccountDB();
        String msg = accountDB.get(this.userOutboxKey(), time);
        if (msg == null)
            throw new ClientException("No outbox entry at time: " + time);
        Parser.DictList reqs;
        try {
            reqs = parser.parse(msg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        for (Parser.Dict req : reqs) {
            Parser.Dict args = this.matchServerReq(req);
            String request = args.stringGet(T.REQUEST);
            if (request == null) {
                throw new ClientException("Missing request in spend message");
            } else if (request.equals(T.ATSPEND)) {
                Parser.Dict innerReq = (Parser.Dict) args.get(T.MSG);
                toid = innerReq.stringGet(T.ID);
            } else if (request.equals(T.COUPONENVELOPE)) {
                String coupon = args.stringGet(T.ENCRYPTEDCOUPON);
                if (coupon != null) {
                    coupon = Crypto.RSAPrivkeyDecrypt(coupon, this.privkey);
                    this.redeem(coupon);
                    return;
                }
            }
        }
        if (note != null) {
            note = Crypto.encryptNote(db.getPubkeyDB(), note, id, toid);
            msg = this.custmsg(T.SPENDREJECT, serverid, time, id, note);
        } else {
            msg = this.custmsg(T.SPENDREJECT, serverid, time, id);
        }
        String servermsg = server.process(msg);
        Parser.Dict args = this.unpackServermsg(servermsg, T.INBOX);
        time = args.stringGet(T.TIME);
        Parser.Dict args2 = (Parser.Dict) args.get(T.MSG);
        String msg2 = Parser.getParseMsg(args2);
        if (!msg.trim().equals(msg2.trim())) {
            throw new ClientException("Server return doesn't wrap request");
        }
        accountDB.put(this.userInboxKey(), time, servermsg);
    }

    /**
     * Return the times for saved history items, sorted arithmetically by time
     * @return
     */
    public String[] getHistoryTimes() throws ClientException {
        this.requireCurrentServer();
        String[] res = db.getAccountDB().contents(this.userHistoryKey());
        Arrays.sort(res, bcm.getComparator());
        return res;
    }

    /**
     * Get the history items for the given time, all parsed and matched.
     * The Parser.Dict instances may have three added properties:
     *   T.ATREQUEST: the outer wrapper's T.REQUEST
     *   T.ASSETNAME: the name for messages with a T.ASSET and T.AMOUNT
     *   T.FORMATTEDAMOUNT: the formatted amount for messages with a T.ASSET and T.AMOUNT
     * @param time
     * @return null if there are no history items at time
     * @throws ClientException
     */
    public Parser.DictList getHistoryItems(String time) throws ClientException {
        this.requireCurrentServer();
        String msg = db.getAccountDB().get(this.userHistoryKey(), time);
        if (msg == null)
            return null;
        Parser.DictList reqs;
        try {
            reqs = parser.parse(msg);
            Parser.DictList res = new Parser.DictList();
            for (Parser.Dict req : reqs) {
                Parser.Dict args = parser.matchPattern(req);
                Parser.Dict inner = (Parser.Dict) (args.get(T.MSG));
                if (inner != null) {
                    String atrequest = args.stringGet(T.REQUEST);
                    args = parser.matchPattern(inner);
                    args.put(T.ATREQUEST, atrequest);
                    String assetid = args.stringGet(T.ASSET);
                    String amount = args.stringGet(T.AMOUNT);
                    if (assetid != null && amount != null) {
                        Asset asset = this.getAsset(assetid);
                        args.put(T.ASSETNAME, asset.name);
                        args.put(T.FORMATTEDAMOUNT, this.formatAssetValue(amount, asset, false));
                    }
                }
                res.add(args);
            }
            return res;
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
    }

    /**
     * Remove the history message at time
     * @param time
     */
    public void removeHistoryItem(String time) throws ClientException {
        this.requireCurrentServer();
        db.getAccountDB().put(this.userHistoryKey(), time, null);
    }

    /**
     * Return the last coupon resulting from a spend.
     * Clear the coupon store, so you can only get the coupon once.
     * @return
     */
    public String getCoupon() throws ClientException {
        String coupon = this.coupon;
        if (coupon == null)
            return null;
        this.coupon = null;
        return Crypto.RSAPrivkeyDecrypt(coupon, this.privkey);
    }

    /**
     * Return value from getInbox();
     * @author billstclair
     *
     */
    public static class Inbox {
        /**
         * T.SPEND, T.SPENDACCEPT, or T.SPENDREJECT
         */
        String request;
        /**
         * The ID of the sender of the inbox entry
         */
        String id;
        /**
         * The time in the server signing of the request
         */
        String time;
        /**
         * The time in the sender's message
         */
        String msgtime;
        /**
         * THe assetid of the spend
         */
        String assetid;
        /**
         * The name of the asset being spent
         */
        String assetname;
        /**
         * The amount being spend
         */
        String amount;
        /**
         * Formatted representation of the amount being spend
         */
        String formattedAmount;
        /**
         * The note that came from the sender
         */
        String note;
        /**
         * Used by the UI to stash replies
         */
        String reply;
        /**
         * Other parts of the spend message, e.g. fees
         */
        Inbox[] items;

        public Inbox() {
            super();
        }

        public Inbox(String request, String id, String time, String msgtime, String assetid, String assetname,
                String amount, String formattedAmount, String note) {
            this.request = request;
            this.id = id;
            this.time = time;
            this.msgtime = msgtime;
            this.assetid = assetid;
            this.assetname = assetname;
            this.amount = amount;
            this.formattedAmount = formattedAmount;
            this.note = note;
        }
    }

    /**
     * A HashMap to map Inbox instances to raw message strings
     * @author billstclair
     *
     */
    public static class InboxRawmap extends HashMap<Inbox, String> {
        private static final long serialVersionUID = 7183634028262373579L;

        public InboxRawmap() {
            super();
        }
    }

    /**
     * Get the inbox contents, sorted by Inbox.time
     * @return
     * @throws ClientException
     */
    public Inbox[] getInbox() throws ClientException {
        return getInbox(null);
    }

    /**
     * Get the inbox contents, sorted by Inbox.time
     * @param rawmap If non-null will map each Inbox instance to its raw message string
     * @return
     * @throws ClientException
     */
    public Inbox[] getInbox(InboxRawmap rawmap) throws ClientException {
        this.requireCurrentServer();
        this.initServerAccts();
        return this.getInboxInternal(rawmap);
    }

    /**
     * Do the work for getInbox()
     * @param rawmap
     * @return
     * @throws ClientException
     */
    protected Inbox[] getInboxInternal(InboxRawmap rawmap) throws ClientException {
        String key = this.userInboxKey();
        ClientDB.AccountDB accountDB = db.getAccountDB();
        this.syncInbox();
        Vector<Inbox> resv = new Vector<Inbox>();
        for (String time : accountDB.contents(key)) {
            String msg = accountDB.get(key, time);
            Inbox lastItem = null;
            Vector<Inbox> lastItems = null;
            Parser.DictList reqs;
            try {
                reqs = parser.parse(msg);
            } catch (Parser.ParseException e) {
                throw new ClientException(e);
            }
            for (Parser.Dict req : reqs) {
                Parser.Dict args = this.matchServerReq(req);
                String argstime = args.stringGet(T.TIME);
                if (!(argstime == null || time.equals(argstime))) {
                    throw new ClientException("Inbox message timestamp mismatch");
                }
                args = (Parser.Dict) args.get(T.MSG);
                String request = args.stringGet(T.REQUEST);
                String id = args.stringGet(T.CUSTOMER);
                String msgtime = args.stringGet(T.TIME);
                String note = args.stringGet(T.NOTE);
                String assetid = null;
                String amount = null;
                String assetname = null;
                String formattedAmount = null;
                if (request.equals(T.SPEND) || request.equals(T.TRANFEE)) {
                    assetid = args.stringGet(T.ASSET);
                    amount = args.stringGet(T.AMOUNT);
                    Asset asset = null;
                    boolean incnegs = false;
                    try {
                        asset = this.getAsset(assetid);
                    } catch (Exception e) {
                    }
                    if (asset != null) {
                        assetname = asset.name;
                        incnegs = !serverid.equals(args.stringGet(T.CUSTOMER));
                        formattedAmount = this.formatAssetValue(amount, asset, incnegs);
                    }
                } else if (request.equals(T.SPENDACCEPT) || request.equals(T.SPENDREJECT)) {
                    // To do: pull in data from outbox to get amounts
                } else {
                    throw new ClientException("Bad request in inbox: " + request);
                }
                try {
                    note = Crypto.decryptNote(this.id, this.privkey, note);
                } catch (Exception e) {
                }
                Inbox item = new Inbox(request, id, time, msgtime, assetid, assetname, amount, formattedAmount,
                        note);
                if (request.equals(T.SPEND)) {
                    resv.add(item);
                    lastItem = item;
                } else if (request.equals(T.TRANFEE)) {
                    if (lastItem == null)
                        throw new ClientException("tranfee without matching spend");
                    if (lastItems == null)
                        lastItems = new Vector<Inbox>();
                    lastItems.add(item);
                } else {
                    resv.add(item);
                    if (lastItem != null) {
                        if (lastItems != null) {
                            lastItem.items = lastItems.toArray(new Inbox[lastItems.size()]);
                            lastItems = null;
                        }
                        lastItem = null;
                    }
                }
                if (rawmap != null)
                    rawmap.put(item, msg);
            }
        }
        Inbox[] res = resv.toArray(new Inbox[resv.size()]);
        Arrays.sort(res, new Comparator<Inbox>() {
            public int compare(Inbox i1, Inbox i2) {
                return Client.this.bcm.compare(i1.time, i2.time);
            }
        });
        return res;
    }

    /**
     * Sync inbox with server.
     * Assumes that there IS a current user and server.
     * @throws ClientException
     */
    protected void syncInbox() throws ClientException {
        try {
            this.syncInboxInternal();
        } catch (ClientException e) {
            this.forceinit();
            this.syncInboxInternal();
        }
    }

    /**
     * Do the work for syncInbox()
     * @throws ClientException
     */
    protected void syncInboxInternal() throws ClientException {
        String msg = this.custmsg(T.GETINBOX, serverid, this.getreq());
        String servermsg = server.process(msg);
        Parser.DictList reqs;
        try {
            reqs = parser.parse(servermsg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        StringMap inbox = new StringMap();
        Vector<String> times = new Vector<String>();
        StringMap storageFees = new StringMap();
        String lastTime = null;
        for (Parser.Dict req : reqs) {
            Parser.Dict args = this.matchServerReq(req);
            servermsg = Parser.getParseMsg(req);
            String request = args.stringGet(T.REQUEST);
            if (request == null) {
                throw new ClientException("No request in server message.");
            } else if (request.equals(T.ATGETINBOX)) {
                String retmsg = Parser.getParseMsg((Parser.Dict) args.get(T.MSG));
                if (!(retmsg.trim().equals(msg.trim()))) {
                    throw new ClientException("getinbox return doesn't wrap message sent");
                }
                lastTime = null;
            } else if (request.equals(T.INBOX)) {
                String time = args.stringGet(T.TIME);
                if (inbox.get(time) != null) {
                    throw new ClientException("getinbox return included multiple entries for time: " + time);
                }
                inbox.put(time, servermsg);
            } else if (request.equals(T.ATTRANFEE)) {
                if (lastTime == null) {
                    throw new ClientException("In getinbox return: @tranfee not after inbox");
                }
                inbox.put(lastTime, inbox.get(lastTime) + '.' + servermsg);
                lastTime = null;
            } else if (request.equals(T.TIME)) {
                times.add(args.stringGet(T.TIME));
                lastTime = null;
            } else if (request.equals(T.STORAGEFEE)) {
                String assetid = args.stringGet(T.ASSET);
                storageFees.put(assetid, servermsg);
            } else if (!request.equals(T.COUPONNUMBERHASH)) {
                throw new ClientException("Unknown request in getinbox return: " + request);
            }
        }
    }

    /*
        
        (let* ((key (userinboxkey client))
       (keys (db-contents db key)))
          (dolist (time keys)
    (let ((inmsg (gethash time inbox)))
      (if inmsg
        (let ((msg (db-get db key time)))
          (unless (equal msg inmsg)
            (error "Inbox mismatch at time: ~s" time))
          (remhash time inbox))
        (setf (db-get db key time) nil))))
          (loop
     for time being the hash-key using (hash-value msg) of inbox
     do
       (setf (db-get db key time) msg)))
        
        (let ((key (userstoragefeekey client)))
          (dolist (assetid (db-contents db key))
    (unless (gethash assetid storagefees)
      (setf (db-get db key assetid) nil)))
          (loop
     for assetid being the hash-key using (hash-value storagefee) of storagefees
     do
       (setf (db-get db key assetid) storagefee)))
        
        (when times
          (setf (db-get db (usertimekey client)) (apply #'implode "," times)))))
        
    (defmethod getinboxignored ((client client))
      "Return a list of the timestamps that were ignored in the last processinbox"
      (explode #\, (db-get (db client) (userinboxignoredkey client))))
        
    (defmethod (setf getinboxignored) (list (client client))
      "Return a list of the timestamps that were ignored in the last processinbox"
      (setf (db-get (db client) (userinboxignoredkey client))
    (apply #'implode #\, list))
      list)
        
    (defstruct process-inbox
      time                                  ;timestamp in the inbox
      request                               ;$SPENDACCEPT, SPENDREJECT, or nil
      note                                  ;note for accept or reject
      acct)                                 ;Account into which to transfer
        
    (defmethod processinbox ((client client) directions)
      "Process the inbox contents.
       DIRECTIONS is a list of PROCESS-INBOX instances."
      (let ((db (db client))
    (need-init-p t))
        (require-current-server client "In processinbox(): Server not set")
        (init-server-accts client)
        
        (unwind-protect
     (with-db-lock (db (userreqkey client))
       (prog1 (processinbox-internal client directions nil)
         (setq need-init-p nil)))
          (when need-init-p (forceinit client)))))
        
    (defmethod processinbox-internal ((client client) directions recursive)
      (let* ((db (db client))
     (serverid (serverid client))
     (id (id client))
     (server (server client))
     (parser (parser client))
     (trans (gettime client))
     (two-phase-commit-p (and (not (equal id serverid))
                              (two-phase-commit-p client)))
     (last-transaction nil)
     inbox inbox-msgs
     outbox outbox-msgs
     (balance (getbalance-internal client t nil))
     (timelist "")
     (deltas (make-equal-hash)) ;(acct => (asset => delta, ...), ...) 
     (outbox-deletions nil)
     (msg "")
     (msgs (make-equal-hash))
     (history "")
     (hist "")
     (charges (make-equal-hash)))
        (multiple-value-setq (inbox inbox-msgs)
          (getinbox-internal client (keep-history-p client)))
        (multiple-value-setq (outbox outbox-msgs)
          (getoutbox-internal client (keep-history-p client)))
        (dolist (dir directions)
          (let* ((time (process-inbox-time dir))
         (request (process-inbox-request dir))
         (note (or (process-inbox-note dir) ""))
         (acct (or (process-inbox-acct dir) $MAIN))
         (in (or (find time inbox :test #'equal :key #'inbox-time)
                 (error "No inbox entry for time: ~s" time)))
         (fee (car (inbox-items in))) ;change this when I add multiple fees
         (inmsg (and inbox-msgs (gethash in inbox-msgs)))
         (inreq (inbox-request in))
         (delta (get-inited-hash acct deltas)))
        
    (unless (equal "" timelist) (dotcat timelist "|"))
    (dotcat timelist time)
        
    (cond ((equal inreq $SPEND)
           (let ((id (inbox-id in))
                 (assetid (inbox-assetid in))
                 (msgtime (inbox-msgtime in))
                 (amount (inbox-amount in)))
             (setf note
                   (encrypt-note (pubkeydb client) (list (id client) id) note))
             (unless (equal msg "") (dotcat msg "."))
             (cond ((equal request $SPENDACCEPT)
                    (setq amount
                          (do-storagefee
                              client charges amount msgtime trans assetid))
                    (setf (gethash assetid delta)
                          (bcadd (gethash assetid delta 0) amount))
                    (let ((smsg (custmsg client $SPENDACCEPT serverid
                                         msgtime id note)))
                      (setf (gethash smsg msgs) t)
                      (dotcat msg smsg)
                      (when inmsg
                        (dotcat hist "." smsg "." inmsg))))
                   ((equal request $SPENDREJECT)
                    (when fee
                      (let ((feeasset (inbox-assetid fee)))
                        (setf (gethash feeasset delta)
                              (bcadd (gethash feeasset delta 0)
                                     (inbox-amount fee)))))
                    (let ((smsg (custmsg client $SPENDREJECT serverid
                                         msgtime id note)))
                      (setf (gethash smsg msgs) t)
                      (dotcat msg smsg)
                      (when inmsg
                        (dotcat hist "." smsg "." inmsg))))
                   (t (error "Illegal request for spend: ~s" request)))))
          ((or (equal inreq $SPENDACCEPT) (equal inreq $SPENDREJECT))
           (let* ((msgtime (inbox-msgtime in))
                  (outspend (or (find msgtime outbox
                                      :test #'equal :key #'outbox-time)
                                (error "Can't find outbox for ~s at time ~s"
                                       inreq msgtime)))
                  (outfee (car (outbox-items outspend)))
                  (outmsg (and outbox-msgs (gethash outspend outbox-msgs))))
             (push msgtime outbox-deletions)
             (cond ((equal inreq $SPENDREJECT)
                    ;; For rejected spends, we get our money back
                    (let ((assetid (outbox-assetid outspend))
                          (amount (outbox-amount outspend)))
                      (setq amount
                            (do-storagefee
                                client charges amount msgtime trans assetid))
                      (setf (gethash assetid delta)
                            (bcadd (gethash assetid delta 0) amount))))
                   (outfee
                    ;; For accepted spends, we get our tranfee back
                    (let ((feeasset (outbox-assetid outfee)))
                      (setf (gethash feeasset delta)
                          (bcadd (gethash feeasset delta 0)
                                 (outbox-amount outfee))))))
             (when outmsg
               (dotcat hist "." inmsg "." outmsg))))
          (t "Unrecognized inbox request: ~s" inreq))))
        
        (let ((pmsg (custmsg client $PROCESSINBOX serverid trans timelist))
      (acctbals (make-equal-hash))
      (outboxhash nil)
      (balancehash nil)
      (fracmsgs nil))
          (setf (gethash pmsg msgs) t)
          (setq msg (if (equal msg "") pmsg (dotcat pmsg "." msg)))
          (when (keep-history-p client)
    (setq history (strcat pmsg hist)))
        
          ;; Compute fees for new balance files
          (let* ((tranfee (getfees client))
         (feeasset (fee-assetid tranfee))
         (delta-main (get-inited-hash $MAIN deltas)))
    (loop
       for acct being the hash-key using (hash-value amounts) of deltas
       for bals = (cdr (assoc acct balance :test #'equal))
       do
       (loop
          for assetid being the hash-key of amounts
          for oldbal = (find assetid bals
                             :test #'equal :key #'balance-assetid)
          for oldamount = (and oldbal (balance-amount oldbal))
          do
            (when (and oldamount (> (bccomp oldamount 0) 0))
              (let ((oldtime (balance-time oldbal)))
                (setf oldamount (do-storagefee
                                    client charges oldamount oldtime
                                    trans assetid)
                      (balance-amount oldbal) oldamount)))
            (unless oldamount
              (setf (gethash feeasset delta-main)
                    (bcsub (gethash feeasset delta-main 0) 1))))))
        
          ;; Create balance, outboxhash, and balancehash messages
          (loop
     for acct being the hash-key using (hash-value amounts) of deltas
     for bals = (cdr (assoc acct balance :test #'equal))
     for acctbal = (get-inited-hash acct acctbals)
     do
     (loop
        for assetid being the hash-key using (hash-value amount) of amounts
        for bal = (find assetid bals :test #'equal :key #'balance-assetid)
        for oldamount = (if bal (balance-amount bal) 0)
        for sum = (bcadd oldamount amount)
        for balmsg = (custmsg client $BALANCE serverid trans assetid sum acct)
        do
          (setf (gethash balmsg msgs) t
                (gethash assetid acctbal) balmsg)
          (dotcat msg "." balmsg)))
        
          (unless (equal id serverid)
    (when outbox-deletions
      (setf outboxhash (outboxhashmsg client trans
                                      :removed-times outbox-deletions
                                      :two-phase-commit-p two-phase-commit-p)
            (gethash outboxhash msgs) t)
      (dotcat msg "." outboxhash))
        
    (setf balancehash (balancehashmsg client trans acctbals two-phase-commit-p)
          (gethash balancehash msgs) t)
    (dotcat msg "." balancehash))
        
          ;; Add storage and fraction messages
          (loop
     for assetid being the hash-key using (hash-value assetinfo) of charges
     for percent = (assetinfo-percent assetinfo)
     do
       (when percent
         (let* ((storagefee (assetinfo-storagefee assetinfo))
                (fraction (assetinfo-fraction  assetinfo))
                (storagefeemsg (custmsg client $STORAGEFEE serverid
                                        trans assetid storagefee))
                (fracmsg (custmsg client $FRACTION serverid
                                  trans assetid fraction)))
           (unless fracmsgs (setq fracmsgs (make-equal-hash)))
           (setf (gethash storagefeemsg msgs) t
                 (gethash fracmsg msgs) t
                 (gethash assetid fracmsgs) fracmsg)
           (dotcat msg "." storagefeemsg "." fracmsg))))
        
          (let* ((retmsg (process server msg)) ;send request to server
         (reqs (parse parser retmsg t)))
    ;; Validate return from server
    (handler-case (match-serverreq client (car reqs) $ATPROCESSINBOX)
      (error ()
        (let ((args
               (handler-case (match-serverreq client (car reqs))
                 (error (c)
                   (unless recursive
                     (with-verify-sigs-p (parser t)
                       ;; Force reload of balances and outbox
                       (forceinit client)
                       ;; Force reload of assets
                       (when charges
                         (loop
                            for assetid being the hash-keys of
                            charges
                            do
                              (reload-asset-p client assetid)))
                       (return-from processinbox-internal
                         (processinbox-internal client directions t))))
                   (error "Error from processinbox request: ~a" c)))))
          (error "Processinbox request returned unknown message type: ~s"
                 (getarg $REQUEST args)))))
    (dolist (req reqs)
      (let* ((reqmsg (get-parsemsg req))
             (args (match-serverreq client req))
             (m (trim (get-parsemsg (getarg $MSG args))))
             (msgm (gethash m msgs)))
        (unless msgm (error "Returned message wasn't sent: ~s" m))
        (when (stringp msgm) (error "Duplicate returned message: ~s" m))
        (setf (gethash m msgs) reqmsg)))
        
    (loop
       for m being the hash-key using (hash-value msg) of msgs
       do
         (when (eq msg t)
           (error "Message not returned from processinbox: ~s" m)))
        
    ;; Do the second phase of the commit
    (when two-phase-commit-p
      (setf last-transaction (send-commit-msg client trans)))
        
    ;; Commit to database
    (loop
       for acct being the hash-key using (hash-value bals) of acctbals
       do
         (loop
            for asset being the hash-key using (hash-value balmsg) of bals
            do
              (setf (db-get db (userbalancekey client acct asset))
                    (gethash balmsg msgs))))
        
    (when fracmsgs
      (loop
         for assetid being the hash-key using (hash-value fracmsg) of fracmsgs
         for key = (userfractionkey client assetid)
         do
           (setf (db-get db key) (gethash fracmsg msgs))))
        
    (when outboxhash
      (dolist (outbox-time outbox-deletions)
        (setf (db-get db (useroutboxkey client outbox-time)) nil))
      (setf (db-get db (useroutboxhashkey client)) (gethash outboxhash msgs)))
        
    (setf (db-get db (userbalancehashkey client)) (gethash balancehash msgs))
        
    (when last-transaction
      (setf (db-get db (user-last-transaction-key client)) last-transaction))
        
    (when history
      (let ((key (userhistorykey client)))
        (setf (db-get db key trans) history)))))))
        
    (defmethod get-last-transaction ((client client) &optional forceserver)
      "Returns the signed commit message for the last two-phase transaction."
      (let* ((db (db client))
     (key (user-last-transaction-key client)))
        (require-current-server client "In get-last-transaction(): Server not set")
        (unless (two-phase-commit-p client)
          (error "Server doesn't support two-phase commit"))
        (with-db-lock (db (userreqkey client))
          (let ((msg (unless forceserver (db-get db key))))
    (unless msg
      (setf msg (sendmsg
                 client $LASTTRANSACTION (serverid client) (getreq client))
            forceserver t))
    ;; We get an error if there is no last commit
    (let ((args (ignore-errors (unpack-servermsg client msg $ATCOMMIT))))
      (when forceserver (setf (db-get db key) (and args msg)))
      (when args
        (getarg $TIME (getarg $MSG args))))))))
        
    (defmethod storagefees ((client client))
      "Tell server to move storage fees to inbox
       You need to call getinbox to see the new data (via its call to sync_inbox)."
      (let ((db (db client)))
        (require-current-server client "In storagefees(): Server not set")
        (init-server-accts client)
        (with-db-lock (db (userreqkey client))
          (let* ((serverid (serverid client))
         (server (server client))
         (req (getreq client))
         (msg (custmsg client $STORAGEFEES serverid req))
         (servermsg (process server msg))
         (args (unpack-servermsg client servermsg))
         (request (getarg $REQUEST args)))
    (unless (equal request $ATSTORAGEFEES)
      (error "Unknown response type: ~s" request))))))
        
    (defstruct assetinfo
      percent
      fraction
      storagefee
      digits)
        
    (defmethod do-storagefee ((client client) charges amount msgtime time assetid)
      "Add storage fee for AMOUNT/MSGTIME to
       (STORAGEINFO-STORAGEFEE (GETHASH ASSETID CHARGES))
       and set (STORAGEINFO-FRACTION (GETHASH ASSETID CHARGES))
       to the fractional balance.
       Return the updated AMOUNT."
      (when (> (bccomp amount 0) 0)
        (let ((assetinfo (gethash assetid charges))
      (digits nil)
      (fracfee nil))
          (unless assetinfo
    (multiple-value-bind (percent fraction fractime)
        (client-storage-info client assetid)
    (when percent
      (setq digits (fraction-digits percent)
            fracfee 0)
      (when fraction
        (multiple-value-setq (fracfee fraction)
          (storage-fee fraction fractime time percent digits))))
    (setf assetinfo (make-assetinfo
                     :percent percent
                     :fraction fraction
                     :storagefee fracfee
                     :digits digits)
          (gethash assetid charges) assetinfo)))
          (let ((percent (assetinfo-percent assetinfo)))
    (when percent
      (let ((digits (assetinfo-digits assetinfo))
            (storagefee (assetinfo-storagefee assetinfo))
            (fraction (assetinfo-fraction assetinfo))
            fee)
        (wbp (digits)
          (multiple-value-setq (fee amount)
            (storage-fee amount msgtime time percent digits))
          (setf (assetinfo-storagefee assetinfo) (bcadd storagefee fee))
          (when fraction
            (multiple-value-setq (amount fraction)
              (normalize-balance amount fraction digits)))
          (setf (assetinfo-fraction assetinfo) fraction)))))))
      amount)
        
    (defstruct outbox
      time
      id
      request
      assetid
      assetname
      amount
      formattedamount
      note
      items
      coupons)
        
    (defmethod getoutbox ((client client) &optional includeraw)
      "Get the outbox contents.
       Returns a list of OUTBOX instances:
       TIME is the timestamp of the outbox entry.
       REQUEST is $SPEND, $TRANFEE, or $COUPONENVELOPE.
       ASSETID is the ID of the asset transferred.
       ASSETNAME is the name of ASSETID.
       AMOUNT is the amount transferred.
       FORMATTEDAMOUNT is amount formatted for output.
       NOTE is the transfer note, omitted for tranfee.
       ITEMS is a list of OUTBOX instances for the fees for this spend.
       COUPONs is a list of coupons in this spend."
      (let ((db (db client)))
        (require-current-server client "In getoutbox(): Server not set")
        (init-server-accts client)
        (with-db-lock (db (userreqkey client))
          (getoutbox-internal client includeraw))))
        
    (defmethod getoutbox-internal ((client client) &optional includeraw)
      (let* ((db (db client))
     (parser (parser client))
     (serverid (serverid client))
     (res nil)
     (msghash (and includeraw (make-hash-table :test #'eq)))
     (key (useroutboxkey client))
     (outbox (db-contents db key)))
        (dolist (time outbox)
          (let* ((msg (db-get db key time))
         (reqs (parse parser msg t))
         (item nil)
         (items nil)
         (coupons nil))
    (dolist (req reqs)
      (let* ((args (match-serverreq client req))
             (request (getarg $REQUEST args))
             (incnegs t)
             assetid
             amount
             assetname
             formattedamount
             id
             outbox)
        (unless (equal request $COUPONENVELOPE)
          (setq args (getarg $MSG args))
          (unless (equal (getarg $TIME args) time)
            (error "Outbox message timestamp mismatch")))
        (setq id (getarg $ID args)
              request (getarg $REQUEST args)
              assetid (getarg $ASSET args)
              amount (getarg $AMOUNT args))
        (when (equal id serverid) (setq incnegs nil))
        (when assetid
          (let ((asset (getasset client assetid)))
            (setq assetname (asset-name asset)
                  formattedamount (format-asset-value
                                   client amount asset incnegs))))
        (setq outbox
              (make-outbox
               :time time
               :id id
               :request request
               :assetid assetid
               :assetname assetname
               :amount amount
               :formattedamount formattedamount))
        
        (cond ((equal request $SPEND)
               (when item
                 (error "More than one spend message in an outbox item"))
               (setf item outbox)
               (let ((note (getarg $NOTE args)))
                 (when note
                   (ignore-errors
                     (setf note
                       (decrypt-note (id client) (privkey client) note)))
                   (setf (outbox-note item) note))))                           
              ((equal request $TRANFEE)
               (push outbox items))
              ((equal request $COUPONENVELOPE)
               (let* ((coupon (privkey-decrypt
                               (or (getarg $ENCRYPTEDCOUPON args)
                                   (error "No encryptedcoupon in a coupon"))
                               (privkey client)))
                      (args (unpack-servermsg client coupon $COUPON))
                      (url (getarg $SERVERURL args))
                      (coupon-number (getarg $COUPON args)))
                 (push (format nil "[~a, ~a]" url coupon-number)
                       coupons)))
              (t (error "Bad request in outbox: ~s" request)))))
    (unless item
      (error "No spend found in outbox item"))
    (setf (outbox-items item) items
          (outbox-coupons item) coupons)
    (push item res)
    (when includeraw
      (setf (gethash item msghash) msg))))
        (values
         (sort res (lambda (x y) (< (bccomp x y) 0))
       :key #'outbox-time)
         msghash)))
    */

    public void redeem(String coupon) throws ClientException {
        // TO DO
    }

    /*
    (defmethod redeem ((client client) coupon)
      "Redeem a coupon
       If successful, add an inbox entry for the coupon spend and return false.
       If fails, return error message.
       Needs an option to process the coupon, intead of just adding it to
       the inbox."
      (let* ((serverid (serverid client))
     (pubkey (or (db-get (pubkeydb client) serverid)
                 (error "Can't get server public key")))
     (coupon (pubkey-encrypt coupon pubkey))
     (msg (sendmsg client $COUPONENVELOPE serverid coupon)))
        (unpack-servermsg client msg $ATCOUPONENVELOPE))
      nil)
    */
    /**
     * Return an array of server features. Currently T.TWOPHASECOMMIT is the only feature.
     * @return
     * @throws ClientException
     */
    String[] getFeatures() throws ClientException {
        return this.getFeatures(false);
    }

    /**
     * Return an array of server features. Currently T.TWOPHASECOMMIT is the only feature.
     * @param forceserver If true, ask the server first
     * @return
     * @throws ClientException
     */
    String[] getFeatures(boolean forceserver) throws ClientException {
        String msg = null;
        ClientDB.ServerDB serverDB = db.getServerDB();
        if (!forceserver)
            msg = serverDB.get(serverid, T.FEATURES);
        if (msg == null) {
            msg = this.sendmsg(T.GETFEATURES, serverid, this.getreq());
            forceserver = true;
        }
        // the "getfeatures" command isn't supported by older servers, so ignore errors
        Parser.Dict args = null;
        try {
            args = this.unpackServermsg(msg, T.FEATURES);
        } catch (ClientException e) {
        }
        if (forceserver)
            serverDB.put(serverid, T.FEATURES);
        if (args == null)
            return null;
        return Utility.splitString('|', args.stringGet(T.FEATURES));
    }

    /**
     * Return true if the server supports two phase commit
     * @return
     * @throws ClientException
     */
    public boolean useTwoPhaseCommit() throws ClientException {
        String[] features = this.getFeatures();
        for (String feature : features) {
            if (T.TWOPHASECOMMIT.equals(feature))
                return true;
        }
        return false;
    }

    /*
    (defmethod getversion ((client client) &optional forceserver)
      "Returns two values: version & time."
      (let* ((db (db client))
     (key (userversionkey client)))
        (require-current-server client "In getversion(): Server not set")
        (with-db-lock (db (userreqkey client))
          (let ((msg (unless forceserver (db-get db key))))
    (unless msg
      (setq msg (sendmsg client $GETVERSION (serverid client) (getreq client))
            forceserver t))
    (let ((args (unpack-servermsg client msg $VERSION)))
      (when forceserver (setf (db-get db key) msg))
      (values (getarg $VERSION args) (getarg $TIME args)))))))
    */

    /**
     * Read some data from the server
     * @param key The key for the data
     * @param anonymously True if the read request is to be anonymous
     * @param serverurl The URL of the server. Use the current server if null;
     * @param sizeOnly True to return the size of the data instead of the data itself
     * @return A 2-element String arrary: [<data>, <time>]
     * @throws ClientException
     */
    public String[] readData(String key, boolean anonymously, String serverurl, boolean sizeOnly)
            throws ClientException {
        String serverid;
        if (anonymously) {
            serverid = (serverurl != null) ? this.verifyServer(serverurl) : this.serverid;
            if (serverid == null)
                throw new ClientException("Can't determine serverid");
        } else {
            serverid = this.requireCurrentServer();
        }
        String sizearg = sizeOnly ? "Y" : null;
        String msg;
        try {
            msg = anonymously ? parser.makemsg(new String[] { "0", T.READDATA, serverid, "0", key, sizearg }) + ":0"
                    : this.custmsg(T.READDATA, serverid, this.getreq(), key, sizearg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        ServerProxy server = (anonymously && serverurl != null) ? new ServerProxy(serverurl) : this.server;
        String saveServerid = this.serverid;
        String servermsg;
        Parser.Dict args;
        try {
            servermsg = server.process(msg);
            args = this.unpackServermsg(servermsg);
        } finally {
            this.serverid = saveServerid;
            if (anonymously && serverurl != null)
                server.close();
        }
        String request = args.stringGet(T.REQUEST);
        String reqid = args.stringGet(T.ID);
        String time = args.stringGet(T.TIME);
        String data = args.stringGet(T.DATA);
        if (!T.ATREADDATA.equals(request)) {
            throw new ClientException("Unknown response type: " + request + ", expected: " + T.ATREADDATA);
        }
        String wantid = (anonymously ? "0" : id);
        if (!wantid.equals(reqid))
            throw new ClientException("Wrong id returned from readdata");
        return new String[] { data, time };
    }

    public String[] readData(String key, boolean anonymously, String serverurl) throws ClientException {
        return this.readData(key, anonymously, serverurl, false);
    }

    public String[] readData(String key) throws ClientException {
        return this.readData(key, false, null, false);
    }

    public void writeData(String key, String data, boolean anonymously) throws ClientException {
        try {
            this.writeDataInternal(key, data, anonymously);
        } catch (ClientException e) {
            this.forceinit();
            this.writeDataInternal(key, data, anonymously);
        }
    }

    public void writeData(String key, String data) throws ClientException {
        this.writeData(key, data, false);
    }

    private void writeDataInternal(String key, String data, boolean anonymously) throws ClientException {
        // TO DO
    }

    /*
    (defmethod writedata-internal ((client client) key data anonymous-p)
      (let ((db (db client)))
        (with-db-lock (db (userreqkey client))
          (let* ((oldsize (ignore-errors
                    (parse-integer (readdata client key
                                             :anonymous-p anonymous-p
                                             :size-p t))))
         (old-cost (if oldsize (data-cost oldsize) 0))
         (new-cost (data-cost data))
         (net-cost (- new-cost old-cost))
         (tokenid (fee-assetid (getfees client)))
         (bal (balance-amount (or (getbalance client $MAIN tokenid)
                                  (error "Insufficient tokens"))))
         (newbal (bcsub bal net-cost)))
    (unless (or (< (bccomp bal 0) 0) (>= (bccomp newbal 0) 0))
      (error "Insufficient balance, need ~a tokens" net-cost))
    (let* ((time (gettime client))
           (serverid (serverid client))
           (anonymous (if anonymous-p "T" ""))
           (msg (custmsg client $WRITEDATA serverid time anonymous key data))
           (balmsg (custmsg client $BALANCE serverid time tokenid newbal))
           (acctbals (make-equal-hash $MAIN (make-equal-hash tokenid newbal)))
           (balhashmsg (balancehashmsg client time acctbals))
           (servermsg (process (server client)
                             (strcat msg "." balmsg "." balhashmsg)))
           (reqs (parse (parser client) servermsg t))
           (args (match-serverreq client (car reqs) $ATWRITEDATA)))
      (unless (and (equal (getarg $ID args) (id client))
                   (equal (getarg $TIME args) time)
                   (equal (getarg $ANONYMOUS args) anonymous)
                   (equal (getarg $KEY args) key))
        (error "Bad return message from server"))
      (unless (eql 3 (length reqs))
        (error "Wrong number of return messages"))
      (let* ((balreq (second reqs))
             (balhashreq (third reqs))
             (serverbalmsg (get-parsemsg balreq))
             (serverbalhashmsg (get-parsemsg balhashreq)))
        (unless (equal balmsg
                       (get-parsemsg
                        (getarg
                         $MSG (match-serverreq client balreq $ATBALANCE))))
          (error "Returned balance message mismatch"))
        (unless (equal balhashmsg
                       (get-parsemsg
                        (getarg
                         $MSG (match-serverreq client balhashreq $ATBALANCEHASH))))
          (error "Returns balancehash message mismatch"))
        (setf (db-get db (userbalancekey client $MAIN tokenid)) serverbalmsg
              (db-get db (userbalancehashkey client)) serverbalhashmsg)
        data))))))
        
    (defstruct permission
      id
      toid
      permission
      grant-p
      time)
        
    (defmethod get-permissions ((client client) &optional permission reload-p)
      "Return a list of the PERMISSION instances for PERMISSION, or all if
       PERMISSION is nil.
       If PERMISSION is included, and the server doesn't require it, return T.
       If PERMISSION is included, return a second value, true if the
       user has permission to grant PERMISSION.
       If RELOAD-p is true, reload all permissions from the server."
      (require-current-server client "In get-permissions(): Server not set")
      (let* ((db (db client))
     (id (id client))
     (serverid (serverid client))
     (serverp (equal id serverid))
     (parser (parser client)))
        (when reload-p
          (with-db-lock (db (userreqkey client serverid))
    (handler-case
        (get-permissions-internal client id)
      (error ()
        (get-permissions-internal client id t)))))
        (flet ((permission-key (permission)
         (if serverp
             (server-permission-key client permission)
             (user-permission-key client permission))))
          (declare (dynamic-extent #'permission-key))
          (let* ((msgs (cond (permission
                      (list
                       (or (db-get db (permission-key permission))
                           (db-get db (server-permission-key
                                       client permission)))))
                     (t (loop for permission in
                             (union 
                              (db-contents
                               db (server-permission-key client))
                              (unless serverp
                                (db-contents
                                 db (user-permission-key client)))
                              :test #'equal)
                           for msg = (or (db-get
                                          db (permission-key permission))
                                         (unless serverp
                                           (db-get
                                            db (server-permission-key
                                                client permission))))
                           collect msg))))
         (grant-p nil)
         (server-permissions nil)
         (permissions nil))
    (dolist (msg msgs)
      (let ((reqs (and msg (parse parser msg))))
        (dolist (req reqs)
          (let* ((args (match-serverreq client req)))
            (setf args (getarg $MSG args))
            (unless (equal $GRANT (getarg $REQUEST args))
              (error "Malformed grant record"))
            (let ((permission (make-permission
                               :id (getarg $CUSTOMER args)
                               :toid (getarg $ID args)
                               :permission (getarg $PERMISSION args)
                               :grant-p (equal $GRANT
                                               (getarg $grant args))
                               :time (getarg $TIME args))))
              (cond ((equal (permission-toid permission) id)
                     (when (blankp (permission-time permission))
                       ;; Default permission. Clear id.
                       (setf (permission-id permission) nil))
                     (push permission permissions)
                     (when (permission-grant-p permission)
                       (setf grant-p t)))
                    ((equal (permission-toid permission) serverid)
                     (setf (permission-id permission) nil
                           (permission-toid permission) nil
                           (permission-grant-p permission) nil)
                     (push permission server-permissions))
                    (t (error "found permission for bad id"))))))))
    (if permission
        (values (or permissions (not server-permissions))
                grant-p)
        (append permissions server-permissions))))))
        
    (defun get-permissions-internal (client id &optional reinit-p)
      (let* ((db (db client))
     (serverid (serverid client))
     (key (if (equal serverid id)
              (server-permission-key client)
              (user-permission-key client)))
     (permissions (db-contents db key))
     (req (getreq client reinit-p))
     (msg (custmsg client $PERMISSION serverid req))
     (*msg* msg)
     (servermsg (process (server client) msg))
     (parser (parser client))
     (reqs (parse parser servermsg))
     (args (match-serverreq client (car reqs) $ATPERMISSION))
     (newmsgs (make-equal-hash)))
        (unless (equal *msg* (get-parsemsg (getarg $MSG args)))
          (error "Malformed response from server for ~s command."
         $PERMISSION))
        (dolist (req (cdr reqs))
          (let* ((args (match-serverreq client req $ATGRANT))
         (msg (get-parsemsg args))
         toid)
    (setf args (getarg $MSG args)
          toid (getarg $ID args))
    (unless (and (equal $GRANT (getarg $REQUEST args))
                 (let ((toid (getarg $ID args)))
                   (or (equal toid id)
                       (equal toid serverid))))
      (error "Malformed ~s message from server." $GRANT))
    (let* ((permission (getarg $PERMISSION args))
           (permkey (if (equal toid serverid)
                        (server-permission-key client permission)
                        (user-permission-key client permission)))
           (newmsg (gethash permkey newmsgs)))
      (unless (and (equal toid serverid)
                   (not (equal toid (id client))))
        (setf permissions (delete permission permissions :test #'equal)))
      (setf (gethash permkey newmsgs)
            (if newmsg (strcat newmsg "." msg) msg)))))
        (maphash (lambda (permkey msg) (db-put db permkey msg)) newmsgs)
        (dolist (permission permissions)
          (setf (db-get db key permission) nil))))
        
    (defmethod get-granted-permissions ((client client))
      "Return the permissions you've directly granted as a list of PERMISSIONS."
      (let* ((db (db client)))
        (with-db-lock (db (userreqkey client))
          (handler-case
      (get-granted-permissions-internal client)
    (error () (get-granted-permissions-internal client t))))))
        
    (defun get-granted-permissions-internal (client &optional reinit-p)
      (let* ((req (getreq client reinit-p))
     (id (id client))
     (serverid (serverid client))
     (parser (parser client))
     (msg (custmsg client $PERMISSION serverid req $GRANT))
     (*msg* msg)
     (servermsg (process (server client) msg))
     (reqs (parse parser servermsg))
     (args (getarg $MSG (match-serverreq client (car reqs) $ATPERMISSION)))
     (res nil))
        (unless (equal *msg* (get-parsemsg args))
          (error "Malformed return from server for ~s command" $PERMISSION))
        (dolist (req (cdr reqs))
          (setf args (getarg $MSG (match-serverreq client req $ATGRANT)))
          (unless (and (equal $GRANT (getarg $REQUEST args))
               (equal id (getarg $CUSTOMER args)))
    (error "Malformed ~s message from server" $GRANT))
          (push (make-permission
         :id id
         :toid (getarg $ID args)
         :permission (getarg $PERMISSION args)
         :grant-p (equal $GRANT (getarg $GRANT args))
         :time (getarg $TIME args))
        res))
        (nreverse res)))
              
    (defmethod grant ((client client) toid permission &optional grantp)
      "Grant PERMISSION to TOID, with transitive grant permission if GRANTP is true."
      (let* ((db (db client))
     (id (id client))
     (serverid (serverid client))
     (serverp (equal id serverid)))
        (unless (or (equal id serverid) (not (equal toid serverid)))
          (error "You may not grant permissions to the server"))
        (unless (or serverp
            (nth-value 1 (get-permissions client permission))
            (nth-value 1 (get-permissions client permission t)))
          (error "You do not have permission to grant permission: ~s" permission))
        (when (equal toid serverid)
          ;; Could error here, but why bother
          (setf grantp t))
        (with-db-lock (db (userreqkey client))
          (handler-case (grant-internal client toid permission grantp)
    (error () (grant-internal client toid permission grantp t))))))
        
    (defun grant-internal (client toid permission grantp &optional forcenew)
      (let* ((time (gettime client forcenew))
     (serverid (serverid client))
     (msg (apply #'custmsg
                 client $GRANT serverid time toid permission
                 (and grantp (list $GRANT))))
     (servermsg (process (server client) msg))
     (args (unpack-servermsg client servermsg $ATGRANT)))
        (unless (equal msg (get-parsemsg (getarg $MSG args)))
          (error "Malformed return from server for ~s command" $GRANT))))
        
    (defmethod deny ((client client) toid permission)
      "Deny PERMISSION to TOID."
      (let* ((db (db client))
     (id (id client))
     (serverid (serverid client))
     (serverp (equal id serverid)))
        (unless (or (equal id serverid) (not (equal toid serverid)))
          (error "You may not grant permissions to the server"))
        (unless (or serverp
            (nth-value 1 (get-permissions client permission))
            (nth-value 1 (get-permissions client permission t)))
          (error "You do not have permission to deny permission: ~s" permission))
        (with-db-lock (db (userreqkey client))
          (handler-case (deny-internal client toid permission)
    (error () (deny-internal client toid permission t))))))
        
    (defun deny-internal (client toid permission &optional reinit-p)
      (let* ((req (getreq client reinit-p))
     (serverid (serverid client))
     (msg (custmsg client $DENY serverid req toid permission))
     (*msg* msg)
     (servermsg (process (server client) msg))
     (args (unpack-servermsg client servermsg $ATDENY)))
        (unless (equal *msg* (get-parsemsg (getarg $MSG args)))
          (error "Malformed return from server for ~s command" $DENY))))
        
    (defmethod audit ((client client) assetid)
      "Audit the asset with ASSETID. Return three values:
       1) The formatted total
       2) The fractional total
       3) The raw total (arg 1 as an integer string)."
      (require-current-server client "In audit(): Server not set")
      (let ((serverid (serverid client))
    (id (id client))
    (asset (getasset client assetid)))
        (unless (and asset
             (or (equal id serverid)
                 (equal id (asset-id asset))))
          (error "Audit only allowed by asset issuer"))
        (handler-case
    (audit-internal client assetid asset)
          (error ()
    (audit-internal client assetid asset t)))))
        
    (defun audit-internal (client assetid asset &optional reinit-p)
      (let* ((serverid (serverid client))
     (req# (getreq client reinit-p))
     (msg (custmsg
           client $AUDIT serverid req# assetid))
     (*msg* msg)
     (servermsg (process (server client) msg))
     (args (unpack-servermsg client servermsg $ATAUDIT serverid))
     (reqs (getarg $UNPACK-REQS-KEY args))
     (parser (parser client))
     amount
     (fraction "0"))
        (unless (equal *msg* (get-parsemsg (getarg $MSG args)))
          (error "Server return doesn't wrap request message"))
        (dolist (req (cdr reqs))
          (let* ((args (match-pattern parser req serverid))
         (op (getarg $REQUEST args)))
    (assert (equal serverid (getarg $CUSTOMER args)))
    (assert (equal serverid (getarg $SERVERID args)))
    (assert (equal req# (getarg $TIME args)))
    (assert (equal assetid (getarg $ASSET args)))
    (cond ((equal op $BALANCE)
           (setf amount (getarg $AMOUNT args)))
          ((equal op $FRACTION)
           (setf fraction (getarg $FRACTION args)))
          (t (error "Unknown operation in ~s return: ~s" $AUDIT op)))))
        (values (format-value
         (if (< (bccomp amount 0) 0) (bcsub amount 1) amount)
         (asset-scale asset) (asset-precision asset))
        fraction
        amount)))
        
    (defmethod backup ((client client) &rest keys&values)
      (backup* client keys&values))
        
    (defmethod backup* ((client client) keys&values)
      (require-current-server client "In backup*(): Server not set")
      (unless (evenp (length keys&values))
        (error "odd length keys&values list"))
      (let* ((req (getreq client))
     (msg (apply #'sendmsg client $BACKUP req keys&values))
     (args (unpack-servermsg client msg $ATBACKUP))
     (id (getarg $CUSTOMER args))
     (msgreq (getarg $REQ args)))
        (unless (equal id (serverid client))
          (error "Return from backup request not from server."))
        (unless (equal req msgreq)
          (error "Mistmatch in req from backup request, sb: ~s, was: ~s"
         req msgreq))))
        
    ;;;
    ;;; End of API methods
    ;;;
    ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
        
    (defmethod tokenid ((client client))
      (fee-assetid (getfees client)))
    */

    /**
     * Return the hash of passphrase
     * @param passphrase
     * @return sha1(passphrase)
     */
    public static String passphraseHash(String passphrase) {
        return Crypto.sha1(passphrase);
    }

    /**
     * Return the hash of passphrase xor'd with the salt
     */
    public static String passphraseHash(String passphrase, String salt) {
        return Utility.xorSalt(passphrase, salt);
    }

    /**
     * Turn an array of strings into a signed message
     * @param args array of strings, not including the customer id
     * @return (<id>,args...):signature
     * @throws ClientException If the message doesn't match a known pattern
     */
    public String custmsg(String... args) throws ClientException {
        int len = args.length;
        String[] args2 = new String[len + 1];
        args2[0] = this.id;
        for (int i = 0; i < len; i++)
            args2[i + 1] = args[i];
        String msg;
        try {
            msg = parser.makemsg(args);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        String sig = Crypto.sign(msg, this.privkey);
        return msg + ':' + sig;
    }

    /**
     * Create a message from args, send it to the server, and return the response
     * @param args Message elements not including initial customer id
     * @return Servers response message
     * @throws ClientException If the args don't match a known pattern or there's an error communicating with the server. 
     */
    public String sendmsg(String... args) throws ClientException {
        String msg = this.custmsg(args);
        return server.process(msg);
    }

    /**
     * Parse a server message and match its first element.
     * @param msg The server message to parse
     * @return A matched table, with the unmatched Parser.DictList as the T.UNPACK_REQS_KEY element.
     * @throws ClientException If the parse or the match fails
     */
    public Parser.Dict unpackServermsg(String msg) throws ClientException {
        return this.unpackServermsg(msg, null, null);
    }

    /**
     * Parse a server message and match its first element.
     * @param msg The server message to parse
     * @param request The expected T.REQUEST element, or null to not check
     * @return A matched table, with the unmatched Parser.DictList as the T.UNPACK_REQS_KEY element.
     * @throws ClientException If the parse or the match or the comparison with request fails
     */
    public Parser.Dict unpackServermsg(String msg, String request) throws ClientException {
        return this.unpackServermsg(msg, request, null);
    }

    /**
     * Parse a server message and match its first element.
     * @param msg The server message to parse
     * @param request The expected T.REQUEST element, or null to not check
     * @param serverid The expected serverid, or null to not check
     * @return A matched table, with the unmatched Parser.DictList as the T.UNPACK_REQS_KEY element.
     * @throws ClientException If the parse or the match or the comparison with request or serverid fails
     */
    public Parser.Dict unpackServermsg(String msg, String request, String serverid) throws ClientException {
        Parser.DictList reqs;
        try {
            reqs = parser.parse(msg);
        } catch (Parser.ParseException e) {
            throw new ClientException(e);
        }
        Parser.Dict req = reqs.get(0);
        Parser.Dict args = this.matchServerReq(req, request, serverid);
        args.put(T.UNPACK_REQS_KEY, reqs);
        return args;
    }

    /**
     * Unpack a server message that has already been parsed.
     * @param req The parsed server message
     * @param request if non-NULL, must match the T.REQUEST in message
     * @param serverid The serverid expected in the server message. Not checked if NULL.
     * @return The matched args
     * @throws ClientException
     */
    public Parser.Dict matchServerReq(Parser.Dict req, String request, String serverid) throws ClientException {
        try {
            Parser.Dict args = parser.matchPattern(req, serverid);
            if (serverid != null && serverid != args.get(T.CUSTOMER)) {
                throw new ClientException("Return message not from server");
            }
            String argsRequest = args.stringGet(T.REQUEST);
            if (T.FAILED.equals(argsRequest)) {
                throw new ClientException("Server error: " + args.stringGet(T.ERRMSG));
            }
            if (request != null && !request.equals(argsRequest)) {
                throw new ClientException(
                        "Wrong return type from server, sb: " + request + ", was: " + argsRequest);
            }
            Parser.Dict msg = (Parser.Dict) args.get(T.MSG);
            Parser.Dict msgargs = (msg != null) ? parser.matchPattern(msg) : null;
            if (msgargs != null) {
                String msgargsServerid = msgargs.stringGet(T.SERVERID);
                if (msgargsServerid != null && serverid != null && msgargsServerid != serverid) {
                    throw new ClientException("While matching server-wrapped msg: serverid mismatch");
                }
                args.put(T.MSG, msgargs);
            }
            return args;
        } catch (Exception e) {
            throw new ClientException(e);
        }
    }

    /**
     * Unpack a server message that has already been parsed.
     * @param req The parsed server message
     * @param request if non-NULL, must match the T.REQUEST in message
     * @return The matched args
     * @throws ClientException
     */
    public Parser.Dict matchServerReq(Parser.Dict req, String request) throws ClientException {
        return this.matchServerReq(req, request, null);
    }

    public Parser.Dict matchServerReq(Parser.Dict req) throws ClientException {
        return this.matchServerReq(req, null, null);
    }

    /**
     * Package up information about storage fees 
     * @author billstclair
     */
    public static class StorageInfo {
        public String percent;
        public String fraction;
        public String fractime;

        public StorageInfo(String percent, String fraction, String fractime) {
            this.percent = percent;
            this.fraction = fraction;
            this.fractime = fractime;
        }
    }

    /**
     * Return StorageInfo on an assetid
     * @param assetid
     * @return
     * @throws ClientException
     */
    public StorageInfo getClientStorageInfo(String assetid) throws ClientException {
        Asset asset = this.getAsset(assetid);
        if (asset == null)
            return null;
        String issuer = asset.issuer;
        String percent = asset.percent;
        if (id.equals(issuer))
            return null;
        if (percent == null || BCMath.isZero(percent))
            return null;
        String msg = db.getAccountDB().get(this.userFractionKey(), assetid);
        if (msg == null)
            return new StorageInfo(percent, "0", "0");
        Parser.Dict args = (Parser.Dict) (this.unpackServermsg(msg, T.FRACTION).get(T.MSG));
        return new StorageInfo(percent, args.stringGet(T.AMOUNT), args.stringGet(T.TIME));
    }

    public String serverKey(String prop) {
        return serverid + '/' + prop;
    }

    public String getServerProp(String prop, String serverid) {
        return db.getServerDB().get(serverid, prop);
    }

    public String getServerProp(String prop) {
        return this.getServerProp(prop, this.serverid);
    }

    public String assetKey() {
        return this.serverKey(T.ASSET);
    }

    /*
    (defmethod assetkey ((client client) &optional assetid)
      (let ((key (serverkey client $ASSET)))
        (if assetid
    (append-db-keys key assetid )
    key)))
        
    (defmethod assetprop ((client client) assetid)
      (db-get (db client) (assetkey client assetid)))
    */

    public String tranfee() {
        return db.getAccountDB().get(this.getServerProp(T.TRANFEE));
    }

    public String regfee() {
        return db.getAccountDB().get(this.getServerProp(T.TRANFEE));
    }

    public String otherfees(String operation) {
        ClientDB.ServerDB serverDB = db.getServerDB();
        String[] operations = (operation != null) ? new String[] { operation } : serverDB.contents(serverid, T.FEE);
        String res = null;
        String dir = serverid + '/' + T.FEE;
        for (String op : operations) {
            String fees = serverDB.get(dir, op);
            if (res != null)
                res += "." + fees;
            res = fees;
        }
        return res;
    }

    public String otherfees() {
        return this.otherfees(null);
    }

    public String userServerKey(String prop, String serverid) {
        return id + '/' + T.SERVER + '/' + serverid + (prop == null ? "" : '/' + prop);
    }

    public String userServerKey(String prop) {
        return userServerKey(prop, serverid);
    }

    public String userServerKey() {
        return userServerKey(null, serverid);
    }

    public String userServerProp(String prop, String serverid) {
        return db.getAccountDB().get(userServerKey(null, serverid), prop);
    }

    public String userServerProp(String prop) {
        return userServerProp(prop, serverid);
    }

    public void setUserServerProp(String prop, String serverid, String value) {
        db.getAccountDB().put(userServerKey(null, serverid), prop, value);
    }

    public void setUserServerProp(String prop, String value) {
        this.setUserServerProp(prop, serverid, value);
    }

    public String getUserReq(String serverid) {
        return db.getAccountDB().get(userServerKey(null, serverid), T.REQ);
    }

    public String getUserReq() {
        return getUserReq(serverid);
    }

    public void setUserReq(String serverid, String req) {
        db.getAccountDB().put(userServerKey(null, serverid), T.REQ, req);
    }

    public void setUserReq(String req) {
        this.setUserReq(serverid, req);
    }

    public String userFractionKey() {
        return this.userServerKey(T.FRACTION);
    }

    public String userStorageFeeKey() {
        return this.userServerKey(T.STORAGEFEE);
    }

    public String userBalanceKey(String acct) {
        String key = this.userServerKey(T.BALANCE);
        return (acct == null) ? key : key + '/' + acct;
    }

    public String userBalanceKey() {
        return this.userBalanceKey(null);
    }

    /**
     * Returns the raw balance for the given acct and assetid, null if no matching balance record
     * @param acct
     * @param assetid
     * @return
     */
    public String userBalance(String acct, String assetid) throws ClientException {
        String[] btm = this.userBalanceAndTime(acct, assetid);
        if (btm == null)
            return null;
        return btm[0];
    }

    /**
     * Returns a 3-element array containing raw balance, time, and raw message
     * @param acct
     * @param assetid
     * @return
     */
    public String[] userBalanceAndTime(String acct, String assetid) throws ClientException {
        if (acct == null)
            acct = T.MAIN;
        String msg = db.getAccountDB().get(this.userBalanceKey(acct), assetid);
        if (msg == null)
            return null;
        Parser.Dict args = this.unpackServermsg(msg);
        args = (Parser.Dict) args.get(T.MSG);
        return new String[] { args.stringGet(T.AMOUNT), args.stringGet(T.TIME), msg };
    }

    public String userOutboxKey() {
        return this.userServerKey(T.OUTBOX);
    }
    /*
    (defmethod useroutbox ((client client) time)
      (db-get (db client) (useroutboxkey client time)))
        
    (defmethod useroutboxhashkey ((client client))
      (userserverkey client $OUTBOXHASH))
        
    (defmethod useroutboxhash ((client client))
      (db-get (db client) (useroutboxhashkey client)))
        
    (defmethod userbalancehashkey ((client client))
      (userserverkey client $BALANCEHASH))
        
    (defmethod userbalancehash ((client client))
      (db-get (db client) (userbalancehashkey client)))
    */

    public String userInboxKey() {
        return this.userServerKey(T.INBOX);
    }

    /*
    (defmethod userinboxkey ((client client))
      (userserverkey client $INBOX))
        
    (defmethod userinboxignoredkey ((client client))
      (userserverkey client $INBOXIGNORED))
        
    (defmethod user-permission-key ((client client) &optional permission)
      (let ((key (userserverkey client $PERMISSION (serverid client))))
        (if permission
          (append-db-keys key permission)
          key)))
        
    (defmethod server-permission-key ((client client) &optional permission)
      (let ((key (serverkey client $PERMISSION (serverid client))))
        (if permission
    (append-db-keys key permission)
    key)))
    */

    public String contactkey() {
        return this.contactkey(null);
    }

    public String contactkey(String otherid) {
        String res = this.id + '/' + T.CONTACT + '/';
        if (otherid != null)
            res += '/' + otherid;
        return res;
    }

    public String getContactProp(String otherid, String prop) {
        return db.getAccountDB().get(this.contactkey(otherid), prop);
    }

    public void setContactProp(String otherid, String prop, String value) {
        db.getAccountDB().put(this.contactkey(otherid), prop, value);
    }

    public String userHistoryKey() {
        return this.userServerKey(T.HISTORY);
    }

    public String formatAssetValue(String value, String assetid) throws ClientException {
        return this.formatAssetValue(value, assetid, false);
    }

    public String formatAssetValue(String value, String assetid, boolean incnegs) throws ClientException {
        return this.formatAssetValue(value, this.getAsset(assetid), incnegs);
    }

    public String formatAssetValue(String value, Asset asset) throws ClientException {
        return this.formatAssetValue(value, asset, false);
    }

    /**
     * Format an asset value by right-shifting it by the asset scale and truncating the fraction to the asset precision
     * @param value The value to format
     * @param asset the asset containing the scale and precision
     * @param incnegs If true, increment negative values by 1 before shifting
     * @return
     * @throws ClientException
     */
    public String formatAssetValue(String value, Asset asset, boolean incnegs) throws ClientException {
        // TO DO
        return "";
    }

    public String unformatAssetValue(String formattedValue, Asset asset) throws ClientException {
        // TO DO
        return "";
    }

    /*
    (defmethod format-asset-value ((client client) value assetid &optional (incnegs t))
      "Format an asset value from the asset ID or $this->getasset($assetid)"
      (let ((asset (if (stringp assetid)
               (getasset client assetid)
               assetid)))
        (format-value
         value (asset-scale asset) (asset-precision asset) incnegs)))
        
    (defmethod unformat-asset-value ((client client) formattedvalue assetid)
      "Unformat an asset value from the asset ID or $this->getasset($assetid)"
      (let ((asset (if (stringp assetid)
               (getasset client assetid)
               assetid)))
        (unformat-value formattedvalue (asset-scale asset))))
        
    (defun fill-string (len &optional (char #\0))
      (make-string len :initial-element char))
        
    (defun format-value (value scale precision &optional (incnegs t))
      ;; format an asset value for user printing
      (let ((sign 1)
    res)
        (when (and incnegs (< (bccomp value 0) 0))
          (setq value (bcadd value 1)
        sign -1))
        (cond ((and (eql 0 (bccomp scale 0)) (eql 0 (bccomp precision 0)))
       (setq res value))
      ((> (bccomp scale 0) 0)
       (let ((pow (bcpow 10 scale)))
         (wbp (scale)
           (setq res (bcdiv value pow))))
       (let ((dotpos (position #\. res))
             (precision (parse-integer precision)))
         (cond ((null dotpos)
                (unless (eql 0 (bccomp precision 0))
                  (dotcat res "." (fill-string precision))))
               (t
                ;; Remove trailing zeroes
                (let ((endpos (1- (length res))))
                  (loop
                     while (> endpos dotpos)
                     do
                       (unless (eql #\0 (aref res endpos)) (return))
                       (decf endpos))
                  (let* ((zeroes (- precision (- endpos dotpos)))
                         (zerostr (if (> zeroes 0) (fill-string zeroes) "")))
                    (setq res (strcat (subseq res 0 (1+ endpos)) zerostr)))))))))
        
        (when (and (eql 0 (bccomp value 0)) (< sign 0))
          (setq res (strcat "-" res)))
        
        (when (integerp res) (setf res (princ-to-string res)))
        
        ;; Insert commas
        (let* ((start 0)
       (dotpos (or (position #\. res) (length res)))
       (len dotpos))
          (when (eql #\- (aref res 0))
    (incf start)
    (decf len))
          (loop
     for pos = (+ len start -3)
     while (> pos start)
     do
       (setq res (strcat (subseq res 0 pos) "," (subseq res pos)))
       (decf len 3)))
        
        res))
        
    (defun unformat-value (formattedvalue scale)
      (let ((value (if (eql 0 (bccomp scale 0))
               formattedvalue
               (split-decimal
                (wbp (scale) (bcmul formattedvalue (bcpow 10 scale)))))))
        (if (or (< (bccomp value 0) 0)
        (and (eql 0 (bccomp value 0))
             (eql #\- (aref formattedvalue 0))))
          (bcsub value 1)
          value)))
        
    (defmethod get-pubkey-from-server ((client client) id)
      "Send an $ID command to the server, if there is one.
     Parse out the pubkey, cache it in the database, and return it.
       Return nil if there is no server or it doesn't know the id."
      (let* ((db (db client))
     (serverid (or (current-server client)
                 (return-from get-pubkey-from-server nil)))
     (msg (sendmsg client $ID serverid id))
     (args (getarg $MSG (unpack-servermsg client msg $ATREGISTER)))
     (pubkey (getarg $PUBKEY args))
     (pubkeykey (pubkeykey id)))
        (when pubkey
          (db-put db pubkeykey pubkey)
          pubkey)))
    */

    public String getreq(boolean reinit) {
        // TO DO
        return "foo";
    }

    public String getreq() {
        return this.getreq(false);
    }

    /*
    (defmethod getreq ((client client) &optional reinit-p)
      "Get a new request"
      (let ((db (db client))
    (key (userreqkey client)))
        (when reinit-p
          (let* ((msg (sendmsg client $GETREQ (serverid client)))
         (args (unpack-servermsg client msg $REQ))
         (req (getarg $REQ args)))
    (setf (db-get db key) req)))
        (with-db-lock (db key)
          (setf (db-get db key) (bcadd (db-get db key) 1)))))
    */

    public String getTime() {
        // TO DO
        return null;
    }

    /*
    (defmethod gettime ((client client) &optional forcenew)
      "Get a timestamp from the server"
      (let ((db (db client))
    (serverid (serverid client))
    (key (usertimekey client)))
        (with-db-lock (db key)
          (cond (forcenew (setf (db-get db key) nil))
        (t
         (let ((times (db-get db key)))
           (when times
             (setf times (explode #\, times)
                   (db-get db key) (cadr times))
             (return-from gettime (car times)))))))
        (flet ((get-time-args ()
         (let* ((req (getreq client))
                (msg (sendmsg client $GETTIME serverid req)))
           (unpack-servermsg client msg $TIME))))
          (let ((args (handler-case (get-time-args)
                (error ()
                  (forceinit client)
                  (get-time-args)))))
    (getarg $TIME args)))))
        
    (defmethod syncreq ((client client))
      "Check once per instance that the local idea of the reqnum matches
       that at the server.
       If it doesn't, clear the account information, so that init-server-accts()
       will reinitialize.
       Eventually, we want to compare to see if we can catch a server error."
      (let* ((db (db client))
     (key (userserverkey client $REQ))
     (reqnum (db-get db key)))
        (when (equal reqnum "-1") (setf (syncedreq-p client) t))
        (unless (syncedreq-p client)
          (let* ((serverid (serverid client))
         (msg (sendmsg client $GETREQ serverid))
         (args (unpack-servermsg client msg $REQ))
         (newreqnum (getarg $REQ args)))
          (unless (equal reqnum newreqnum)
    (setq reqnum "-1")
    (let* ((balkey (userbalancekey client))
           (accts (db-contents db balkey)))
      (dolist (acct accts)
        (let* ((acctkey (append-db-keys balkey acct))
               (assetids (db-contents db acctkey)))
          (dolist (assetid assetids)
            (setf (db-get db acctkey assetid) nil)))))
    (let* ((frackey (userfractionkey client))
           (assetids (db-contents db frackey)))
      (dolist (assetid assetids)
        (setf (db-get db frackey assetid) nil)))
    (let* ((outboxkey (useroutboxkey client))
           (outtimes (db-contents db outboxkey)))
      (dolist (outtime outtimes)
        (setf (db-get db outboxkey outtime) nil)))
    (setf (db-get db (userbalancehashkey client)) nil
          (db-get db (useroutboxhashkey client)) nil)
    (setf (syncedreq-p client) t))))
        reqnum))
        
    (defmethod reinit-balances ((client client))
      "Synchronize with the server"
      (require-current-server client "Can't reinitialize balances")
      (forceinit client))
    */

    public void forceinit() throws ClientException {
        // TO DO
    }

    public void initServerAccts() throws ClientException {
        // TO DO
    }

    /*
    ;; Internal implementation of reinit-balances
    (defmethod forceinit ((client client))
      "Force a reinit of the client database for the current user"
      (let ((db (db client)))
        (setf (db-get db (userreqkey client)) "0"
      (syncedreq-p client) nil)
        (init-server-accts client)))
        
    (defmethod init-server-accts ((client client))
      "If we haven't yet downloaded accounts from the server, do so now.
       This is how a new client instance gets initialized from an existing
       server instance."
      (let* ((db (db client))
     (id (id client))
     (serverid (serverid client))
     (parser (parser client))
     (reqnum (syncreq client)))
        
        (when (equal reqnum "-1")
          ;; Get $REQ
          (let* ((msg (sendmsg client $GETREQ serverid))
         (args (unpack-servermsg client msg $REQ))
         (reqnum (bcadd (getarg $REQ args) 1)))
        
    ;; Get account balances
    (setq msg (sendmsg client $GETBALANCE serverid reqnum))
    (let ((reqs (and msg (parse parser msg t)))
          (balances (make-equal-hash))
          (fractions (make-equal-hash))
          (balancehash nil))
      (dolist (req reqs)
        (setq args (match-serverreq client req))
        (let* ((request (getarg $REQUEST args))
               (msgargs (getarg $MSG args))
               (customer (and msgargs (getarg $CUSTOMER msgargs))))
          (when (and msgargs (not (equal customer id)))
            (error "Server wrapped somebody else's (~a) message: ~s" customer msg))
          (cond ((equal request $ATBALANCE)
                 (unless (equal (getarg $REQUEST msgargs) $BALANCE)
                   (error "Server wrapped a non-balance request with @balance"))
                 (let ((assetid
                        (or (getarg $ASSET msgargs)
                            (error "Server wrapped balance missing asset ID")))
                       (acct (or (getarg $ACCT msgargs) $MAIN)))
                   (setf (gethash assetid (get-inited-hash acct balances))
                         (get-parsemsg req))))
                ((equal request $ATBALANCEHASH)
                 (unless (equal (getarg $REQUEST msgargs) $BALANCEHASH)
                   (error "Server wrapped a non-balancehash request with @balancehash"))
                 (setq balancehash (get-parsemsg req)))
                ((equal request $ATFRACTION)
                 (unless (equal (getarg $REQUEST msgargs) $FRACTION)
                   (error "Server wrapped a non-fraction request with @fraction"))
                 (let ((assetid
                        (or (getarg $ASSET msgargs)
                            (error "Server wrapped fraction missing asset ID")))
                       (fraction (get-parsemsg req)))
                   (setf (gethash assetid fractions) fraction))))))
      ;; Get outbox
      (setq reqnum (bcadd reqnum 1)
            msg (sendmsg client $GETOUTBOX serverid reqnum))
      (let ((reqs (parse parser msg t))
            (outbox (make-equal-hash))
            (outboxhash nil)
            (outboxtime nil))
        (dolist (req reqs)
          (setq args (match-serverreq client req))
          (let* ((request (getarg $REQUEST args))
                 (msgargs (getarg $MSG args))
                 (customer (and msgargs (getarg $CUSTOMER msgargs))))
            (when (and msgargs (not (equal customer id)))
              (error "Server wrapped somebody else's (~a) message: ~s"
                     customer msg))
            (cond ((equal request $ATGETOUTBOX))
                  ((equal request $ATSPEND)
                   (unless (equal (getarg $REQUEST msgargs) $SPEND)
                     (error "Server wrapped a non-spend request with @spend"))
                   (let ((time (getarg $TIME msgargs)))
                     (setf outboxtime time
                           (gethash time outbox) (get-parsemsg req))))
                  ((equal request $ATTRANFEE)
                   (unless (equal (getarg $REQUEST msgargs) $TRANFEE)
                     (error "Server wrapped a non-tranfee request with @tranfee"))
                   (let* ((time (getarg $TIME msgargs))
                          (msg (or (gethash time outbox)
                                   (error "No spend message for time: ~s" time))))
                     (setf (gethash time outbox)
                           (strcat msg "." (get-parsemsg req)))))
                  ((equal request $ATOUTBOXHASH)
                   (unless (equal (getarg $REQUEST msgargs) $OUTBOXHASH)
                     (error "Server wrapped a non-outbox request with @outboxhash"))
                   (setq outboxhash (get-parsemsg req)))
                  ((equal request $COUPONENVELOPE)
                   (unless outboxtime
                     (error "Got a coupon envelope with no outboxtime"))
                   (let ((msg (or (gethash outboxtime outbox)
                                  (error "No spend message for coupon envelope"))))
                     (setq msg (strcat msg "." (get-parsemsg req)))
                     (setf (gethash outboxtime outbox) msg
                           outboxtime nil)))
                  (t
                   (error "While processing getoutbox: bad request: ~s"
                          request)))))
        
        (when (and (not (equal id serverid))
                   (not outboxhash)
                   outbox
                   (> (hash-table-count outbox) 0))
          (error "While procesing getoutbox: outbox items but no outboxhash"))
        
        ;; All is well. Write the data
        (loop
           for acct being the hash-key using (hash-value assets) of balances
           do
           (loop
              for assetid being the hash-key using (hash-value msg) of assets
              do
              (setf (db-get db (userbalancekey client acct assetid)) msg)))
        
        (loop
           for assetid being the hash-key using (hash-value fraction)
           of fractions
           do
           (setf (db-get db (userfractionkey client assetid)) fraction))
        
        (loop
           for time being the hash-key using (hash-value msg) of outbox
           do
           (setf (db-get db (useroutboxkey client time)) msg))
        
        (setf (db-get db (userbalancehashkey client)) balancehash
              (db-get db (useroutboxhashkey client)) outboxhash
              (db-get db (userreqkey client)) reqnum)))
    ;; update fees
    (ignore-errors                  ;server may not implement fees
      (getfees client t))
    ;; update permissions
    (ignore-errors                  ;server may not implement permissions
      (get-permissions client nil t))
    (when (member $TWOPHASECOMMIT (getfeatures client t) :test #'equal)
      (get-last-transaction client t))
    nil))))
        
    */

    public String balancehashmsg(String time, StringMapMap acctbals) {
        // TO DO
        return null;
    }

    /**
     * Cached value for unpacker()
     */
    protected Utility.MsgUnpacker unpacker = null;

    /**
     * Cache and return a MsgUnpacker instance for this Client instance
     * @return
     */
    protected Utility.MsgUnpacker unpacker() {
        if (unpacker != null)
            return unpacker;
        unpacker = new Utility.MsgUnpacker() {
            public Parser.Dict unpack(String msg) throws Exception {
                return Client.this.unpackServermsg(msg);
            }
        };
        return unpacker;
    }

    /**
     * Return a balance hash message
     * @param time the commit time
     * @param acctbals map acct to map of assetid to balance message
     * @param useTwoPhaseCommit
     * @return
     * @throws ClientException
     */
    public String balancehashmsg(String time, StringMapMap acctbals, boolean useTwoPhaseCommit)
            throws ClientException {
        Utility.DirHash dirhash;
        try {
            dirhash = this.balancehash(db.getAccountDB(), this.unpacker(), this.userBalanceKey(), acctbals);
        } catch (Exception e) {
            throw new ClientException(e);
        }
        String hashcnt = String.valueOf(dirhash.count);
        String hash = dirhash.hash;
        return useTwoPhaseCommit ? this.custmsg(T.BALANCEHASH, serverid, time, hashcnt, hash, T.TWOPHASECOMMIT)
                : this.custmsg(T.BALANCEHASH, serverid, time, hashcnt, hash);
    }

    /**
     * Return the hash of the outbox messages
     * @param transtime the transaction time
     * @param newitems new outbox messages
     * @param removedtimes times of removed outbox messages
     * @param useTwoPhaseCommit
     * @return
     * @throws ClientException
     */
    public String outboxhashmsg(String transtime, String[] newitems, String[] removedtimes,
            boolean useTwoPhaseCommit) throws ClientException {
        FSDB accountDB = db.getAccountDB();
        String key = this.userOutboxKey();
        Utility.DirHash dirHash;
        try {
            dirHash = Utility.dirhash(accountDB, key, this.unpacker(), removedtimes, newitems);
        } catch (Exception e) {
            throw new ClientException(e);
        }
        String hash = "";
        String hashcnt = "0";
        if (dirHash != null) {
            hash = dirHash.hash;
            hashcnt = String.valueOf(dirHash.count);
        }
        return useTwoPhaseCommit ? this.custmsg(T.OUTBOXHASH, serverid, transtime, hashcnt, hash, T.TWOPHASECOMMIT)
                : this.custmsg(T.OUTBOXHASH, serverid, transtime, hashcnt, hash);
    }

    /*
    (defmethod outboxhashmsg ((client client) transtime &key
                      newitem removed-times two-phase-commit-p)
      (let ((db (db client))
    (serverid (serverid client))
    (key (useroutboxkey client)))
        (multiple-value-bind (hash hashcnt)
    (dirhash db key (unpacker client) newitem removed-times)
          (if two-phase-commit-p
      (custmsg client $OUTBOXHASH serverid transtime
               (or hashcnt 0) (or hash "") $TWOPHASECOMMIT)
      (custmsg client $OUTBOXHASH serverid transtime
               (or hashcnt 0) (or hash ""))))))
    */

    /**
     * Compute the balance hash of all the server-signed messages in subdirs of balanceKey of db.
     * @param db
     * @param unpacker Parses and matches a server-signed message string into a Parser.Dict instance 
     * @param balancekey
     * @param acctbals if non-null, maps acct names to maps of assetids to non-server-signed balance messages.
     * @return
     * @throws ClientException
     */
    public Utility.DirHash balancehash(FSDB db, Utility.MsgUnpacker unpacker, String balancekey,
            StringMapMap acctbals) throws ClientException {
        String hash = null;
        int hashcnt = 0;
        String[] accts = db.contents(balancekey);
        if (acctbals != null) {
            Vector<String> acctsv = new Vector<String>();
            Set<String> keys = acctbals.keySet();
            for (String key : keys) {
                if (Utility.position(key, accts) < 0)
                    acctsv.add(key);
            }
            int size = acctsv.size();
            if (size > 0) {
                String[] newaccts = new String[accts.length + size];
                int i = 0;
                for (String acct : accts)
                    newaccts[i++] = acct;
                for (String acct : acctsv)
                    newaccts[i++] = acct;
                accts = newaccts;
            }
        }
        Vector<String> newitemsv = new Vector<String>();
        Vector<String> removednamesv = new Vector<String>();
        for (String acct : accts) {
            newitemsv.clear();
            removednamesv.clear();
            StringMap newacct = acctbals != null ? acctbals.get(acct) : null;
            if (newacct != null) {
                Set<String> assetids = newacct.keySet();
                for (String assetid : assetids) {
                    String msg = newacct.get(assetid);
                    newitemsv.add(msg);
                    removednamesv.add(assetid);
                }
            }
            int cnt = newitemsv.size();
            String[] newitems = cnt > 0 ? newitemsv.toArray(new String[cnt]) : null;
            cnt = removednamesv.size();
            String[] removednames = cnt > 0 ? removednamesv.toArray(new String[cnt]) : null;
            try {
                Utility.DirHash dirHash = Utility.dirhash(db, balancekey + '.' + acct, unpacker, removednames,
                        newitems);
                if (dirHash != null) {
                    hash = hash == null ? dirHash.hash : hash + '.' + dirHash.hash;
                    hashcnt += dirHash.count;
                }
            } catch (Exception e) {
                throw new ClientException(e);
            }
        }
        if (hashcnt > 1)
            hash = Crypto.sha1(hash);
        return new Utility.DirHash(hash == null ? "" : hash, hashcnt);
    }

    // Web client session support

    /**
     * Generate a new sessionID
     * @return a 40-digit random hex string
     */
    public static String newSessionid() {
        SecureRandom random = Crypto.getRandom();
        byte[] bytes = new byte[20];
        random.nextBytes(bytes);
        return Utility.bin2hex(bytes);
    }

    /**
     * xor hashed copies of KEY with STRING and return the result.
      * This is a really simple encryption that only really works if
      * KEY is known to be random, e.g. the output of newSessionid()
     * @param key
     * @param string
     * @return
     */
    public static String xorcrypt(String key, String string) {
        int len = string.length();
        StringBuilder buf = new StringBuilder(len);
        String keybin = Utility.hex2bin(Crypto.sha1(key));
        int keylen = keybin.length();
        int idx = 0;
        for (int i = 0; i < len; i++) {
            buf.append((char) ((int) string.charAt(i) ^ (int) keybin.charAt(idx)));
            if (++idx >= keylen)
                idx = 0;
        }
        return buf.toString();
    }

    /**
     * @return The path to the user session directory in the AccountDB
     */
    public String userSessionKey() {
        return id + "/" + T.SESSION;
    }

    /**
     * Get the hash to look up the session encrypted passphrase for the current user
     * @return the hash
     */
    public String userSessionHash() {
        return db.getAccountDB().get(this.userSessionKey());
    }

    /**
     * Get the passphrase for a session
     * @param sessionid The ID of the session
     * @return The user passphrase for the session
     * @throws ClientException If sessionid has no saved encrypted passphrase
     */
    public String sessionPassphrase(String sessionid) throws ClientException {
        String passcrypt = db.getSessionDB().get(Crypto.sha1(sessionid));
        if (passcrypt == null)
            throw new ClientException("No passphrase for session");
        return xorcrypt(sessionid, passcrypt);
    }

    /**
     * Make a new session for the current user
     * @param passphrase The user's passphrase
     * @return the new session ID
     */
    public String makeSession(String passphrase) {
        String sessionid = newSessionid();
        String passcrypt = xorcrypt(sessionid, passphrase);
        String userSessionKey = this.userSessionKey();
        ClientDB.AccountDB accountDB = db.getAccountDB();
        ClientDB.SessionDB sessionDB = db.getSessionDB();
        String oldhash = accountDB.get(userSessionKey);
        if (oldhash != null)
            sessionDB.put(oldhash, null);
        String newhash = Crypto.sha1(sessionid);
        sessionDB.put(newhash, passcrypt);
        accountDB.put(userSessionKey, newhash);
        return sessionid;
    }

    /**
     * Remove the current user's session 
     */
    public void removeSession() {
        ClientDB.SessionDB sessiondb = db.getSessionDB();
        ClientDB.AccountDB accountdb = db.getAccountDB();
        String usersessionkey = this.userSessionKey();
        String oldhash = accountdb.get(usersessionkey);
        if (oldhash != null) {
            accountdb.put(usersessionkey, null);
            sessiondb.put(oldhash, null);
        }
    }

    /*
    (defmethod user-preference-key (client pref)
      "Preferences"
      (append-db-keys $ACCOUNT (id client) $PREFERENCE pref))
        
    (defmethod user-preference ((client client) pref)
      "Get or set a user preference.
       Include the $value to set."
      (require-current-user client)
      (let* ((db (db client))
     (key (user-preference-key client pref)))
        (db-get db key)))
        
    (defmethod (setf user-preference) (value (client client) pref)
      (require-current-user client)
      (let* ((db (db client))
     (key (user-preference-key client pref)))
        (setf (db-get db key) value)))
    */
    private static final Hashtable<String, String> nonEncryptionServerHash = new Hashtable<String, String>();

    public static boolean isNoServerEncryption(String serverid) {
        return !nonEncryptionServerHash.get(serverid).equals(null);
    }

    public static void setIsNoServerEncryption(String serverid, boolean value) {
        if (value)
            nonEncryptionServerHash.put(serverid, serverid);
        else
            nonEncryptionServerHash.remove(serverid);
    }

    /*
    (defvar *inside-opensession-p* nil)
        
    (defmethod opensession ((client client) &key
                    timeout inactivetime auto-session-p)
      (unless *inside-opensession-p*
        (require-current-server client "In opensession(): server not set")
        (let ((*inside-opensession-p* t))
          (handler-case
      (opensession-internal
       client timeout inactivetime auto-session-p)
    (error ()
      (opensession-internal
       client timeout inactivetime auto-session-p t))))))
        
    (defun opensession-internal (client timeout inactivetime auto-session-p
                         &optional reinit-p)
      (unless (and auto-session-p (no-server-encryption-p (serverid client)))
        (let* ((req (getreq client reinit-p))
       (msg (cond (inactivetime
                   (custmsg client $OPENSESSION (serverid client) req
                            (or timeout "") inactivetime))
                  (timeout
                   (custmsg client $OPENSESSION (serverid client) req
                            timeout))
                  (t (custmsg client $OPENSESSION (serverid client) req))))
       (*msg* msg)
       (servermsg (process (server client) msg))
       (args (unpack-servermsg client servermsg $ATOPENSESSION))
       (ciphertext (getarg $CIPHERTEXT args))
       (plaintext (privkey-decrypt ciphertext (privkey client)))
       (id&key (parse-square-bracket-string plaintext)))
          (unless (equal *msg* (get-parsemsg (getarg $MSG args)))
    (error "Server return doesn't wrap request message"))
          (new-client-crypto-session (first id&key) (id client) (second id&key)))))
        
    (defmethod closesession ((client client))
      (require-current-server client "In close-session: server not set")
      (let ((*inside-opensession-p* t))
        (handler-case
    (closesession-internal client)
          (error ()
    (closesession-internal client t)))))
        
    (defun closesession-internal (client &optional reinit-p)
      (let* ((id (id client))
     (session (get-client-userid-crypto-session id)))
        (when session
          (let* ((serverid (serverid client))
         (req (getreq client reinit-p))
         (sessionid (and session (crypto-session-id session)))
         (msg (sendmsg client $CLOSESESSION serverid req sessionid)))
    (unpack-servermsg client msg $ATCLOSESESSION)
    (remove-client-crypto-session id)))))
    */
    // For reporting client errors
    public static class ClientException extends Exception {
        private static final long serialVersionUID = -7740576192574990988L;

        public ClientException(String msg) {
            super(msg);
        }

        public ClientException(String msg, Exception e) {
            super((msg == null ? "" : msg + " - ") + e.getClass().getName() + ": " + e.getMessage());
        }

        public ClientException(Exception e) {
            super(null, e);
        }
    }

    /**
     * Create a new ServerProxy
     * @param url The URL of the server
     * @return 
     */
    public ServerProxy makeServerProxy(String url) {
        return new ServerProxy(url);
    }

    /**
     * This class controls the connection to the Truledger server
     * It also encapsulates the wire encryption.
     * @author billstclair
     */
    public class ServerProxy {
        String url;
        AndroidHttpClient httpClient;

        public ServerProxy(String url) {
            this.url = url;
        }

        public void close() {
            AndroidHttpClient c = httpClient;
            if (c != null) {
                httpClient = null;
                c.close();
            }
        }

        public String post(String msg, boolean debug) throws ClientException {
            AndroidHttpClient c = httpClient;
            if (c == null) {
                c = httpClient = AndroidHttpClient.newInstance("Truledger-Android", ctx);
            }

            // Add parameters to the post request
            HttpPost post = new HttpPost(url);
            List<NameValuePair> nvp = new ArrayList<NameValuePair>(2);
            nvp.add(new BasicNameValuePair("msg", msg));
            if (debug) {
                nvp.add(new BasicNameValuePair("debugmsgs", "true"));
            }
            try {
                post.setEntity(new UrlEncodedFormEntity(nvp));

                // Send the post to the server
                HttpResponse response = httpClient.execute(post);

                // Turn the response into a string
                InputStream stream = response.getEntity().getContent();
                StringBuilder res = new StringBuilder();
                BufferedReader rd = new BufferedReader(new InputStreamReader(stream), 4096);
                String line;
                while ((line = rd.readLine()) != null) {
                    res.append(line);
                    res.append("\n");
                }
                return res.toString();
            } catch (Exception e) {
                throw new ClientException(e);
            }
        }

        public String post(String msg) throws ClientException {
            return this.post(msg, false);
        }

        /*
        (defun ensure-client-crypto-session (client)
          (and (id client)
               (serverid client)
               (not (no-server-encryption-p (serverid client)))
               (or (get-client-userid-crypto-session (id client))
                   (values (ignore-errors (opensession client :auto-session-p t))
               t))))
            
        ;; This prevents thrashing after a new crypto-session is created.
        (defun update-msg-req-numbers (client msg)
          (let* ((parser (parser client))
                 (reqs (parse parser msg nil))
                 (newmsg nil))
            (when (and reqs (null (cdr reqs)))
              (let* ((req (car reqs))
         (m (get-parsemsg req))
         (args (match-pattern parser req))
         (req (getarg $REQ args)))
                (when req
                  (setf req (getreq client))
                  (let* ((pattern (gethash (getarg $REQUEST args) (patterns)))
             (names `(,$REQUEST ,@pattern))
             (newargs (mapcar (lambda (name)
                                (cond ((equal name $REQ) req)
                                      (t (getarg (if (listp name)
                                                     (car name)
                                                     name)
                                                 args))))
                              names)))
        (setf newargs (nreverse newargs))
        (loop
           (when (or (null newargs) (car newargs))
             (return))
           (pop newargs))
        (setf newargs (mapcar (lambda (x) (or x ""))
                              (nreverse newargs))
              m (apply #'custmsg client newargs))))
                (setf newmsg m)
                (when (equal msg *msg*)
                  (setf *msg* newmsg))))
            newmsg))
        */

        /**
         * This will eventually add the wire encryption to post().
         * For now, it just calls post()
         * @param msg
         * @return
         */
        public String process(String msg) throws ClientException {
            return this.post(msg);
        }

        /*
        (defmethod process ((proxy serverproxy) msg)
          (let* ((url (url proxy))
                 (client (client proxy))
                 (test-server (test-server client)))
            
            ;; This is a kluge to get around versions of Apache that insist
            ;; on sending "301 Moved Permanently" for directory URLs that
            ;; are missing a trailing slash.
            ;; Drakma can likely handle this, but I'm just copying the PHP
            ;; code for now.
            (unless (eql #\/ (aref url (1- (length url))))
              (dotcat url "/"))
            
            (let* ((vars `(("msg" . ,msg)))
                   (id (id client)))
            
              (when (debug-stream-p)
                (push '("debugmsgs" . "true") vars))
            
              (let ((text nil)
        res)
                (if test-server
        (setf res (truledger-server:process test-server msg))
        (multiple-value-bind (session new-session-p)
            (ensure-client-crypto-session client)
          (when (and session new-session-p)
            (setf msg (update-msg-req-numbers client msg)))
          (flet
              ((doit ()
                 (when (debug-stream-p)
                   (debugmsg "<b>===SENT</b>: ~a~%" (trimmsg msg)))
                 (when session
                   (when (debug-stream-p)
                     (debugmsg "<b>Using crypto session: ~s~%"
                               (crypto-session-id session)))
                   (setf (cdr (assoc "msg" vars :test #'equal))
                         (encrypt-for-crypto-session session msg)))
                 (multiple-value-bind (res status headers)
                     (post proxy url vars)
                   (when (eql status 301)
                     (let ((location
                            (cdr (assoc :location headers
                                        :test #'eq))))
                       (when location
                         (setf (url proxy) location
                               res (post proxy location vars)))))
                   res)))
            (declare (dynamic-extent #'doit))
            (setf res (doit))
            (when session
              (cond ((not (square-bracket-string-p res))
                     ;; Server doesn't do wire encryption
                     (remove-client-crypto-session id)
                     (setf (no-server-encryption-p (serverid client)) t
                           session nil))
                    (t
                     (block nil
                       (when (setf res (ignore-errors
                                         (decrypt-for-crypto-session res)))
                         (return))
                       (remove-client-crypto-session id)
                       (setf session
                             (ensure-client-crypto-session client)
                             msg (update-msg-req-numbers client msg)
                             res (doit))
                       (unless session (return))
                       (when (setf res (ignore-errors
                                         (decrypt-for-crypto-session res)))
                         (return))
                       (remove-client-crypto-session id)
                       (error
                        "Unable to privately communicate with server"))))))))
                (when (and (> (length res) 2)
               (equal "<<" (subseq res 0 2)))
                  (let ((pos (search #.(format nil ">>~%") res)))
        (when pos
          (setq text (subseq res 2 pos)
                res (subseq res (+ pos 3))))))
                (when text
                  (debugmsg "<b>===SERVER SAID</b>: ~a" (hsc text))
                  (let ((len (length text)))
        (unless (and (> len 0) (eql #\newline (aref text (1- len))))
          (debugmsg "~%"))))
                
                (when (debug-stream-p)
                  (debugmsg "<b>===RETURNED</b>: ~a~%" (and msg (trimmsg res))))
            
                res))))
        */

    }

    /*
    (defun trimmsg (msg)
      (let* ((msg (remove-signatures msg))
     (tokens (mapcar #'cdr (tokenize msg)))
     (res ""))
        (dolist (token tokens)
          (cond ((characterp token) (dotcat res (hsc (string token))))
        ((ishex-p token) (dotcat res (hsc token)))
        (t (dotcat res "<b>" (hsc token) "</b>"))))
        res))
        
    (defun ishex-p (str)
      (let ((len (length str)))
        (dotimes (i len t)
          (unless (position (aref str i) "0123456789abcdef")
    (return nil)))))
        
    ;; Look up a public key, from the client database first, then from the
    ;; current server.
    (defclass pubkeydb (db)
      ((client :type client
       :initarg :client
       :accessor client)
       (db :type db
           :initarg :db
           :accessor db)))
        
    (defvar *insidep* nil)
        
    (defmethod db-get ((pubkeydb pubkeydb) id &rest more-keys)
      (assert (null more-keys) nil "PUBKEYDB takes only a single DB-GET key")
      (or (db-get (db pubkeydb) id)
          (and (not *insidep*)
       (let ((*insidep* t))
         (get-pubkey-from-server (client pubkeydb) id)))))
        
    ;;;
    ;;; Loom client db access
    ;;;
        
    ;; Don't go over the wire for hashing
    (setf (loom:sha256-function) #'sha256)
        
    (defun make-client-db ()
      (fsdb:make-fsdb (client-db-dir)))
        
    (defun folded-hash (string)
      (loom:fold-hash (sha256 string)))
        
    (defun random-hash ()
      (string-downcase (format nil "~64,'0x" (cl-crypto:get-random-bits 256))))
        
    (defun loom-get-salt (db)
      (or (db-get db $LOOM $SALT)
          (setf (db-get db $LOOM $SALT)
        (random-hash))))
        
    (defun format-sha256 (integer)
      (format nil "~(~64,'0x~)" integer))
        
    (defun salted-hash (db string &optional fold-p)
      (let* ((salt (parse-integer (loom-get-salt db) :radix 16))
     (hash (sha256
            (format-sha256
             (logxor salt (parse-integer string :radix 16))))))
        (if fold-p
    (loom:fold-hash hash)
    hash)))
        
    (defun loom-urlhash (url)
      (loom:fold-hash (sha256 url)))
        
    (defun loom-account-hash (db passphrase)
      (salted-hash db (sha256 passphrase) t))
        
    (defun loom-get-server-url (db urlhash)
      (db-get db $LOOM $SERVER urlhash))
        
    (defun (setf loom-get-server-url) (url db urlhash)
      (setf (db-get db $LOOM $SERVER urlhash) url))
        
    (defun loom-add-server-url (db url)
      (setf (loom-get-server-url db (loom-urlhash url)) url))
        
    (defun loom-account-key (account-hash)
      (fsdb:append-db-keys $LOOM $ACCOUNT account-hash))
        
    (defun loom-account-server-key (account-hash &optional urlhash)
      (let ((res (fsdb:append-db-keys (loom-account-key account-hash) $SERVER)))
        (if urlhash
    (fsdb:append-db-keys res urlhash)
    res)))
        
    (defun loom-account-preference (db account-hash &rest pref-path)
      (apply #'fsdb:db-get db (loom-account-key account-hash) $PREFERENCE pref-path))
        
    (defun (setf loom-account-preference) (value db account-hash &rest pref-path)
      (let ((key (apply #'fsdb:append-db-keys $PREFERENCE pref-path)))
        (setf (fsdb:db-get db (loom-account-key account-hash) key)
      value)))
        
    (defun loom-urlhash-preference (db account-hash)
      (loom-account-preference db account-hash $URLHASH))
        
    (defun (setf loom-urlhash-preference) (value db account-hash)
      (check-type value string)
      (setf (loom-account-preference db account-hash $URLHASH) value))
        
    (defun loom-namehash-preference (db account-hash urlhash)
      (loom-account-preference db account-hash $NAMEHASH urlhash))
        
    (defun (setf loom-namehash-preference) (value db account-hash urlhash)
      (check-type value (or null string))
      (setf (loom-account-preference db account-hash $NAMEHASH urlhash) value))
        
    (defstruct loom-server
      url
      urlhash
      wallets)
        
    (defstruct loom-wallet
      name
      urlhash
      namehash
      encrypted-passphrase
      encrypted-wallet-string
      private-p)
        
    ;; We modify the passphrase a little so that it hashes
    ;; differently for encryption.
    ;; The loom address of an unencrypted wallet is the sha1 hash of
    ;; the passphrase. Don't want to give away the sha1 of the encryption
    ;; passphrase.
    (defun loom-passphrase (passphrase)
      (concatenate 'string passphrase (reverse passphrase)))
        
    (defun encrypt (plain-text passphrase)
      (multiple-value-bind (res iv)
          (cl-crypto:aes-encrypt-string plain-text (loom-passphrase passphrase))
        (concatenate 'string
             (cl-base64:usb8-array-to-base64-string iv)
             "|"
             res)))
        
    (defun decrypt (cipher-text passphrase)
      (let ((iv-and-res (split-sequence:split-sequence #\| cipher-text)))
        (cl-crypto:aes-decrypt-to-string
         (second iv-and-res) (loom-passphrase passphrase) :iv (first iv-and-res))))
        
    (defun loom-wallet-passphrase (wallet account-passphrase)
      (check-type wallet loom-wallet)
      (decrypt (loom-wallet-encrypted-passphrase wallet) account-passphrase))
        
    (defun loom-wallet-location (wallet account-passphrase)
      (check-type wallet loom-wallet)
      (let ((passphrase (loom-wallet-passphrase wallet account-passphrase)))
        (loom:passphrase-location passphrase t (loom-wallet-private-p wallet))))
        
    (defun loom-stored-wallet-string (wallet account-passphrase)
      (check-type wallet loom-wallet)
      (let ((str (loom-wallet-encrypted-wallet-string wallet)))
        (and str (decrypt str account-passphrase))))
        
    (defun loom-stored-wallet (wallet account-passphrase)
      (let ((str (loom-stored-wallet-string wallet account-passphrase)))
        (and str (loom:parse-wallet-string str))))
        
    ;; Adds to the local database only. Doesn't touch the remote server.
    (defun add-loom-wallet (db account-passphrase url name passphrase &optional private-p)
      (let* ((urlhash (loom-urlhash url))
     (account-hash (loom-account-hash db account-passphrase))
     (server-key (loom-account-server-key account-hash urlhash))
     (namehash (folded-hash name))
     (wallet-key (fsdb:append-db-keys server-key $WALLET namehash)))
        (unless (loom-get-server-url db urlhash)
          (setf (loom-get-server-url db urlhash) url))
        (when (fsdb:db-get db server-key $WALLETNAME namehash)
          (error "A wallet named ~s already exists for ~s"
         name url))
        (setf (fsdb:db-get db server-key $WALLETNAME namehash) name
      (fsdb:db-get db wallet-key $PASSPHRASE)
      (encrypt passphrase account-passphrase)
      (fsdb:db-get db wallet-key $PRIVATE) (and private-p "yes"))
        namehash))
        
    (defun store-loom-wallet (db account-passphrase wallet loom-wallet &optional
                      (account-hash
                       (loom-account-hash db account-passphrase)))
      (check-type wallet loom-wallet)
      (check-type loom-wallet loom:wallet)
      (let* ((server-key (loom-account-server-key
                 account-hash (loom-wallet-urlhash wallet)))
     (wallet-key (fsdb:append-db-keys
                  server-key $WALLET (loom-wallet-namehash wallet)))
     (wallet-string (loom:wallet-string loom-wallet))
     (encrypted-wallet-string (encrypt wallet-string account-passphrase)))
        (setf (loom-wallet-encrypted-wallet-string wallet)
      encrypted-wallet-string
      (fsdb:db-get db wallet-key $WALLET) encrypted-wallet-string)))
        
    (defun loom-account-servers (db account-hash &optional include-wallets-p)
      (let ((server-key (loom-account-server-key account-hash))
    (res nil))
        (dolist (urlhash (fsdb:db-contents db server-key))
          (let ((url (loom-get-server-url db urlhash))
        (wallets (and include-wallets-p
                      (loom-account-wallets db account-hash urlhash))))
    (push (make-loom-server :url url
                            :urlhash urlhash
                            :wallets wallets)
          res)))
        res))
        
    (defun loom-account-wallets (db account-hash urlhash)
      (let* ((server-key (loom-account-server-key account-hash))
     (walletname-key (fsdb:append-db-keys server-key urlhash $WALLETNAME))
     (wallet-key (fsdb:append-db-keys server-key urlhash $WALLET))
     (wallets nil))
        (dolist (namehash (fsdb:db-contents db walletname-key))
          (let ((name (fsdb:db-get db walletname-key namehash))
        (encrypted-passphrase
         (fsdb:db-get db wallet-key namehash $PASSPHRASE))
        (encrypted-wallet-string
         (fsdb:db-get db wallet-key namehash $WALLET))
        (private-p
         (fsdb:db-get db wallet-key namehash $PRIVATE)))
    (push (make-loom-wallet :name name
                            :urlhash urlhash
                            :namehash namehash
                            :encrypted-passphrase encrypted-passphrase
                            :encrypted-wallet-string encrypted-wallet-string
                            :private-p (not (null private-p)))
          wallets)))
        (sort wallets 'string-lessp :key #'loom-wallet-name)))
        
    (defconstant $truledger-saved-servers "truledger-saved-servers")
        
    ;; Encrypted with passphrase:
    ;;   (
    ;;   :url-<urlhash>=<url>
    ;;   :wallet-<urlhash>-<namehash>=<name>
    ;;   :passphrase-<urlhash>-<namehash>=<wallet-passphrase>
    ;;   :private-<urlhash>-<namehash>=1
    ;;   ...
    ;;   )
    (defun loom-encode-servers-for-save (account-passphrase servers passphrase)
      (let (alist)
        (dolist (server servers)
          (let ((urlhash (loom-server-urlhash server)))
    (push (cons (strcat "url-" urlhash)
                (loom-server-url server))
          alist)
    (dolist (wallet (loom-server-wallets server))
      (let* ((namehash (loom-wallet-namehash wallet))
             (urlhash-namehash (strcat urlhash "-" namehash)))
        (push (cons (strcat "wallet-" urlhash-namehash)
                    (loom-wallet-name wallet))
              alist)
        (push (cons (strcat "passphrase-" urlhash-namehash)
                    (loom-wallet-passphrase wallet account-passphrase))
              alist)
        (when (loom-wallet-private-p wallet)
          (push (cons (strcat "private-" urlhash-namehash) "1")
                alist))))))
        (encrypt (loom:alist-to-kv-string (nreverse alist))
         passphrase)))
        
    (defun loom-decode-servers-from-cipher-text
        (account-passphrase cipher-text passphrase)
      (let* ((kv (decrypt cipher-text passphrase))
     (alist (loom:parse-kv kv))
     (urls nil)
     (wallet-names (make-hash-table :test #'equal))
     (passphrases (make-hash-table :test #'equal))
     (privates (make-hash-table :test #'equal)))
        (labels ((hashes (str prefix)
           (apply #'values (split-sequence:split-sequence
                            #\- (subseq str (length prefix)))))
         (try (k v prefix hash)
           (when (eql 0 (search prefix k :test #'equal))
             (multiple-value-bind (urlhash namehash) (hashes k prefix)
               (push (cons namehash v) (gethash urlhash hash))
               t))))
          (loop for (k . v) in alist
     do
       (cond ((eql 0 (search "url-" k :test #'equal))
              (push (cons (subseq k 4) v) urls))
             (t (or (try k v "wallet-" wallet-names)
                    (try k v "passphrase-" passphrases)
                    (try k v "private-" privates)
                    (error "Unknown key: ~s" k)))))
          (loop for (urlhash . url) in urls
     for wallets =
       (loop for (namehash . name) in (gethash urlhash wallet-names)
          for passphrase = (or (cdr (assocequal
                                     namehash (gethash urlhash passphrases)))
                               (error "Missing passphrase."))
          for private = (cdr (assocequal namehash (gethash urlhash privates)))
          collect (make-loom-wallet
                   :name name
                   :namehash namehash
                   :urlhash urlhash
                   :encrypted-passphrase (encrypt passphrase account-passphrase)
                   :private-p (not (null private))))
     collect (make-loom-server :url url :urlhash urlhash :wallets wallets)))))
        
    ;; Returns three booleans:
    ;;   1) saving servers will change something
    ;;   2) saving servers will lose a passphrase
    ;;   2) restoring saved-servers will change something
    (defun loom-compare-servers-to-saved (account-passphrase servers saved-servers)
      (multiple-value-bind (saving-changes-p saving-loses-p)
          (servers-change-saved-servers-p
           account-passphrase servers saved-servers)
        (values saving-changes-p saving-loses-p
        (saved-servers-change-servers-p
         account-passphrase servers saved-servers))))
        
    ;; Returns two booleans:
    ;;   1) saving servers will change something
    ;;   2) saving server will lose a passphrase
    (defun servers-change-saved-servers-p (account-passphrase servers saved-servers)
      (when (set-difference saved-servers servers
                    :test #'equal :key #'loom-server-urlhash)
        (return-from servers-change-saved-servers-p (values t t)))
      (loop with change-p = (not (eql (length servers) (length saved-servers)))
         for server in servers
         for urlhash = (loom-server-urlhash server)
         for saved-server = (find urlhash saved-servers
                          :test #'equal :key #'loom-server-urlhash)
         do
           (if (not saved-server)
       (setf change-p t)
       (let* ((wallets (loom-server-wallets server))
              (saved-wallets (loom-server-wallets saved-server))
              (saved-passphrases
               (loop for wallet in saved-wallets
                  collect (loom-wallet-passphrase
                           wallet account-passphrase))))
         (unless (eql (length wallets) (length saved-wallets))
           (setf change-p t))
         (loop for wallet in wallets
            for namehash = (loom-wallet-namehash wallet)
            for passphrase = (loom-wallet-passphrase wallet account-passphrase)
            for saved-wallet = (find namehash saved-wallets
                                     :test #'equal :key #'loom-wallet-namehash)
            do
              (setf saved-passphrases
                    (delete passphrase saved-passphrases :test #'equal))
              (if (not saved-wallet)
                  (setf change-p t)
                  (unless (equal passphrase
                                 (loom-wallet-passphrase
                                  saved-wallet account-passphrase))
                    (setf change-p t)))
            finally
              (when saved-passphrases
                (return-from servers-change-saved-servers-p
                  (values t t))))))
         finally
           (return (values change-p nil))))
        
    (defun saved-servers-change-servers-p (account-passphrase servers saved-servers)
      (loop for saved-server in saved-servers
         for urlhash = (loom-server-urlhash saved-server)
         for server = (find urlhash servers
                    :test #'equal :key #'loom-server-urlhash)
         do
           (unless server (return t))
           (let ((saved-wallets (loom-server-wallets saved-server))
         (passphrases (loop for wallet in  (loom-server-wallets server)
                         collect (loom-wallet-passphrase
                                  wallet account-passphrase))))
     (loop for wallet in saved-wallets
        for passphrase = (loom-wallet-passphrase wallet account-passphrase)
        unless (member passphrase passphrases :test #'equal)
        do (return-from saved-servers-change-servers-p t)))))
        
    (defmethod loom-save-wallets ((db fsdb:fsdb) account-passphrase urlhash namehash)
      (let* ((account-hash (loom-account-hash db account-passphrase))
     (servers (loom-account-servers db account-hash t))
     (save-server (or (find urlhash servers
                            :test #'equal :key #'loom-server-urlhash)
                      (error "Can't find save server.")))
     (save-wallet (or (and save-server
                           (find namehash (loom-server-wallets save-server)
                                 :test #'equal :key #'loom-wallet-namehash))
                      (error "Can't find save wallet"))))
        (let* ((loom-server (make-loom-uri-server db (loom-server-url save-server)))
       (passphrase (loom-wallet-passphrase save-wallet account-passphrase))
       (save-string (loom-encode-servers-for-save
                     account-passphrase servers passphrase))
       (private-p (loom-wallet-private-p save-wallet)))
          (loom:with-loom-transaction (:server loom-server)
    (let ((wallet (loom:get-wallet
                   passphrase t nil private-p)))
      (setf (loom:wallet-get-property wallet $truledger-saved-servers)
            save-string
            (loom:get-wallet passphrase t nil private-p) wallet))))))
        
    (defmethod loom-load-saved-wallets
        ((db fsdb:fsdb) account-passphrase urlhash namehash)
      (let* ((account-hash (loom-account-hash db account-passphrase))
     (servers (loom-account-servers db account-hash t))
     (server (or (find urlhash servers :test #'equal :key #'loom-server-urlhash)
                 (error "Can't find load server.")))
     (wallet (or (find namehash (loom-server-wallets server)
                       :test #'equal :key #'loom-wallet-namehash)
                 (error "Can't find save wallet.")))
     (passphrase (loom-wallet-passphrase wallet account-passphrase))
     (private-p (loom-wallet-private-p wallet))
     (loom-server (make-loom-uri-server db (loom-server-url server))))
        (loom:with-loom-server (loom-server)
          (let* ((loom-wallet (loom:get-wallet passphrase t nil private-p))
         (cipher-text (or (loom:wallet-get-property
                           loom-wallet $truledger-saved-servers)
                          (error "No saved servers in Loom wallet."))))
    (values
     (loom-decode-servers-from-cipher-text
      account-passphrase cipher-text passphrase)
     servers)))))
        
    (defun make-unique-wallet-name (name wallets)
      (loop for i from 1
         with res = name
         do
           (unless (find res wallets :test #'equal :key #'loom-wallet-name)
     (return res))
           (setf res (format nil "~a ~d" name i))))
        
    (defmethod loom-restore-saved-wallets
        ((db fsdb:fsdb) account-passphrase urlhash namehash)
      (multiple-value-bind (saved-servers servers)
          (loom-load-saved-wallets db account-passphrase urlhash namehash)
        (loop for saved-server in saved-servers
           for url = (loom-server-url saved-server)
           for urlhash = (loom-server-urlhash saved-server)
           for server = (find urlhash servers :test #'equal :key #'loom-server-urlhash)
           for wallets = (and server (loom-server-wallets server))
           for passphrases = (loop for wallet in wallets
                        collect (loom-wallet-passphrase
                                 wallet account-passphrase))
           do
     (loop for wallet in (loom-server-wallets saved-server)
        for name = (loom-wallet-name wallet)
        for passphrase = (loom-wallet-passphrase
                          wallet account-passphrase)
        for private-p = (loom-wallet-private-p wallet)
        unless (member passphrase passphrases :test #'equal)
        do
          (let ((name (make-unique-wallet-name name wallets)))
            (add-loom-wallet
             db account-passphrase url name passphrase private-p))))))
        
    (defmethod loom-remove-saved-wallets
        ((db fsdb) account-passphrase urlhash namehash)
      (let* ((account-hash (loom-account-hash db account-passphrase))
     (servers (loom-account-servers db account-hash t))
     (server (or (find urlhash servers :test #'equal :key #'loom-server-urlhash)
                 (error "Can't find load server.")))
     (wallet (or (find namehash (loom-server-wallets server)
                       :test #'equal :key #'loom-wallet-namehash)
                 (error "Can't find save wallet.")))
     (passphrase (loom-wallet-passphrase wallet account-passphrase))
     (private-p (loom-wallet-private-p wallet))
     (loom-server (make-loom-uri-server db (loom-server-url server))))
        (loom:with-loom-server (loom-server)
          (let* ((loom-wallet (loom:get-wallet passphrase t nil private-p)))
    (setf (loom:wallet-get-property loom-wallet $truledger-saved-servers) nil)
    (setf (loom:get-wallet passphrase t nil private-p) loom-wallet)))))
        
    (defun loom-rename-wallet (db passphrase urlhash namehash new-wallet-name)
      (let* ((account-hash (loom-account-hash db passphrase))
     (wallets (loom-account-wallets db account-hash urlhash))
     (wallet (find namehash wallets :test #'equal :key #'loom-wallet-namehash))
     (new-wallet (find new-wallet-name wallets
                       :test #'equal :key #'loom-wallet-name)))
        (unless wallet (error "No such wallet."))
        (when new-wallet (error "There is already a wallet with that name."))
        (let* ((url (loom-get-server-url db urlhash))
       (loom-server (make-loom-uri-server db url)))
          (loom:with-loom-transaction (:server loom-server)
    (let* ((wallet-passphrase (loom-wallet-passphrase wallet passphrase))
           (private-p (loom-wallet-private-p wallet))
           (loom-wallet (loom:get-wallet wallet-passphrase t nil private-p))
           (locations (loom:wallet-locations loom-wallet))
           (location (find-if #'loom:location-wallet-p locations))
           (new-location (find new-wallet-name locations
                               :test #'equal :key #'loom:location-name)))
      (when new-location
        (error "There is already a Loom contact with that name."))
      (setf (loom:location-name location) new-wallet-name
            (loom:get-wallet wallet-passphrase t nil private-p) loom-wallet)
      (let* ((server-key (loom-account-server-key account-hash urlhash))
             (old-file (fsdb:db-filename
                        db (fsdb:append-db-keys server-key $WALLET namehash)))
             (old-name-file (fsdb:db-filename
                             db (fsdb:append-db-keys
                                 server-key $WALLETNAME namehash)))
             (new-namehash (folded-hash new-wallet-name))
             (new-file (fsdb:db-filename
                        db (fsdb:append-db-keys
                            server-key $WALLET new-namehash)))
             (new-name-key (fsdb:append-db-keys
                            server-key $WALLETNAME new-namehash))
             (new-name-file (fsdb:db-filename db new-name-key)))
        (rename-file old-file new-file)
        (rename-file old-name-file new-name-file)
        (setf (fsdb:db-get db new-name-key) new-wallet-name)
        (when (equal namehash (loom-namehash-preference
                               db account-hash urlhash))
          (setf (loom-namehash-preference db account-hash urlhash)
                new-namehash))))))))
        
    (defun loom-remove-wallet (db account-hash urlhash namehash)
      (let* ((server-key (loom-account-server-key account-hash urlhash))
     (wallet-key (fsdb:append-db-keys server-key $WALLET namehash))
     (walletname-key (fsdb:append-db-keys server-key $WALLETNAME namehash)))
        (setf (fsdb:db-get db walletname-key) nil)
        (fsdb:recursive-delete-directory (fsdb:db-filename db wallet-key)
                                 :if-does-not-exist nil)
        (when (equal namehash (loom-namehash-preference db account-hash urlhash))
          (setf (loom-namehash-preference db account-hash urlhash) nil))))
        
    (defun find-unique-string (prefix strings)
      (loop with s = prefix
         for suffix from 2
         do (unless (member s strings :test #'equal) (return s))
           (setf s (format nil "~a ~d" prefix suffix))))
        
    (defun loom-merge-wallet-locations-and-assets (wallet merge-wallet)
      (check-type wallet loom:wallet)
      (check-type merge-wallet loom:wallet)
      (let* ((merge-locations (loom:wallet-locations merge-wallet))
     (merge-assets (loom:wallet-assets merge-wallet))
     (location-names (mapcar 'loom:location-name merge-locations))
     (asset-names (mapcar 'loom:asset-name merge-assets))
    new-locations new-assets)
        (dolist (location (loom:wallet-locations wallet))
          (unless (or (loom:location-wallet-p location)
              (loom:find-location-by-loc
               (loom:location-loc location) merge-locations))
    (setf (loom:location-name location)
          (find-unique-string (loom:location-name location) location-names))
    (push location new-locations)))
        (dolist (asset (loom:wallet-assets wallet))
          (unless (loom:find-asset-by-id (loom:asset-id asset) merge-assets)
    (setf (loom:asset-name asset)
          (find-unique-string (loom:asset-name asset) asset-names))
    (push asset new-assets)))
        (setf (loom:wallet-locations merge-wallet)
      (nconc (loom:wallet-locations merge-wallet) (nreverse new-locations)))
        (setf (loom:wallet-assets merge-wallet)
      (nconc (loom:wallet-assets merge-wallet) (nreverse new-assets)))
        merge-wallet))
        
    ;; This doesn't go through all the saved servers in all the wallets,
    ;; just the wallet being deleted and the one being merged into.
    ;; Maybe it should remove the deleted wallet from all saved servers in all wallets.
    (defun loom-merge-wallet-saved-servers
        (account-passphrase urlhash wallet merge-wallet &key
         (passphrase (loom-wallet-passphrase wallet account-passphrase))
         (merge-passphrase (loom-wallet-passphrase merge-wallet account-passphrase)))
      (check-type wallet loom:wallet)
      (check-type passphrase string)
      (check-type merge-wallet loom:wallet)
      (check-type merge-passphrase string)
      (let* ((cipher-text
      (loom:wallet-get-property wallet $truledger-saved-servers))
     (servers (and cipher-text
                   (loom-decode-servers-from-cipher-text
                    account-passphrase cipher-text passphrase)))
     (merge-cipher-text
      (loom:wallet-get-property merge-wallet $truledger-saved-servers))
     (merge-servers
      (and merge-cipher-text
           (loom-decode-servers-from-cipher-text
            account-passphrase merge-cipher-text merge-passphrase)))
     new-servers)
        (when servers
          (loop for server in servers
     for merge-server = (find (loom-server-urlhash server) merge-servers
                              :test #'equal :key #'loom-server-urlhash)
     with wallet-names
     for passphrases = (and merge-server
                            (loop for wallet in (loom-server-wallets
                                                 merge-server)
                               for pass = (loom-wallet-passphrase
                                           wallet account-passphrase)
                               collect pass
                               do
                                 (push (loom-wallet-name wallet) wallet-names)))
     for new-wallets = nil
     do
       (cond (merge-server
              (loop for wallet in (loom-server-wallets server)
                 for pass = (loom-wallet-passphrase wallet account-passphrase)
                 do
                   (unless (or (equal pass passphrase)
                               (member (loom-wallet-passphrase
                                        wallet account-passphrase)
                                       passphrases
                                       :test #'equal))
                     (setf (loom-wallet-name wallet)
                           (find-unique-string (loom-wallet-name wallet)
                                               wallet-names))
                     (push wallet new-wallets)))
              (setf (loom-server-wallets merge-server)
                    (nconc (loom-server-wallets merge-server)
                           (nreverse new-wallets))))
             (t (push server new-servers))))
          (setf merge-servers
        (nconc merge-servers (nreverse new-servers))))
        (when merge-servers
          ;; Don't save wallet we're deleting
          (let ((server (find urlhash merge-servers
                      :test #'equal :key #'loom-server-urlhash)))
    (when server
      (let* ((wallets (loom-server-wallets server))
             (deleted-wallet (loop for wallet in wallets
                                when (equal passphrase
                                            (loom-wallet-passphrase
                                             wallet account-passphrase))
                                do
                                  (return wallet))))
        (when deleted-wallet
          (setf (loom-server-wallets server)
                (delete deleted-wallet wallets))))))
          (setf (loom:wallet-get-property merge-wallet $truledger-saved-servers)
        (loom-encode-servers-for-save
         account-passphrase merge-servers merge-passphrase))
          t)))
        
    (defun loom-move-wallet-quantities (wallet merge-wallet)
      (let* ((wallet-loc (loom:location-loc
                  (find-if #'loom:location-wallet-p
                           (loom:wallet-locations wallet))))
     (merge-loc (loom:location-loc
                 (find-if #'loom:location-wallet-p
                          (loom:wallet-locations merge-wallet))))
     (asset-ids (mapcar #'loom:asset-id (loom:wallet-assets wallet)))
     (id.qtys (cdar (loom:grid-scan-wallet
                     nil :locations (list wallet-loc) :assets asset-ids))))
        (loop for (id . qty) in id.qtys
           do
     (unless (equal id loom:*zero*)
       (loom:grid-buy id merge-loc merge-loc))
     (if (eql #\- (aref qty 0))
         (loom:grid-issuer id wallet-loc merge-loc)
         (loom:grid-move id (parse-integer qty) wallet-loc merge-loc))
     (loom:grid-sell id wallet-loc merge-loc t))))
        
    (defun loom-merge-wallet (db passphrase urlhash namehash mergehash)
      "Merge the wallet at urlhash/namehash into the one at urlhash/mergehash."
      (let* ((account-hash (loom-account-hash db passphrase))
     (wallets (loom-account-wallets db account-hash urlhash))
     (wallet (find namehash wallets :test #'equal :key #'loom-wallet-namehash))
     (merge-wallet (find mergehash wallets
                         :test #'equal :key #'loom-wallet-namehash)))
        (unless wallet (error "Can't find wallet."))
        (unless merge-wallet (error "Can't find merge wallet."))
        (let* ((url (loom-get-server-url db urlhash))
       (loom-server (make-loom-uri-server db url))
       (wallet-passphrase (loom-wallet-passphrase wallet passphrase))
       (wallet-private-p (loom-wallet-private-p wallet))
       (merge-wallet-passphrase (loom-wallet-passphrase merge-wallet passphrase))
       (merge-wallet-private-p (loom-wallet-private-p merge-wallet)))
          (loom:with-loom-transaction (:server loom-server)
    (let ((loom-wallet
           (loom:get-wallet wallet-passphrase t nil wallet-private-p))
          (loom-merge-wallet
           (loom:get-wallet
            merge-wallet-passphrase t nil merge-wallet-private-p)))
      ;; Need to delete before we move, to recover the usage tokens
      ;; for the old wallet. All inside a transaction, so will undo
      ;; if something fails.
      (loom:delete-wallet wallet-passphrase merge-wallet-passphrase
                          :location-is-passphrase-p t
                          :usage-is-passphrase-p t
                          :private-p wallet-private-p
                          :usage-private-p merge-wallet-private-p)
      (loom-merge-wallet-locations-and-assets loom-wallet loom-merge-wallet)
      (loom-merge-wallet-saved-servers
       passphrase urlhash loom-wallet loom-merge-wallet
       :passphrase wallet-passphrase
       :merge-passphrase merge-wallet-passphrase)
      (setf (loom:get-wallet
             merge-wallet-passphrase t nil merge-wallet-private-p)
            loom-merge-wallet)
      (loom-move-wallet-quantities loom-wallet loom-merge-wallet)
      (loom-remove-wallet db account-hash urlhash namehash))))))
        
    ;;
    ;; Loom session stuff
    ;;
        
    (defmethod loom-login-with-sessionid ((db fsdb:fsdb) sessionid)
      (session-passphrase db sessionid))
          
    (defmethod loom-login-new-session ((db fsdb:fsdb) passphrase)
      "Check for existing loom servers for passphrase, create a new session, and return a sessionid."
      (let ((account-hash (loom-account-hash db passphrase)))
        (unless (loom-urlhash-preference db account-hash)
          (error "No loom account for that passphrase.")))
      (loom-make-session db passphrase))
        
    (defun loom-account-session-key (account-hash)
      (fsdb:append-db-keys (loom-account-key account-hash) $SESSION))
        
    (defvar *ssl-certificates-initialized-p* nil)
        
    (defvar *ssl-certificates-dir*
      "ssl-certificates")
        
    (defun ssl-certificates-dir ()
      (let ((dir *ssl-certificates-dir*))
        (if (functionp dir) (funcall dir) dir)))
        
    (defun (setf ssl-certificates-dir) (dir)
      (setf *ssl-certificates-dir* dir))
        
    (defun initialize-ssl-certificates (&optional (db (make-client-db)))
      (unless *ssl-certificates-initialized-p*
        (setf (loom:ssl-certificate-temp-dir) (fsdb:db-filename db "/"))
        (let ((files (directory
              (fsdb:append-db-keys (ssl-certificates-dir) "*.pem"))))
          (when files
    (cl+ssl:ssl-verify-init :verify-locations files)))
        (setf *ssl-certificates-initialized-p* t)))
        
    (defun make-loom-uri-server (db uri-string)
      (initialize-ssl-certificates db)
      (loom:make-loom-uri-server uri-string))
        
    (defmethod loom-make-session ((db fsdb:fsdb) passphrase)
      "Create a new loom user session, encoding $passphrase with a new session id.
       Return the new session id.
       If the user already has a session stored with another session id,
       remove that one first."
      (let* ((sessionid (newsessionid))
     (passcrypt (xorcrypt sessionid passphrase))
     (account-hash (loom-account-hash db passphrase))
     (loom-session-key (loom-account-session-key account-hash)))
        (with-db-lock (db loom-session-key)
          (let ((oldhash (db-get db loom-session-key)))
    (when oldhash
      (setf (db-get db (sessionkey oldhash)) nil)))
          (let ((newhash (sha1 sessionid)))
    (setf (db-get db (sessionkey newhash)) passcrypt
          (db-get db loom-session-key) newhash)))
        sessionid))
        
    (defmethod loom-remove-session ((db fsdb:fsdb) account-hash)
      "Remove the current user's session"
      (let* ((loom-session-key (loom-account-session-key account-hash)))
        (with-db-lock (db loom-session-key)
          (let ((oldhash (db-get db loom-session-key)))
    (when oldhash
      (setf (db-get db (sessionkey oldhash)) nil
            (db-get db loom-session-key) nil))))))
        
    (defmethod loom-logout ((db fsdb:fsdb) account-hash)
      (loom-remove-session db account-hash))
    */

}

//////////////////////////////////////////////////////////////////////
///
/// Copyright 2011-2012 Bill St. Clair
///
/// Licensed under the Apache License, Version 2.0 (the "License")
/// you may not use this file except in compliance with the License.
/// You may obtain a copy of the License at
///
///     http://www.apache.org/licenses/LICENSE-2.0
///
/// Unless required by applicable law or agreed to in writing, software
/// distributed under the License is distributed on an "AS IS" BASIS,
/// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
/// See the License for the specific language governing permissions
/// and limitations under the License.
///
//////////////////////////////////////////////////////////////////////