com.bonsai.wallet32.HDWallet.java Source code

Java tutorial

Introduction

Here is the source code for com.bonsai.wallet32.HDWallet.java

Source

// Copyright (C) 2013-2014  Bonsai Software, Inc.
// 
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
// 
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
// GNU General Public License for more details.
// 
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.

package com.bonsai.wallet32;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.math.BigInteger;
import java.nio.charset.Charset;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.BufferedBlockCipher;
import org.spongycastle.crypto.DataLengthException;
import org.spongycastle.crypto.InvalidCipherTextException;
import org.spongycastle.crypto.engines.AESFastEngine;
import org.spongycastle.crypto.modes.CBCBlockCipher;
import org.spongycastle.crypto.paddings.PaddedBufferedBlockCipher;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.crypto.params.ParametersWithIV;

import android.content.Context;

import com.bonsai.wallet32.WalletService.AmountAndFee;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.AddressFormatException;
import com.google.bitcoin.core.Base58;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.InsufficientMoneyException;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.ScriptException;
import com.google.bitcoin.core.Transaction;
import com.google.bitcoin.core.TransactionConfidence;
import com.google.bitcoin.core.TransactionConfidence.ConfidenceType;
import com.google.bitcoin.core.TransactionInput;
import com.google.bitcoin.core.TransactionOutput;
import com.google.bitcoin.core.Utils;
import com.google.bitcoin.core.Wallet;
import com.google.bitcoin.core.Wallet.SendRequest;
import com.google.bitcoin.crypto.ChildNumber;
import com.google.bitcoin.crypto.DeterministicKey;
import com.google.bitcoin.crypto.HDKeyDerivation;
import com.google.bitcoin.crypto.KeyCrypter;
import com.google.bitcoin.crypto.KeyCrypterGroestl;
//import com.google.bitcoin.crypto.KeyCrypterScrypt;
import com.google.bitcoin.crypto.MnemonicCodeX;
import com.google.bitcoin.script.Script;
import com.google.bitcoin.wallet.WalletTransaction;

public class HDWallet {

    private static Logger mLogger = LoggerFactory.getLogger(HDWallet.class);

    private static final transient SecureRandom secureRandom = new SecureRandom();

    private final NetworkParameters mParams;
    private KeyCrypter mKeyCrypter;
    private KeyParameter mAesKey;

    private final DeterministicKey mMasterKey;
    private final DeterministicKey mWalletRoot;

    private final byte[] mWalletSeed;
    private final String mPassphrase;
    private final MnemonicCodeX.Version mBIP39Version;

    public enum HDStructVersion {
        HDSV_L0PUB, // Level0, public derivation.   M/<acct>/<chnge>/<n>
        HDSV_L0PRV, // Level0, private derivation.   M/<acct>'/<chnge>/<n>
        HDSV_STDV0, // Standard, version 0.         M/0/0'/<acct>'/<chnge>/<n>
        HDSV_STDV1 // BIP-0044.               M/44'/0'/<acct>'/<chnge>/<n>
    }

    private HDStructVersion mHDStructVersion;

    private ArrayList<HDAccount> mAccounts;

    // Create an HDWallet from persisted file data.
    public static HDWallet restore(WalletApplication walletApp, NetworkParameters params, KeyCrypter keyCrypter,
            KeyParameter aesKey) throws InvalidCipherTextException, IOException {

        try {
            JSONObject node = deserialize(walletApp, keyCrypter, aesKey);

            return new HDWallet(walletApp, params, keyCrypter, aesKey, node, false);
        } catch (JSONException ex) {
            String msg = "trouble deserializing wallet: " + ex.toString();

            // Have to break the message into chunks for big messages ...
            while (msg.length() > 1024) {
                String chunk = msg.substring(0, 1024);
                mLogger.error(chunk);
                msg = msg.substring(1024);
            }
            mLogger.error(msg);

            throw new RuntimeException(msg);
        }
    }

