Browse Source

WIP: trade-bot: initial API call for listing completed trades

Also: renamed trade bot field/column "receiving_public_key_hash"
to "receiving_account_info" as Alice's trade bot uses it to
store Alice's Qortal address, not PKH.

Added some extra simplistic repository calls to support above,
like BlockRepository.getTimestampFromHeight,
ATRepository.getCreatorPublicKey(atAddress)
pull/16/head
catbref 4 years ago
parent
commit
ea9b0d4588
  1. 43
      src/main/java/org/qortal/api/model/CrossChainTradeSummary.java
  2. 51
      src/main/java/org/qortal/api/resource/CrossChainResource.java
  3. 6
      src/main/java/org/qortal/controller/TradeBot.java
  4. 38
      src/main/java/org/qortal/crosschain/BTCACCT.java
  5. 10
      src/main/java/org/qortal/data/crosschain/TradeBotData.java
  6. 21
      src/main/java/org/qortal/repository/ATRepository.java
  7. 9
      src/main/java/org/qortal/repository/BlockRepository.java
  8. 76
      src/main/java/org/qortal/repository/hsqldb/HSQLDBATRepository.java
  9. 14
      src/main/java/org/qortal/repository/hsqldb/HSQLDBBlockRepository.java
  10. 14
      src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java
  11. 14
      src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java

43
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;
}
}

51
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<CrossChainTradeSummary> 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<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, BTCACCT.MODE_BYTE_OFFSET, (long) BTCACCT.Mode.REDEEMED.value, limit, offset, reverse);
List<CrossChainTradeSummary> 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)

6
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<TransactionOutput> 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<TransactionOutput> fundingOutputs = BTC.getInstance().getUnspentOutputs(p2shAddress);
byte[] receivePublicKeyHash = tradeBotData.getReceivingPublicKeyHash();
byte[] receivePublicKeyHash = tradeBotData.getReceivingAccountInfo();
Transaction p2shRedeemTransaction = BTCP2SH.buildRedeemTransaction(redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, secretA, receivePublicKeyHash);

38
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
/** <b>Value</b> 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;
/** <b>Byte</b> 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);

10
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;
}
}

21
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<ATData> 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).
* <p>
* If searching for specific data segment value, both <tt>dataByteOffset</tt>
* and <tt>expectedValue</tt> need to be non-null.
* <p>
* Note that <tt>dataByteOffset</tt> starts from 0 and will typically be
* a multiple of <tt>MachineState.VALUE_SIZE</tt>, which is usually 8:
* width of a long.
* <p>
* 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,
Integer limit, Integer offset, Boolean reverse) throws DataException;
/**
* Returns all ATStateData for a given block height.
* <p>

9
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.
*

76
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<ATData> 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<ATStateData> 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<ATStateData> 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<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
String sql = "SELECT AT_address, state_hash, fees, is_initial "

14
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";

14
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<TradeBotData> 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);

14
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:

Loading…
Cancel
Save