WIP: trade-bot: added WS for streaming existing/new trades in OFFERING state

This commit is contained in:
catbref 2020-08-03 17:57:22 +01:00
parent a351756883
commit f90bd6ee45
7 changed files with 202 additions and 35 deletions

View File

@ -43,6 +43,7 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket;
import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings;
public class ApiService {
@ -197,6 +198,7 @@ public class ApiService {
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
// Start server
this.server.start();

View File

@ -0,0 +1,72 @@
package org.qortal.api.model;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.crosschain.CrossChainTradeData;
import io.swagger.v3.oas.annotations.media.Schema;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
public class CrossChainOfferSummary {
// Properties
@Schema(description = "AT's Qortal address")
public String qortalAtAddress;
@Schema(description = "AT creator's Qortal address")
public String qortalCreator;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long qortAmount;
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
private long btcAmount;
@Schema(description = "Suggested trade timeout (minutes)", example = "10080")
private int tradeTimeout;
private BTCACCT.Mode mode;
protected CrossChainOfferSummary() {
/* For JAXB */
}
public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData) {
this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
this.qortalCreator = crossChainTradeData.qortalCreator;
this.qortAmount = crossChainTradeData.qortAmount;
this.btcAmount = crossChainTradeData.expectedBitcoin;
this.tradeTimeout = crossChainTradeData.tradeTimeout;
this.mode = crossChainTradeData.mode;
}
public String getQortalAtAddress() {
return this.qortalAtAddress;
}
public String getQortalCreator() {
return this.qortalCreator;
}
public long getQortAmount() {
return this.qortAmount;
}
public long getBtcAmount() {
return this.btcAmount;
}
public int getTradeTimeout() {
return this.tradeTimeout;
}
public BTCACCT.Mode getMode() {
return this.mode;
}
}

View File

@ -1126,10 +1126,15 @@ public class CrossChainResource {
if (limit != null && limit > 100)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
byte[] codeHash = BTCACCT.CODE_BYTES_HASH;
final Boolean isFinished = Boolean.TRUE;
final Integer minimumFinalHeight = null;
try (final Repository repository = RepositoryManager.getRepository()) {
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value, limit, offset, reverse);
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished,
BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value,
minimumFinalHeight,
limit, offset, reverse);
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
for (ATStateData atState : atStates) {

View File

@ -0,0 +1,100 @@
package org.qortal.api.websocket;
import java.io.IOException;
import java.io.StringWriter;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
import org.eclipse.jetty.websocket.api.Session;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
import org.eclipse.jetty.websocket.api.annotations.WebSocket;
import org.eclipse.jetty.websocket.servlet.WebSocketServlet;
import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
import org.qortal.api.model.CrossChainOfferSummary;
import org.qortal.controller.BlockNotifier;
import org.qortal.crosschain.BTCACCT;
import org.qortal.data.at.ATStateData;
import org.qortal.data.block.BlockData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
@WebSocket
@SuppressWarnings("serial")
public class TradeOffersWebSocket extends WebSocketServlet implements ApiWebSocket {
@Override
public void configure(WebSocketServletFactory factory) {
factory.register(TradeOffersWebSocket.class);
}
@OnWebSocketConnect
public void onWebSocketConnect(Session session) {
BlockNotifier.Listener listener = blockData -> onNotify(session, blockData);
BlockNotifier.getInstance().register(session, listener);
this.onNotify(session, null);
}
@OnWebSocketClose
public void onWebSocketClose(Session session, int statusCode, String reason) {
BlockNotifier.getInstance().deregister(session);
}
@OnWebSocketMessage
public void onWebSocketMessage(Session session, String message) {
}
private void onNotify(Session session, BlockData blockData) {
List<CrossChainTradeData> crossChainTradeDataList = new ArrayList<>();
try (final Repository repository = RepositoryManager.getRepository()) {
Integer minimumFinalHeight;
if (blockData == null) {
// If blockData is null then we send all known trade offers
minimumFinalHeight = null;
} else {
// Find any new trade ATs since this block
minimumFinalHeight = blockData.getHeight();
}
final Boolean isFinished = Boolean.FALSE;
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(BTCACCT.CODE_BYTES_HASH,
isFinished,
BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.OFFERING.value,
minimumFinalHeight,
null, null, null);
// Don't send anything if no results and this isn't initial on-connection message
if (atStates == null || (atStates.isEmpty() && blockData != null))
return;
for (ATStateData atState : atStates) {
CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState);
crossChainTradeDataList.add(crossChainTradeData);
}
} catch (DataException e) {
// No output this time?
return;
}
try {
List<CrossChainOfferSummary> crossChainOffers = crossChainTradeDataList.stream().map(crossChainTradeData -> new CrossChainOfferSummary(crossChainTradeData)).collect(Collectors.toList());
StringWriter stringWriter = new StringWriter();
this.marshall(stringWriter, crossChainOffers);
String output = stringWriter.toString();
session.getRemote().sendString(output);
} catch (IOException e) {
// No output this time?
}
}
}

View File

@ -2,10 +2,8 @@ package org.qortal.data.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
import org.qortal.crosschain.BTC;
import org.qortal.crosschain.BTCACCT;
import io.swagger.v3.oas.annotations.media.Schema;
@ -55,7 +53,7 @@ public class CrossChainTradeData {
public Long tradeModeTimestamp;
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
public int refundTimeout;
public Integer refundTimeout;
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
public Integer tradeRefundHeight;
@ -81,24 +79,4 @@ public class CrossChainTradeData {
public CrossChainTradeData() {
}
// We can represent BitcoinPKH as an address
@XmlElement(name = "creatorBitcoinAddress")
@Schema(description = "AT creator's trading Bitcoin PKH in address form")
public String getCreatorBitcoinAddress() {
if (this.creatorBitcoinPKH == null)
return null;
return BTC.getInstance().pkhToAddress(this.creatorBitcoinPKH);
}
// We can represent BitcoinPKH as an address
@XmlElement(name = "recipientBitcoinAddress")
@Schema(description = "Trade partner's trading Bitcoin PKH in address form")
public String getRecipientBitcoinAddress() {
if (this.partnerBitcoinPKH == null)
return null;
return BTC.getInstance().pkhToAddress(this.partnerBitcoinPKH);
}
}

View File

@ -71,8 +71,8 @@ public interface ATRepository {
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
* the data segment comparison is done via unsigned hex string.
*/
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash,
Integer dataByteOffset, Long expectedValue,
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**

View File

@ -288,8 +288,8 @@ public class HSQLDBATRepository implements ATRepository {
}
@Override
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash,
Integer dataByteOffset, Long expectedValue,
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
Integer limit, Integer offset, Boolean reverse) throws DataException {
StringBuilder sql = new StringBuilder(1024);
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
@ -301,9 +301,15 @@ public class HSQLDBATRepository implements ATRepository {
+ "ORDER BY height DESC "
+ "LIMIT 1"
+ ") AS FinalATStates "
+ "WHERE code_hash = ? AND is_finished ");
+ "WHERE code_hash = ? ");
Object[] bindParams;
List<Object> bindParams = new ArrayList<>();
bindParams.add(codeHash);
if (isFinished != null) {
sql.append("AND is_finished = ?");
bindParams.add(isFinished);
}
if (dataByteOffset != null && expectedValue != null) {
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
@ -312,9 +318,13 @@ public class HSQLDBATRepository implements ATRepository {
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
// SQL binary data offsets start at 1
bindParams = new Object[] { codeHash, dataByteOffset + 1, expectedHexValue };
} else {
bindParams = new Object[] { codeHash };
bindParams.add(dataByteOffset + 1);
bindParams.add(expectedHexValue);
}
if (minimumFinalHeight != null) {
sql.append("AND height >= ");
sql.append(minimumFinalHeight);
}
sql.append(" ORDER BY height ");
@ -325,7 +335,7 @@ public class HSQLDBATRepository implements ATRepository {
List<ATStateData> atStates = new ArrayList<>();
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams)) {
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
if (resultSet == null)
return atStates;