diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java b/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java new file mode 100644 index 00000000..52ac7de3 --- /dev/null +++ b/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java @@ -0,0 +1,43 @@ +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.data.crosschain.CrossChainTradeData; + +// All properties to be converted to JSON via JAXB +@XmlAccessorType(XmlAccessType.FIELD) +public class CrossChainTradeSummary { + + private long tradeTimestamp; + + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + private long qortAmount; + + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + private long btcAmount; + + protected CrossChainTradeSummary() { + /* For JAXB */ + } + + public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) { + this.tradeTimestamp = timestamp; + this.qortAmount = crossChainTradeData.qortAmount; + this.btcAmount = crossChainTradeData.expectedBitcoin; + } + + public long getTradeTimestamp() { + return this.tradeTimestamp; + } + + public long getQortAmount() { + return this.qortAmount; + } + + public long getBtcAmount() { + return this.btcAmount; + } + +} diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index dd77ed3b..efe86f23 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -43,6 +43,7 @@ import org.qortal.api.Security; import org.qortal.api.model.CrossChainCancelRequest; import org.qortal.api.model.CrossChainSecretRequest; import org.qortal.api.model.CrossChainTradeRequest; +import org.qortal.api.model.CrossChainTradeSummary; import org.qortal.api.model.TradeBotCreateRequest; import org.qortal.api.model.TradeBotRespondRequest; import org.qortal.api.model.CrossChainBitcoinP2SHStatus; @@ -57,6 +58,7 @@ import org.qortal.crosschain.BTCACCT; import org.qortal.crosschain.BTCP2SH; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; +import org.qortal.data.at.ATStateData; import org.qortal.data.crosschain.CrossChainTradeData; import org.qortal.data.crosschain.TradeBotData; import org.qortal.data.transaction.BaseTransactionData; @@ -92,7 +94,6 @@ public class CrossChainResource { summary = "Find cross-chain trade offers", responses = { @ApiResponse( - description = "automated transactions", content = @Content( array = @ArraySchema( schema = @Schema( @@ -1099,6 +1100,54 @@ public class CrossChainResource { } } + @GET + @Path("/trades") + @Operation( + summary = "Find completed cross-chain trades", + description = "Returns summary info about successfully completed cross-chain trades", + responses = { + @ApiResponse( + content = @Content( + array = @ArraySchema( + schema = @Schema( + implementation = CrossChainTradeSummary.class + ) + ) + ) + ) + } + ) + @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE}) + public List getCompletedTrades( + @Parameter( ref = "limit") @QueryParam("limit") Integer limit, + @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset, + @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) { + // Impose a limit on 'limit' + if (limit != null && limit > 100) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + byte[] codeHash = BTCACCT.CODE_BYTES_HASH; + + try (final Repository repository = RepositoryManager.getRepository()) { + List atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value, limit, offset, reverse); + + List crossChainTrades = new ArrayList<>(); + for (ATStateData atState : atStates) { + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atState); + + // We also need block timestamp for use as trade timestamp + long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight()); + + CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp); + crossChainTrades.add(crossChainTradeSummary); + } + + return crossChainTrades; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException { ATData atData = repository.getATRepository().fromATAddress(atAddress); if (atData == null) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 8c085256..8286e843 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -718,7 +718,7 @@ public class TradeBot { Coin redeemAmount = Coin.ZERO; // The real funds are in P2SH-A ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); - byte[] receivePublicKeyHash = tradeBotData.getReceivingPublicKeyHash(); + byte[] receivePublicKeyHash = tradeBotData.getReceivingAccountInfo(); Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, tradeBotData.getSecret(), receivePublicKeyHash); @@ -787,7 +787,7 @@ public class TradeBot { // Send 'redeem' MESSAGE to AT using both secrets byte[] secretA = tradeBotData.getSecret(); - String qortalReceiveAddress = Base58.encode(tradeBotData.getReceivingPublicKeyHash()); // Actually contains whole address, not just PKH + String qortalReceiveAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH byte[] messageData = BTCACCT.buildRedeemMessage(secretA, secretB, qortalReceiveAddress); PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); @@ -873,7 +873,7 @@ public class TradeBot { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedBitcoin); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); List fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress); - byte[] receivePublicKeyHash = tradeBotData.getReceivingPublicKeyHash(); + byte[] receivePublicKeyHash = tradeBotData.getReceivingAccountInfo(); Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivePublicKeyHash); diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 8e3312c6..8f9948d9 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -108,6 +108,11 @@ public class BTCACCT { public static final int MIN_LOCKTIME = 1500000000; public static final byte[] CODE_BYTES_HASH = HashCode.fromString("fad14381b77ae1a2bfe7e16a1a8b571839c5f405fca0490ead08499ac170f65b").asBytes(); // SHA256 of AT code bytes + /** Value offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */ + private static final int MODE_VALUE_OFFSET = 63; + /** Byte offset into AT state data where 'mode' variable (long) is stored. */ + public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE); + public static class OfferMessageData { public byte[] partnerBitcoinPKH; public byte[] hashOfSecretA; @@ -235,6 +240,7 @@ public class BTCACCT { addrCounter += 4; final int addrMode = addrCounter++; + assert addrMode == MODE_VALUE_OFFSET : "MODE_VALUE_OFFSET does not match addrMode"; // Data segment ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE); @@ -584,18 +590,40 @@ public class BTCACCT { * @throws DataException */ public static CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException { - String atAddress = atData.getATAddress(); + ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress()); + return populateTradeData(repository, atData.getCreatorPublicKey(), atStateData); + } - ATStateData atStateData = repository.getATRepository().getLatestATState(atAddress); - byte[] stateData = atStateData.getStateData(); + /** + * Returns CrossChainTradeData with useful info extracted from AT. + * + * @param repository + * @param atAddress + * @throws DataException + */ + public static CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException { + byte[] creatorPublicKey = repository.getATRepository().getCreatorPublicKey(atStateData.getATAddress()); + return populateTradeData(repository, creatorPublicKey, atStateData); + } + + /** + * Returns CrossChainTradeData with useful info extracted from AT. + * + * @param repository + * @param atAddress + * @throws DataException + */ + public static CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, ATStateData atStateData) throws DataException { + String atAddress = atStateData.getATAddress(); QortalAtLoggerFactory loggerFactory = QortalAtLoggerFactory.getInstance(); + byte[] stateData = atStateData.getStateData(); byte[] dataBytes = MachineState.extractDataBytes(loggerFactory, stateData); CrossChainTradeData tradeData = new CrossChainTradeData(); tradeData.qortalAtAddress = atAddress; - tradeData.qortalCreator = Crypto.toAddress(atData.getCreatorPublicKey()); - tradeData.creationTimestamp = atData.getCreation(); + tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey); + tradeData.creationTimestamp = atStateData.getCreation(); Account atAccount = new Account(repository, atAddress); tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT); diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index 87dfde21..fdfa9926 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -58,7 +58,7 @@ public class TradeBotData { private Integer lockTimeA; // Could be Bitcoin or Qortal... - private byte[] receivingPublicKeyHash; + private byte[] receivingAccountInfo; protected TradeBotData() { /* JAXB */ @@ -68,7 +68,7 @@ public class TradeBotData { byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress, byte[] secret, byte[] hashOfSecret, byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, - long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingPublicKeyHash) { + long bitcoinAmount, String xprv58, byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingAccountInfo) { this.tradePrivateKey = tradePrivateKey; this.tradeState = tradeState; this.atAddress = atAddress; @@ -83,7 +83,7 @@ public class TradeBotData { this.xprv58 = xprv58; this.lastTransactionSignature = lastTransactionSignature; this.lockTimeA = lockTimeA; - this.receivingPublicKeyHash = receivingPublicKeyHash; + this.receivingAccountInfo = receivingAccountInfo; } public byte[] getTradePrivateKey() { @@ -158,8 +158,8 @@ public class TradeBotData { this.lockTimeA = lockTimeA; } - public byte[] getReceivingPublicKeyHash() { - return this.receivingPublicKeyHash; + public byte[] getReceivingAccountInfo() { + return this.receivingAccountInfo; } } diff --git a/src/main/java/org/qortal/repository/ATRepository.java b/src/main/java/org/qortal/repository/ATRepository.java index 887672d8..05d9fb21 100644 --- a/src/main/java/org/qortal/repository/ATRepository.java +++ b/src/main/java/org/qortal/repository/ATRepository.java @@ -15,6 +15,9 @@ public interface ATRepository { /** Returns where AT with passed address exists in repository */ public boolean exists(String atAddress) throws DataException; + /** Returns AT creator's public key, or null if not found */ + public byte[] getCreatorPublicKey(String atAddress) throws DataException; + /** Returns list of executable ATs, empty if none found */ public List getAllExecutableATs() throws DataException; @@ -54,6 +57,24 @@ public interface ATRepository { */ public ATStateData getLatestATState(String atAddress) throws DataException; + /** + * Returns final ATStateData for ATs matching codeHash (required) + * and specific data segment value (optional). + *

