From cac68ccc14ff0abeb8967a0d3b24f686c17f59af Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 5 Aug 2020 13:23:24 +0100 Subject: [PATCH] Added trade-bot websocket --- src/main/java/org/qortal/api/ApiService.java | 2 + .../api/websocket/TradeBotWebSocket.java | 119 ++++++++++++++++++ .../java/org/qortal/controller/TradeBot.java | 42 +++++++ 3 files changed, 163 insertions(+) create mode 100644 src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java index 97b42960..9b230601 100644 --- a/src/main/java/org/qortal/api/ApiService.java +++ b/src/main/java/org/qortal/api/ApiService.java @@ -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.TradeBotWebSocket; import org.qortal.api.websocket.TradeOffersWebSocket; import org.qortal.settings.Settings; @@ -199,6 +200,7 @@ public class ApiService { context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*"); context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages"); context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers"); + context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot"); // Start server this.server.start(); diff --git a/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java new file mode 100644 index 00000000..e97e54bc --- /dev/null +++ b/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java @@ -0,0 +1,119 @@ +package org.qortal.api.websocket; + +import java.io.IOException; +import java.io.StringWriter; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +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.WebSocketServletFactory; +import org.qortal.controller.TradeBot; +import org.qortal.data.crosschain.TradeBotData; +import org.qortal.event.Event; +import org.qortal.event.EventBus; +import org.qortal.event.Listener; +import org.qortal.repository.DataException; +import org.qortal.repository.Repository; +import org.qortal.repository.RepositoryManager; +import org.qortal.utils.Base58; + +@WebSocket +@SuppressWarnings("serial") +public class TradeBotWebSocket extends ApiWebSocket implements Listener { + + /** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */ + private static final Map PREVIOUS_STATES = new HashMap<>(); + + @Override + public void configure(WebSocketServletFactory factory) { + factory.register(TradeBotWebSocket.class); + + try (final Repository repository = RepositoryManager.getRepository()) { + List tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData(); + if (tradeBotEntries == null) + // How do we properly fail here? + return; + + PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getState))); + } catch (DataException e) { + // No output this time + } + + EventBus.INSTANCE.addListener(this::listen); + } + + @Override + public void listen(Event event) { + if (!(event instanceof TradeBot.StateChangeEvent)) + return; + + TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData(); + String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey()); + + synchronized (PREVIOUS_STATES) { + if (PREVIOUS_STATES.get(tradePrivateKey58) == tradeBotData.getState()) + // Not changed + return; + + PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getState()); + } + + List tradeBotEntries = Collections.singletonList(tradeBotData); + for (Session session : getSessions()) + sendEntries(session, tradeBotEntries); + } + + @OnWebSocketConnect + public void onWebSocketConnect(Session session) { + // Send all known trade-bot entries + try (final Repository repository = RepositoryManager.getRepository()) { + List tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData(); + if (tradeBotEntries == null) { + session.close(4001, "repository issue fetching trade-bot entries"); + return; + } + + if (!sendEntries(session, tradeBotEntries)) { + session.close(4002, "websocket issue"); + return; + } + } catch (DataException e) { + // No output this time + } + + super.onWebSocketConnect(session); + } + + @OnWebSocketClose + public void onWebSocketClose(Session session, int statusCode, String reason) { + super.onWebSocketClose(session, statusCode, reason); + } + + @OnWebSocketMessage + public void onWebSocketMessage(Session session, String message) { + /* ignored */ + } + + private boolean sendEntries(Session session, List tradeBotEntries) { + try { + StringWriter stringWriter = new StringWriter(); + marshall(stringWriter, tradeBotEntries); + + String output = stringWriter.toString(); + session.getRemote().sendStringByFuture(output); + } catch (IOException e) { + // No output this time? + return false; + } + + return true; + } + +} diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 0c22cb48..74bec0bc 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -30,6 +30,8 @@ import org.qortal.data.crosschain.TradeBotData; import org.qortal.data.transaction.BaseTransactionData; import org.qortal.data.transaction.DeployAtTransactionData; import org.qortal.data.transaction.MessageTransactionData; +import org.qortal.event.Event; +import org.qortal.event.EventBus; import org.qortal.group.Group; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -47,6 +49,18 @@ public class TradeBot { public enum ResponseResult { OK, INSUFFICIENT_FUNDS, BTC_BALANCE_ISSUE, BTC_NETWORK_ISSUE } + public static class StateChangeEvent implements Event { + private final TradeBotData tradeBotData; + + public StateChangeEvent(TradeBotData tradeBotData) { + this.tradeBotData = tradeBotData; + } + + public TradeBotData getTradeBotData() { + return this.tradeBotData; + } + } + private static final Logger LOGGER = LogManager.getLogger(TradeBot.class); private static final Random RANDOM = new SecureRandom(); private static final long FEE_AMOUNT = 1000L; @@ -158,6 +172,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("Built AT %s. Waiting for deployment", atAddress)); + notifyStateChange(tradeBotData); // Return to user for signing and broadcast as we don't have their Qortal private key try { @@ -258,6 +273,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("Funding P2SH-A %s. Waiting for confirmation", p2shAddress)); + notifyStateChange(tradeBotData); return ResponseResult.OK; } @@ -368,6 +384,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); } /** @@ -405,6 +422,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddress)); + notifyStateChange(tradeBotData); return; } @@ -443,6 +461,7 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-A %s funding confirmed. Messaged %s. Waiting for AT %s to lock to us", p2shAddress, crossChainTradeData.qortalCreatorTradeAddress, tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); } /** @@ -478,6 +497,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); return; } @@ -550,6 +570,7 @@ public class TradeBot { String p2shBAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); LOGGER.info(() -> String.format("Locked AT %s to %s. Waiting for P2SH-B %s", tradeBotData.getAtAddress(), aliceNativeAddress, p2shBAddress)); + notifyStateChange(tradeBotData); return; } @@ -558,6 +579,7 @@ public class TradeBot { if (tradeBotData.getLastTransactionSignature() != originalLastTransactionSignature) { repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); + notifyStateChange(tradeBotData); } } @@ -597,6 +619,8 @@ public class TradeBot { else LOGGER.info(() -> String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddress)); + notifyStateChange(tradeBotData); + return; } @@ -622,6 +646,8 @@ public class TradeBot { repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); + notifyStateChange(tradeBotData); + return; } @@ -679,6 +705,8 @@ public class TradeBot { LOGGER.info(() -> String.format("AT %s locked to us (%s). P2SH-B %s funded. Watching P2SH-B for secret-B", tradeBotData.getAtAddress(), tradeBotData.getTradeNativeAddress(), p2shAddress)); + + notifyStateChange(tradeBotData); } /** @@ -706,6 +734,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); return; } @@ -746,6 +775,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("P2SH-B %s redeemed (exposing secret-B). Watching AT %s for secret-A", p2shAddress, tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); } /** @@ -782,6 +812,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("LockTime-B reached, refunding P2SH-B %s - aborting trade", p2shAddress)); + notifyStateChange(tradeBotData); return; } @@ -825,6 +856,8 @@ public class TradeBot { LOGGER.info(() -> String.format("P2SH-B %s redeemed, using secrets to redeem AT %s. Funds should arrive at %s", p2shAddress, tradeBotData.getAtAddress(), receivingAddress)); + + notifyStateChange(tradeBotData); } /** @@ -867,6 +900,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); + notifyStateChange(tradeBotData); return; } @@ -902,6 +936,7 @@ public class TradeBot { String receivingAddress = BTC.getInstance().pkhToAddress(receivingAccountInfo); LOGGER.info(() -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress)); + notifyStateChange(tradeBotData); } /** @@ -943,6 +978,7 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("Refunded P2SH-B %s. Waiting for LockTime-A", p2shAddress)); + notifyStateChange(tradeBotData); } /** Trade-bot is attempting to refund P2SH-A. */ @@ -987,6 +1023,12 @@ public class TradeBot { repository.saveChanges(); LOGGER.info(() -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddress)); + notifyStateChange(tradeBotData); + } + + private static void notifyStateChange(TradeBotData tradeBotData) { + StateChangeEvent stateChangeEvent = new StateChangeEvent(tradeBotData); + EventBus.INSTANCE.notify(stateChangeEvent); } }