mirror of
https://github.com/Qortal/altcoinj.git
synced 2025-07-31 20:11:23 +00:00
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:
@@ -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.
|
||||
|
@@ -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,
|
||||
|
@@ -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;
|
||||
|
@@ -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());
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user