diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index b6297fe2..02dddcc9 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -32,6 +32,7 @@ import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; import org.qortal.api.ApiService; import org.qortal.api.DomainMapService; import org.qortal.api.GatewayService; +import org.qortal.api.resource.TransactionsResource; import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.BlockTimingByHeight; @@ -200,6 +201,15 @@ public class Controller extends Thread { } public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public static class GetAccountTransactionsMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + + public GetAccountTransactionsMessageStats() { + } + } + public GetAccountTransactionsMessageStats getAccountTransactionsMessageStats = new GetAccountTransactionsMessageStats(); + public static class GetAccountNamesMessageStats { public AtomicLong requests = new AtomicLong(); public AtomicLong unknownAccounts = new AtomicLong(); @@ -1280,6 +1290,10 @@ public class Controller extends Thread { onNetworkGetAccountBalanceMessage(peer, message); break; + case GET_ACCOUNT_TRANSACTIONS: + onNetworkGetAccountTransactionsMessage(peer, message); + break; + case GET_ACCOUNT_NAMES: onNetworkGetAccountNamesMessage(peer, message); break; @@ -1610,6 +1624,50 @@ public class Controller extends Thread { } } + private void onNetworkGetAccountTransactionsMessage(Peer peer, Message message) { + GetAccountTransactionsMessage getAccountTransactionsMessage = (GetAccountTransactionsMessage) message; + String address = getAccountTransactionsMessage.getAddress(); + int limit = Math.min(getAccountTransactionsMessage.getLimit(), 100); + int offset = getAccountTransactionsMessage.getOffset(); + this.stats.getAccountTransactionsMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + List signatures = repository.getTransactionRepository().getSignaturesMatchingCriteria(null, null, null, + null, null, null, address, TransactionsResource.ConfirmationStatus.CONFIRMED, limit, offset, false); + + // Expand signatures to transactions + List transactions = new ArrayList<>(signatures.size()); + for (byte[] signature : signatures) { + transactions.add(repository.getTransactionRepository().fromSignature(signature)); + } + + if (transactions == null) { + // We don't have this account + this.stats.getAccountTransactionsMessageStats.unknownAccounts.getAndIncrement(); + + // Send valid, yet unexpected message type in response, so peer doesn't have to wait for timeout + LOGGER.debug(() -> String.format("Sending 'account unknown' response to peer %s for GET_ACCOUNT_TRANSACTIONS request for unknown account %s", peer, address)); + + // We'll send empty block summaries message as it's very short + Message accountUnknownMessage = new BlockSummariesMessage(Collections.emptyList()); + accountUnknownMessage.setId(message.getId()); + if (!peer.sendMessage(accountUnknownMessage)) + peer.disconnect("failed to send account-unknown response"); + return; + } + + TransactionsMessage transactionsMessage = new TransactionsMessage(transactions); + transactionsMessage.setId(message.getId()); + + if (!peer.sendMessage(transactionsMessage)) { + peer.disconnect("failed to send account transactions"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send transactions for account %s %d to peer %s", address, peer), e); + } + } + private void onNetworkGetAccountNamesMessage(Peer peer, Message message) { GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message; String address = getAccountNamesMessage.getAddress(); diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index b047d295..535142b0 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -5,6 +5,7 @@ import org.apache.logging.log4j.Logger; import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.naming.NameData; +import org.qortal.data.transaction.TransactionData; import org.qortal.network.Network; import org.qortal.network.Peer; import org.qortal.network.message.*; @@ -24,6 +25,8 @@ public class LiteNode { public Map pendingRequests = Collections.synchronizedMap(new HashMap<>()); + public int MAX_TRANSACTIONS_PER_MESSAGE = 100; + public LiteNode() { @@ -66,6 +69,37 @@ public class LiteNode { return accountMessage.getAccountBalanceData(); } + /** + * Fetch list of transactions for given QORT address + * @param address - the QORT address to query + * @param limit - the maximum number of results to return + * @param offset - the starting index + * @return a list of TransactionData objects, or null if not retrieved + */ + public List fetchAccountTransactions(String address, int limit, int offset) { + List allTransactions = new ArrayList<>(); + if (limit == 0) { + limit = Integer.MAX_VALUE; + } + int batchSize = Math.min(limit, MAX_TRANSACTIONS_PER_MESSAGE); + + while (allTransactions.size() < limit) { + GetAccountTransactionsMessage getAccountTransactionsMessage = new GetAccountTransactionsMessage(address, batchSize, offset); + TransactionsMessage transactionsMessage = (TransactionsMessage) this.sendMessage(getAccountTransactionsMessage, TRANSACTIONS); + if (transactionsMessage == null) { + // An error occurred, so give up instead of returning partial results + return null; + } + allTransactions.addAll(transactionsMessage.getTransactions()); + if (transactionsMessage.getTransactions().size() < batchSize) { + // No more transactions to fetch + break; + } + offset += batchSize; + } + return allTransactions; + } + /** * Fetch list of names for given QORT address * @param address - the QORT address to query diff --git a/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java new file mode 100644 index 00000000..3ca802b7 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountTransactionsMessage.java @@ -0,0 +1,71 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.transform.Transformer; +import org.qortal.utils.Base58; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; + +public class GetAccountTransactionsMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + private int limit; + private int offset; + + public GetAccountTransactionsMessage(String address, int limit, int offset) { + this(-1, address, limit, offset); + } + + private GetAccountTransactionsMessage(int id, String address, int limit, int offset) { + super(id, MessageType.GET_ACCOUNT_TRANSACTIONS); + + this.address = address; + this.limit = limit; + this.offset = offset; + } + + public String getAddress() { + return this.address; + } + + public int getLimit() { return this.limit; } + + public int getOffset() { return this.offset; } + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + int limit = bytes.getInt(); + + int offset = bytes.getInt(); + + return new GetAccountTransactionsMessage(id, address, limit, offset); + } + + @Override + protected byte[] toData() { + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + // Send raw address instead of base58 encoded + byte[] address = Base58.decode(this.address); + bytes.write(address); + + bytes.write(Ints.toByteArray(this.limit)); + + bytes.write(Ints.toByteArray(this.offset)); + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + +} diff --git a/src/main/java/org/qortal/network/message/Message.java b/src/main/java/org/qortal/network/message/Message.java index c675cd96..747988be 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -110,7 +110,10 @@ public abstract class Message { NAMES(180), GET_ACCOUNT_NAMES(181), - GET_NAME(182); + GET_NAME(182), + + TRANSACTIONS(190), + GET_ACCOUNT_TRANSACTIONS(191); public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/TransactionsMessage.java b/src/main/java/org/qortal/network/message/TransactionsMessage.java new file mode 100644 index 00000000..8a8cf11e --- /dev/null +++ b/src/main/java/org/qortal/network/message/TransactionsMessage.java @@ -0,0 +1,77 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import org.qortal.data.transaction.TransactionData; +import org.qortal.transform.TransformationException; +import org.qortal.transform.transaction.TransactionTransformer; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.nio.ByteBuffer; +import java.util.ArrayList; +import java.util.List; + +public class TransactionsMessage extends Message { + + private List transactions; + + public TransactionsMessage(List transactions) { + this(-1, transactions); + } + + private TransactionsMessage(int id, List transactions) { + super(id, MessageType.TRANSACTIONS); + + this.transactions = transactions; + } + + public List getTransactions() { + return this.transactions; + } + + public static Message fromByteBuffer(int id, ByteBuffer byteBuffer) throws UnsupportedEncodingException { + try { + final int transactionCount = byteBuffer.getInt(); + + List transactions = new ArrayList<>(); + + for (int i = 0; i < transactionCount; ++i) { + TransactionData transactionData = TransactionTransformer.fromByteBuffer(byteBuffer); + transactions.add(transactionData); + } + + if (byteBuffer.hasRemaining()) { + return null; + } + + return new TransactionsMessage(id, transactions); + } catch (TransformationException e) { + return null; + } + } + + @Override + protected byte[] toData() { + if (this.transactions == null) + return null; + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(this.transactions.size())); + + for (int i = 0; i < this.transactions.size(); ++i) { + TransactionData transactionData = this.transactions.get(i); + + byte[] serializedTransactionData = TransactionTransformer.toBytes(transactionData); + bytes.write(serializedTransactionData); + } + + return bytes.toByteArray(); + } catch (TransformationException | IOException e) { + return null; + } + } + +}