Implement a way of getting a list of transactions in the wallet, ordered by recency. This doesn't yet support pending transactions, as those can't (yet) be added to the wallet.

This commit is contained in:
Mike Hearn
2011-09-18 19:40:04 +00:00
parent 2ef36efcce
commit 3191d5684b
4 changed files with 123 additions and 14 deletions

View File

@@ -56,6 +56,13 @@ public class Transaction extends Message implements Serializable {
// If this transaction is not stored in the wallet, appearsIn is null.
Set<StoredBlock> appearsIn;
// Stored only in Java serialization. This is either the time the transaction was broadcast as measured from the
// local clock, or the time from the block in which it was included. Note that this can be changed by re-orgs so
// the wallet may update this field. Old serialized transactions don't have this field, thus null is valid.
// It is used for returning an ordered list of transactions from a wallet, which is helpful for presenting to
// users.
Date updatedAt;
// This is an in memory helper only.
transient Sha256Hash hash;
@@ -119,9 +126,7 @@ public class Transaction extends Message implements Serializable {
return v;
}
/**
* Calculates the sum of the outputs that are sending coins to a key in the wallet.
*/
/** Calculates the sum of the outputs that are sending coins to a key in the wallet. */
public BigInteger getValueSentToMe(Wallet wallet) {
return getValueSentToMe(wallet, true);
}
@@ -138,8 +143,14 @@ public class Transaction extends Message implements Serializable {
* 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
* block stores do not save the transaction data at all.
*
* @param block The {@link StoredBlock} in which the transaction has appeared.
* @param bestChain whether to set the updatedAt timestamp from the block header (only if not already set)
*/
void addBlockAppearance(StoredBlock block) {
void addBlockAppearance(StoredBlock block, boolean bestChain) {
if (bestChain && updatedAt == null) {
updatedAt = new Date(block.getHeader().getTimeSeconds());
}
if (appearsIn == null) {
appearsIn = new HashSet<StoredBlock>();
}
@@ -215,6 +226,32 @@ public class Transaction extends Message implements Serializable {
return true;
}
/**
* Returns the earliest time at which the transaction was seen (broadcast or included into the chain),
* or null if that information isn't available.
*/
public Date getUpdateTime() {
if (updatedAt == null) {
// Older wallets did not store this field. If we can, fill it out based on the block pointers. We might
// "guess wrong" in the case of transactions appearing on chain forks, but this is unlikely to matter in
// practice. Note, some patched copies of BitCoinJ store dates in this field that do not correspond to any
// block but rather broadcast time.
if (appearsIn == null || appearsIn.size() == 0) {
// Transaction came from somewhere that doesn't provide time info.
return null;
}
long earliestTimeSecs = Long.MAX_VALUE;
// We might return a time that is different to the best chain, as we don't know here which block is part
// of the active chain and which are simply inactive. We just ignore this for now.
// TODO: At some point we'll want to store storing full block headers in the wallet. Remove at that time.
for (StoredBlock b : appearsIn) {
earliestTimeSecs = Math.min(b.getHeader().getTimeSeconds(), earliestTimeSecs);
}
updatedAt = new Date(earliestTimeSecs);
}
return updatedAt;
}
/**
* These constants are a part of a scriptSig signature on the inputs. They define the details of how a
* transaction can be redeemed, specifically, they control how the hash of the transaction is calculated.

View File

@@ -245,7 +245,7 @@ public class Wallet implements Serializable {
// accepted by the network.
//
// Mark the tx as appearing in this block so we can find it later after a re-org.
wtx.addBlockAppearance(block);
wtx.addBlockAppearance(block, bestChain);
if (bestChain) {
if (valueSentToMe.equals(BigInteger.ZERO)) {
// There were no change transactions so this tx is fully spent.
@@ -277,7 +277,7 @@ public class Wallet implements Serializable {
} else {
if (!reorg) {
// Mark the tx as appearing in this block so we can find it later after a re-org.
tx.addBlockAppearance(block);
tx.addBlockAppearance(block, bestChain);
}
// This TX didn't originate with us. It could be sending us coins and also spending our own coins if keys
// are being shared between different wallets.
@@ -433,6 +433,44 @@ public class Wallet implements Serializable {
pending.put(tx.getHash(), tx);
}
/** Returns all transactions in the wallet ordered by recency. See {@link Wallet#getRecentTransactions(int)}. */
public List<Transaction> getTransactionsByTime() {
return getRecentTransactions(0);
}
/**
* Returns an list of N transactions, ordered by increasing age. Transactions which exist only on
* inactive side-chains are not included or which are dead (overridden by double spends) are not included.<p>
*
* Note: the current implementation is O(num transactions in wallet). Regardless of how many transactions are
* requested, the cost is always the same. In future, requesting smaller numbers of transactions may be faster
* depending on how the wallet is implemented (eg if backed by a database).
*/
public List<Transaction> getRecentTransactions(int numTransactions) {
assert numTransactions >= 0;
// Firstly, put all transactions into an array.
int size = getPoolSize(Pool.UNSPENT) + getPoolSize(Pool.SPENT) + getPoolSize(Pool.PENDING);
if (numTransactions > size || numTransactions == 0) {
numTransactions = size;
}
ArrayList<Transaction> all = new ArrayList<Transaction>(size);
all.addAll(unspent.values());
all.addAll(spent.values());
all.addAll(pending.values());
// Order by date.
Collections.sort(all, Collections.reverseOrder(new Comparator<Transaction>() {
public int compare(Transaction t1, Transaction t2) {
return t1.getUpdateTime().compareTo(t2.getUpdateTime());
}
}));
if (numTransactions == all.size()) {
return all;
} else {
all.subList(numTransactions, all.size()).clear();
return all;
}
}
// This is used only for unit testing, it's an internal API.
enum Pool {
UNSPENT,

View File

@@ -42,10 +42,10 @@ public class TestUtils {
}
// Emulates receiving a valid block that builds on top of the chain.
public static BlockPair createFakeBlock(NetworkParameters params, BlockStore blockStore,
public static BlockPair createFakeBlock(NetworkParameters params, BlockStore blockStore, long timeSeconds,
Transaction... transactions) {
try {
Block b = makeTestBlock(params, blockStore);
Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params), timeSeconds);
// Coinbase tx was already added.
for (Transaction tx : transactions)
b.addTransaction(tx);
@@ -63,20 +63,20 @@ public class TestUtils {
}
}
public static Block makeTestBlock(NetworkParameters params,
BlockStore blockStore) throws BlockStoreException {
return blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params));
public static BlockPair createFakeBlock(NetworkParameters params, BlockStore blockStore,
Transaction... transactions) {
return createFakeBlock(params, blockStore, System.currentTimeMillis() / 1000, transactions);
}
public static Block makeSolvedTestBlock(NetworkParameters params,
BlockStore blockStore) throws BlockStoreException {
BlockStore blockStore) throws BlockStoreException {
Block b = blockStore.getChainHead().getHeader().createNextBlock(new ECKey().toAddress(params));
b.solve();
return b;
}
public static Block makeSolvedTestBlock(NetworkParameters params,
Block prev) throws BlockStoreException {
Block prev) throws BlockStoreException {
Block b = prev.createNextBlock(new ECKey().toAddress(params));
b.solve();
return b;

View File

@@ -17,12 +17,12 @@
package com.google.bitcoin.core;
import com.google.bitcoin.store.BlockStore;
import com.google.bitcoin.store.BlockStoreException;
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;
@@ -272,4 +272,38 @@ public class WalletTest {
assertEquals(send1, eventDead[0]);
assertEquals(send2, eventReplacement[0]);
}
@Test
public void transactionsList() throws Exception {
// Check the wallet can give us an ordered list of all received transactions.
long time = System.currentTimeMillis() / 1000;
// Receive a coin.
Transaction tx1 = createFakeTx(params, Utils.toNanoCoins(1, 0), myAddress);
StoredBlock b1 = createFakeBlock(params, blockStore, time, tx1).storedBlock;
wallet.receive(tx1, b1, BlockChain.NewBlockType.BEST_CHAIN);
// Receive half a coin 10 minutes later.
time += 60 * 10;
Transaction tx2 = createFakeTx(params, Utils.toNanoCoins(0, 5), myAddress);
StoredBlock b2 = createFakeBlock(params, blockStore, time, tx1).storedBlock;
wallet.receive(tx2, b2, BlockChain.NewBlockType.BEST_CHAIN);
// Check we got them back in order.
List<Transaction> transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
assertEquals(tx1, transactions.get(1));
assertEquals(2, transactions.size());
// Check we get only the last transaction if we request a subrage.
transactions = wallet.getRecentTransactions(1);
assertEquals(1, transactions.size());
assertEquals(tx2, transactions.get(0));
// Verify we can handle the case of older wallets in which the timestamp is null (guessed from the
// block appearances list).
tx1.updatedAt = null;
tx2.updatedAt = null;
// Check we got them back in order.
transactions = wallet.getTransactionsByTime();
assertEquals(tx2, transactions.get(0));
assertEquals(tx1, transactions.get(1));
assertEquals(2, transactions.size());
}
}