forked from Qortal/qortal
Add trading price estimate API call GET /crosschain/price/{blockchain} where blockchain is something like LITECOIN
This commit is contained in:
parent
1c6ea0a860
commit
0f0266609f
@ -22,6 +22,7 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
import javax.ws.rs.DELETE;
|
import javax.ws.rs.DELETE;
|
||||||
import javax.ws.rs.GET;
|
import javax.ws.rs.GET;
|
||||||
import javax.ws.rs.Path;
|
import javax.ws.rs.Path;
|
||||||
|
import javax.ws.rs.PathParam;
|
||||||
import javax.ws.rs.QueryParam;
|
import javax.ws.rs.QueryParam;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
@ -51,6 +52,7 @@ import org.qortal.transaction.Transaction.ValidationResult;
|
|||||||
import org.qortal.transform.TransformationException;
|
import org.qortal.transform.TransformationException;
|
||||||
import org.qortal.transform.Transformer;
|
import org.qortal.transform.Transformer;
|
||||||
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
import org.qortal.transform.transaction.MessageTransactionTransformer;
|
||||||
|
import org.qortal.utils.Amounts;
|
||||||
import org.qortal.utils.Base58;
|
import org.qortal.utils.Base58;
|
||||||
import org.qortal.utils.ByteArray;
|
import org.qortal.utils.ByteArray;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
@ -201,6 +203,63 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/price/{blockchain}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Request current estimated trading price",
|
||||||
|
description = "Returns price based on most recent completed trades. Price is expressed in terms of QORT per unit foreign currency.",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
type = "number"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public long getTradePriceEstimate(
|
||||||
|
@Parameter(
|
||||||
|
description = "foreign blockchain",
|
||||||
|
example = "LITECOIN",
|
||||||
|
schema = @Schema(implementation = SupportedBlockchain.class)
|
||||||
|
) @PathParam("blockchain") SupportedBlockchain foreignBlockchain) {
|
||||||
|
// foreignBlockchain is required
|
||||||
|
if (foreignBlockchain == null)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// We want both a minimum of 5 trades and enough trades to span at least 4 hours
|
||||||
|
int minimumCount = 5;
|
||||||
|
long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
|
||||||
|
Boolean isFinished = Boolean.TRUE;
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
|
||||||
|
|
||||||
|
long totalForeign = 0;
|
||||||
|
long totalQort = 0;
|
||||||
|
|
||||||
|
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||||
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStatesQuorum(codeHash,
|
||||||
|
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumCount, minimumPeriod);
|
||||||
|
|
||||||
|
for (ATStateData atState : atStates) {
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
totalForeign += crossChainTradeData.expectedForeignAmount;
|
||||||
|
totalQort += crossChainTradeData.qortAmount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return Amounts.scaledDivide(totalQort, totalForeign);
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DELETE
|
@DELETE
|
||||||
@Path("/tradeoffer")
|
@Path("/tradeoffer")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -80,6 +80,26 @@ public interface ATRepository {
|
|||||||
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
Integer dataByteOffset, Long expectedValue, Integer minimumFinalHeight,
|
||||||
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns final ATStateData for ATs matching codeHash (required)
|
||||||
|
* and specific data segment value (optional), returning at least
|
||||||
|
* <tt>minimumCount</tt> entries over a span of at least
|
||||||
|
* <tt>minimumPeriod</tt> ms, given enough entries in repository.
|
||||||
|
* <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> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
||||||
|
Integer dataByteOffset, Long expectedValue,
|
||||||
|
int minimumCount, long minimumPeriod) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns all ATStateData for a given block height.
|
* Returns all ATStateData for a given block height.
|
||||||
* <p>
|
* <p>
|
||||||
|
@ -447,6 +447,92 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<ATStateData> getMatchingFinalATStatesQuorum(byte[] codeHash, Boolean isFinished,
|
||||||
|
Integer dataByteOffset, Long expectedValue,
|
||||||
|
int minimumCount, long minimumPeriod) throws DataException {
|
||||||
|
// We need most recent entry first so we can use its timestamp to slice further results
|
||||||
|
List<ATStateData> mostRecentStates = this.getMatchingFinalATStates(codeHash, isFinished,
|
||||||
|
dataByteOffset, expectedValue, null,
|
||||||
|
1, 0, true);
|
||||||
|
|
||||||
|
if (mostRecentStates == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
if (mostRecentStates.isEmpty())
|
||||||
|
return mostRecentStates;
|
||||||
|
|
||||||
|
ATStateData mostRecentState = mostRecentStates.get(0);
|
||||||
|
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
List<Object> bindParams = new ArrayList<>();
|
||||||
|
|
||||||
|
sql.append("SELECT AT_address, height, state_data, state_hash, fees, is_initial "
|
||||||
|
+ "FROM ATs "
|
||||||
|
+ "CROSS JOIN LATERAL("
|
||||||
|
+ "SELECT height, state_data, state_hash, fees, is_initial "
|
||||||
|
+ "FROM ATStates "
|
||||||
|
+ "JOIN ATStatesData USING (AT_address, height) "
|
||||||
|
+ "WHERE ATStates.AT_address = ATs.AT_address ");
|
||||||
|
|
||||||
|
// Order by AT_address and height to use compound primary key as index
|
||||||
|
// Both must be the same direction (DESC) also
|
||||||
|
sql.append("ORDER BY ATStates.AT_address DESC, ATStates.height DESC "
|
||||||
|
+ "LIMIT 1 "
|
||||||
|
+ ") AS FinalATStates "
|
||||||
|
+ "WHERE code_hash = ? ");
|
||||||
|
bindParams.add(codeHash);
|
||||||
|
|
||||||
|
if (isFinished != null) {
|
||||||
|
sql.append("AND is_finished = ? ");
|
||||||
|
bindParams.add(isFinished);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataByteOffset != null && expectedValue != null) {
|
||||||
|
sql.append("AND SUBSTRING(state_data FROM ? FOR 8) = ? ");
|
||||||
|
|
||||||
|
// We convert our long on Java-side to control endian
|
||||||
|
byte[] rawExpectedValue = Longs.toByteArray(expectedValue);
|
||||||
|
|
||||||
|
// SQL binary data offsets start at 1
|
||||||
|
bindParams.add(dataByteOffset + 1);
|
||||||
|
bindParams.add(rawExpectedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Slice so that we meet both minimumCount and minimumPeriod
|
||||||
|
int minimumHeight = mostRecentState.getHeight() - (int) (minimumPeriod / 60 * 1000L); // XXX assumes 60 second blocks
|
||||||
|
|
||||||
|
sql.append("AND (FinalATStates.height >= ? OR ROWNUM() < ?) ");
|
||||||
|
bindParams.add(minimumHeight);
|
||||||
|
bindParams.add(minimumCount);
|
||||||
|
|
||||||
|
sql.append("ORDER BY FinalATStates.height DESC");
|
||||||
|
|
||||||
|
List<ATStateData> atStates = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), bindParams.toArray())) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return atStates;
|
||||||
|
|
||||||
|
do {
|
||||||
|
String atAddress = resultSet.getString(1);
|
||||||
|
int height = resultSet.getInt(2);
|
||||||
|
byte[] stateData = resultSet.getBytes(3); // Actually BLOB
|
||||||
|
byte[] stateHash = resultSet.getBytes(4);
|
||||||
|
long fees = resultSet.getLong(5);
|
||||||
|
boolean isInitial = resultSet.getBoolean(6);
|
||||||
|
|
||||||
|
ATStateData atStateData = new ATStateData(atAddress, height, 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
|
@Override
|
||||||
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
public List<ATStateData> getBlockATStatesAtHeight(int height) throws DataException {
|
||||||
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
String sql = "SELECT AT_address, state_hash, fees, is_initial "
|
||||||
|
Loading…
x
Reference in New Issue
Block a user