/**
* Copyright 2013 Google Inc.
* Copyright 2014 Andreas Schildbach
* Copyright 2014 John L. Jegutanis
*
* 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.
*/
package com.mygeopay.core.wallet;
import com.mygeopay.core.coins.CoinType;
import com.mygeopay.core.network.interfaces.ConnectionEventListener;
import com.mygeopay.core.network.interfaces.TransactionEventListener;
import com.mygeopay.core.protos.Protos;
import com.mygeopay.core.wallet.exceptions.Bip44KeyLookAheadExceededException;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.InsufficientMoneyException;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.script.Script;
import org.bitcoinj.utils.Threading;
import org.bitcoinj.wallet.RedeemData;
import org.bitcoinj.wallet.WalletTransaction;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.spongycastle.crypto.params.KeyParameter;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Executor;
import java.util.concurrent.locks.ReentrantLock;
import javax.annotation.Nullable;
import static org.bitcoinj.wallet.KeyChain.KeyPurpose.RECEIVE_FUNDS;
import static org.bitcoinj.wallet.KeyChain.KeyPurpose.CHANGE;
import static com.mygeopay.core.Preconditions.checkArgument;
import static com.mygeopay.core.Preconditions.checkNotNull;
import static com.mygeopay.core.Preconditions.checkState;
import static org.bitcoinj.wallet.KeyChain.KeyPurpose.REFUND;
/**
* @author John L. Jegutanis
*
*
*/
public class WalletPocketHD extends AbstractWallet {
private static final Logger log = LoggerFactory.getLogger(WalletPocketHD.class);
private final TransactionCreator transactionCreator;
@VisibleForTesting SimpleHDKeyChain keys;
public WalletPocketHD(DeterministicKey rootKey, CoinType coinType,
@Nullable KeyCrypter keyCrypter, @Nullable KeyParameter key) {
this(new SimpleHDKeyChain(rootKey, keyCrypter, key), coinType);
}
WalletPocketHD(SimpleHDKeyChain keys, CoinType coinType) {
this(keys.getId(coinType.getId()), keys, coinType);
}
WalletPocketHD(String id, SimpleHDKeyChain keys, CoinType coinType) {
super(checkNotNull(coinType), id);
this.keys = checkNotNull(keys);
transactionCreator = new TransactionCreator(this);
}
/******************************************************************************************************************/
//region Vending transactions and other internal state
/**
* Get the BIP44 index of this account
*/
public int getAccountIndex() {
lock.lock();
try {
return keys.getAccountIndex();
} finally {
lock.unlock();
}
}
@Override
public boolean isEquals(WalletAccount other) {
return other != null &&
getId().equals(other.getId()) &&
getCoinType().equals(other.getCoinType());
}
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Serialization support
//
//////////////////////////////////////////////////////////////////////////////////////////////////////////////////
List<Protos.Key> serializeKeychainToProtobuf() {
lock.lock();
try {
return keys.toProtobuf();
} finally {
lock.unlock();
}
}
@VisibleForTesting Protos.WalletPocket toProtobuf() {
lock.lock();
try {
return WalletPocketProtobufSerializer.toProtobuf(this);
} finally {
lock.unlock();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Encryption support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
@Override
public boolean isEncryptable() {
return true;
}
@Override
public boolean isEncrypted() {
lock.lock();
try {
return keys.isEncrypted();
} finally {
lock.unlock();
}
}
/**
* Get the wallet pocket's KeyCrypter, or null if the wallet pocket is not encrypted.
* (Used in encrypting/ decrypting an ECKey).
*/
@Nullable
@Override
public KeyCrypter getKeyCrypter() {
lock.lock();
try {
return keys.getKeyCrypter();
} finally {
lock.unlock();
}
}
/**
* Encrypt the keys in the group using the KeyCrypter and the AES key. A good default KeyCrypter to use is
* {@link org.bitcoinj.crypto.KeyCrypterScrypt}.
*
* @throws org.bitcoinj.crypto.KeyCrypterException Thrown if the wallet encryption fails for some reason,
* leaving the group unchanged.
*/
@Override
public void encrypt(KeyCrypter keyCrypter, KeyParameter aesKey) {
checkNotNull(keyCrypter);
checkNotNull(aesKey);
lock.lock();
try {
this.keys = this.keys.toEncrypted(keyCrypter, aesKey);
} finally {
lock.unlock();
}
}
/**
* Decrypt the keys in the group using the previously given key crypter and the AES key. A good default
* KeyCrypter to use is {@link org.bitcoinj.crypto.KeyCrypterScrypt}.
*
* @throws org.bitcoinj.crypto.KeyCrypterException Thrown if the wallet decryption fails for some reason, leaving the group unchanged.
*/
@Override
public void decrypt(KeyParameter aesKey) {
checkNotNull(aesKey);
lock.lock();
try {
this.keys = this.keys.toDecrypted(aesKey);
} finally {
lock.unlock();
}
}
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
//
// Transaction signing support
//
///////////////////////////////////////////////////////////////////////////////////////////////////////////////////
/**
* Sends coins to the given address but does not broadcast the resulting pending transaction.
*/
public SendRequest sendCoinsOffline(Address address, Coin amount) throws InsufficientMoneyException {
return sendCoinsOffline(address, amount, (KeyParameter) null);
}
/**
* {@link #sendCoinsOffline(Address, Coin)}
*/
public SendRequest sendCoinsOffline(Address address, Coin amount, @Nullable String password)
throws InsufficientMoneyException {
KeyParameter key = null;
if (password != null) {
checkState(isEncrypted());
key = checkNotNull(getKeyCrypter()).deriveKey(password);
}
return sendCoinsOffline(address, amount, key);
}
/**
* {@link #sendCoinsOffline(Address, Coin)}
*/
public SendRequest sendCoinsOffline(Address address, Coin amount, @Nullable KeyParameter aesKey)
throws InsufficientMoneyException {
checkState(address.getParameters() instanceof CoinType);
SendRequest request = SendRequest.to(address, amount);
request.aesKey = aesKey;
return request;
}
@Override
public boolean isAddressMine(Address address) {
return address != null && address.getParameters().equals(coinType) &&
(address.isP2SHAddress() ?
isPayToScriptHashMine(address.getHash160()) :
isPubKeyHashMine(address.getHash160()));
}
@Override
public boolean isPubKeyHashMine(byte[] pubkeyHash) {
return findKeyFromPubHash(pubkeyHash) != null;
}
@Override
public boolean isWatchedScript(Script script) {
// Not supported
return false;
}
@Override
public boolean isPubKeyMine(byte[] pubkey) {
return findKeyFromPubKey(pubkey) != null;
}
@Override
public boolean isPayToScriptHashMine(byte[] payToScriptHash) {
// Not supported
return false;
}
public void completeAndSignTx(SendRequest request) throws InsufficientMoneyException {
if (request.completed) {
signTransaction(request);
} else {
completeTx(request);
}
}
/**
* Given a spend request containing an incomplete transaction, makes it valid by adding outputs and signed inputs
* according to the instructions in the request. The transaction in the request is modified by this method.
*
* @param req a SendRequest that contains the incomplete transaction and details for how to make it valid.
* @throws InsufficientMoneyException if the request could not be completed due to not enough balance.
* @throws IllegalArgumentException if you try and complete the same SendRequest twice
*/
public void completeTx(SendRequest req) throws InsufficientMoneyException {
lock.lock();
try {
transactionCreator.completeTx(req);
} finally {
lock.unlock();
}
}
/**
* <p>Given a send request containing transaction, attempts to sign it's inputs. This method expects transaction
* to have all necessary inputs connected or they will be ignored.</p>
* <p>Actual signing is done by pluggable {@link org.bitcoinj.signers.LocalTransactionSigner}
* and it's not guaranteed that transaction will be complete in the end.</p>
*/
public void signTransaction(SendRequest req) {
lock.lock();
try {
transactionCreator.signTransaction(req);
} finally {
lock.unlock();
}
}
/**
* Locates a keypair from the basicKeyChain given the hash of the public key. This is needed
* when finding out which key we need to use to redeem a transaction output.
*
* @return ECKey object or null if no such key was found.
*/
@Nullable
@Override
public ECKey findKeyFromPubHash(byte[] pubkeyHash) {
lock.lock();
try {
return keys.findKeyFromPubHash(pubkeyHash);
} finally {
lock.unlock();
}
}
/**
* Locates a keypair from the basicKeyChain given the raw public key bytes.
* @return ECKey or null if no such key was found.
*/
@Nullable
@Override
public ECKey findKeyFromPubKey(byte[] pubkey) {
lock.lock();
try {
return keys.findKeyFromPubKey(pubkey);
} finally {
lock.unlock();
}
}
@Nullable
@Override
public RedeemData findRedeemDataFromScriptHash(byte[] bytes) {
return null;
}
/** {@inheritDoc} */
@Override
public Address getChangeAddress() {
return currentAddress(CHANGE);
}
/** {@inheritDoc} */
@Override
public Address getReceiveAddress() {
return currentAddress(RECEIVE_FUNDS);
}
/** {@inheritDoc} */
@Override
public Address getRefundAddress() {
return currentAddress(REFUND);
}
public Address getReceiveAddress(boolean isManualAddressManagement) {
return getAddress(RECEIVE_FUNDS, isManualAddressManagement);
}
public Address getRefundAddress(boolean isManualAddressManagement) {
return getAddress(REFUND, isManualAddressManagement);
}
/**
* Get the last used receiving address
*/
@Nullable
public Address getLastUsedAddress(SimpleHDKeyChain.KeyPurpose purpose) {
lock.lock();
try {
DeterministicKey lastUsedKey = keys.getLastIssuedKey(purpose);
if (lastUsedKey != null) {
return lastUsedKey.toAddress(coinType);
} else {
return null;
}
} finally {
lock.unlock();
}
}
/**
* Returns true is it is possible to create new fresh receive addresses, false otherwise
*/
public boolean canCreateFreshReceiveAddress() {
lock.lock();
try {
DeterministicKey currentUnusedKey = keys.getCurrentUnusedKey(RECEIVE_FUNDS);
int maximumKeyIndex = SimpleHDKeyChain.LOOKAHEAD - 1;
// If there are used keys
if (!addressesStatus.isEmpty()) {
int lastUsedKeyIndex = 0;
// Find the last used key index
for (Map.Entry<Address, String> entry : addressesStatus.entrySet()) {
if (entry.getValue() == null) continue;
DeterministicKey usedKey = keys.findKeyFromPubHash(entry.getKey().getHash160());
if (usedKey != null && keys.isExternal(usedKey) && usedKey.getChildNumber().num() > lastUsedKeyIndex) {
lastUsedKeyIndex = usedKey.getChildNumber().num();
}
}
maximumKeyIndex = lastUsedKeyIndex + SimpleHDKeyChain.LOOKAHEAD;
}
log.info("Maximum key index for new key is {}", maximumKeyIndex);
// If we exceeded the BIP44 look ahead threshold
return currentUnusedKey.getChildNumber().num() < maximumKeyIndex;
} finally {
lock.unlock();
}
}
/**
* Get a fresh address by marking the current receive address as used. It will throw
* {@link Bip44KeyLookAheadExceededException} if we requested too many addresses that
* exceed the BIP44 look ahead threshold.
*/
public Address getFreshReceiveAddress() throws Bip44KeyLookAheadExceededException {
lock.lock();
try {
if (!canCreateFreshReceiveAddress()) {
throw new Bip44KeyLookAheadExceededException();
}
keys.getKey(RECEIVE_FUNDS);
return currentAddress(RECEIVE_FUNDS);
} finally {
lock.unlock();
walletSaveNow();
}
}
public Address getFreshReceiveAddress(boolean isManualAddressManagement) throws Bip44KeyLookAheadExceededException {
lock.lock();
try {
Address newAddress = null;
Address freshAddress = getFreshReceiveAddress();
if (isManualAddressManagement) {
newAddress = getLastUsedAddress(RECEIVE_FUNDS);
}
if (newAddress == null) {
newAddress = freshAddress;
}
return newAddress;
} finally {
lock.unlock();
walletSaveNow();
}
}
private static final Comparator<DeterministicKey> HD_KEY_COMPARATOR =
new Comparator<DeterministicKey>() {
@Override
public int compare(final DeterministicKey k1, final DeterministicKey k2) {
int key1Num = k1.getChildNumber().num();
int key2Num = k2.getChildNumber().num();
// In reality Integer.compare(key2Num, key1Num) but is not available on older devices
return (key2Num < key1Num) ? -1 : ((key2Num == key1Num) ? 0 : 1);
}
};
/**
* Returns the number of issued receiving keys
*/
public int getNumberIssuedReceiveAddresses() {
lock.lock();
try {
return keys.getNumIssuedExternalKeys();
} finally {
lock.unlock();
}
}
/**
* Returns a list of addresses that have been issued.
* The list is sorted in descending chronological order: older in the end
*/
public List<Address> getIssuedReceiveAddresses() {
lock.lock();
try {
List<DeterministicKey> issuedKeys = keys.getIssuedExternalKeys();
List<Address> receiveAddresses = new ArrayList<Address>();
Collections.sort(issuedKeys, HD_KEY_COMPARATOR);
for (ECKey key : issuedKeys) {
receiveAddresses.add(key.toAddress(coinType));
}
return receiveAddresses;
} finally {
lock.unlock();
}
}
/**
* Get the currently used receiving and change addresses
*/
public Set<Address> getUsedAddresses() {
lock.lock();
try {
Set<Address> usedAddresses = new HashSet<Address>();
for (Map.Entry<Address, String> entry : addressesStatus.entrySet()) {
if (entry.getValue() != null) {
usedAddresses.add(entry.getKey());
}
}
return usedAddresses;
} finally {
lock.unlock();
}
}
public Address getAddress(SimpleHDKeyChain.KeyPurpose purpose,
boolean isManualAddressManagement) {
Address receiveAddress = null;
if (isManualAddressManagement) {
receiveAddress = getLastUsedAddress(purpose);
}
if (receiveAddress == null) {
receiveAddress = currentAddress(purpose);
}
return receiveAddress;
}
/**
* Get the currently latest unused address by purpose.
*/
@VisibleForTesting Address currentAddress(SimpleHDKeyChain.KeyPurpose purpose) {
lock.lock();
try {
return keys.getCurrentUnusedKey(purpose).toAddress(coinType);
} finally {
lock.unlock();
subscribeIfNeeded();
}
}
/**
* Used to force keys creation, could take long time to complete so use it in a background
* thread.
*/
@VisibleForTesting void maybeInitializeAllKeys() {
lock.lock();
try {
keys.maybeLookAhead();
} finally {
lock.unlock();
}
}
public List<Address> getActiveAddresses() {
ImmutableList.Builder<Address> activeAddresses = ImmutableList.builder();
for (DeterministicKey key : keys.getActiveKeys()) {
activeAddresses.add(key.toAddress(coinType));
}
return activeAddresses.build();
}
public void markAddressAsUsed(Address address) {
keys.markPubHashAsUsed(address.getHash160());
}
@Override
public String toString() {
return WalletPocketHD.class.getSimpleName() + " " + id.substring(0, 4)+ " " + coinType;
}
}