package com.mygeopay.core.wallet;
import com.mygeopay.core.coins.BitcoinMain;
import com.mygeopay.core.coins.CoinType;
import com.mygeopay.core.coins.DogecoinTest;
import com.mygeopay.core.network.AddressStatus;
import com.mygeopay.core.network.ServerClient.HistoryTx;
import com.mygeopay.core.network.interfaces.BlockchainConnection;
import com.mygeopay.core.network.interfaces.TransactionEventListener;
import com.mygeopay.core.protos.Protos;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.ChildMessage;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.ECKey;
import org.bitcoinj.core.Sha256Hash;
import org.bitcoinj.core.Transaction;
import org.bitcoinj.core.TransactionInput;
import org.bitcoinj.core.TransactionOutput;
import org.bitcoinj.core.Utils;
import org.bitcoinj.crypto.DeterministicHierarchy;
import org.bitcoinj.crypto.DeterministicKey;
import org.bitcoinj.crypto.HDKeyDerivation;
import org.bitcoinj.crypto.KeyCrypter;
import org.bitcoinj.crypto.KeyCrypterScrypt;
import org.bitcoinj.utils.BriefLogFormatter;
import org.bitcoinj.wallet.DeterministicSeed;
import com.mygeopay.core.wallet.exceptions.Bip44KeyLookAheadExceededException;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.Lists;
import org.bitcoinj.wallet.KeyChain;
import org.json.JSONArray;
import org.json.JSONException;
import org.junit.Before;
import org.junit.Test;
import org.spongycastle.crypto.params.KeyParameter;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Random;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;
/**
* @author John L. Jegutanis
*/
public class WalletPocketHDTest {
final CoinType BTC = BitcoinMain.get();
static final List<String> MNEMONIC = ImmutableList.of("citizen", "fever", "scale", "nurse", "brief", "round", "ski", "fiction", "car", "fitness", "pluck", "act");
static final byte[] aesKeyBytes = {0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7, 0, 1, 2, 3, 4, 5, 6, 7};
private static final long AMOUNT_TO_SEND = 2700000000L;
DeterministicSeed seed = new DeterministicSeed(MNEMONIC, null, "", 0);
private DeterministicKey masterKey = HDKeyDerivation.createMasterPrivateKey(seed.getSeedBytes());
CoinType type = DogecoinTest.get();
DeterministicHierarchy hierarchy = new DeterministicHierarchy(masterKey);
DeterministicKey rootKey = hierarchy.get(type.getBip44Path(0), false, true);
WalletPocketHD pocket;
KeyParameter aesKey = new KeyParameter(aesKeyBytes);
KeyCrypter crypter = new KeyCrypterScrypt();
@Before
public void setup() {
BriefLogFormatter.init();
pocket = new WalletPocketHD(rootKey, type, null, null);
pocket.keys.setLookaheadSize(20);
}
@Test
public void watchingAddresses() {
List<Address> watchingAddresses = pocket.getAddressesToWatch();
assertEquals(40, watchingAddresses.size()); // 20 + 20 lookahead size
for (int i = 0; i < addresses.size(); i++) {
assertEquals(addresses.get(i), watchingAddresses.get(i).toString());
}
}
@Test
public void issuedKeys() throws Bip44KeyLookAheadExceededException {
LinkedList<Address> issuedAddresses = new LinkedList<Address>();
assertEquals(0, pocket.getIssuedReceiveAddresses().size());
assertEquals(0, pocket.keys.getNumIssuedExternalKeys());
issuedAddresses.add(0, pocket.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
Address freshAddress = pocket.getFreshReceiveAddress();
assertEquals(freshAddress, pocket.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
assertEquals(1, pocket.getIssuedReceiveAddresses().size());
assertEquals(1, pocket.keys.getNumIssuedExternalKeys());
assertEquals(issuedAddresses, pocket.getIssuedReceiveAddresses());
issuedAddresses.add(0, pocket.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
freshAddress = pocket.getFreshReceiveAddress();
assertEquals(freshAddress, pocket.currentAddress(KeyChain.KeyPurpose.RECEIVE_FUNDS));
assertEquals(2, pocket.getIssuedReceiveAddresses().size());
assertEquals(2, pocket.keys.getNumIssuedExternalKeys());
assertEquals(issuedAddresses, pocket.getIssuedReceiveAddresses());
}
@Test
public void issuedKeysLimit() throws Exception {
assertTrue(pocket.canCreateFreshReceiveAddress());
try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
assertFalse(pocket.canCreateFreshReceiveAddress());
// We haven't used any key so the total must be 20 - 1 (the unused key)
assertEquals(19, pocket.getNumberIssuedReceiveAddresses());
assertEquals(19, pocket.getIssuedReceiveAddresses().size());
}
pocket.onConnection(getBlockchainConnection(type));
assertTrue(pocket.canCreateFreshReceiveAddress());
try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
try {
pocket.getFreshReceiveAddress();
} catch (Bip44KeyLookAheadExceededException e1) { }
assertFalse(pocket.canCreateFreshReceiveAddress());
// We used 18, so the total must be (20-1)+18=37
assertEquals(37, pocket.getNumberIssuedReceiveAddresses());
assertEquals(37, pocket.getIssuedReceiveAddresses().size());
}
}
@Test
public void issuedKeysLimit2() throws Exception {
assertTrue(pocket.canCreateFreshReceiveAddress());
try {
for (int i = 0; i < 100; i++) {
pocket.getFreshReceiveAddress();
}
} catch (Bip44KeyLookAheadExceededException e) {
assertFalse(pocket.canCreateFreshReceiveAddress());
// We haven't used any key so the total must be 20 - 1 (the unused key)
assertEquals(19, pocket.getNumberIssuedReceiveAddresses());
assertEquals(19, pocket.getIssuedReceiveAddresses().size());
}
}
@Test
public void usedAddresses() throws Exception {
assertEquals(0, pocket.getUsedAddresses().size());
pocket.onConnection(getBlockchainConnection(type));
// Receive and change addresses
assertEquals(13, pocket.getUsedAddresses().size());
}
private Transaction send(Coin value, WalletPocketHD w1, WalletPocketHD w2) throws Exception {
SendRequest req;
req = w1.sendCoinsOffline(w2.getReceiveAddress(), value);
req.feePerKb = Coin.ZERO;
w1.completeAndSignTx(req);
byte[] txBytes = req.tx.bitcoinSerialize();
w1.addNewTransactionIfNeeded(new Transaction(w1.getCoinType(), txBytes));
w2.addNewTransactionIfNeeded(new Transaction(w1.getCoinType(), txBytes));
return req.tx;
}
@Test
public void testSendingAndBalances() throws Exception {
DeterministicHierarchy h = new DeterministicHierarchy(masterKey);
WalletPocketHD account1 = new WalletPocketHD(h.get(BTC.getBip44Path(0), false, true), BTC, null, null);
WalletPocketHD account2 = new WalletPocketHD(h.get(BTC.getBip44Path(1), false, true), BTC, null, null);
WalletPocketHD account3 = new WalletPocketHD(h.get(BTC.getBip44Path(2), false, true), BTC, null, null);
Transaction tx = new Transaction(BTC);
tx.addOutput(BTC.oneCoin().toCoin(), account1.getReceiveAddress());
account1.addNewTransactionIfNeeded(tx);
assertEquals(BTC.value("1"), account1.getBalance());
assertEquals(BTC.value("0"), account2.getBalance());
assertEquals(BTC.value("0"), account3.getBalance());
send(Coin.CENT.multiply(5), account1, account2);
assertEquals(BTC.value("0.95"), account1.getBalance());
assertEquals(BTC.value("0.05"), account2.getBalance());
assertEquals(BTC.value("0"), account3.getBalance());
send(Coin.CENT.multiply(7), account1, account3);
assertEquals(BTC.value("0.88"), account1.getBalance());
assertEquals(BTC.value("0.05"), account2.getBalance());
assertEquals(BTC.value("0.07"), account3.getBalance());
send(Coin.CENT.multiply(3), account2, account3);
assertEquals(BTC.value("0.88"), account1.getBalance());
assertEquals(BTC.value("0.02"), account2.getBalance());
assertEquals(BTC.value("0.1"), account3.getBalance());
}
@Test
public void fillTransactions() throws Exception {
pocket.onConnection(getBlockchainConnection(type));
// Issued keys
assertEquals(18, pocket.keys.getNumIssuedExternalKeys());
assertEquals(9, pocket.keys.getNumIssuedInternalKeys());
// No addresses left to subscribe
List<Address> addressesToWatch = pocket.getAddressesToWatch();
assertEquals(0, addressesToWatch.size());
// 18 external issued + 20 lookahead + 9 external issued + 20 lookahead
assertEquals(67, pocket.addressesStatus.size());
assertEquals(67, pocket.addressesSubscribed.size());
Address receiveAddr = pocket.getReceiveAddress();
// This key is not issued
assertEquals(18, pocket.keys.getNumIssuedExternalKeys());
assertEquals(67, pocket.addressesStatus.size());
assertEquals(67, pocket.addressesSubscribed.size());
DeterministicKey key = pocket.keys.findKeyFromPubHash(receiveAddr.getHash160());
assertNotNull(key);
// 18 here is the key index, not issued keys count
assertEquals(18, key.getChildNumber().num());
assertEquals(11000000000L, pocket.getBalance().value);
// TODO added more tests to insure it uses the "holes" in the keychain
}
@Test
public void serializeUnencryptedNormal() throws Exception {
pocket.onConnection(getBlockchainConnection(type));
Protos.WalletPocket walletPocketProto = pocket.toProtobuf();
WalletPocketHD newPocket = new WalletPocketProtobufSerializer().readWallet(walletPocketProto, null);
assertEquals(pocket.getBalance().value, newPocket.getBalance().value);
assertEquals(pocket.getCoinType(), newPocket.getCoinType());
assertEquals(pocket.getDescription(), newPocket.getDescription());
assertEquals(pocket.keys.toProtobuf().toString(), newPocket.keys.toProtobuf().toString());
assertEquals(pocket.getLastBlockSeenHash(), newPocket.getLastBlockSeenHash());
assertEquals(pocket.getLastBlockSeenHeight(), newPocket.getLastBlockSeenHeight());
assertEquals(pocket.getLastBlockSeenTimeSecs(), newPocket.getLastBlockSeenTimeSecs());
for (Transaction tx : pocket.getTransactions(false)) {
assertEquals(tx, newPocket.getTransaction(tx.getHash()));
}
for (AddressStatus status : pocket.getAllAddressStatus()) {
if (status.getStatus() == null) continue;
assertEquals(status, newPocket.getAddressStatus(status.getAddress()));
}
// Issued keys
assertEquals(18, newPocket.keys.getNumIssuedExternalKeys());
assertEquals(9, newPocket.keys.getNumIssuedInternalKeys());
newPocket.onConnection(getBlockchainConnection(type));
// No addresses left to subscribe
List<Address> addressesToWatch = newPocket.getAddressesToWatch();
assertEquals(0, addressesToWatch.size());
// 18 external issued + 20 lookahead + 9 external issued + 20 lookahead
assertEquals(67, newPocket.addressesStatus.size());
assertEquals(67, newPocket.addressesSubscribed.size());
}
@Test
public void serializeUnencryptedEmpty() throws Exception {
pocket.maybeInitializeAllKeys();
Protos.WalletPocket walletPocketProto = pocket.toProtobuf();
WalletPocketHD newPocket = new WalletPocketProtobufSerializer().readWallet(walletPocketProto, null);
assertEquals(walletPocketProto.toString(), newPocket.toProtobuf().toString());
// Issued keys
assertEquals(0, newPocket.keys.getNumIssuedExternalKeys());
assertEquals(0, newPocket.keys.getNumIssuedInternalKeys());
// 20 lookahead + 20 lookahead
assertEquals(40, newPocket.keys.getActiveKeys().size());
}
@Test
public void serializeEncryptedEmpty() throws Exception {
pocket.maybeInitializeAllKeys();
pocket.encrypt(crypter, aesKey);
Protos.WalletPocket walletPocketProto = pocket.toProtobuf();
WalletPocketHD newPocket = new WalletPocketProtobufSerializer().readWallet(walletPocketProto, crypter);
assertEquals(walletPocketProto.toString(), newPocket.toProtobuf().toString());
pocket.decrypt(aesKey);
// One is encrypted, so they should not match
assertNotEquals(pocket.toProtobuf().toString(), newPocket.toProtobuf().toString());
newPocket.decrypt(aesKey);
assertEquals(pocket.toProtobuf().toString(), newPocket.toProtobuf().toString());
}
@Test
public void serializeEncryptedNormal() throws Exception {
pocket.maybeInitializeAllKeys();
pocket.encrypt(crypter, aesKey);
pocket.onConnection(getBlockchainConnection(type));
assertEquals(type.value(11000000000l), pocket.getBalance());
assertAllKeysEncrypted(pocket);
WalletPocketHD newPocket = new WalletPocketProtobufSerializer().readWallet(pocket.toProtobuf(), crypter);
assertAllKeysEncrypted(newPocket);
pocket.decrypt(aesKey);
newPocket.decrypt(aesKey);
assertAllKeysDecrypted(pocket);
assertAllKeysDecrypted(newPocket);
}
private void assertAllKeysDecrypted(WalletPocketHD pocket) {
List<ECKey> keys = pocket.keys.getKeys(false);
for (ECKey k : keys) {
DeterministicKey key = (DeterministicKey) k;
assertFalse(key.isEncrypted());
}
}
private void assertAllKeysEncrypted(WalletPocketHD pocket) {
List<ECKey> keys = pocket.keys.getKeys(false);
for (ECKey k : keys) {
DeterministicKey key = (DeterministicKey) k;
assertTrue(key.isEncrypted());
}
}
@Test
public void createDustTransactionFee() throws Exception {
pocket.onConnection(getBlockchainConnection(type));
Address toAddr = new Address(type, "nUEkQ3LjH9m4ScbP6NGtnAdnnUsdtWv99Q");
Coin softDust = type.getSoftDustLimit();
assertNotNull(softDust);
// Send a soft dust
SendRequest sendRequest = pocket.sendCoinsOffline(toAddr, softDust.subtract(Coin.SATOSHI));
pocket.completeTx(sendRequest);
assertEquals(type.getFeePerKb().multiply(2), sendRequest.tx.getFee());
}
@Test
public void createTransactionAndBroadcast() throws Exception {
pocket.onConnection(getBlockchainConnection(type));
Address toAddr = new Address(type, "nUEkQ3LjH9m4ScbP6NGtnAdnnUsdtWv99Q");
long orgBalance = pocket.getBalance().value;
SendRequest sendRequest = pocket.sendCoinsOffline(toAddr, Coin.valueOf(AMOUNT_TO_SEND));
sendRequest.shuffleOutputs = false;
pocket.completeTx(sendRequest);
Transaction tx = sendRequest.tx;
assertEquals(expectedTx, Utils.HEX.encode(tx.bitcoinSerialize()));
// FIXME, mock does not work here
// pocket.broadcastTx(tx);
// assertEquals(orgBalance - AMOUNT_TO_SEND, pocket.getBalance().value);
}
// Util methods
////////////////////////////////////////////////////////////////////////////////////////////////
public class MessageComparator implements Comparator<ChildMessage> {
@Override
public int compare(ChildMessage o1, ChildMessage o2) {
String s1 = Utils.HEX.encode(o1.bitcoinSerialize());
String s2 = Utils.HEX.encode(o2.bitcoinSerialize());
return s1.compareTo(s2);
}
}
HashMap<Address, AddressStatus> getDummyStatuses() throws AddressFormatException {
HashMap<Address, AddressStatus> status = new HashMap<Address, AddressStatus>(40);
for (int i = 0; i < addresses.size(); i++) {
Address address = new Address(type, addresses.get(i));
status.put(address, new AddressStatus(address, statuses[i]));
}
return status;
}
// private HashMap<Address, ArrayList<UnspentTx>> getDummyUTXs() throws AddressFormatException, JSONException {
// HashMap<Address, ArrayList<UnspentTx>> utxs = new HashMap<Address, ArrayList<UnspentTx>>(40);
//
// for (int i = 0; i < statuses.length; i++) {
// List<UnspentTx> utxList = (List<UnspentTx>) UnspentTx.fromArray(new JSONArray(unspent[i]));
// utxs.put(new Address(type, addresses.get(i)), Lists.newArrayList(utxList));
// }
//
// return utxs;
// }
private HashMap<Address, ArrayList<HistoryTx>> getDummyHistoryTXs() throws AddressFormatException, JSONException {
HashMap<Address, ArrayList<HistoryTx>> htxs = new HashMap<Address, ArrayList<HistoryTx>>(40);
for (int i = 0; i < statuses.length; i++) {
List<HistoryTx> utxList = (List<HistoryTx>) HistoryTx.fromArray(new JSONArray(unspent[i]));
htxs.put(new Address(type, addresses.get(i)), Lists.newArrayList(utxList));
}
return htxs;
}
private HashMap<Sha256Hash, byte[]> getDummyRawTXs() throws AddressFormatException, JSONException {
HashMap<Sha256Hash, byte[]> rawTxs = new HashMap<Sha256Hash, byte[]>();
for (int i = 0; i < txs.length; i++) {
String[] txEntry = txs[i];
rawTxs.put(new Sha256Hash(txs[i][0]), Utils.HEX.decode(txs[i][1]));
}
return rawTxs;
}
class MockBlockchainConnection implements BlockchainConnection {
final HashMap<Address, AddressStatus> statuses;
// final HashMap<Address, ArrayList<UnspentTx>> utxs;
final HashMap<Address, ArrayList<HistoryTx>> historyTxs;
final HashMap<Sha256Hash, byte[]> rawTxs;
private CoinType coinType;
MockBlockchainConnection(CoinType coinType) throws Exception {
this.coinType = coinType;
statuses = getDummyStatuses();
// utxs = getDummyUTXs();
historyTxs = getDummyHistoryTXs();
rawTxs = getDummyRawTXs();
}
@Override
public void subscribeToBlockchain(TransactionEventListener listener) {
}
@Override
public void subscribeToAddresses(List<Address> addresses, TransactionEventListener listener) {
for (Address a : addresses) {
AddressStatus status = statuses.get(a);
if (status == null) {
status = new AddressStatus(a, null);
}
listener.onAddressStatusUpdate(status);
}
}
// @Override
// public void getUnspentTx(AddressStatus status, TransactionEventListener listener) {
// List<UnspentTx> utx = utxs.get(status.getAddress());
// if (status == null) {
// utx = ImmutableList.of();
// }
// listener.onUnspentTransactionUpdate(status, utx);
// }
@Override
public void getHistoryTx(AddressStatus status, TransactionEventListener listener) {
List<HistoryTx> htx = historyTxs.get(status.getAddress());
if (status == null) {
htx = ImmutableList.of();
}
listener.onTransactionHistory(status, htx);
}
@Override
public void getTransaction(Sha256Hash txHash, TransactionEventListener listener) {
Transaction tx = new Transaction(coinType, rawTxs.get(txHash));
listener.onTransactionUpdate(tx);
}
@Override
public void broadcastTx(Transaction tx, TransactionEventListener listener) {
// List<AddressStatus> newStatuses = new ArrayList<AddressStatus>();
// Random rand = new Random();
// byte[] randBytes = new byte[32];
// // Get spent outputs and modify statuses
// for (TransactionInput txi : tx.getInputs()) {
// UnspentTx unspentTx = new UnspentTx(
// txi.getOutpoint(), txi.getValue().value, 0);
//
// for (Map.Entry<Address, ArrayList<UnspentTx>> entry : utxs.entrySet()) {
// if (entry.getValue().remove(unspentTx)) {
// rand.nextBytes(randBytes);
// AddressStatus newStatus = new AddressStatus(entry.getKey(), Utils.HEX.encode(randBytes));
// statuses.put(entry.getKey(), newStatus);
// newStatuses.add(newStatus);
// }
// }
// }
//
// for (TransactionOutput txo : tx.getOutputs()) {
// if (txo.getAddressFromP2PKHScript(coinType) != null) {
// Address address = txo.getAddressFromP2PKHScript(coinType);
// if (addresses.contains(address.toString())) {
// AddressStatus newStatus = new AddressStatus(address, tx.getHashAsString());
// statuses.put(address, newStatus);
// newStatuses.add(newStatus);
// if (!utxs.containsKey(address)) {
// utxs.put(address, new ArrayList<UnspentTx>());
// }
// ArrayList<UnspentTx> unspentTxs = utxs.get(address);
// unspentTxs.add(new UnspentTx(txo.getOutPointFor(),
// txo.getValue().value, 0));
// if (!historyTxs.containsKey(address)) {
// historyTxs.put(address, new ArrayList<HistoryTx>());
// }
// ArrayList<HistoryTx> historyTxes = historyTxs.get(address);
// historyTxes.add(new HistoryTx(txo.getOutPointFor(), 0));
// }
// }
// }
//
// rawTxs.put(tx.getHash(), tx.bitcoinSerialize());
//
// for (AddressStatus newStatus : newStatuses) {
// listener.onAddressStatusUpdate(newStatus);
// }
}
@Override
public boolean broadcastTxSync(Transaction tx) {
return false;
}
@Override
public void ping() {}
}
private MockBlockchainConnection getBlockchainConnection(CoinType coinType) throws Exception {
return new MockBlockchainConnection(coinType);
}
// Mock data
List<String> addresses = ImmutableList.of(
"nnfP8VuPfZXhNtMDzvX1bKurzcV1k7HNrQ",
"nf4AUKiaGdx4GTbbh222KvtuCbAuvbcdE2",
"npGkmbWtdWybFNSZQXUK6zZxbEocMtrTzz",
"nVaN45bbs6AUc1EUpP71hHGBGb2qciNrJc",
"nrdHFZP1AfdKBrjsSQmwFm8R2i2mzMef75",
"niGZgfbhFYn6tJqksmC8CnzSRL1GHNsu7e",
"nh6w8yu1zoKYoT837ffkVmuPjTaP69Pc5E",
"nbyMgmEghsL9tpk7XfdH9gLGudh6Lrbbuf",
"naX9akzYuWY1gKbcZo3t36aBKc1gqbzgSs",
"nqcPVTGeAfCowELB2D5PdVF3FWFjFtkkFf",
"nd4vVvPTpp2LfcsMPsmG3Dh7MFAsqRHp4g",
"nVK4Uz5Sf56ygrr6RiwXcRvH8AuUVbjjHi",
"nbipkRwT1NCSXZSrm8uAAEgQAA2s2NWMkG",
"nZU6QAMAdCQcVDxEmL7GEY6ykFm8m6u6am",
"nbFqayssM5s7hLjLFwf2d95JXKRWBk2pBH",
"nacZfcNgVi47hamLscWdUGQhUQGGpSdgq8",
"niiMT7ZYeFnaLPYtqQFBBX1sP3dT5JtcEw",
"ns6GyTaniLWaiYrvAf5tk4D8tWrbcCLGyj",
"nhdwQfptLTzLJGbLF3vvtqyBaqPMPecDmE",
"neMUAGeTzwxLbSkXMsdmWf1fTKS1UsJjXY",
"nXsAMeXxtpx8jaxnU3Xv9ZQ6ZcRcD1xYhR",
"ns35rKnnWf6xP3KSj5WPkMCVVaADGi6Ndk",
"nk4wcXYBEWs5HkhNLsuaQJoAjJHoK6SQmG",
"npsJQWu8fsALoTPum8D4j8FDkyeusk8fU8",
"nZNhZo4Vz3DnwQGPL3SJTZgUw2Kh4g9oay",
"nnxDTYB8WMHMgLnCY2tuaEtysvdhRnWehZ",
"nb2iaDXG1EqkcE47H87WQFkQNkeVK66Z21",
"nWHAkpn2DB3DJRsbiK3vpbigoa3M2uVuF8",
"nViKdC7Gm6TMmCuWTBwVE9i4rJhyfwbfqg",
"nZQV5BifbGPzaxTrB4efgHruWH5rufemqP",
"nVvZaLvnkCVAzpLYPoHeqU4w9zJ5yrZgUn",
"nrMp6pRCk98WYWkCWq9Pqthz9HbpQu8BT3",
"nnA3aYsLqysKT6gAu1dr4EKm586cmKiRxS",
"nVfcVgMY7DL6nqoSxwJy7B7hKXirQwj6ic",
"ni4oAzi6nCVuEdjoHyVMVKWb1DqTd3qY3H",
"nnpf3gx442yomniRJPMGPapgjHrraPZXxJ",
"nkuFnF8wUmHFkMycaFMvyjBoiMeR5KBKGd",
"nXKccwjaUyrQkLrdqKT6aq6cDiFgBBVgNz",
"nZMSNsXSAL7i1YD6KP5FrhATuZ2CWvnxqR",
"nUEkQ3LjH9m4ScbP6NGtnAdnnUsdtWv99Q"
);
String[] statuses = {
"fe7c109d8bd90551a406cf0b3499117db04bc9c4f48e1df27ac1cf3ddcb3d464",
"8a53babd831c6c3a857e20190e884efe75a005bdd7cd273c4f27ab1b8ec81c2d",
"86bc2f0cf0112fd59c9aadfe5c887062c21d7a873db260dff68dcfe4417fe212",
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
null,
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
null,
"64a575b5605671831185ca715e8197f0455733e721a6c6c5b8add31bd6eabbe9",
null,
null,
null,
null,
null,
null,
null,
null,
null,
null,
null
};
String[] unspent = {
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 0, \"value\": 500000000, \"height\": 160267}, {\"tx_hash\": \"89a72ba4732505ce9b09c30668db985952701252ce0adbd7c43336396697d6ae\", \"tx_pos\": 0, \"value\": 500000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 1, \"value\": 1000000000, \"height\": 160267}, {\"tx_hash\": \"edaf445288d8e65cf7963bc8047c90f53681acaadc5ccfc5ecc67aedbd73cddb\", \"tx_pos\": 0, \"value\": 500000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 2, \"value\": 500000000, \"height\": 160267}, {\"tx_hash\": \"81a1f0f8242d5e71e65ff9e8ec51e8e85d641b607d7f691c1770d4f25918ebd7\", \"tx_pos\": 0, \"value\": 1000000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 3, \"value\": 500000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 4, \"value\": 1000000000, \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 11, \"value\": 500000000, \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 12, \"value\": 1000000000, \"height\": 160267}]",
"[]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 13, \"value\": 500000000, \"height\": 160267}]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 6, \"value\": 500000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 7, \"value\": 1000000000, \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 8, \"value\": 500000000, \"height\": 160267}]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 9, \"value\": 500000000, \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"tx_pos\": 10, \"value\": 1000000000, \"height\": 160267}]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]"
};
String[] history = {
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}, {\"tx_hash\": \"89a72ba4732505ce9b09c30668db985952701252ce0adbd7c43336396697d6ae\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}, {\"tx_hash\": \"edaf445288d8e65cf7963bc8047c90f53681acaadc5ccfc5ecc67aedbd73cddb\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}, {\"tx_hash\": \"81a1f0f8242d5e71e65ff9e8ec51e8e85d641b607d7f691c1770d4f25918ebd7\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[]",
"[{\"tx_hash\": \"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f\", \"height\": 160267}]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]",
"[]"
};
String[][] txs = {
{"ef74da273e8a77e2d60b707414fb7e0ccb35c7b1b936800a49fe953195b1799f", "0100000001b8778dff640ccb144346d9db48201639b2707a0cc59e19672d2dd76cc6d1a5a6010000006b48304502210098d2e5b8a6c72442430bc09f2f4bcb56612c5b9e5eee821d65b412d099bb723402204f7f008ac052e5d7be0ab5b0c85ea5e627d725a521bd9e9b193d1fdf81c317a0012102d26e423c9da9ff4a7bf6b756b2dafb75cca34fbd34f64c4c3b77c37179c5bba2ffffffff0e0065cd1d000000001976a914ca983c2da690de3cdc693ca013d93e569810c52c88ac00ca9a3b000000001976a91477263ab93a49b1d3eb5887187704cdb82e1c60ce88ac0065cd1d000000001976a914dc40fbbc8caa1f7617d275aec9a3a14ce8d8652188ac0065cd1d000000001976a9140f2b1e5b376e22c5d1e635233eb90cf50ad9095188ac00ca9a3b000000001976a914f612ffd50b6a69df5e34ee0c5b529dfaaedca03d88ac00f633bce60000001976a914937258e3a8c463ec07e78ce62326c488170ad25e88ac0065cd1d000000001976a9142848ad5ff4cc32df3649646017c47bc04e8d211788ac00ca9a3b000000001976a914fa937737a6df2b8a5e39cce8b0bdd9510683023a88ac0065cd1d000000001976a914ae248a5d710143b6b309aaab9ce17059536e2aa388ac0065cd1d000000001976a91438d6eb11eca405be497a7183c50e437851425e0088ac00ca9a3b000000001976a91410ac6b2704146b50a1dd8f6df70736856ebf8b3488ac0065cd1d000000001976a914456816dccb3a4f33ae2dbd3ba623997075d5c73d88ac00ca9a3b000000001976a91452957c44e5bee9a60402a739fc8959c2850ea98488ac0065cd1d000000001976a914fb2dffa402d05f74335d137e21e41d8920c745fb88ac00000000"},
{"89a72ba4732505ce9b09c30668db985952701252ce0adbd7c43336396697d6ae", "01000000011a656d67706db286d1e6fad57eb4f411cb14f8880cea8348da339b9d434a5ec7050000006a47304402201d69fddb269b53aa742ff6437a45adb4ca5c59f666c9b4eabc4a0c7a6e6f4c0f022015a747b7a6d9371a4020f5b396dcd094b0f36af3fc82e95091da856181912dfa012102c9a8d5b2f768afe30ee772d185e7a61f751be05649a79508b38a2be8824adec3ffffffff020065cd1d000000001976a914ca983c2da690de3cdc693ca013d93e569810c52c88ac00b07098e60000001976a9141630d812e219e6bcbe494eb96f7a7900c216ad5d88ac00000000"},
{"edaf445288d8e65cf7963bc8047c90f53681acaadc5ccfc5ecc67aedbd73cddb", "010000000164a3990893c012b20287d43d1071ac26f4b93648ff4213db6da6979beed6b7dc010000006b48304502210086ac11d4a8146b4176a72059960690c72a9776468cd671fd07c064b51f24961d02205bcf008d6995014f3cfd79100ee9beab5688c88cca15c5cea38b769563785d900121036530415a7b3b9c5976f26a63a57d119ab39491762121723c773399a2531a1bd7ffffffff020065cd1d000000001976a91477263ab93a49b1d3eb5887187704cdb82e1c60ce88ac006aad74e60000001976a914e5616848352c328c9f61b167eb1b0fde39b5cb6788ac00000000"},
{"81a1f0f8242d5e71e65ff9e8ec51e8e85d641b607d7f691c1770d4f25918ebd7", "010000000141c217dfea3a1d8d6a06e9d3daf75b292581f652256d73a7891e5dc9c7ee3cca000000006a47304402205cce451228f98fece9645052546b82c2b2d425a4889b03999001fababfc7f4690220583b2189faef07d6b0191c788301cfab1b3f47ffe2c403d632b92c6dde27e14f012102d26e423c9da9ff4a7bf6b756b2dafb75cca34fbd34f64c4c3b77c37179c5bba2ffffffff0100ca9a3b000000001976a914dc40fbbc8caa1f7617d275aec9a3a14ce8d8652188ac00000000"}
};
String expectedTx = "01000000039f79b1953195fe490a8036b9b1c735cb0c7efb1474700bd6e2778a3e27da74ef010000006a473044022006e44424f56393c9f1bdd02c2d9a3d42bda1955ebc1ebdf5067b202df284afe302207e2dc6b5ad99059c58c1c32e540419ad44816f3005d859baaef3a2e9718e0d0e0121033daee143740ae505dd588be89f659b34ba30f587bcebece11d72ec7a115bc41bffffffff9f79b1953195fe490a8036b9b1c735cb0c7efb1474700bd6e2778a3e27da74ef040000006b483045022100a1911352fac69365d0fbd1a2b5a37adc5ed43fbf91133b6c597d92281229703102206756b65b25435b1e8aceffe34693087760cbee45aa8dea010ff94321bbdcc6c7012103c956c491833b8f1ebfde275cd7d5660824c53efe215f9956356b85f6c86031ffffffffffd7eb1859f2d470171c697f7d601b645de8e851ece8f95fe6715e2d24f8f0a181000000006b483045022100b7e64ea240c3d9080801ca90891d54acd4a2ee52e402a8b9513699b38fd81dba0220136627bb760db2eee9f7cd71d6d2a8df9ab5049b336ebf495f07ccf0f324a5bb01210392ed3b840c8474f8b6b57e71d9a60fbf75adea6fc68d8985330e8d782b80621fffffffff0200bbeea0000000001976a914007d5355731b44e274eb495a26f4c33a734ee3eb88ac00c2eb0b000000001976a914392d52419e94e237f0d5817de1c9e21d09b515a688ac00000000";
}