    // Deserialize the wallet data.
    public static JSONObject deserialize(WalletApplication walletApp, KeyCrypter keyCrypter, KeyParameter aesKey)
            throws IOException, InvalidCipherTextException, JSONException {

        File file = walletApp.getHDWalletFile(null);
        String path = file.getPath();

        try {
            mLogger.info("restoring HDWallet from " + path);
            int len = (int) file.length();

            // Open persisted file.
            DataInputStream dis = new DataInputStream(new FileInputStream(file));

            // Read IV from file.
            byte[] iv = new byte[KeyCrypterGroestl.BLOCK_LENGTH/*KeyCrypterScrypt.BLOCK_LENGTH*/];
            dis.readFully(iv);

            // Read the ciphertext from the file.
            byte[] cipherBytes = new byte[len - iv.length];
            dis.readFully(cipherBytes);
            dis.close();

            // Decrypt the ciphertext.
            ParametersWithIV keyWithIv = new ParametersWithIV(new KeyParameter(aesKey.getKey()), iv);
            BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine()));
            cipher.init(false, keyWithIv);
            int minimumSize = cipher.getOutputSize(cipherBytes.length);
            byte[] outputBuffer = new byte[minimumSize];
            int length1 = cipher.processBytes(cipherBytes, 0, cipherBytes.length, outputBuffer, 0);
            int length2 = cipher.doFinal(outputBuffer, length1);
            int actualLength = length1 + length2;
            byte[] decryptedBytes = new byte[actualLength];
            System.arraycopy(outputBuffer, 0, decryptedBytes, 0, actualLength);

            // Parse the decryptedBytes.
            String jsonstr = new String(decryptedBytes);

            /*
            // THIS CONTAINS THE SEED!
            // Have to break the message into chunks for big messages ...
            String msg = jsonstr;
            while (msg.length() > 1024) {
            String chunk = msg.substring(0, 1024);
            mLogger.error(chunk);
            msg = msg.substring(1024);
            }
            mLogger.error(msg);
            */