+ * If searching for specific data segment value, both dataByteOffset + * and expectedValue need to be non-null. + *

+ * Note that dataByteOffset starts from 0 and will typically be + * a multiple of MachineState.VALUE_SIZE, which is usually 8: + * width of a long. + *

+ * Although expectedValue, if provided, is natively an unsigned long, + * the data segment comparison is done via unsigned hex string. + */ + public List getMatchingFinalATStates(byte[] codeHash, + Integer dataByteOffset, Long expectedValue, + Integer limit, Integer offset, Boolean reverse) throws DataException; + /** * Returns all ATStateData for a given block height. *

diff --git a/src/main/java/org/qortal/repository/BlockRepository.java b/src/main/java/org/qortal/repository/BlockRepository.java index 24372212..d7671f28 100644 --- a/src/main/java/org/qortal/repository/BlockRepository.java +++ b/src/main/java/org/qortal/repository/BlockRepository.java @@ -60,6 +60,15 @@ public interface BlockRepository { */ public int getHeightFromTimestamp(long timestamp) throws DataException; + /** + * Returns block timestamp for a given height. + * + * @param height + * @return timestamp, or 0 if height is out of bounds. + * @throws DataException + */ + public long getTimestampFromHeight(int height) throws DataException; + /** * Return highest block height from repository. * diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java index 808cc44d..3318d715 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java @@ -68,6 +68,20 @@ public class HSQLDBATRepository implements ATRepository { } } + @Override + public byte[] getCreatorPublicKey(String atAddress) throws DataException { + String sql = "SELECT creator FROM ATs WHERE AT_address = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, atAddress)) { + if (resultSet == null) + return null; + + return resultSet.getBytes(1); + } catch (SQLException e) { + throw new DataException("Unable to fetch AT creator's public key from repository", e); + } + } + @Override public List getAllExecutableATs() throws DataException { String sql = "SELECT AT_address, creator, created_when, version, asset_id, code_bytes, code_hash, " @@ -273,6 +287,68 @@ public class HSQLDBATRepository implements ATRepository { } } + @Override + public List getMatchingFinalATStates(byte[] codeHash, + Integer dataByteOffset, Long expectedValue, + 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 " + + "FROM ATs " + + "CROSS JOIN LATERAL(" + + "SELECT height, created_when, state_data, state_hash, fees, is_initial " + + "FROM ATStates " + + "WHERE ATStates.AT_address = ATs.AT_address " + + "ORDER BY height DESC " + + "LIMIT 1" + + ") AS FinalATStates " + + "WHERE code_hash = ? AND is_finished "); + + Object[] bindParams; + + if (dataByteOffset != null && expectedValue != null) { + sql.append("AND RAWTOHEX(SUBSTRING(state_data FROM ? FOR 8)) = ? "); + + // We convert our long to hex Java-side to control endian + 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 }; + } + + sql.append(" ORDER BY height "); + if (reverse != null && reverse) + sql.append("DESC"); + + HSQLDBRepository.limitOffsetSql(sql, limit, offset); + + List atStates = new ArrayList<>(); + + try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams)) { + if (resultSet == null) + return atStates; + + do { + String atAddress = resultSet.getString(1); + int height = resultSet.getInt(2); + long created = resultSet.getLong(3); + byte[] stateData = resultSet.getBytes(4); // Actually BLOB + byte[] stateHash = resultSet.getBytes(5); + long fees = resultSet.getLong(6); + boolean isInitial = resultSet.getBoolean(7); + + ATStateData atStateData = new ATStateData(atAddress, height, created, stateData, stateHash, fees, isInitial); + + atStates.add(atStateData); + } while (resultSet.next()); + + return atStates; + } catch (SQLException e) { + throw new DataException("Unable to fetch matching AT states from repository", e); + } + } + @Override public List getBlockATStatesAtHeight(int height) throws DataException { String sql = "SELECT AT_address, state_hash, fees, is_initial " diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java index 0860e1b1..a6ef51c4 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java @@ -131,6 +131,20 @@ public class HSQLDBBlockRepository implements BlockRepository { } } + @Override + public long getTimestampFromHeight(int height) throws DataException { + String sql = "SELECT minted_when FROM Blocks WHERE height = ?"; + + try (ResultSet resultSet = this.repository.checkedExecute(sql, height)) { + if (resultSet == null) + return 0; + + return resultSet.getLong(1); + } catch (SQLException e) { + throw new DataException("Error obtaining block timestamp by height from repository", e); + } + } + @Override public int getBlockchainHeight() throws DataException { String sql = "SELECT height FROM Blocks ORDER BY height DESC LIMIT 1"; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index 23fe2801..0eb1ef00 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -23,7 +23,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { + "trade_native_public_key, trade_native_public_key_hash, " + "trade_native_address, secret, hash_of_secret, " + "trade_foreign_public_key, trade_foreign_public_key_hash, " - + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_public_key_hash " + + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info " + "FROM TradeBotStates " + "WHERE trade_private_key = ?"; @@ -50,14 +50,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { Integer lockTimeA = resultSet.getInt(13); if (lockTimeA == 0 && resultSet.wasNull()) lockTimeA = null; - byte[] receivingPublicKeyHash = resultSet.getBytes(14); + byte[] receivingAccountInfo = resultSet.getBytes(14); return new TradeBotData(tradePrivateKey, tradeState, atAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, secret, hashOfSecret, tradeForeignPublicKey, tradeForeignPublicKeyHash, - bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash); + bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo); } catch (SQLException e) { throw new DataException("Unable to fetch trade-bot trading state from repository", e); } @@ -69,7 +69,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { + "trade_native_public_key, trade_native_public_key_hash, " + "trade_native_address, secret, hash_of_secret, " + "trade_foreign_public_key, trade_foreign_public_key_hash, " - + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_public_key_hash " + + "bitcoin_amount, xprv58, last_transaction_signature, locktime_a, receiving_account_info " + "FROM TradeBotStates"; List allTradeBotData = new ArrayList<>(); @@ -99,14 +99,14 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { Integer lockTimeA = resultSet.getInt(14); if (lockTimeA == 0 && resultSet.wasNull()) lockTimeA = null; - byte[] receivingPublicKeyHash = resultSet.getBytes(15); + byte[] receivingAccountInfo = resultSet.getBytes(15); TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, atAddress, tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, secret, hashOfSecret, tradeForeignPublicKey, tradeForeignPublicKeyHash, - bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingPublicKeyHash); + bitcoinAmount, xprv58, lastTransactionSignature, lockTimeA, receivingAccountInfo); allTradeBotData.add(tradeBotData); } while (resultSet.next()); @@ -133,7 +133,7 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { .bind("xprv58", tradeBotData.getXprv58()) .bind("last_transaction_signature", tradeBotData.getLastTransactionSignature()) .bind("locktime_a", tradeBotData.getLockTimeA()) - .bind("receiving_public_key_hash", tradeBotData.getReceivingPublicKeyHash()); + .bind("receiving_account_info", tradeBotData.getReceivingAccountInfo()); try { saveHelper.execute(this.repository); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 11e5a6a0..2706bb5d 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -626,7 +626,19 @@ public class HSQLDBDatabaseUpdates { + "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, " + "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " + "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, " - + "receiving_public_key_hash VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))"); + + "receiving_account_info VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))"); + break; + + case 21: + // AT functionality index + stmt.execute("CREATE INDEX IF NOT EXISTS ATCodeHashIndex ON ATs (code_hash, is_finished)"); + break; + + case 22: + // XXX for testing only - do not merge in 'master' + stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN IF NOT EXISTS receiving_public_key_hash VARBINARY(32)"); + stmt.execute("ALTER TABLE TradeBotStates DROP COLUMN receiving_public_key_hash"); + stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN IF NOT EXISTS receiving_account_info VARBINARY(32)"); break; default: