diff --git a/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java b/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java index 06f03fb5..51b2b075 100644 --- a/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java @@ -1,5 +1,7 @@ package org.qortal.controller.tradebot; +import java.util.List; + import org.qortal.api.model.crosschain.TradeBotCreateRequest; import org.qortal.crosschain.ACCT; import org.qortal.crosschain.ForeignBlockchainException; @@ -11,7 +13,10 @@ import org.qortal.repository.Repository; public interface AcctTradeBot { - public enum ResponseResult { OK, BALANCE_ISSUE, NETWORK_ISSUE } + public enum ResponseResult { OK, BALANCE_ISSUE, NETWORK_ISSUE, TRADE_ALREADY_EXISTS } + + /** Returns list of state names for trade-bot entries that have ended, e.g. redeemed, refunded or cancelled. */ + public List getEndStates(); public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException; diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java index 6fe746bd..fe0f41c1 100644 --- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java @@ -6,6 +6,7 @@ import static java.util.stream.Collectors.toMap; import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -110,6 +111,10 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { private static BitcoinACCTv1TradeBot instance; + private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDING_B, State.ALICE_REFUNDED).stream() + .map(State::name) + .collect(Collectors.toUnmodifiableList()); + private BitcoinACCTv1TradeBot() { } @@ -120,6 +125,11 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { return instance; } + @Override + public List getEndStates() { + return this.endStates; + } + /** * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for BTC. *

diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java index ea244c27..a847b2a7 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java @@ -3,8 +3,10 @@ package org.qortal.controller.tradebot; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toMap; +import java.util.Arrays; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -102,6 +104,10 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { private static LitecoinACCTv1TradeBot instance; + private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream() + .map(State::name) + .collect(Collectors.toUnmodifiableList()); + private LitecoinACCTv1TradeBot() { } @@ -112,6 +118,11 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { return instance; } + @Override + public List getEndStates() { + return this.endStates; + } + /** * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC. *

diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index d98e8fa7..407d934a 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -176,6 +176,10 @@ public class TradeBot implements Listener { return ResponseResult.NETWORK_ISSUE; } + // Check Alice doesn't already have an existing, on-going trade-bot entry for this AT. + if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(atData.getATAddress(), acctTradeBot.getEndStates())) + return ResponseResult.TRADE_ALREADY_EXISTS; + return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress); } diff --git a/src/main/java/org/qortal/repository/CrossChainRepository.java b/src/main/java/org/qortal/repository/CrossChainRepository.java index cee1dc69..70ebdbf9 100644 --- a/src/main/java/org/qortal/repository/CrossChainRepository.java +++ b/src/main/java/org/qortal/repository/CrossChainRepository.java @@ -8,6 +8,9 @@ public interface CrossChainRepository { public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException; + /** Returns true if there is an existing trade-bot entry relating to given AT address, excluding trade-bot entries with given states. */ + public boolean existsTradeWithAtExcludingStates(String atAddress, List excludeStates) throws DataException; + public List getAllTradeBotData() throws DataException; public void save(TradeBotData tradeBotData) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index 6d962a31..29f2994c 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -3,6 +3,7 @@ package org.qortal.repository.hsqldb; import java.sql.ResultSet; import java.sql.SQLException; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.qortal.data.crosschain.TradeBotData; @@ -68,6 +69,36 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { } } + @Override + public boolean existsTradeWithAtExcludingStates(String atAddress, List excludeStates) throws DataException { + if (excludeStates == null) + excludeStates = Collections.emptyList(); + + StringBuilder whereClause = new StringBuilder(256); + whereClause.append("at_address = ?"); + + Object[] bindParams = new Object[1 + excludeStates.size()]; + bindParams[0] = atAddress; + + if (!excludeStates.isEmpty()) { + whereClause.append(" AND trade_state NOT IN (?"); + bindParams[1] = excludeStates.get(0); + + for (int i = 1; i < excludeStates.size(); ++i) { + whereClause.append(", ?"); + bindParams[1 + i] = excludeStates.get(i); + } + + whereClause.append(")"); + } + + try { + return this.repository.exists("TradeBotStates", whereClause.toString(), bindParams); + } catch (SQLException e) { + throw new DataException("Unable to check for trade-bot state in repository", e); + } + } + @Override public List getAllTradeBotData() throws DataException { String sql = "SELECT trade_private_key, acct_name, trade_state, trade_state_value, "