            JSONObject node = new JSONObject(jsonstr);
            return node;

        } catch (IOException ex) {
            mLogger.warn("trouble reading " + path + ": " + ex.toString());
            throw ex;
        } catch (RuntimeException ex) {
            mLogger.warn("trouble restoring wallet: " + ex.toString());
            throw ex;
        } catch (InvalidCipherTextException ex) {
            mLogger.warn("wallet decrypt failed: " + ex.toString());
            throw ex;
        }
    }

    public HDWallet(WalletApplication walletApp, NetworkParameters params, KeyCrypter keyCrypter,
            KeyParameter aesKey, JSONObject walletNode, boolean isPairing) throws JSONException {

        mParams = params;
        mKeyCrypter = keyCrypter;
        mAesKey = aesKey;

        try {
            mWalletSeed = Base58.decode(walletNode.getString("seed"));

            mPassphrase = walletNode.has("passphrase") ? walletNode.getString("passphrase") : "";

            if (!walletNode.has("bip39_version")) {
                mBIP39Version = MnemonicCodeX.Version.V0_5;
                mLogger.info("defaulting BIP39 version to V0_5");
            } else {
                String bipverstr = walletNode.getString("bip39_version");
                if (bipverstr.equals("V0_5")) {
                    mBIP39Version = MnemonicCodeX.Version.V0_5;
                    mLogger.info("setting BIP39 version to V0_5");
                } else if (bipverstr.equals("V0_6")) {
                    mBIP39Version = MnemonicCodeX.Version.V0_6;
                    mLogger.info("setting BIP39 version to V0_6");
                } else {
                    throw new RuntimeException("unknown BIP39 version: " + bipverstr);
                }
            }

            if (!walletNode.has("acct_derive")) {
                mHDStructVersion = HDStructVersion.HDSV_L0PUB;
                mLogger.info("defaulting mHDStructVersion to HDSV_L0PUB");
            } else {
                String acctderivstr = walletNode.getString("acct_derive");
                if (acctderivstr.equals("PRV")) {
                    mHDStructVersion = HDStructVersion.HDSV_L0PRV;
                    mLogger.info("setting mHDStructVersion to HDSV_L0PRV");
                } else if (acctderivstr.equals("PUB")) {
                    mHDStructVersion = HDStructVersion.HDSV_L0PUB;
                    mLogger.info("setting mHDStructVersion to HDSV_L0PUB");
                } else if (acctderivstr.equals("STDV0")) {
                    mHDStructVersion = HDStructVersion.HDSV_STDV0;
                    mLogger.info("setting mHDStructVersion to HDSV_STDV0");
                } else if (acctderivstr.equals("STDV1")) {
                    mHDStructVersion = HDStructVersion.HDSV_STDV1;
                    mLogger.info("setting mHDStructVersion to HDSV_STDV1");
                } else {
                    throw new RuntimeException("unknown acct_derive value: " + acctderivstr);
                }
            }

        } catch (AddressFormatException e) {
            throw new RuntimeException("trouble decoding wallet");
        }

        byte[] hdseed;
        try {
            InputStream wis = walletApp.getAssets().open("wordlist/english.txt");
            MnemonicCodeX mc = new MnemonicCodeX(wis, MnemonicCodeX.BIP39_ENGLISH_SHA256);
            List<String> wordlist = mc.toMnemonic(mWalletSeed);
            hdseed = MnemonicCodeX.toSeed(wordlist, mPassphrase, mBIP39Version);
        } catch (Exception ex) {
            throw new RuntimeException("trouble decoding seed");
        }

        mMasterKey = HDKeyDerivation.createMasterPrivateKey(hdseed);

        switch (mHDStructVersion) {
        case HDSV_L0PUB:
        case HDSV_L0PRV:
            // Both of the level 0 derivations use the master as the
            // root of the accounts.
            mWalletRoot = mMasterKey;
            break;
        case HDSV_STDV0:
            // Standard derivation starts from M/0/0'
            DeterministicKey t0 = HDKeyDerivation.deriveChildKey(mMasterKey, 0);
            mWalletRoot = HDKeyDerivation.deriveChildKey(t0, ChildNumber.PRIV_BIT);
            break;
        case HDSV_STDV1:
            // BIP-0044 starts from M/44'/0'
            DeterministicKey t1 = HDKeyDerivation.deriveChildKey(mMasterKey, 44 | ChildNumber.PRIV_BIT);
            mWalletRoot = HDKeyDerivation.deriveChildKey(t1, ChildNumber.PRIV_BIT);
            break;
        default:
            throw new RuntimeException("invalid HDStructVersion");
        }

        mLogger.info("restoring HDWallet " + mWalletRoot.getPath());

        mAccounts = new ArrayList<HDAccount>();
        JSONArray accounts = walletNode.getJSONArray("accounts");
        for (int ii = 0; ii < accounts.length(); ++ii) {
            mLogger.info(String.format("deserializing account %d", ii));
            JSONObject acctNode = accounts.getJSONObject(ii);
            mAccounts.add(new HDAccount(mParams, mWalletRoot, acctNode, isPairing, mHDStructVersion));
        }
    }

    public JSONObject dumps(boolean isPairing) {
        try {
            JSONObject obj = new JSONObject();

            obj.put("seed", Base58.encode(mWalletSeed));

            obj.put("passphrase", mPassphrase);

            switch (mBIP39Version) {
            case V0_5:
                obj.put("bip39_version", "V0_5");
                break;
            case V0_6:
                obj.put("bip39_version", "V0_6");
                break;
            default:
                throw new RuntimeException("unknown BIP39 version");
            }

            switch (mHDStructVersion) {
            case HDSV_L0PUB:
                obj.put("acct_derive", "PUB");
                break;
            case HDSV_L0PRV:
                obj.put("acct_derive", "PRV");
                break;
            case HDSV_STDV0:
                obj.put("acct_derive", "STDV0");
                break;
            case HDSV_STDV1:
                obj.put("acct_derive", "STDV1");
                break;
            }

            JSONArray accts = new JSONArray();
            for (HDAccount acct : mAccounts)
                accts.put(acct.dumps(isPairing));

            obj.put("accounts", accts);

            return obj;
        } catch (JSONException ex) {
            throw new RuntimeException(ex); // Shouldn't happen.
        }
    }

    public HDWallet(WalletApplication walletApp, NetworkParameters params, KeyCrypter keyCrypter,
            KeyParameter aesKey, byte[] walletSeed, String passphrase, int numAccounts,
            MnemonicCodeX.Version bip39Version, HDStructVersion hdsv) {
        mParams = params;
        mKeyCrypter = keyCrypter;
        mAesKey = aesKey;
        mWalletSeed = walletSeed;
        mPassphrase = passphrase;
        mBIP39Version = bip39Version;
        mHDStructVersion = hdsv;

        switch (mBIP39Version) {
        case V0_5:
            mLogger.info("BIP39 version V0_5");
            break;
        case V0_6:
            mLogger.info("BIP39 version V0_6");
            break;
        default:
            throw new RuntimeException("unknown BIP39 version");
        }

        byte[] hdseed;
        try {
            InputStream wis = walletApp.getAssets().open("wordlist/english.txt");
            MnemonicCodeX mc = new MnemonicCodeX(wis, MnemonicCodeX.BIP39_ENGLISH_SHA256);
            List<String> wordlist = mc.toMnemonic(mWalletSeed);
            hdseed = MnemonicCodeX.toSeed(wordlist, mPassphrase, mBIP39Version);
        } catch (Exception ex) {
            throw new RuntimeException("trouble decoding seed: " + ex);
        }

        mMasterKey = HDKeyDerivation.createMasterPrivateKey(hdseed);

        switch (mHDStructVersion) {
        case HDSV_L0PUB:
        case HDSV_L0PRV:
            // Both of the level 0 derivations use the master as the
            // root of the accounts.
            mWalletRoot = mMasterKey;
            break;
        case HDSV_STDV0:
            // Standard derivation starts from M/0/0'
            DeterministicKey t0 = HDKeyDerivation.deriveChildKey(mMasterKey, 0);
            mWalletRoot = HDKeyDerivation.deriveChildKey(t0, ChildNumber.PRIV_BIT);
            break;
        case HDSV_STDV1:
            // BIP-0044 starts from M/44'/0'
            DeterministicKey t1 = HDKeyDerivation.deriveChildKey(mMasterKey, 44 | ChildNumber.PRIV_BIT);
            mWalletRoot = HDKeyDerivation.deriveChildKey(t1, ChildNumber.PRIV_BIT);
            break;
        default:
            throw new RuntimeException("invalid HDStructVersion");
        }

        mLogger.info("created HDWallet " + mWalletRoot.getPath());

        // Add some accounts.
        mAccounts = new ArrayList<HDAccount>();
        for (int ii = 0; ii < numAccounts; ++ii) {
            String acctName = String.format("Account %d", ii);
            mAccounts.add(new HDAccount(mParams, mWalletRoot, acctName, ii, mHDStructVersion));
        }
    }

    public void setPersistCrypter(KeyCrypter keyCrypter, KeyParameter aesKey) {
        mKeyCrypter = keyCrypter;
        mAesKey = aesKey;
    }

    public byte[] getWalletSeed() {
        return mWalletSeed;
    }

    public String getFormatVersionString() {
        if (mBIP39Version == MnemonicCodeX.Version.V0_5) {
            return "0.1";
        } else {
            switch (mHDStructVersion) {
            case HDSV_L0PUB:
                return "0.2";
            case HDSV_L0PRV:
                return "0.3";
            case HDSV_STDV0:
                return "0.4";
            case HDSV_STDV1:
                return "0.5";
            default:
                throw new RuntimeException("unknown HDStructVersion");
            }
        }
    }

    public HDStructVersion getHDStructVersion() {
        return mHDStructVersion;
    }

    public MnemonicCodeX.Version getBIP39Version() {
        return mBIP39Version;
    }

    public List<HDAccount> getAccounts() {
        return mAccounts;
    }

    public HDAccount getAccount(int accountId) {
        return mAccounts.get(accountId);
    }

    public void addAccount() {
        int ndx = mAccounts.size();
        String acctName = String.format("Account %d", ndx);
        mAccounts.add(new HDAccount(mParams, mWalletRoot, acctName, ndx, mHDStructVersion));
    }

    public void gatherAllKeys(long creationTime, List<ECKey> keys) {
        for (HDAccount acct : mAccounts)
            acct.gatherAllKeys(mKeyCrypter, mAesKey, creationTime, keys);
    }

    public void clearBalances() {
        // Clears the balance and tx counters.
        for (HDAccount acct : mAccounts)
            acct.clearBalance();
    }

    public void applyAllTransactions(Iterable<WalletTransaction> iwt) {
        // Clear the balance and tx counters.
        clearBalances();

        for (WalletTransaction wtx : iwt) {
            // WalletTransaction.Pool pool = wtx.getPool();
            Transaction tx = wtx.getTransaction();
            boolean avail = !tx.isPending();
            TransactionConfidence conf = tx.getConfidence();
            ConfidenceType ct = conf.getConfidenceType();

            // Skip dead transactions.
            if (ct != ConfidenceType.DEAD) {

                // Traverse the HDAccounts with all outputs.
                List<TransactionOutput> lto = tx.getOutputs();
                for (TransactionOutput to : lto) {
                    long value = to.getValue().longValue();
                    try {
                        byte[] pubkey = null;
                        byte[] pubkeyhash = null;
                        Script script = to.getScriptPubKey();
                        if (script.isSentToRawPubKey())
                            pubkey = script.getPubKey();
                        else
                            pubkeyhash = script.getPubKeyHash();
                        for (HDAccount hda : mAccounts)
                            hda.applyOutput(pubkey, pubkeyhash, value, avail);
                    } catch (ScriptException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }

                // Traverse the HDAccounts with all inputs.
                List<TransactionInput> lti = tx.getInputs();
                for (TransactionInput ti : lti) {
                    // Get the connected TransactionOutput to see value.
                    TransactionOutput cto = ti.getConnectedOutput();
                    if (cto == null) {
                        // It appears we land here when processing transactions
                        // where we handled the output above.
                        //
                        // mLogger.warn("couldn't find connected output for input");
                        continue;
                    }
                    long value = cto.getValue().longValue();
                    try {
                        byte[] pubkey = ti.getScriptSig().getPubKey();
                        for (HDAccount hda : mAccounts)
                            hda.applyInput(pubkey, value);
                    } catch (ScriptException e) {
                        // This happens if the input doesn't have a
                        // public key (eg P2SH).  No worries in this
                        // case, it isn't one of ours ...
                    }
                }
            }
        }

        // This is too noisy
        // // Log balance summary.
        // for (HDAccount acct : mAccounts)
        //     acct.logBalance();
    }

    public long balanceForAccount(int acctnum) {
        // Which accounts are we considering?  (-1 means all)
        if (acctnum != -1) {
            return mAccounts.get(acctnum).balance();
        } else {
            long sum = 0;
            for (HDAccount hda : mAccounts)
                sum += hda.balance();
            return sum;
        }
    }

    public long availableForAccount(int acctnum) {
        // Which accounts are we considering?  (-1 means all)
        if (acctnum != -1) {
            return mAccounts.get(acctnum).available();
        } else {
            long sum = 0;
            for (HDAccount hda : mAccounts)
                sum += hda.available();
            return sum;
        }
    }

    public long amountForAccount(WalletTransaction wtx, int acctnum) {

        // This routine is only called from the View Transactions
        // activity, so it is OK if it uses all balance and not
        // available balance (since the confirmation count is shown).

        long credits = 0;
        long debits = 0;

        // Which accounts are we considering?  (-1 means all)
        ArrayList<HDAccount> accts = new ArrayList<HDAccount>();
        if (acctnum != -1) {
            accts.add(mAccounts.get(acctnum));
        } else {
            for (HDAccount hda : mAccounts)
                accts.add(hda);
        }

        Transaction tx = wtx.getTransaction();

        // Consider credits.
        List<TransactionOutput> lto = tx.getOutputs();
        for (TransactionOutput to : lto) {
            long value = to.getValue().longValue();
            try {
                byte[] pubkey = null;
                byte[] pubkeyhash = null;
                Script script = to.getScriptPubKey();
                if (script.isSentToRawPubKey())
                    pubkey = script.getPubKey();
                else
                    pubkeyhash = script.getPubKeyHash();
                for (HDAccount hda : accts) {
                    if (hda.hasPubKey(pubkey, pubkeyhash))
                        credits += value;
                }
            } catch (ScriptException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        // Traverse the HDAccounts with all inputs.
        List<TransactionInput> lti = tx.getInputs();
        for (TransactionInput ti : lti) {
            // Get the connected TransactionOutput to see value.
            TransactionOutput cto = ti.getConnectedOutput();
            if (cto == null) {
                // It appears we land here when processing transactions
                // where we handled the output above.
                //
                // mLogger.warn("couldn't find connected output for input");
                continue;
            }
            long value = cto.getValue().longValue();
            try {
                byte[] pubkey = ti.getScriptSig().getPubKey();
                for (HDAccount hda : accts)
                    if (hda.hasPubKey(pubkey, null))
                        debits += value;
            } catch (ScriptException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        return credits - debits;
    }

    public void getBalances(List<Balance> balances) {
        for (HDAccount acct : mAccounts)
            balances.add(new Balance(acct.getId(), acct.getName(), acct.balance(), acct.available()));
    }

    public Address nextReceiveAddress(int acctnum) {
        // Which account are we using for this receive?
        HDAccount acct = mAccounts.get(acctnum);
        return acct.nextReceiveAddress();
    }

    public void sendAccountCoins(Wallet wallet, int acctnum, Address dest, long value, long fee,
            boolean spendUnconfirmed) throws RuntimeException {

        // Which account are we using for this send?
        HDAccount acct = mAccounts.get(acctnum);

        SendRequest req = SendRequest.to(dest, BigInteger.valueOf(value));
        req.fee = BigInteger.valueOf(fee);
        req.feePerKb = BigInteger.ZERO;
        req.ensureMinRequiredFee = false;
        req.changeAddress = acct.nextChangeAddress();
        req.coinSelector = acct.coinSelector(spendUnconfirmed);
        req.aesKey = mAesKey;

        try {
            wallet.sendCoins(req);
        } catch (InsufficientMoneyException e) {
            throw new RuntimeException("Not enough BTC in account");
        }
    }

    public AmountAndFee useAll(Wallet wallet, int acctnum, boolean spendUnconfirmed)
            throws InsufficientMoneyException {

        // Create a pretend send request and extract the recommended
        // fee.  Which account are we using for this send?
        HDAccount acct = mAccounts.get(acctnum);

        // Pretend we are sending the bitcoin to ourselves.
        Address dest = acct.nextReceiveAddress();

        SendRequest req = SendRequest.emptyWallet(dest);
        req.coinSelector = acct.coinSelector(spendUnconfirmed);
        req.aesKey = mAesKey;

        // Let the wallet do the heavy lifting ...
        wallet.completeTx(req);

        // It doesn't look like req.fee gets set to the required fee
        // when using emptyWallet.  Figure out the fee ourselves ...
        //
        BigInteger outAmt = req.tx.getValueSentFromMe(wallet);
        BigInteger inAmt = req.tx.getValueSentToMe(wallet);
        BigInteger feeAmt = outAmt.subtract(inAmt);

        return new AmountAndFee(inAmt.longValue(), feeAmt.longValue());
    }

    public long computeRecommendedFee(Wallet wallet, int acctnum, long value, boolean spendUnconfirmed)
            throws IllegalArgumentException, InsufficientMoneyException {

        // Create a pretend send request and extract the recommended
        // fee.  Which account are we using for this send?
        HDAccount acct = mAccounts.get(acctnum);

        // Pretend we are sending the bitcoin to ourselves.
        Address dest = acct.nextReceiveAddress();

        SendRequest req = SendRequest.to(dest, BigInteger.valueOf(value));
        req.changeAddress = acct.nextChangeAddress();
        req.coinSelector = acct.coinSelector(spendUnconfirmed);
        req.aesKey = mAesKey;

        // Let the wallet do the heavy lifting ...
        wallet.completeTx(req);

        return req.fee != null ? req.fee.longValue() : 0;
    }

    public void persist(WalletApplication walletApp) {
        File tmpFile = walletApp.getHDWalletFile(".tmp");
        File newFile = walletApp.getHDWalletFile(null);
        try {
            // Serialize into a byte array.
            JSONObject jsonobj = dumps(false);
            String jsonstr = jsonobj.toString(4); // indentation
            byte[] plainBytes = jsonstr.getBytes(Charset.forName("UTF-8"));

            // Generate an IV.
            byte[] iv = new byte[KeyCrypterGroestl.BLOCK_LENGTH];
            secureRandom.nextBytes(iv);

            // Encrypt the serialized data.
            ParametersWithIV keyWithIv = new ParametersWithIV(mAesKey, iv);
            BufferedBlockCipher cipher = new PaddedBufferedBlockCipher(new CBCBlockCipher(new AESFastEngine()));
            cipher.init(true, keyWithIv);
            byte[] encryptedBytes = new byte[cipher.getOutputSize(plainBytes.length)];
            int length = cipher.processBytes(plainBytes, 0, plainBytes.length, encryptedBytes, 0);
            cipher.doFinal(encryptedBytes, length);

            // Ready a tmp file.
            if (tmpFile.exists())
                tmpFile.delete();

            // Write the IV followed by the data.
            FileOutputStream ostrm = new FileOutputStream(tmpFile);
            ostrm.write(iv);
            ostrm.write(encryptedBytes);
            ostrm.close();

            // Swap the tmp file into place.
            if (!tmpFile.renameTo(newFile))
                mLogger.warn("failed to rename to " + newFile.getPath());
            else
                mLogger.info("persisted to " + newFile.getPath());

        } catch (JSONException ex) {
            mLogger.warn("failed generating JSON: " + ex.toString());
        } catch (IOException ex) {
            mLogger.warn("failed to write to " + tmpFile.getPath() + ": " + ex.toString());
        } catch (DataLengthException ex) {
            mLogger.warn("encryption failed: " + ex.toString());
        } catch (IllegalStateException ex) {
            mLogger.warn("encryption failed: " + ex.toString());
        } catch (InvalidCipherTextException ex) {
            mLogger.warn("encryption failed: " + ex.toString());
        }
    }

    // Ensure that there are enough spare addresses on all chains.
    // Returns the most number of addresses added to a chain.
    public int ensureMargins(Wallet wallet) {
        int maxAdded = 0;
        for (HDAccount acct : mAccounts) {
            int numAdded = acct.ensureMargins(wallet, mKeyCrypter, mAesKey);
            if (maxAdded < numAdded)
                maxAdded = numAdded;
        }
        return maxAdded;
    }

    // Finds an address (if present) and returns a description
    // of it's wallet location.
    public HDAddressDescription findAddress(Address addr) {
        HDAddressDescription retval = null;
        for (HDAccount acct : mAccounts) {
            retval = acct.findAddress(addr);
            if (retval != null)
                return retval;
        }
        return retval;
    }

    public long getEarliestCreationTime() {
        long time = Utils.currentTimeSeconds();
        for (HDAccount hda : mAccounts) {
            time = Math.min(hda.getEarliestCreationTime(), time);
        }
        return time;
    }
}

// Local Variables:
// mode: java
// c-basic-offset: 4
// tab-width: 4
// End: