diff --git a/src/main/java/org/qortal/api/resource/NamesResource.java b/src/main/java/org/qortal/api/resource/NamesResource.java index e380ab55..1b6f0bc8 100644 --- a/src/main/java/org/qortal/api/resource/NamesResource.java +++ b/src/main/java/org/qortal/api/resource/NamesResource.java @@ -26,6 +26,7 @@ import org.qortal.api.ApiErrors; import org.qortal.api.ApiException; import org.qortal.api.ApiExceptionFactory; import org.qortal.api.model.NameSummary; +import org.qortal.controller.LiteNode; import org.qortal.crypto.Crypto; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.BuyNameTransactionData; @@ -101,7 +102,14 @@ public class NamesResource { throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); try (final Repository repository = RepositoryManager.getRepository()) { - List names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse); + List names; + + if (Settings.getInstance().isLite()) { + names = LiteNode.getInstance().fetchAccountNames(address); + } + else { + names = repository.getNameRepository().getNamesByOwner(address, limit, offset, reverse); + } return names.stream().map(NameSummary::new).collect(Collectors.toList()); } catch (DataException e) { diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java index e73eecbd..a3e2befb 100644 --- a/src/main/java/org/qortal/controller/Controller.java +++ b/src/main/java/org/qortal/controller/Controller.java @@ -43,6 +43,7 @@ import org.qortal.data.account.AccountBalanceData; import org.qortal.data.account.AccountData; import org.qortal.data.block.BlockData; import org.qortal.data.block.BlockSummaryData; +import org.qortal.data.naming.NameData; import org.qortal.data.network.PeerChainTipData; import org.qortal.data.network.PeerData; import org.qortal.data.transaction.ChatTransactionData; @@ -202,6 +203,15 @@ public class Controller extends Thread { } public GetAccountBalanceMessageStats getAccountBalanceMessageStats = new GetAccountBalanceMessageStats(); + public static class GetAccountNamesMessageStats { + public AtomicLong requests = new AtomicLong(); + public AtomicLong unknownAccounts = new AtomicLong(); + + public GetAccountNamesMessageStats() { + } + } + public GetAccountNamesMessageStats getAccountNamesMessageStats = new GetAccountNamesMessageStats(); + public AtomicLong latestBlocksCacheRefills = new AtomicLong(); public StatsSnapshot() { @@ -1264,6 +1274,10 @@ public class Controller extends Thread { onNetworkGetAccountBalanceMessage(peer, message); break; + case GET_ACCOUNT_NAMES: + onNetworkGetAccountNamesMessage(peer, message); + break; + default: LOGGER.debug(() -> String.format("Unhandled %s message [ID %d] from peer %s", message.getType().name(), message.getId(), peer)); break; @@ -1586,6 +1600,41 @@ public class Controller extends Thread { } } + private void onNetworkGetAccountNamesMessage(Peer peer, Message message) { + GetAccountNamesMessage getAccountNamesMessage = (GetAccountNamesMessage) message; + String address = getAccountNamesMessage.getAddress(); + this.stats.getAccountNamesMessageStats.requests.incrementAndGet(); + + try (final Repository repository = RepositoryManager.getRepository()) { + List namesDataList = repository.getNameRepository().getNamesByOwner(address); + + if (namesDataList == null) { + // We don't have this account + this.stats.getAccountNamesMessageStats.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_NAMES 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; + } + + NamesMessage namesMessage = new NamesMessage(namesDataList); + namesMessage.setId(message.getId()); + + if (!peer.sendMessage(namesMessage)) { + peer.disconnect("failed to send account names"); + } + + } catch (DataException e) { + LOGGER.error(String.format("Repository issue while send names for account %s to peer %s", address, peer), e); + } + } + // Utilities diff --git a/src/main/java/org/qortal/controller/LiteNode.java b/src/main/java/org/qortal/controller/LiteNode.java index c5e6affe..8d36b414 100644 --- a/src/main/java/org/qortal/controller/LiteNode.java +++ b/src/main/java/org/qortal/controller/LiteNode.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.LogManager; 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.network.Network; import org.qortal.network.Peer; import org.qortal.network.message.*; @@ -65,6 +66,20 @@ public class LiteNode { return accountMessage.getAccountBalanceData(); } + /** + * Fetch list of names for given QORT address + * @param address - the QORT address to query + * @return a list of NameData objects, or null if not retrieved + */ + public List fetchAccountNames(String address) { + GetAccountNamesMessage getAccountNamesMessage = new GetAccountNamesMessage(address); + NamesMessage namesMessage = (NamesMessage) this.sendMessage(getAccountNamesMessage, NAMES); + if (namesMessage == null) { + return null; + } + return namesMessage.getNameDataList(); + } + private Message sendMessage(Message message, MessageType expectedResponseMessageType) { // This asks a random peer for the data diff --git a/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java new file mode 100644 index 00000000..b95c7eb0 --- /dev/null +++ b/src/main/java/org/qortal/network/message/GetAccountNamesMessage.java @@ -0,0 +1,55 @@ +package org.qortal.network.message; + +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 GetAccountNamesMessage extends Message { + + private static final int ADDRESS_LENGTH = Transformer.ADDRESS_LENGTH; + + private String address; + + public GetAccountNamesMessage(String address) { + this(-1, address); + } + + private GetAccountNamesMessage(int id, String address) { + super(id, MessageType.GET_ACCOUNT_NAMES); + + this.address = address; + } + + public String getAddress() { + return this.address; + } + + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + byte[] addressBytes = new byte[ADDRESS_LENGTH]; + bytes.get(addressBytes); + String address = Base58.encode(addressBytes); + + return new GetAccountNamesMessage(id, address); + } + + @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); + + 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 a4667df7..f01ac8c3 100644 --- a/src/main/java/org/qortal/network/message/Message.java +++ b/src/main/java/org/qortal/network/message/Message.java @@ -101,10 +101,15 @@ public abstract class Message { ARBITRARY_METADATA(150), GET_ARBITRARY_METADATA(151), + // Lite node support ACCOUNT(160), GET_ACCOUNT(161), - ACCOUNT_BALANCE(162), - GET_ACCOUNT_BALANCE(163); + + ACCOUNT_BALANCE(170), + GET_ACCOUNT_BALANCE(171), + + NAMES(180), + GET_ACCOUNT_NAMES(181); public final int value; public final Method fromByteBufferMethod; diff --git a/src/main/java/org/qortal/network/message/NamesMessage.java b/src/main/java/org/qortal/network/message/NamesMessage.java new file mode 100644 index 00000000..d8f0b857 --- /dev/null +++ b/src/main/java/org/qortal/network/message/NamesMessage.java @@ -0,0 +1,149 @@ +package org.qortal.network.message; + +import com.google.common.primitives.Ints; +import com.google.common.primitives.Longs; +import org.qortal.data.naming.NameData; +import org.qortal.naming.Name; +import org.qortal.transform.TransformationException; +import org.qortal.transform.Transformer; +import org.qortal.utils.Serialization; + +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 NamesMessage extends Message { + + private static final int SIGNATURE_LENGTH = Transformer.SIGNATURE_LENGTH; + + private final List nameDataList; + + public NamesMessage(List nameDataList) { + super(MessageType.NAMES); + + this.nameDataList = nameDataList; + } + + public NamesMessage(int id, List nameDataList) { + super(id, MessageType.NAMES); + + this.nameDataList = nameDataList; + } + + public List getNameDataList() { + return this.nameDataList; + } + + + public static Message fromByteBuffer(int id, ByteBuffer bytes) throws UnsupportedEncodingException { + try { + final int nameCount = bytes.getInt(); + + List nameDataList = new ArrayList<>(nameCount); + + for (int i = 0; i < nameCount; ++i) { + String name = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); + + String reducedName = Serialization.deserializeSizedStringV2(bytes, Name.MAX_NAME_SIZE); + + String owner = Serialization.deserializeAddress(bytes); + + String data = Serialization.deserializeSizedStringV2(bytes, Name.MAX_DATA_SIZE); + + long registered = bytes.getLong(); + + int wasUpdated = bytes.getInt(); + + Long updated = null; + if (wasUpdated == 1) { + updated = bytes.getLong(); + } + + boolean isForSale = (bytes.getInt() == 1); + + Long salePrice = null; + if (isForSale) { + salePrice = bytes.getLong(); + } + + byte[] reference = new byte[SIGNATURE_LENGTH]; + bytes.get(reference); + + int creationGroupId = bytes.getInt(); + + NameData nameData = new NameData(name, reducedName, owner, data, registered, updated, + isForSale, salePrice, reference, creationGroupId); + nameDataList.add(nameData); + } + + if (bytes.hasRemaining()) { + return null; + } + + return new NamesMessage(id, nameDataList); + + } catch (TransformationException e) { + return null; + } + } + + @Override + protected byte[] toData() { + if (this.nameDataList == null) { + return null; + } + + try { + ByteArrayOutputStream bytes = new ByteArrayOutputStream(); + + bytes.write(Ints.toByteArray(this.nameDataList.size())); + + for (int i = 0; i < this.nameDataList.size(); ++i) { + NameData nameData = this.nameDataList.get(i); + + Serialization.serializeSizedStringV2(bytes, nameData.getName()); + + Serialization.serializeSizedStringV2(bytes, nameData.getReducedName()); + + Serialization.serializeAddress(bytes, nameData.getOwner()); + + Serialization.serializeSizedStringV2(bytes, nameData.getData()); + + bytes.write(Longs.toByteArray(nameData.getRegistered())); + + Long updated = nameData.getUpdated(); + int wasUpdated = (updated != null) ? 1 : 0; + bytes.write(Ints.toByteArray(wasUpdated)); + + if (updated != null) { + bytes.write(Longs.toByteArray(nameData.getUpdated())); + } + + int isForSale = nameData.isForSale() ? 1 : 0; + bytes.write(Ints.toByteArray(isForSale)); + + if (nameData.isForSale()) { + bytes.write(Longs.toByteArray(nameData.getSalePrice())); + } + + bytes.write(nameData.getReference()); + + bytes.write(Ints.toByteArray(nameData.getCreationGroupId())); + } + + return bytes.toByteArray(); + } catch (IOException e) { + return null; + } + } + + public NamesMessage cloneWithNewId(int newId) { + NamesMessage clone = new NamesMessage(this.nameDataList); + clone.setId(newId); + return clone; + } + +}