// 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.math.BigInteger;
import java.util.LinkedList;
import java.util.List;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import com.bonsai.wallet32.HDAccount.AccountCoinSelector;
import com.google.bitcoin.core.Address;
import com.google.bitcoin.core.ECKey;
import com.google.bitcoin.core.NetworkParameters;
import com.google.bitcoin.core.ScriptException;
import com.google.bitcoin.core.TransactionOutput;
import com.google.bitcoin.core.Wallet;
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.script.Script;
import com.google.bitcoin.wallet.AllowUnconfirmedCoinSelector;
import com.google.bitcoin.wallet.CoinSelection;
import com.google.bitcoin.wallet.CoinSelector;
import com.google.bitcoin.wallet.DefaultCoinSelector;
public class HDAccount {
private static Logger mLogger =
LoggerFactory.getLogger(HDAccount.class);
private NetworkParameters mParams;
private DeterministicKey mAccountKey;
private String mAccountName;
private int mAccountId;
private HDChain mReceiveChain;
private HDChain mChangeChain;
public HDAccount(NetworkParameters params,
DeterministicKey masterKey,
JSONObject acctNode,
boolean isPairing,
HDWallet.HDStructVersion hdsv)
throws RuntimeException, JSONException {
mParams = params;
mAccountName = acctNode.getString("name");
mAccountId = acctNode.getInt("id");
int childnum = mAccountId;
switch (hdsv) {
case HDSV_L0PUB:
// Old level 0 public just uses the child number.
break;
case HDSV_L0PRV:
case HDSV_STDV0:
case HDSV_STDV1:
// Both L0PRV and STDVx use private derivation.
childnum |= ChildNumber.PRIV_BIT;
break;
}
mAccountKey =
HDKeyDerivation.deriveChildKey(masterKey, childnum);
mLogger.info("created HDAccount " + mAccountName + ": " +
mAccountKey.getPath());
if (isPairing) {
int numReceive = acctNode.getInt("nrcv");
int numChange = acctNode.getInt("nchg");
mReceiveChain = new HDChain(mParams, mAccountKey,
true, "Receive", numReceive);
mChangeChain = new HDChain(mParams, mAccountKey,
false, "Change", numChange);
} else {
mReceiveChain =
new HDChain(mParams, mAccountKey,
acctNode.getJSONObject("receive"));
mChangeChain =
new HDChain(mParams, mAccountKey,
acctNode.getJSONObject("change"));
}
}
public JSONObject dumps(boolean isPairing) {
try {
JSONObject obj = new JSONObject();
obj.put("name", mAccountName);
obj.put("id", mAccountId);
if (isPairing) {
obj.put("nrcv", mReceiveChain.numAddrs());
obj.put("nchg", mChangeChain.numAddrs());
} else {
obj.put("receive", mReceiveChain.dumps());
obj.put("change", mChangeChain.dumps());
}
return obj;
}
catch (JSONException ex) {
throw new RuntimeException(ex); // Shouldn't happen.
}
}
public HDAccount(NetworkParameters params,
DeterministicKey masterKey,
String accountName,
int acctnum,
HDWallet.HDStructVersion hdsv) {
mParams = params;
int childnum = acctnum;
switch (hdsv) {
case HDSV_L0PUB:
// Old level 0 public just uses the child number.
break;
case HDSV_L0PRV:
case HDSV_STDV0:
case HDSV_STDV1:
// Both L0PRV and STDVx use private derivation.
childnum |= ChildNumber.PRIV_BIT;
break;
}
mAccountKey = HDKeyDerivation.deriveChildKey(masterKey, childnum);
mAccountName = accountName;
mAccountId = acctnum;
mLogger.info("created HDAccount " + mAccountName + ": " +
mAccountKey.getPath());
mReceiveChain = new HDChain(mParams, mAccountKey, true, "Receive", 0);
mChangeChain = new HDChain(mParams, mAccountKey, false, "Change", 0);
}
public void gatherAllKeys(KeyCrypter keyCrypter,
KeyParameter aesKey,
long creationTime,
List<ECKey> keys) {
mReceiveChain.gatherAllKeys(keyCrypter, aesKey, creationTime, keys);
mChangeChain.gatherAllKeys(keyCrypter, aesKey, creationTime, keys);
}
public void applyOutput(byte[] pubkey,
byte[] pubkeyhash,
long value,
boolean avail) {
mReceiveChain.applyOutput(pubkey, pubkeyhash, value, avail);
mChangeChain.applyOutput(pubkey, pubkeyhash, value, avail);
}
public void applyInput(byte[] pubkey, long value) {
mReceiveChain.applyInput(pubkey, value);
mChangeChain.applyInput(pubkey, value);
}
public void clearBalance() {
mReceiveChain.clearBalance();
mChangeChain.clearBalance();
}
public boolean hasPubKey(byte[] pubkey, byte[] pubkeyhash) {
if (mReceiveChain.hasPubKey(pubkey, pubkeyhash))
return true;
return mChangeChain.hasPubKey(pubkey, pubkeyhash);
}
public String xpubstr() {
return mAccountKey.serializePubB58();
}
public String getName() {
return mAccountName;
}
public void setName(String name) {
mAccountName = name;
}
public int getId() {
return mAccountId;
}
public HDChain getReceiveChain() {
return mReceiveChain;
}
public HDChain getChangeChain() {
return mChangeChain;
}
public long balance() {
long balance = 0;
balance += mReceiveChain.balance();
balance += mChangeChain.balance();
return balance;
}
public long available() {
long available = 0;
available += mReceiveChain.available();
available += mChangeChain.available();
return available;
}
public void logBalance() {
mLogger.info(mAccountName + " balance " + Long.toString(balance()) +
", " + "available " + Long.toString(available()));
// Now log any active addresses in this account.
mReceiveChain.logBalance();
mChangeChain.logBalance();
}
public Address nextReceiveAddress() {
return mReceiveChain.nextUnusedAddress();
}
public Address nextChangeAddress() {
return mChangeChain.nextUnusedAddress();
}
public CoinSelector coinSelector(boolean spendUnconfirmed) {
return new AccountCoinSelector(spendUnconfirmed);
}
public class AccountCoinSelector implements CoinSelector {
private DefaultCoinSelector mDefaultCoinSelector;
public AccountCoinSelector(boolean spendUnconfirmed) {
mDefaultCoinSelector = spendUnconfirmed ?
new AllowUnconfirmedCoinSelector() :
new DefaultCoinSelector();
}
public CoinSelection select(BigInteger biTarget,
LinkedList<TransactionOutput> candidates) {
// Filter the candidates so only coins from this account
// are considered. Let the Wallet.DefaultCoinSelector do
// all the remaining work.
LinkedList<TransactionOutput> filtered =
new LinkedList<TransactionOutput>();
for (TransactionOutput to : candidates) {
try {
byte[] pubkey = null;
byte[] pubkeyhash = null;
Script script = to.getScriptPubKey();
if (script.isSentToRawPubKey())
pubkey = script.getPubKey();
else
pubkeyhash = script.getPubKeyHash();
if (mReceiveChain.hasPubKey(pubkey, pubkeyhash))
filtered.add(to);
else if (mChangeChain.hasPubKey(pubkey, pubkeyhash))
filtered.add(to);
else
// Not in this account ...
continue;
} catch (ScriptException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
// Does all the real work ...
return mDefaultCoinSelector.select(biTarget, filtered);
}
}
// Returns the largest number of addresses added to a chain.
public int ensureMargins(Wallet wallet,
KeyCrypter keyCrypter,
KeyParameter aesKey) {
int receiveAdded =
mReceiveChain.ensureMargins(wallet, keyCrypter, aesKey);
int changeAdded =
mChangeChain.ensureMargins(wallet, keyCrypter, aesKey);
return (receiveAdded > changeAdded) ? receiveAdded : changeAdded;
}
// Finds an address (if present) and returns a description
// of it's wallet location.
public HDAddressDescription findAddress(Address addr) {
HDAddressDescription retval;
// Try the receive chain first.
retval = mReceiveChain.findAddress(addr);
// If it wasn't there try the change chain.
if (retval == null)
retval = mChangeChain.findAddress(addr);
// If we found this address update the hdAccount.
if (retval != null)
retval.setHDAccount(this);
return retval;
}
}
// Local Variables:
// mode: java
// c-basic-offset: 4
// tab-width: 4
// End: