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.AdminStatusWebSocket;
|
||||||
import org.qortal.api.websocket.BlocksWebSocket;
|
import org.qortal.api.websocket.BlocksWebSocket;
|
||||||
import org.qortal.api.websocket.ChatMessagesWebSocket;
|
import org.qortal.api.websocket.ChatMessagesWebSocket;
|
||||||
|
import org.qortal.api.websocket.TradeOffersWebSocket;
|
||||||
import org.qortal.settings.Settings;
|
import org.qortal.settings.Settings;
|
||||||
|
|
||||||
public class ApiService {
|
public class ApiService {
|
||||||
@ -197,6 +198,7 @@ public class ApiService {
|
|||||||
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
|
||||||
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
|
||||||
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
|
||||||
|
context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
this.server.start();
|
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)
|
if (limit != null && limit > 100)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
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()) {
|
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<>();
|
List<CrossChainTradeSummary> crossChainTrades = new ArrayList<>();
|
||||||
for (ATStateData atState : atStates) {
|
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.XmlAccessType;
|
||||||
import javax.xml.bind.annotation.XmlAccessorType;
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
import javax.xml.bind.annotation.XmlElement;
|
|
||||||
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
import org.qortal.crosschain.BTC;
|
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -55,7 +53,7 @@ public class CrossChainTradeData {
|
|||||||
public Long tradeModeTimestamp;
|
public Long tradeModeTimestamp;
|
||||||
|
|
||||||
@Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)")
|
@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)")
|
@Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)")
|
||||||
public Integer tradeRefundHeight;
|
public Integer tradeRefundHeight;
|
||||||
@ -81,24 +79,4 @@ public class CrossChainTradeData {
|
|||||||
public 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,
|
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
||||||
* the data segment comparison is done via unsigned hex string.
|
* the data segment comparison is done via unsigned hex string.
|
||||||
*/
|
*/
|
||||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash,
|
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||||
Integer dataByteOffset, Long expectedValue,
|
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -288,8 +288,8 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash,
|
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, Boolean isFinished,
|
||||||
Integer dataByteOffset, Long expectedValue,
|
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
StringBuilder sql = new StringBuilder(1024);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
sql.append("SELECT AT_address, height, created_when, state_data, state_hash, fees, is_initial "
|
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 "
|
+ "ORDER BY height DESC "
|
||||||
+ "LIMIT 1"
|
+ "LIMIT 1"
|
||||||
+ ") AS FinalATStates "
|
+ ") 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) {
|
if (dataByteOffset != null && expectedValue != null) {
|
||||||
sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? ");
|
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
|
String expectedHexValue = String.format("%016x", expectedValue); // left-zero-padding and conversion
|
||||||
|
|
||||||
// SQL binary data offsets start at 1
|
// SQL binary data offsets start at 1
|
||||||
bindParams = new Object[] { codeHash, dataByteOffset + 1, expectedHexValue };
|
bindParams.add(dataByteOffset + 1);
|
||||||
} else {
|
bindParams.add(expectedHexValue);
|
||||||
bindParams = new Object[] { codeHash };
|
}
|
||||||
|
|
||||||
|
if (minimumFinalHeight != null) {
|
||||||
|
sql.append("AND height >= ");
|
||||||
|
sql.append(minimumFinalHeight);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" ORDER BY height ");
|
sql.append(" ORDER BY height ");
|
||||||
@ -325,7 +335,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
|
|
||||||
List<ATStateData> atStates = new ArrayList<>();
|
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)
|
if (resultSet == null)
|
||||||
return atStates;
|
return atStates;
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user