forked from Qortal/qortal
WIP: trade-bot: added WS for streaming existing/new trades in OFFERING state
This commit is contained in:
parent
a351756883
commit
f90bd6ee45
@ -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();
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
@ -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) {
|
||||
|
100
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal file
100
src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
Normal 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?
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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;
|
||||
|
||||
/**
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user