mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-11-01 21:17:13 +00:00
First cut at a pending transactions patch. This isn't the final API, which will
involve some changes to the wallet event listener/tx to have a concept of confidence levels.
This commit is contained in:
committed by
Miron Cuperman
parent
e6acc153ad
commit
7aa485110a
@@ -35,4 +35,9 @@ public class AbstractPeerEventListener extends Object implements PeerEventListen
|
||||
|
||||
public void onPeerDisconnected(Peer peer, int peerCount) {
|
||||
}
|
||||
|
||||
public Message onPreMessageReceived(Peer peer, Message m) {
|
||||
// Just pass the message right through for further processing.
|
||||
return m;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,7 +312,7 @@ public class BlockChain {
|
||||
try {
|
||||
List<Transaction> txns = entry.getValue();
|
||||
for (Transaction tx : txns) {
|
||||
entry.getKey().receive(tx, block, blockType);
|
||||
entry.getKey().receiveFromBlock(tx, block, blockType);
|
||||
}
|
||||
} catch (ScriptException e) {
|
||||
// We don't want scripts we don't understand to break the block chain so just note that this tx was
|
||||
@@ -427,28 +427,9 @@ public class BlockChain {
|
||||
for (Transaction tx : block.transactions) {
|
||||
try {
|
||||
for (Wallet wallet : wallets) {
|
||||
boolean shouldReceive = false;
|
||||
for (TransactionOutput output : tx.getOutputs()) {
|
||||
// TODO: Handle more types of outputs, not just regular to address outputs.
|
||||
if (output.getScriptPubKey().isSentToIP()) continue;
|
||||
// This is not thread safe as a key could be removed between the call to isMine and receive.
|
||||
if (output.isMine(wallet)) {
|
||||
shouldReceive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Coinbase transactions don't have anything useful in their inputs (as they create coins out of thin air).
|
||||
if (!shouldReceive && !tx.isCoinBase()) {
|
||||
for (TransactionInput i : tx.getInputs()) {
|
||||
byte[] pubkey = i.getScriptSig().getPubKey();
|
||||
// This is not thread safe as a key could be removed between the call to isPubKeyMine and receive.
|
||||
if (wallet.isPubKeyMine(pubkey)) {
|
||||
shouldReceive = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (tx.isCoinBase())
|
||||
continue;
|
||||
boolean shouldReceive = wallet.isTransactionRelevant(tx, true);
|
||||
if (!shouldReceive) continue;
|
||||
List<Transaction> txList = walletToTxMap.get(wallet);
|
||||
if (txList == null) {
|
||||
|
||||
@@ -41,6 +41,7 @@ public class Peer {
|
||||
private final BlockChain blockChain;
|
||||
// When an API user explicitly requests a block or transaction from a peer, the InventoryItem is put here
|
||||
// whilst waiting for the response. Synchronized on itself. Is not used for downloads Peer generates itself.
|
||||
// TODO: Make this work for transactions as well.
|
||||
private final List<GetDataFuture<Block>> pendingGetBlockFutures;
|
||||
// Height of the chain advertised in the peers version message.
|
||||
private int bestHeight;
|
||||
@@ -64,6 +65,9 @@ public class Peer {
|
||||
// received at which point it gets set to true again. This isn't relevant unless downloadData is true.
|
||||
private boolean downloadBlockBodies = true;
|
||||
|
||||
// Wallets that will be notified of pending transactions.
|
||||
private ArrayList<Wallet> wallets = new ArrayList<Wallet>();
|
||||
|
||||
/**
|
||||
* Construct a peer that reads/writes from the given block chain. Note that communication won't occur until
|
||||
* you call connect(), which will set up a new NetworkConnection.
|
||||
@@ -151,10 +155,23 @@ public class Peer {
|
||||
try {
|
||||
while (true) {
|
||||
Message m = conn.readMessage();
|
||||
|
||||
// Allow event listeners to filter the message stream. Listeners are allowed to drop messages by
|
||||
// returning null.
|
||||
for (PeerEventListener listener : eventListeners) {
|
||||
synchronized (listener) {
|
||||
m = listener.onPreMessageReceived(this, m);
|
||||
if (m == null) break;
|
||||
}
|
||||
}
|
||||
if (m == null) continue;
|
||||
|
||||
if (m instanceof InventoryMessage) {
|
||||
processInv((InventoryMessage) m);
|
||||
} else if (m instanceof Block) {
|
||||
processBlock((Block) m);
|
||||
} else if (m instanceof Transaction) {
|
||||
processTransaction((Transaction) m);
|
||||
} else if (m instanceof AddressMessage) {
|
||||
// We don't care about addresses of the network right now. But in future,
|
||||
// we should save them in the wallet so we don't put too much load on the seed nodes and can
|
||||
@@ -225,8 +242,21 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
|
||||
private void processTransaction(Transaction m) {
|
||||
log.info("Received broadcast tx {}", m.getHashAsString());
|
||||
for (Wallet wallet : wallets) {
|
||||
try {
|
||||
wallet.receivePending(m);
|
||||
} catch (VerificationException e) {
|
||||
log.warn("Received invalid transaction, ignoring", e);
|
||||
} catch (ScriptException e) {
|
||||
log.warn("Received invalid transaction, ignoring", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void processBlock(Block m) throws IOException {
|
||||
// This should called in the network loop thread for this peer
|
||||
log.info("Received broadcast block {}", m.getHashAsString());
|
||||
try {
|
||||
// Was this block requested by getBlock()?
|
||||
synchronized (pendingGetBlockFutures) {
|
||||
@@ -293,18 +323,13 @@ public class Peer {
|
||||
blockChainDownload(topHash);
|
||||
return;
|
||||
}
|
||||
// Just copy the message contents across - request whatever we're told about.
|
||||
// TODO: Don't re-request items that were already fetched.
|
||||
GetDataMessage getdata = new GetDataMessage(params);
|
||||
boolean dirty = false;
|
||||
for (InventoryItem item : items) {
|
||||
if (item.type != InventoryItem.Type.Block) continue;
|
||||
getdata.addItem(item);
|
||||
dirty = true;
|
||||
}
|
||||
// No blocks to download. This probably contained transactions instead, but right now we can't prove they are
|
||||
// valid so we don't bother downloading transactions that aren't in blocks yet.
|
||||
if (!dirty)
|
||||
return;
|
||||
// This will cause us to receive a bunch of block messages.
|
||||
// This will cause us to receive a bunch of block or tx messages.
|
||||
conn.writeMessage(getdata);
|
||||
}
|
||||
|
||||
@@ -361,6 +386,21 @@ public class Peer {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a wallet that will receive notifications of broadcast transactions. You need to connect the peer to each
|
||||
* wallet this way if you care about handling un-confirmed transactions. If you don't,
|
||||
* it's unnecessary as the wallet will be informed of transactions in new blocks via the block chain object.
|
||||
*/
|
||||
public void addWallet(Wallet wallet) {
|
||||
if (wallet == null)
|
||||
throw new IllegalArgumentException("Wallet must not be null");
|
||||
wallets.add(wallet);
|
||||
}
|
||||
|
||||
public void removeWallet(Wallet wallet) {
|
||||
wallets.remove(wallet);
|
||||
}
|
||||
|
||||
// A GetDataFuture wraps the result of a getBlock or (in future) getTransaction so the owner of the object can
|
||||
// decide whether to wait forever, wait for a short while or check later after doing other work.
|
||||
@@ -418,7 +458,7 @@ public class Peer {
|
||||
|
||||
/**
|
||||
* Send the given Transaction, ie, make a payment with BitCoins. To create a transaction you can broadcast, use
|
||||
* a {@link Wallet}. After the broadcast completes, confirm the send using the wallet confirmSend() method.
|
||||
* a {@link Wallet}. After the broadcast completes, confirm the send using the wallet commitTx() method.
|
||||
* @throws IOException
|
||||
*/
|
||||
void broadcastTransaction(Transaction tx) throws IOException {
|
||||
|
||||
@@ -60,4 +60,12 @@ public interface PeerEventListener {
|
||||
* @param peerCount the total number of connected peers
|
||||
*/
|
||||
public void onPeerDisconnected(Peer peer, int peerCount);
|
||||
|
||||
/**
|
||||
* Called when a message is received by a peer, before the message is processed. The returned message is
|
||||
* processed instead. Returning null will cause the message to be ignored by the Peer returning the same message
|
||||
* object allows you to see the messages received but not change them. The result from one event listeners
|
||||
* callback is passed as "m" to the next, forming a chain.
|
||||
*/
|
||||
public Message onPreMessageReceived(Peer peer, Message m);
|
||||
}
|
||||
|
||||
@@ -19,8 +19,6 @@ package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.discovery.PeerDiscovery;
|
||||
import com.google.bitcoin.discovery.PeerDiscoveryException;
|
||||
import com.google.bitcoin.store.BlockStore;
|
||||
import com.google.bitcoin.store.BlockStoreException;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@@ -75,28 +73,33 @@ public class PeerGroup {
|
||||
private Set<PeerDiscovery> peerDiscoverers;
|
||||
|
||||
private NetworkParameters params;
|
||||
private BlockStore blockStore;
|
||||
private BlockChain chain;
|
||||
private int connectionDelayMillis;
|
||||
private long fastCatchupTimeSecs;
|
||||
private Set<Wallet> wallets;
|
||||
|
||||
/**
|
||||
* Creates a PeerGroup with the given parameters and a default 5 second connection timeout.
|
||||
* Creates a PeerGroup with the given parameters and a default 5 second connection timeout. If you don't care
|
||||
* about blocks or pending transactions, you can just provide a MemoryBlockStore and a newly created Wallet.
|
||||
*
|
||||
* @param params Network parameters
|
||||
* @param chain a BlockChain object that will receive and handle block messages.
|
||||
* @param wallet a Wallet object that will receive pending transactions.
|
||||
*/
|
||||
public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain) {
|
||||
this(blockStore, params, chain, DEFAULT_CONNECTION_DELAY_MILLIS);
|
||||
public PeerGroup(NetworkParameters params, BlockChain chain) {
|
||||
this(params, chain, DEFAULT_CONNECTION_DELAY_MILLIS);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a PeerGroup with the given parameters. The connectionDelayMillis parameter controls how long the
|
||||
* PeerGroup will wait between attempts to connect to nodes or read from any added peer discovery sources.
|
||||
*/
|
||||
public PeerGroup(BlockStore blockStore, NetworkParameters params, BlockChain chain, int connectionDelayMillis) {
|
||||
this.blockStore = blockStore;
|
||||
public PeerGroup(NetworkParameters params, BlockChain chain, int connectionDelayMillis) {
|
||||
this.params = params;
|
||||
this.chain = chain;
|
||||
this.connectionDelayMillis = connectionDelayMillis;
|
||||
this.fastCatchupTimeSecs = params.genesisBlock.getTimeSeconds();
|
||||
this.wallets = new HashSet<Wallet>();
|
||||
|
||||
inactives = new LinkedBlockingQueue<PeerAddress>();
|
||||
peers = Collections.synchronizedSet(new HashSet<Peer>());
|
||||
@@ -122,6 +125,20 @@ public class PeerGroup {
|
||||
return peerEventListeners.remove(listener);
|
||||
}
|
||||
|
||||
/**
|
||||
* The given wallet will receive broadcast/pending transactions that did not appear in a block yet.
|
||||
* @return Whether the wallet was added successfully.
|
||||
*/
|
||||
public boolean addWallet(Wallet wallet) {
|
||||
if (wallet == null)
|
||||
throw new IllegalArgumentException("Wallet must not be null");
|
||||
return wallets.add(wallet);
|
||||
}
|
||||
|
||||
public boolean removeWallet(Wallet wallet) {
|
||||
return wallets.remove(wallet);
|
||||
}
|
||||
|
||||
/**
|
||||
* Use this to directly add an already initialized and connected {@link Peer} object. Normally, you would prefer
|
||||
* to use {@link PeerGroup#addAddress(PeerAddress)} and let this object handle construction of the peer for you.
|
||||
@@ -291,7 +308,7 @@ public class PeerGroup {
|
||||
final PeerAddress address = inactives.take();
|
||||
while (true) {
|
||||
try {
|
||||
Peer peer = new Peer(params, address, blockStore.getChainHead().getHeight(), chain);
|
||||
Peer peer = new Peer(params, address, chain.getChainHead().getHeight(), chain);
|
||||
executePeer(address, peer, true, ExecuteBlockMode.RETURN_IMMEDIATELY);
|
||||
break;
|
||||
} catch (RejectedExecutionException e) {
|
||||
@@ -301,11 +318,6 @@ public class PeerGroup {
|
||||
// if we reached maxConnections or if peer queue is empty. Also consider
|
||||
// exponential backoff on peers and adjusting the sleep time according to the
|
||||
// lowest backoff value in queue.
|
||||
} catch (BlockStoreException e) {
|
||||
// Fatal error
|
||||
log.error("Block store corrupt?", e);
|
||||
running = false;
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
// If we got here, we should retry this address because an error unrelated
|
||||
@@ -442,10 +454,15 @@ public class PeerGroup {
|
||||
|
||||
private synchronized void setDownloadPeer(Peer peer) {
|
||||
if (downloadPeer != null) {
|
||||
log.info("Unsetting download peer: {}", downloadPeer);
|
||||
downloadPeer.setDownloadData(false);
|
||||
for (Wallet w : wallets) {
|
||||
downloadPeer.removeWallet(w);
|
||||
}
|
||||
}
|
||||
downloadPeer = peer;
|
||||
if (downloadPeer != null) {
|
||||
log.info("Setting download peer: {}", downloadPeer);
|
||||
downloadPeer.setDownloadData(true);
|
||||
downloadPeer.setFastCatchupTime(fastCatchupTimeSecs);
|
||||
}
|
||||
@@ -459,6 +476,9 @@ public class PeerGroup {
|
||||
fastCatchupTimeSecs = secondsSinceEpoch;
|
||||
if (downloadPeer != null) {
|
||||
downloadPeer.setFastCatchupTime(secondsSinceEpoch);
|
||||
for (Wallet w : wallets) {
|
||||
downloadPeer.addWallet(w);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
inputs = new ArrayList<TransactionInput>();
|
||||
outputs = new ArrayList<TransactionOutput>();
|
||||
// We don't initialize appearsIn deliberately as it's only useful for transactions stored in the wallet.
|
||||
length = 10; //8 for std fields + 1 for each 0 varint
|
||||
length = 10; // 8 for std fields + 1 for each 0 varint
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -176,6 +176,15 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
return appearsIn;
|
||||
}
|
||||
|
||||
/** Returns true if this transaction hasn't been seen in any block yet. */
|
||||
public boolean isPending() {
|
||||
if (appearsIn == null)
|
||||
return true;
|
||||
if (appearsIn.size() == 0)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the given block to the internal serializable set of blocks in which this transaction appears. This is
|
||||
* used by the wallet to ensure transactions that appear on side chains are recorded properly even though the
|
||||
@@ -451,7 +460,6 @@ public class Transaction extends ChildMessage implements Serializable {
|
||||
s.append(in.getScriptSig().getFromAddress().toString());
|
||||
} catch (Exception e) {
|
||||
s.append("[exception: ").append(e.getMessage()).append("]");
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
s.append("\n");
|
||||
}
|
||||
|
||||
@@ -21,8 +21,6 @@ import java.io.ObjectOutputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.Serializable;
|
||||
|
||||
// TODO: Fold this class into the TransactionInput class. It's not necessary.
|
||||
|
||||
/**
|
||||
* This message is a reference or pointer to an output of a different transaction.
|
||||
*/
|
||||
@@ -103,8 +101,9 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* If this transaction was created using the explicit constructor rather than deserialized,
|
||||
* retrieves the connected output transaction. Asserts if there is no connected transaction.
|
||||
* An outpoint is a part of a transaction input that points to the output of another transaction. If we have both
|
||||
* sides in memory, and they have been linked together, this returns a pointer to the connected output, or null
|
||||
* if there is no such connection.
|
||||
*/
|
||||
TransactionOutput getConnectedOutput() {
|
||||
if (fromTx == null) return null;
|
||||
@@ -157,14 +156,6 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
return index;
|
||||
}
|
||||
|
||||
// /**
|
||||
// * @param index the index to set
|
||||
// */
|
||||
// public void setIndex(long index) {
|
||||
// unCache();
|
||||
// this.index = index;
|
||||
// }
|
||||
|
||||
/**
|
||||
* Ensure object is fully parsed before invoking java serialization. The backing byte array
|
||||
* is transient so if the object has parseLazy = true and hasn't invoked checkParse yet
|
||||
@@ -174,4 +165,16 @@ public class TransactionOutPoint extends ChildMessage implements Serializable {
|
||||
maybeParse();
|
||||
out.defaultWriteObject();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (!(other instanceof TransactionOutPoint)) return false;
|
||||
TransactionOutPoint o = (TransactionOutPoint) other;
|
||||
return o.getIndex() == getIndex() && o.getHash().equals(getHash());
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return getHash().hashCode();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,7 +178,7 @@ public class Wallet implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a wallet deserialied from the given file input stream.
|
||||
* Returns a wallet deserialized from the given file input stream.
|
||||
*/
|
||||
public static Wallet loadFromFileStream(FileInputStream f) throws IOException {
|
||||
ObjectInputStream ois = null;
|
||||
@@ -200,25 +200,113 @@ public class Wallet implements Serializable {
|
||||
/**
|
||||
* Called by the {@link BlockChain} when we receive a new block that sends coins to one of our addresses or
|
||||
* spends coins from one of our addresses (note that a single transaction can do both).<p>
|
||||
* <p/>
|
||||
*
|
||||
* This is necessary for the internal book-keeping Wallet does. When a transaction is received that sends us
|
||||
* coins it is added to a pool so we can use it later to create spends. When a transaction is received that
|
||||
* consumes outputs they are marked as spent so they won't be used in future.<p>
|
||||
* <p/>
|
||||
*
|
||||
* A transaction that spends our own coins can be received either because a spend we created was accepted by the
|
||||
* network and thus made it into a block, or because our keys are being shared between multiple instances and
|
||||
* some other node spent the coins instead. We still have to know about that to avoid accidentally trying to
|
||||
* double spend.<p>
|
||||
* <p/>
|
||||
*
|
||||
* A transaction may be received multiple times if is included into blocks in parallel chains. The blockType
|
||||
* parameter describes whether the containing block is on the main/best chain or whether it's on a presently
|
||||
* inactive side chain. We must still record these transactions and the blocks they appear in because a future
|
||||
* block might change which chain is best causing a reorganize. A re-org can totally change our balance!
|
||||
*/
|
||||
synchronized void receive(Transaction tx, StoredBlock block, BlockChain.NewBlockType blockType) throws VerificationException, ScriptException {
|
||||
synchronized void receiveFromBlock(Transaction tx, StoredBlock block,
|
||||
BlockChain.NewBlockType blockType) throws VerificationException, ScriptException {
|
||||
receive(tx, block, blockType, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when we have found a tranasction (via network broadcast or otherwise) that is relevant to this wallet
|
||||
* and want to record it. Note that we <b>cannot verify these transactions at all</b>, they may spend fictional
|
||||
* coins or be otherwise invalid. They are useful to inform the user about coins they can expect to receive soon,
|
||||
* and if you trust the sender of the transaction you can choose to assume they are in fact valid and will not
|
||||
* be double spent as an optimization.
|
||||
*
|
||||
* @param tx
|
||||
* @throws VerificationException
|
||||
* @throws ScriptException
|
||||
*/
|
||||
synchronized void receivePending(Transaction tx) throws VerificationException, ScriptException {
|
||||
// TODO: Add a notion of confidence levels to this API.
|
||||
// Runs in a peer thread.
|
||||
log.info(tx.getHashAsString());
|
||||
|
||||
// Ignore it if we already know about this transaction. Receiving a pending transaction never moves it
|
||||
// between pools.
|
||||
EnumSet<Pool> containingPools = getContainingPools(tx);
|
||||
if (!containingPools.equals(EnumSet.noneOf(Pool.class))) {
|
||||
log.info("Received tx we already saw in a block or created ourselves: " + tx.getHashAsString());
|
||||
return;
|
||||
}
|
||||
|
||||
// We only care about transactions that:
|
||||
// - Send us coins
|
||||
// - Spend our coins
|
||||
if (!isTransactionRelevant(tx, true)) {
|
||||
log.info("Received tx that isn't relevant to this wallet, discarding.");
|
||||
return;
|
||||
}
|
||||
|
||||
BigInteger value = tx.getValueSentToMe(this);
|
||||
log.info("Received a pending transaction {} that sends us {} BTC", tx.getHashAsString(),
|
||||
Utils.bitcoinValueToFriendlyString(value));
|
||||
|
||||
// If this tx spends any of our unspent outputs, mark them as spent now, then add to the pending pool. This
|
||||
// ensures that if some other client that has our keys broadcasts a spend we stay in sync. Also updates the
|
||||
// timestamp on the transaction.
|
||||
commitTx(tx);
|
||||
|
||||
// Event listeners may re-enter so we cannot make assumptions about wallet state after this loop completes.
|
||||
BigInteger balance = getBalance();
|
||||
BigInteger newBalance = balance.add(value);
|
||||
for (WalletEventListener l : eventListeners) {
|
||||
synchronized (l) {
|
||||
l.onCoinsReceived(this, tx, balance, newBalance);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given transaction sends coins to any of our keys, or has inputs spending any of our outputs,
|
||||
* and if includeDoubleSpending is true, also returns true if tx has inputs that are spending outputs which are
|
||||
* not ours but which are spent by pending transactions.<p>
|
||||
*
|
||||
* Note that if the tx has inputs containing one of our keys, but the connected transaction is not in the wallet,
|
||||
* it will not be considered relevant.
|
||||
*/
|
||||
public boolean isTransactionRelevant(Transaction tx, boolean includeDoubleSpending) throws ScriptException {
|
||||
return tx.getValueSentFromMe(this).compareTo(BigInteger.ZERO) > 0 ||
|
||||
tx.getValueSentToMe(this).compareTo(BigInteger.ZERO) > 0 ||
|
||||
(includeDoubleSpending && (findDoubleSpendAgainstPending(tx) != null));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if "tx" is spending any inputs of pending transactions. Not a general check, but it can work even if
|
||||
* the double spent inputs are not ours. Returns the pending tx that was double spent or null if none found.
|
||||
*/
|
||||
private Transaction findDoubleSpendAgainstPending(Transaction tx) {
|
||||
// Compile a set of outpoints that are spent by tx.
|
||||
HashSet<TransactionOutPoint> outpoints = new HashSet<TransactionOutPoint>();
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
outpoints.add(input.getOutpoint());
|
||||
}
|
||||
// Now for each pending transaction, see if it shares any outpoints with this tx.
|
||||
for (Transaction p : pending.values()) {
|
||||
for (TransactionInput input : p.getInputs()) {
|
||||
if (outpoints.contains(input.getOutpoint())) {
|
||||
// It does, it's a double spend against the pending pool, which makes it relevant.
|
||||
return p;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private synchronized void receive(Transaction tx, StoredBlock block,
|
||||
BlockChain.NewBlockType blockType, boolean reorg) throws VerificationException, ScriptException {
|
||||
// Runs in a peer thread.
|
||||
@@ -311,31 +399,61 @@ public class Wallet implements Serializable {
|
||||
* Handle when a transaction becomes newly active on the best chain, either due to receiving a new block or a
|
||||
* re-org making inactive transactions active.
|
||||
*/
|
||||
private void processTxFromBestChain(Transaction tx) throws VerificationException {
|
||||
private void processTxFromBestChain(Transaction tx) throws VerificationException, ScriptException {
|
||||
// This TX may spend our existing outputs even though it was not pending. This can happen in unit
|
||||
// tests and if keys are moved between wallets.
|
||||
updateForSpends(tx);
|
||||
// tests, if keys are moved between wallets, and if we're catching up to the chain given only a set of keys.
|
||||
updateForSpends(tx, true);
|
||||
if (!tx.getValueSentToMe(this).equals(BigInteger.ZERO)) {
|
||||
// It's sending us coins.
|
||||
log.info(" new tx ->unspent");
|
||||
boolean alreadyPresent = unspent.put(tx.getHash(), tx) != null;
|
||||
assert !alreadyPresent : "TX was received twice";
|
||||
} else {
|
||||
} else if (!tx.getValueSentFromMe(this).equals(BigInteger.ZERO)) {
|
||||
// It spent some of our coins and did not send us any.
|
||||
log.info(" new tx ->spent");
|
||||
boolean alreadyPresent = spent.put(tx.getHash(), tx) != null;
|
||||
assert !alreadyPresent : "TX was received twice";
|
||||
} else {
|
||||
// It didn't send us coins nor spend any of our coins. If we're processing it, that must be because it
|
||||
// spends outpoints that are also spent by some pending transactions - maybe a double spend of somebody
|
||||
// elses coins that were originally sent to us? ie, this might be a Finney attack where we think we
|
||||
// received some money and then the sender co-operated with a miner to take back the coins, using a tx
|
||||
// that isn't involving our keys at all.
|
||||
Transaction doubleSpend = findDoubleSpendAgainstPending(tx);
|
||||
if (doubleSpend != null) {
|
||||
// This is mostly the same as the codepath in updateForSpends, but that one is only triggered when
|
||||
// the transaction being double spent is actually in our wallet (ie, maybe we're double spending).
|
||||
log.warn("Saw double spend from chain override pending tx {}", doubleSpend.getHashAsString());
|
||||
log.warn(" <-pending ->dead");
|
||||
pending.remove(doubleSpend.getHash());
|
||||
dead.put(doubleSpend.getHash(), doubleSpend);
|
||||
// Inform the event listeners of the newly dead tx.
|
||||
for (WalletEventListener listener : eventListeners) {
|
||||
synchronized (listener) {
|
||||
listener.onDeadTransaction(this, doubleSpend, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the wallet by checking if this TX spends any of our outputs. This is not used normally because
|
||||
* when we receive our own spends, we've already marked the outputs as spent previously (during tx creation) so
|
||||
* there's no need to go through and do it again.
|
||||
* Updates the wallet by checking if this TX spends any of our outputs, and marking them as spent if so. It can
|
||||
* be called in two contexts. One is when we receive a transaction on the best chain but it wasn't pending, this
|
||||
* most commonly happens when we have a set of keys but the wallet transactions were wiped and we are catching up
|
||||
* with the block chain. It can also happen if a block includes a transaction we never saw at broadcast time.
|
||||
* If this tx double spends, it takes precedence over our pending transactions and the pending tx goes dead.
|
||||
*
|
||||
* The other context it can be called is from {@link Wallet#receivePending(Transaction)} ie we saw a tx be
|
||||
* broadcast or one was submitted directly that spends our own coins. If this tx double spends it does NOT take
|
||||
* precedence because the winner will be resolved by the miners - we assume that our version will win,
|
||||
* if we are wrong then when a block appears the tx will go dead.
|
||||
*/
|
||||
private void updateForSpends(Transaction tx) throws VerificationException {
|
||||
private void updateForSpends(Transaction tx, boolean fromChain) throws VerificationException {
|
||||
// tx is on the best chain by this point.
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
List<TransactionInput> inputs = tx.getInputs();
|
||||
for (int i = 0; i < inputs.size(); i++) {
|
||||
TransactionInput input = inputs.get(i);
|
||||
TransactionInput.ConnectionResult result = input.connect(unspent, false);
|
||||
if (result == TransactionInput.ConnectionResult.NO_SUCH_TX) {
|
||||
// Not found in the unspent map. Try again with the spent map.
|
||||
@@ -347,10 +465,7 @@ public class Wallet implements Serializable {
|
||||
}
|
||||
|
||||
if (result == TransactionInput.ConnectionResult.ALREADY_SPENT) {
|
||||
// Double spend! This must have overridden a pending tx, or the block is bad (contains transactions
|
||||
// that illegally double spend: should never occur if we are connected to an honest node).
|
||||
//
|
||||
// Work backwards like so:
|
||||
// Double spend! Work backwards like so:
|
||||
//
|
||||
// A -> spent by B [pending]
|
||||
// \-> spent by C [chain]
|
||||
@@ -362,19 +477,31 @@ public class Wallet implements Serializable {
|
||||
assert spentBy != null;
|
||||
Transaction connected = spentBy.getParentTransaction();
|
||||
assert connected != null;
|
||||
if (pending.containsKey(connected.getHash())) {
|
||||
log.info("Saw double spend from chain override pending tx {}", connected.getHashAsString());
|
||||
log.info(" <-pending ->dead");
|
||||
pending.remove(connected.getHash());
|
||||
dead.put(connected.getHash(), connected);
|
||||
// Now forcibly change the connection.
|
||||
input.connect(unspent, true);
|
||||
// Inform the event listeners of the newly dead tx.
|
||||
for (WalletEventListener listener : eventListeners) {
|
||||
synchronized (listener) {
|
||||
listener.onDeadTransaction(this, connected, tx);
|
||||
if (fromChain) {
|
||||
// This must have overridden a pending tx, or the block is bad (contains transactions
|
||||
// that illegally double spend: should never occur if we are connected to an honest node).
|
||||
if (pending.containsKey(connected.getHash())) {
|
||||
log.warn("Saw double spend from chain override pending tx {}", connected.getHashAsString());
|
||||
log.warn(" <-pending ->dead");
|
||||
pending.remove(connected.getHash());
|
||||
dead.put(connected.getHash(), connected);
|
||||
// Now forcibly change the connection.
|
||||
input.connect(unspent, true);
|
||||
// Inform the event listeners of the newly dead tx.
|
||||
for (WalletEventListener listener : eventListeners) {
|
||||
synchronized (listener) {
|
||||
listener.onDeadTransaction(this, connected, tx);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// A pending transaction that tried to double spend our coins - we log and ignore it, because either
|
||||
// 1) The double-spent tx is confirmed and thus this tx has no effect .... or
|
||||
// 2) Both txns are pending, neither has priority. Miners will decide in a few minutes which won.
|
||||
log.warn("Saw double spend from another pending transaction, ignoring tx {}",
|
||||
tx.getHashAsString());
|
||||
log.warn(" offending input is input {}", i);
|
||||
return;
|
||||
}
|
||||
} else if (result == TransactionInput.ConnectionResult.SUCCESS) {
|
||||
// Otherwise we saw a transaction spend our coins, but we didn't try and spend them ourselves yet.
|
||||
@@ -423,19 +550,23 @@ public class Wallet implements Serializable {
|
||||
}
|
||||
|
||||
/**
|
||||
* Call this when we have successfully transmitted the send tx to the network, to update the wallet.
|
||||
* Updates the wallet with the given transaction: puts it into the pending pool and sets the spent flags. Used in
|
||||
* two situations:<p>
|
||||
*
|
||||
* <ol>
|
||||
* <li>When we have just successfully transmitted the tx we created to the network.</li>
|
||||
* <li>When we receive a pending transaction that didn't appear in the chain yet,
|
||||
* and we did not create it, and it spends some of our outputs.</li>
|
||||
* </ol>
|
||||
*/
|
||||
synchronized void confirmSend(Transaction tx) {
|
||||
assert !pending.containsKey(tx.getHash()) : "confirmSend called on the same transaction twice";
|
||||
log.info("confirmSend of {}", tx.getHashAsString());
|
||||
// Mark the outputs of the used transcations as spent, so we don't try and spend it again.
|
||||
for (TransactionInput input : tx.getInputs()) {
|
||||
TransactionOutput connectedOutput = input.getOutpoint().getConnectedOutput();
|
||||
Transaction connectedTx = connectedOutput.parentTransaction;
|
||||
connectedOutput.markAsSpent(input);
|
||||
maybeMoveTxToSpent(connectedTx, "spent tx");
|
||||
}
|
||||
synchronized void commitTx(Transaction tx) throws VerificationException {
|
||||
assert !pending.containsKey(tx.getHash()) : "commitTx called on the same transaction twice";
|
||||
log.info("commitTx of {}", tx.getHashAsString());
|
||||
tx.updatedAt = Utils.now();
|
||||
// Mark the outputs we're spending as spent so we won't try and use them in future creations. This will also
|
||||
// move any transactions that are now fully spent to the spent map so we can skip them when creating future
|
||||
// spends.
|
||||
updateForSpends(tx, false);
|
||||
// Add to the pending pool. It'll be moved out once we receive this transaction on the best chain.
|
||||
pending.put(tx.getHash(), tx);
|
||||
}
|
||||
@@ -505,6 +636,27 @@ public class Wallet implements Serializable {
|
||||
ALL,
|
||||
}
|
||||
|
||||
EnumSet<Pool> getContainingPools(Transaction tx) {
|
||||
EnumSet<Pool> result = EnumSet.noneOf(Pool.class);
|
||||
Sha256Hash txHash = tx.getHash();
|
||||
if (unspent.containsKey(txHash)) {
|
||||
result.add(Pool.UNSPENT);
|
||||
}
|
||||
if (spent.containsKey(txHash)) {
|
||||
result.add(Pool.SPENT);
|
||||
}
|
||||
if (pending.containsKey(txHash)) {
|
||||
result.add(Pool.PENDING);
|
||||
}
|
||||
if (inactive.containsKey(txHash)) {
|
||||
result.add(Pool.INACTIVE);
|
||||
}
|
||||
if (dead.containsKey(txHash)) {
|
||||
result.add(Pool.DEAD);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int getPoolSize(Pool pool) {
|
||||
switch (pool) {
|
||||
case UNSPENT:
|
||||
@@ -529,7 +681,7 @@ public class Wallet implements Serializable {
|
||||
* <p/>
|
||||
* This method is stateless in the sense that calling it twice with the same inputs will result in two
|
||||
* Transaction objects which are equal. The wallet is not updated to track its pending status or to mark the
|
||||
* coins as spent until confirmSend is called on the result.
|
||||
* coins as spent until commitTx is called on the result.
|
||||
*/
|
||||
synchronized Transaction createSend(Address address, BigInteger nanocoins) {
|
||||
// For now let's just pick the first key in our keychain. In future we might want to do something else to
|
||||
@@ -557,7 +709,11 @@ public class Wallet implements Serializable {
|
||||
}
|
||||
|
||||
// TODO - retry logic
|
||||
confirmSend(tx);
|
||||
try {
|
||||
commitTx(tx);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
@@ -575,7 +731,11 @@ public class Wallet implements Serializable {
|
||||
if (tx == null) // Not enough money! :-(
|
||||
return null;
|
||||
peer.broadcastTransaction(tx);
|
||||
confirmSend(tx);
|
||||
try {
|
||||
commitTx(tx);
|
||||
} catch (VerificationException e) {
|
||||
throw new RuntimeException(e); // Cannot happen unless there's a bug, as we just created this ourselves.
|
||||
}
|
||||
return tx;
|
||||
}
|
||||
|
||||
@@ -583,7 +743,7 @@ public class Wallet implements Serializable {
|
||||
* Creates a transaction that sends $coins.$cents BTC to the given address.<p>
|
||||
* <p/>
|
||||
* IMPORTANT: This method does NOT update the wallet. If you call createSend again you may get two transactions
|
||||
* that spend the same coins. You have to call confirmSend on the created transaction to prevent this,
|
||||
* that spend the same coins. You have to call commitTx on the created transaction to prevent this,
|
||||
* but that should only occur once the transaction has been accepted by the network. This implies you cannot have
|
||||
* more than one outstanding sending tx at once.
|
||||
*
|
||||
@@ -714,8 +874,6 @@ public class Wallet implements Serializable {
|
||||
AVAILABLE
|
||||
}
|
||||
|
||||
;
|
||||
|
||||
/**
|
||||
* Returns the AVAILABLE balance of this wallet. See {@link BalanceType#AVAILABLE} for details on what this
|
||||
* means.<p>
|
||||
|
||||
@@ -74,8 +74,9 @@ public class DerbyPingService {
|
||||
// Connect to the localhost node. One minute timeout since we won't try any other peers
|
||||
System.out.println("Connecting ...");
|
||||
BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
|
||||
peerGroup.addWallet(wallet);
|
||||
peerGroup.start();
|
||||
|
||||
// We want to know when the balance changes.
|
||||
|
||||
@@ -81,10 +81,11 @@ public class PingService {
|
||||
System.out.println("Connecting ...");
|
||||
BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
|
||||
// Download headers only until a day ago.
|
||||
peerGroup.setFastCatchupTimeSecs((new Date().getTime() / 1000) - (60 * 60 * 24));
|
||||
peerGroup.addWallet(wallet);
|
||||
peerGroup.start();
|
||||
|
||||
// We want to know when the balance changes.
|
||||
@@ -93,13 +94,22 @@ public class PingService {
|
||||
public void onCoinsReceived(Wallet w, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||
// Running on a peer thread.
|
||||
assert !newBalance.equals(BigInteger.ZERO);
|
||||
|
||||
BigInteger value = tx.getValueSentToMe(w);
|
||||
|
||||
if (tx.isPending()) {
|
||||
System.out.println("Received pending tx for " + Utils.bitcoinValueToFriendlyString(value));
|
||||
// Ignore for now, as we won't be allowed to spend until the tx is no longer pending. We'll get
|
||||
// another callback here when the tx is confirmed.
|
||||
return;
|
||||
}
|
||||
|
||||
// It's impossible to pick one specific identity that you receive coins from in BitCoin as there
|
||||
// could be inputs from many addresses. So instead we just pick the first and assume they were all
|
||||
// owned by the same person.
|
||||
try {
|
||||
TransactionInput input = tx.getInputs().get(0);
|
||||
Address from = input.getFromAddress();
|
||||
BigInteger value = tx.getValueSentToMe(w);
|
||||
System.out.println("Received " + Utils.bitcoinValueToFriendlyString(value) + " from " + from.toString());
|
||||
// Now send the coins back!
|
||||
Transaction sendTx = w.sendCoins(peerGroup, from, value);
|
||||
|
||||
@@ -58,7 +58,7 @@ public class PrivateKeys {
|
||||
final MemoryBlockStore blockStore = new MemoryBlockStore(params);
|
||||
BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
|
||||
peerGroup.start();
|
||||
peerGroup.downloadBlockChain();
|
||||
|
||||
@@ -38,7 +38,7 @@ public class RefreshWallet {
|
||||
BlockStore blockStore = new MemoryBlockStore(params);
|
||||
BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
|
||||
final PeerGroup peerGroup = new PeerGroup(blockStore, params, chain);
|
||||
final PeerGroup peerGroup = new PeerGroup(params, chain);
|
||||
peerGroup.addAddress(new PeerAddress(InetAddress.getLocalHost()));
|
||||
peerGroup.start();
|
||||
|
||||
|
||||
@@ -129,7 +129,7 @@ public class ChainSplitTests {
|
||||
assertEquals("50.00", Utils.bitcoinValueToFriendlyString(wallet.getBalance()));
|
||||
Address dest = new ECKey().toAddress(unitTestParams);
|
||||
Transaction spend = wallet.createSend(dest, Utils.toNanoCoins(10, 0));
|
||||
wallet.confirmSend(spend);
|
||||
wallet.commitTx(spend);
|
||||
// Waiting for confirmation ...
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
Block b2 = b1.createNextBlock(someOtherGuy);
|
||||
@@ -198,7 +198,7 @@ public class ChainSplitTests {
|
||||
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
|
||||
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
|
||||
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
|
||||
wallet.confirmSend(t1);
|
||||
wallet.commitTx(t1);
|
||||
// Receive t1 as confirmed by the network.
|
||||
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
|
||||
b2.addTransaction(t1);
|
||||
@@ -240,7 +240,7 @@ public class ChainSplitTests {
|
||||
Transaction t1 = wallet.createSend(someOtherGuy, Utils.toNanoCoins(10, 0));
|
||||
Address yetAnotherGuy = new ECKey().toAddress(unitTestParams);
|
||||
Transaction t2 = wallet.createSend(yetAnotherGuy, Utils.toNanoCoins(20, 0));
|
||||
wallet.confirmSend(t1);
|
||||
wallet.commitTx(t1);
|
||||
// t1 is still pending ...
|
||||
Block b2 = b1.createNextBlock(new ECKey().toAddress(unitTestParams));
|
||||
chain.add(b2);
|
||||
|
||||
@@ -23,6 +23,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.net.InetSocketAddress;
|
||||
import java.util.concurrent.BlockingQueue;
|
||||
import java.util.concurrent.LinkedBlockingQueue;
|
||||
@@ -33,7 +34,6 @@ import static org.junit.Assert.*;
|
||||
public class PeerGroupTest extends TestWithNetworkConnections {
|
||||
static final NetworkParameters params = NetworkParameters.unitTests();
|
||||
|
||||
private Wallet wallet;
|
||||
private PeerGroup peerGroup;
|
||||
private final BlockingQueue<Peer> disconnectedPeers = new LinkedBlockingQueue<Peer>();
|
||||
|
||||
@@ -41,10 +41,10 @@ public class PeerGroupTest extends TestWithNetworkConnections {
|
||||
@Before
|
||||
public void setUp() throws Exception {
|
||||
super.setUp();
|
||||
wallet = new Wallet(params);
|
||||
|
||||
blockStore = new MemoryBlockStore(params);
|
||||
BlockChain chain = new BlockChain(params, wallet, blockStore);
|
||||
peerGroup = new PeerGroup(blockStore, params, chain, 1000);
|
||||
peerGroup = new PeerGroup(params, chain, 1000);
|
||||
|
||||
// Support for testing disconnect events in a non-racy manner.
|
||||
peerGroup.addEventListener(new AbstractPeerEventListener() {
|
||||
@@ -95,6 +95,33 @@ public class PeerGroupTest extends TestWithNetworkConnections {
|
||||
peerGroup.stop();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void receiveTxBroadcast() throws Exception {
|
||||
// Check that when we receive transactions on all our peers, we do the right thing.
|
||||
|
||||
// Create a couple of peers.
|
||||
peerGroup.addWallet(wallet);
|
||||
MockNetworkConnection n1 = createMockNetworkConnection();
|
||||
Peer p1 = new Peer(params, blockChain, n1);
|
||||
MockNetworkConnection n2 = createMockNetworkConnection();
|
||||
Peer p2 = new Peer(params, blockChain, n2);
|
||||
peerGroup.start();
|
||||
peerGroup.addPeer(p1);
|
||||
peerGroup.addPeer(p2);
|
||||
|
||||
BigInteger value = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = TestUtils.createFakeTx(unitTestParams, value, address);
|
||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||
inv.addItem(new InventoryItem(InventoryItem.Type.Transaction, t1.getHash()));
|
||||
n1.inbound(inv);
|
||||
n2.inbound(inv);
|
||||
GetDataMessage getdata = (GetDataMessage) n1.outbound();
|
||||
assertNull(n2.outbound()); // Only one peer is used to download.
|
||||
n1.inbound(t1);
|
||||
n1.outbound(); // Wait for processing to complete.
|
||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void singleDownloadPeer1() throws Exception {
|
||||
// Check that we don't attempt to retrieve blocks on multiple peers.
|
||||
|
||||
@@ -20,6 +20,7 @@ import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.Future;
|
||||
@@ -38,6 +39,7 @@ public class PeerTest extends TestWithNetworkConnections {
|
||||
|
||||
conn = createMockNetworkConnection();
|
||||
peer = new Peer(unitTestParams, blockChain, conn);
|
||||
peer.addWallet(wallet);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -139,6 +141,26 @@ public class PeerTest extends TestWithNetworkConnections {
|
||||
assertNull(message != null ? message.toString() : "", message);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void invDownloadTx() throws Exception {
|
||||
peer.setDownloadData(true);
|
||||
// Make a transaction and tell the peer we have it.;
|
||||
BigInteger value = Utils.toNanoCoins(1, 0);
|
||||
Transaction tx = TestUtils.createFakeTx(unitTestParams, value, address);
|
||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||
InventoryItem item = new InventoryItem(InventoryItem.Type.Transaction, tx.getHash());
|
||||
inv.addItem(item);
|
||||
conn.inbound(inv);
|
||||
// Peer hasn't seen it before, so will ask for it.
|
||||
runPeer(peer, conn);
|
||||
GetDataMessage message = (GetDataMessage) conn.popOutbound();
|
||||
assertEquals(1, message.getItems().size());
|
||||
assertEquals(tx.getHash(), message.getItems().get(0).hash);
|
||||
conn.inbound(tx);
|
||||
runPeer(peer, conn);
|
||||
assertEquals(value, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
}
|
||||
|
||||
// Check that inventory message containing blocks we want is processed correctly.
|
||||
@Test
|
||||
public void newBlock() throws Exception {
|
||||
@@ -148,7 +170,6 @@ public class PeerTest extends TestWithNetworkConnections {
|
||||
Block b1 = TestUtils.createFakeBlock(unitTestParams, blockStore).block;
|
||||
blockChain.add(b1);
|
||||
Block b2 = TestUtils.makeSolvedTestBlock(unitTestParams, b1);
|
||||
Block b3 = TestUtils.makeSolvedTestBlock(unitTestParams, b2);
|
||||
conn.setVersionMessageForHeight(unitTestParams, 100);
|
||||
// Receive notification of a new block.
|
||||
InventoryMessage inv = new InventoryMessage(unitTestParams);
|
||||
@@ -158,6 +179,8 @@ public class PeerTest extends TestWithNetworkConnections {
|
||||
// Response to the getdata message.
|
||||
conn.inbound(b2);
|
||||
|
||||
expect(listener.onPreMessageReceived(eq(peer), eq(inv))).andReturn(inv);
|
||||
expect(listener.onPreMessageReceived(eq(peer), eq(b2))).andReturn(b2);
|
||||
listener.onBlocksDownloaded(eq(peer), anyObject(Block.class), eq(98));
|
||||
expectLastCall();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.store.MemoryBlockStore;
|
||||
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||
import org.easymock.IMocksControl;
|
||||
|
||||
import java.io.IOException;
|
||||
@@ -31,14 +32,23 @@ public class TestWithNetworkConnections {
|
||||
protected NetworkParameters unitTestParams;
|
||||
protected MemoryBlockStore blockStore;
|
||||
protected BlockChain blockChain;
|
||||
protected Wallet wallet;
|
||||
protected ECKey key;
|
||||
protected Address address;
|
||||
|
||||
public void setUp() throws Exception {
|
||||
BriefLogFormatter.init();
|
||||
|
||||
control = createStrictControl();
|
||||
control.checkOrder(true);
|
||||
|
||||
unitTestParams = NetworkParameters.unitTests();
|
||||
blockStore = new MemoryBlockStore(unitTestParams);
|
||||
blockChain = new BlockChain(unitTestParams, new Wallet(unitTestParams), blockStore);
|
||||
wallet = new Wallet(unitTestParams);
|
||||
key = new ECKey();
|
||||
address = key.toAddress(unitTestParams);
|
||||
wallet.addKey(key);
|
||||
blockChain = new BlockChain(unitTestParams, wallet, blockStore);
|
||||
}
|
||||
|
||||
protected MockNetworkConnection createMockNetworkConnection() {
|
||||
|
||||
@@ -16,19 +16,23 @@
|
||||
|
||||
package com.google.bitcoin.core;
|
||||
|
||||
import com.google.bitcoin.store.BlockStore;
|
||||
import com.google.bitcoin.store.MemoryBlockStore;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import static com.google.bitcoin.core.TestUtils.createFakeBlock;
|
||||
import static com.google.bitcoin.core.TestUtils.createFakeTx;
|
||||
import static com.google.bitcoin.core.Utils.bitcoinValueToFriendlyString;
|
||||
import static com.google.bitcoin.core.Utils.toNanoCoins;
|
||||
import static org.junit.Assert.*;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
import static org.junit.Assert.assertFalse;
|
||||
import static org.junit.Assert.assertTrue;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import com.google.bitcoin.store.BlockStore;
|
||||
import com.google.bitcoin.store.MemoryBlockStore;
|
||||
import com.google.bitcoin.utils.BriefLogFormatter;
|
||||
|
||||
public class WalletTest {
|
||||
static final NetworkParameters params = NetworkParameters.unitTests();
|
||||
@@ -45,6 +49,8 @@ public class WalletTest {
|
||||
wallet = new Wallet(params);
|
||||
wallet.addKey(myKey);
|
||||
blockStore = new MemoryBlockStore(params);
|
||||
|
||||
BriefLogFormatter.init();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -53,7 +59,7 @@ public class WalletTest {
|
||||
BigInteger v1 = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(params, v1, myAddress);
|
||||
|
||||
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
@@ -70,7 +76,7 @@ public class WalletTest {
|
||||
|
||||
// We have NOT proven that the signature is correct!
|
||||
|
||||
wallet.confirmSend(t2);
|
||||
wallet.commitTx(t2);
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING));
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT));
|
||||
assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
@@ -82,14 +88,14 @@ public class WalletTest {
|
||||
BigInteger v1 = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(params, v1, myAddress);
|
||||
|
||||
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(v1, wallet.getBalance());
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
|
||||
BigInteger v2 = toNanoCoins(0, 50);
|
||||
Transaction t2 = createFakeTx(params, v2, myAddress);
|
||||
wallet.receive(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
|
||||
wallet.receiveFromBlock(t2, null, BlockChain.NewBlockType.SIDE_CHAIN);
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.INACTIVE));
|
||||
assertEquals(2, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
|
||||
@@ -110,7 +116,7 @@ public class WalletTest {
|
||||
}
|
||||
};
|
||||
wallet.addEventListener(listener);
|
||||
wallet.receive(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(fakeTx, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertTrue(didRun[0]);
|
||||
}
|
||||
|
||||
@@ -125,19 +131,16 @@ public class WalletTest {
|
||||
StoredBlock b2 = createFakeBlock(params, blockStore, t2).storedBlock;
|
||||
BigInteger expected = toNanoCoins(5, 50);
|
||||
assertEquals(0, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
wallet.receive(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
wallet.receive(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(t2, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(2, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
assertEquals(expected, wallet.getBalance());
|
||||
|
||||
// Now spend one coin.
|
||||
BigInteger v3 = toNanoCoins(1, 0);
|
||||
Transaction spend = wallet.createSend(new ECKey().toAddress(params), v3);
|
||||
wallet.confirmSend(spend);
|
||||
// TODO: This test depends on the coin selection which is non-deterministic. FIX!
|
||||
// assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
// assertEquals(1, wallet.getPoolSize(Wallet.Pool.SPENT));
|
||||
wallet.commitTx(spend);
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.PENDING));
|
||||
|
||||
// Available and estimated balances should not be the same. We don't check the exact available balance here
|
||||
@@ -148,7 +151,7 @@ public class WalletTest {
|
||||
|
||||
// Now confirm the transaction by including it into a block.
|
||||
StoredBlock b3 = createFakeBlock(params, blockStore, spend).storedBlock;
|
||||
wallet.receive(spend, b3, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(spend, b3, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
|
||||
// Change is confirmed. We started with 5.50 so we should have 4.50 left.
|
||||
BigInteger v4 = toNanoCoins(4, 50);
|
||||
@@ -164,21 +167,21 @@ public class WalletTest {
|
||||
public void blockChainCatchup() throws Exception {
|
||||
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
|
||||
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
|
||||
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Send 0.10 to somebody else.
|
||||
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
|
||||
// Pretend it makes it into the block chain, our wallet state is cleared but we still have the keys, and we
|
||||
// want to get back to our previous state. We can do this by just not confirming the transaction as
|
||||
// createSend is stateless.
|
||||
StoredBlock b2 = createFakeBlock(params, blockStore, send1).storedBlock;
|
||||
wallet.receive(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(send1, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.90");
|
||||
// And we do it again after the catchup.
|
||||
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
|
||||
// What we'd really like to do is prove the official client would accept it .... no such luck unfortunately.
|
||||
wallet.confirmSend(send2);
|
||||
wallet.commitTx(send2);
|
||||
StoredBlock b3 = createFakeBlock(params, blockStore, send2).storedBlock;
|
||||
wallet.receive(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(send2, b3, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(bitcoinValueToFriendlyString(wallet.getBalance()), "0.80");
|
||||
}
|
||||
|
||||
@@ -186,7 +189,7 @@ public class WalletTest {
|
||||
public void balances() throws Exception {
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
Transaction tx1 = createFakeTx(params, nanos, myAddress);
|
||||
wallet.receive(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(tx1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(nanos, tx1.getValueSentToMe(wallet, true));
|
||||
// Send 0.10 to somebody else.
|
||||
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 10), myAddress);
|
||||
@@ -206,7 +209,7 @@ public class WalletTest {
|
||||
// Note that tx is no longer valid: it spends more than it imports. However checking transactions balance
|
||||
// correctly isn't possible in SPV mode because value is a property of outputs not inputs. Without all
|
||||
// transactions you can't check they add up.
|
||||
wallet.receive(tx, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(tx, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Now the other guy creates a transaction which spends that change.
|
||||
Transaction tx2 = new Transaction(params);
|
||||
tx2.addInput(output);
|
||||
@@ -223,20 +226,20 @@ public class WalletTest {
|
||||
BigInteger coinHalf = Utils.toNanoCoins(0, 50);
|
||||
// Start by giving us 1 coin.
|
||||
Transaction inbound1 = createFakeTx(params, coin1, myAddress);
|
||||
wallet.receive(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(inbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Send half to some other guy. Sending only half then waiting for a confirm is important to ensure the tx is
|
||||
// in the unspent pool, not pending or spent.
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.UNSPENT));
|
||||
assertEquals(1, wallet.getPoolSize(Wallet.Pool.ALL));
|
||||
Address someOtherGuy = new ECKey().toAddress(params);
|
||||
Transaction outbound1 = wallet.createSend(someOtherGuy, coinHalf);
|
||||
wallet.confirmSend(outbound1);
|
||||
wallet.receive(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.commitTx(outbound1);
|
||||
wallet.receiveFromBlock(outbound1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// That other guy gives us the coins right back.
|
||||
Transaction inbound2 = new Transaction(params);
|
||||
inbound2.addOutput(new TransactionOutput(params, inbound2, coinHalf, myAddress));
|
||||
inbound2.addInput(outbound1.getOutputs().get(0));
|
||||
wallet.receive(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(inbound2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(coin1, wallet.getBalance());
|
||||
}
|
||||
|
||||
@@ -266,19 +269,124 @@ public class WalletTest {
|
||||
// Receive 1 BTC.
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(params, nanos, myAddress);
|
||||
wallet.receive(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(t1, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Create a send to a merchant.
|
||||
Transaction send1 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
|
||||
// Create a double spend.
|
||||
Transaction send2 = wallet.createSend(new ECKey().toAddress(params), toNanoCoins(0, 50));
|
||||
// Broadcast send1.
|
||||
wallet.confirmSend(send1);
|
||||
wallet.commitTx(send1);
|
||||
// Receive a block that overrides it.
|
||||
wallet.receive(send2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(send2, null, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(send1, eventDead[0]);
|
||||
assertEquals(send2, eventReplacement[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pending1() throws Exception {
|
||||
// Check that if we receive a pending transaction that is then confirmed, we are notified as appropriate.
|
||||
final BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
final Transaction t1 = createFakeTx(params, nanos, myAddress);
|
||||
|
||||
// First one is "called" second is "pending".
|
||||
final boolean[] flags = new boolean[2];
|
||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||
@Override
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||
// Check we got the expected transaction.
|
||||
assertEquals(tx, t1);
|
||||
// Check that it's considered to be pending inclusion in the block chain.
|
||||
assertEquals(prevBalance, BigInteger.ZERO);
|
||||
assertEquals(newBalance, nanos);
|
||||
flags[0] = true;
|
||||
flags[1] = tx.isPending();
|
||||
}
|
||||
});
|
||||
|
||||
wallet.receivePending(t1);
|
||||
assertTrue(flags[0]);
|
||||
assertTrue(flags[1]); // is pending
|
||||
flags[0] = false;
|
||||
// Check we don't get notified if we receive it again.
|
||||
wallet.receivePending(t1);
|
||||
assertFalse(flags[0]);
|
||||
// Now check again when we receive it via a block.
|
||||
flags[1] = true;
|
||||
wallet.receiveFromBlock(t1, createFakeBlock(params, blockStore, t1).storedBlock,
|
||||
BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertTrue(flags[0]);
|
||||
assertFalse(flags[1]); // is not pending
|
||||
// Check we don't get notified about an irrelevant transaction.
|
||||
flags[0] = false;
|
||||
flags[1] = false;
|
||||
Transaction irrelevant = createFakeTx(params, nanos, new ECKey().toAddress(params));
|
||||
wallet.receivePending(irrelevant);
|
||||
assertFalse(flags[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pending2() throws Exception {
|
||||
// Check that if we receive a pending tx we did not send, it updates our spent flags correctly.
|
||||
|
||||
// Receive some coins.
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
Transaction t1 = createFakeTx(params, nanos, myAddress);
|
||||
StoredBlock b1 = createFakeBlock(params, blockStore, t1).storedBlock;
|
||||
wallet.receiveFromBlock(t1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(nanos, wallet.getBalance());
|
||||
// Create a spend with them, but don't commit it (ie it's from somewhere else but using our keys).
|
||||
Transaction t2 = wallet.createSend(new ECKey().toAddress(params), nanos);
|
||||
// Now receive it as pending.
|
||||
wallet.receivePending(t2);
|
||||
// Our balance is now zero.
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pending3() throws Exception {
|
||||
// Check that if we receive a pending tx, and it's overridden by a double spend from the main chain, we
|
||||
// are notified that it's dead. This should work even if the pending tx inputs are NOT ours, ie, they don't
|
||||
// connect to anything.
|
||||
BigInteger nanos = Utils.toNanoCoins(1, 0);
|
||||
|
||||
// Create two transactions that share the same input tx.
|
||||
Address badGuy = new ECKey().toAddress(params);
|
||||
Transaction doubleSpentTx = new Transaction(params);
|
||||
TransactionOutput doubleSpentOut = new TransactionOutput(params, doubleSpentTx, nanos, badGuy);
|
||||
doubleSpentTx.addOutput(doubleSpentOut);
|
||||
Transaction t1 = new Transaction(params);
|
||||
TransactionOutput o1 = new TransactionOutput(params, t1, nanos, myAddress);
|
||||
t1.addOutput(o1);
|
||||
t1.addInput(doubleSpentOut);
|
||||
Transaction t2 = new Transaction(params);
|
||||
TransactionOutput o2 = new TransactionOutput(params, t2, nanos, badGuy);
|
||||
t2.addOutput(o2);
|
||||
t2.addInput(doubleSpentOut);
|
||||
|
||||
final Transaction[] called = new Transaction[2];
|
||||
wallet.addEventListener(new AbstractWalletEventListener() {
|
||||
public void onCoinsReceived(Wallet wallet, Transaction tx, BigInteger prevBalance, BigInteger newBalance) {
|
||||
called[0] = tx;
|
||||
}
|
||||
|
||||
public void onDeadTransaction(Wallet wallet, Transaction deadTx, Transaction replacementTx) {
|
||||
called[0] = deadTx;
|
||||
called[1] = replacementTx;
|
||||
}
|
||||
});
|
||||
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
wallet.receivePending(t1);
|
||||
assertEquals(t1, called[0]);
|
||||
assertEquals(nanos, wallet.getBalance(Wallet.BalanceType.ESTIMATED));
|
||||
// Now receive a double spend on the main chain.
|
||||
called[0] = called[1] = null;
|
||||
wallet.receiveFromBlock(t2, createFakeBlock(params, blockStore, t2).storedBlock, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
assertEquals(BigInteger.ZERO, wallet.getBalance());
|
||||
assertEquals(t1, called[0]); // dead
|
||||
assertEquals(t2, called[1]); // replacement
|
||||
}
|
||||
|
||||
@Test
|
||||
public void transactionsList() throws Exception {
|
||||
// Check the wallet can give us an ordered list of all received transactions.
|
||||
@@ -286,12 +394,12 @@ public class WalletTest {
|
||||
// Receive a coin.
|
||||
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
|
||||
StoredBlock b1 = createFakeBlock(params, blockStore, tx1).storedBlock;
|
||||
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Receive half a coin 10 minutes later.
|
||||
Utils.rollMockClock(60 * 10);
|
||||
Transaction tx2 = createFakeTx(params, Utils.toNanoCoins(0, 5), myAddress);
|
||||
StoredBlock b2 = createFakeBlock(params, blockStore, tx1).storedBlock;
|
||||
wallet.receive(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
wallet.receiveFromBlock(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN);
|
||||
// Check we got them back in order.
|
||||
List<Transaction> transactions = wallet.getTransactionsByTime();
|
||||
assertEquals(tx2, transactions.get(0));
|
||||
@@ -307,7 +415,7 @@ public class WalletTest {
|
||||
Transaction tx3 = wallet.createSend(new ECKey().toAddress(params), Utils.toNanoCoins(0, 5));
|
||||
// Does not appear in list yet.
|
||||
assertEquals(2, wallet.getTransactionsByTime().size());
|
||||
wallet.confirmSend(tx3);
|
||||
wallet.commitTx(tx3);
|
||||
// Now it does.
|
||||
transactions = wallet.getTransactionsByTime();
|
||||
assertEquals(3, transactions.size());
|
||||
|
||||
Reference in New Issue
Block a user