mirror of
https://github.com/Qortal/qortal.git
synced 2025-04-24 03:47:52 +00:00
trade ledger export implementation, completed trades bug fix
This commit is contained in:
parent
91ceafe0e3
commit
df37372180
@ -0,0 +1,72 @@
|
|||||||
|
package org.qortal.api.model;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
|
|
||||||
|
import javax.xml.bind.annotation.XmlAccessType;
|
||||||
|
import javax.xml.bind.annotation.XmlAccessorType;
|
||||||
|
import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
|
||||||
|
|
||||||
|
// All properties to be converted to JSON via JAXB
|
||||||
|
@XmlAccessorType(XmlAccessType.FIELD)
|
||||||
|
public class CrossChainTradeLedgerEntry {
|
||||||
|
|
||||||
|
private String market;
|
||||||
|
|
||||||
|
private String currency;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long quantity;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long feeAmount;
|
||||||
|
|
||||||
|
private String feeCurrency;
|
||||||
|
|
||||||
|
@XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
|
||||||
|
private long totalPrice;
|
||||||
|
|
||||||
|
private long tradeTimestamp;
|
||||||
|
|
||||||
|
protected CrossChainTradeLedgerEntry() {
|
||||||
|
/* For JAXB */
|
||||||
|
}
|
||||||
|
|
||||||
|
public CrossChainTradeLedgerEntry(String market, String currency, long quantity, long feeAmount, String feeCurrency, long totalPrice, long tradeTimestamp) {
|
||||||
|
this.market = market;
|
||||||
|
this.currency = currency;
|
||||||
|
this.quantity = quantity;
|
||||||
|
this.feeAmount = feeAmount;
|
||||||
|
this.feeCurrency = feeCurrency;
|
||||||
|
this.totalPrice = totalPrice;
|
||||||
|
this.tradeTimestamp = tradeTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getMarket() {
|
||||||
|
return market;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCurrency() {
|
||||||
|
return currency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getQuantity() {
|
||||||
|
return quantity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getFeeAmount() {
|
||||||
|
return feeAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFeeCurrency() {
|
||||||
|
return feeCurrency;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTotalPrice() {
|
||||||
|
return totalPrice;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getTradeTimestamp() {
|
||||||
|
return tradeTimestamp;
|
||||||
|
}
|
||||||
|
}
|
@ -10,11 +10,13 @@ import io.swagger.v3.oas.annotations.parameters.RequestBody;
|
|||||||
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
import io.swagger.v3.oas.annotations.responses.ApiResponse;
|
||||||
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import org.glassfish.jersey.media.multipart.ContentDisposition;
|
||||||
import org.qortal.api.ApiError;
|
import org.qortal.api.ApiError;
|
||||||
import org.qortal.api.ApiErrors;
|
import org.qortal.api.ApiErrors;
|
||||||
import org.qortal.api.ApiExceptionFactory;
|
import org.qortal.api.ApiExceptionFactory;
|
||||||
import org.qortal.api.Security;
|
import org.qortal.api.Security;
|
||||||
import org.qortal.api.model.CrossChainCancelRequest;
|
import org.qortal.api.model.CrossChainCancelRequest;
|
||||||
|
import org.qortal.api.model.CrossChainTradeLedgerEntry;
|
||||||
import org.qortal.api.model.CrossChainTradeSummary;
|
import org.qortal.api.model.CrossChainTradeSummary;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
import org.qortal.crosschain.ACCT;
|
import org.qortal.crosschain.ACCT;
|
||||||
@ -44,10 +46,14 @@ 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;
|
||||||
|
|
||||||
|
import javax.servlet.ServletContext;
|
||||||
import javax.servlet.http.HttpServletRequest;
|
import javax.servlet.http.HttpServletRequest;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
import javax.ws.rs.*;
|
import javax.ws.rs.*;
|
||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
|
import javax.ws.rs.core.HttpHeaders;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.function.Supplier;
|
import java.util.function.Supplier;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@ -61,6 +67,13 @@ public class CrossChainResource {
|
|||||||
@Context
|
@Context
|
||||||
HttpServletRequest request;
|
HttpServletRequest request;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
HttpServletResponse response;
|
||||||
|
|
||||||
|
@Context
|
||||||
|
ServletContext context;
|
||||||
|
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/tradeoffers")
|
@Path("/tradeoffers")
|
||||||
@Operation(
|
@Operation(
|
||||||
@ -258,11 +271,11 @@ public class CrossChainResource {
|
|||||||
example = "1597310000000"
|
example = "1597310000000"
|
||||||
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
|
) @QueryParam("minimumTimestamp") Long minimumTimestamp,
|
||||||
@Parameter(
|
@Parameter(
|
||||||
description = "Optionally filter by buyer Qortal address"
|
description = "Optionally filter by buyer Qortal public key"
|
||||||
) @QueryParam("buyerAddress") String buyerAddress,
|
) @QueryParam("buyerPublicKey") String buyerPublicKey58,
|
||||||
@Parameter(
|
@Parameter(
|
||||||
description = "Optionally filter by seller Qortal address"
|
description = "Optionally filter by seller Qortal public key"
|
||||||
) @QueryParam("sellerAddress") String sellerAddress,
|
) @QueryParam("sellerPublicKey") String sellerPublicKey58,
|
||||||
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
@Parameter( ref = "limit") @QueryParam("limit") Integer limit,
|
||||||
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
@Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
|
||||||
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
@Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
|
||||||
@ -274,6 +287,10 @@ public class CrossChainResource {
|
|||||||
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
||||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
// Decode public keys
|
||||||
|
byte[] buyerPublicKey = decodePublicKey(buyerPublicKey58);
|
||||||
|
byte[] sellerPublicKey = decodePublicKey(sellerPublicKey58);
|
||||||
|
|
||||||
final Boolean isFinished = Boolean.TRUE;
|
final Boolean isFinished = Boolean.TRUE;
|
||||||
|
|
||||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
@ -304,7 +321,7 @@ public class CrossChainResource {
|
|||||||
byte[] codeHash = acctInfo.getKey().value;
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
ACCT acct = acctInfo.getValue().get();
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerAddress, sellerAddress,
|
List<ATStateData> atStates = repository.getATRepository().getMatchingFinalATStates(codeHash, buyerPublicKey, sellerPublicKey,
|
||||||
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
|
isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
|
||||||
limit, offset, reverse);
|
limit, offset, reverse);
|
||||||
|
|
||||||
@ -343,6 +360,120 @@ public class CrossChainResource {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Decode Public Key
|
||||||
|
*
|
||||||
|
* @param publicKey58 the public key in a string
|
||||||
|
*
|
||||||
|
* @return the public key in bytes
|
||||||
|
*/
|
||||||
|
private byte[] decodePublicKey(String publicKey58) {
|
||||||
|
|
||||||
|
if( publicKey58 == null ) return null;
|
||||||
|
if( publicKey58.isEmpty() ) return new byte[0];
|
||||||
|
|
||||||
|
byte[] publicKey;
|
||||||
|
try {
|
||||||
|
publicKey = Base58.decode(publicKey58);
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY, e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct size for public key?
|
||||||
|
if (publicKey.length != Transformer.PUBLIC_KEY_LENGTH)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
|
||||||
|
|
||||||
|
return publicKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GET
|
||||||
|
@Path("/ledger/{publicKey}")
|
||||||
|
@Operation(
|
||||||
|
summary = "Accounting entries for all trades.",
|
||||||
|
description = "Returns accounting entries for all completed cross-chain trades",
|
||||||
|
responses = {
|
||||||
|
@ApiResponse(
|
||||||
|
content = @Content(
|
||||||
|
schema = @Schema(
|
||||||
|
type = "string",
|
||||||
|
format = "byte"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
|
||||||
|
public HttpServletResponse getLedgerEntries(
|
||||||
|
@PathParam("publicKey") String publicKey58,
|
||||||
|
@Parameter(
|
||||||
|
description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
|
||||||
|
example = "1597310000000"
|
||||||
|
) @QueryParam("minimumTimestamp") Long minimumTimestamp) {
|
||||||
|
|
||||||
|
byte[] publicKey = decodePublicKey(publicKey58);
|
||||||
|
|
||||||
|
// minimumTimestamp (if given) needs to be positive
|
||||||
|
if (minimumTimestamp != null && minimumTimestamp <= 0)
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||||
|
|
||||||
|
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||||
|
Integer minimumFinalHeight = null;
|
||||||
|
|
||||||
|
if (minimumTimestamp != null) {
|
||||||
|
minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
|
||||||
|
// If not found in the block repository it will return either 0 or 1
|
||||||
|
if (minimumFinalHeight == 0 || minimumFinalHeight == 1) {
|
||||||
|
// Try the archive
|
||||||
|
minimumFinalHeight = repository.getBlockArchiveRepository().getHeightFromTimestamp(minimumTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minimumFinalHeight == 0)
|
||||||
|
// We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
|
||||||
|
return response;
|
||||||
|
|
||||||
|
// height returned from repository is for block BEFORE timestamp
|
||||||
|
// but we want trades AFTER timestamp so bump height accordingly
|
||||||
|
minimumFinalHeight++;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<CrossChainTradeLedgerEntry> crossChainTradeLedgerEntries = new ArrayList<>();
|
||||||
|
|
||||||
|
Map<ByteArray, Supplier<ACCT>> acctsByCodeHash = SupportedBlockchain.getAcctMap();
|
||||||
|
|
||||||
|
// collect ledger entries for each ACCT
|
||||||
|
for (Map.Entry<ByteArray, Supplier<ACCT>> acctInfo : acctsByCodeHash.entrySet()) {
|
||||||
|
byte[] codeHash = acctInfo.getKey().value;
|
||||||
|
ACCT acct = acctInfo.getValue().get();
|
||||||
|
|
||||||
|
// collect buys and sells
|
||||||
|
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, true);
|
||||||
|
CrossChainUtils.collectLedgerEntries(publicKey, repository, minimumFinalHeight, crossChainTradeLedgerEntries, codeHash, acct, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
crossChainTradeLedgerEntries.sort((a, b) -> Longs.compare(a.getTradeTimestamp(), b.getTradeTimestamp()));
|
||||||
|
|
||||||
|
response.setStatus(HttpServletResponse.SC_OK);
|
||||||
|
response.setContentType("text/csv");
|
||||||
|
response.setHeader(
|
||||||
|
HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
ContentDisposition
|
||||||
|
.type("attachment")
|
||||||
|
.fileName(CrossChainUtils.createLedgerFileName(Crypto.toAddress(publicKey)))
|
||||||
|
.build()
|
||||||
|
.toString()
|
||||||
|
);
|
||||||
|
|
||||||
|
CrossChainUtils.writeToLedger( response.getWriter(), crossChainTradeLedgerEntries);
|
||||||
|
|
||||||
|
return response;
|
||||||
|
} catch (DataException e) {
|
||||||
|
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
|
||||||
|
} catch (IOException e) {
|
||||||
|
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
@Path("/price/{blockchain}")
|
@Path("/price/{blockchain}")
|
||||||
@Operation(
|
@Operation(
|
||||||
|
@ -10,21 +10,36 @@ import org.bitcoinj.script.ScriptBuilder;
|
|||||||
|
|
||||||
import org.bouncycastle.util.Strings;
|
import org.bouncycastle.util.Strings;
|
||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
|
import org.qortal.api.model.CrossChainTradeLedgerEntry;
|
||||||
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
|
import org.qortal.api.model.crosschain.BitcoinyTBDRequest;
|
||||||
import org.qortal.crosschain.*;
|
import org.qortal.crosschain.*;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
|
import org.qortal.data.at.ATStateData;
|
||||||
import org.qortal.data.crosschain.*;
|
import org.qortal.data.crosschain.*;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
|
import org.qortal.utils.Amounts;
|
||||||
import org.qortal.utils.BitTwiddling;
|
import org.qortal.utils.BitTwiddling;
|
||||||
|
|
||||||
|
import java.io.BufferedWriter;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.OutputStreamWriter;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.io.Writer;
|
||||||
|
import java.text.DateFormat;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.time.Instant;
|
||||||
|
import java.time.ZoneId;
|
||||||
|
import java.time.ZonedDateTime;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
|
||||||
public class CrossChainUtils {
|
public class CrossChainUtils {
|
||||||
|
public static final String QORT_CURRENCY_CODE = "QORT";
|
||||||
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
|
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
|
||||||
public static final String CORE_API_CALL = "Core API Call";
|
public static final String CORE_API_CALL = "Core API Call";
|
||||||
|
public static final String QORTAL_EXCHANGE_LABEL = "Qortal";
|
||||||
|
|
||||||
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
|
public static ServerConfigurationInfo buildServerConfigurationInfo(Bitcoiny blockchain) {
|
||||||
|
|
||||||
@ -632,4 +647,128 @@ public class CrossChainUtils {
|
|||||||
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
|
||||||
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
|
return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write To Ledger
|
||||||
|
*
|
||||||
|
* @param writer the writer to the ledger
|
||||||
|
* @param entries the entries to write to the ledger
|
||||||
|
*
|
||||||
|
* @throws IOException
|
||||||
|
*/
|
||||||
|
public static void writeToLedger(Writer writer, List<CrossChainTradeLedgerEntry> entries) throws IOException {
|
||||||
|
|
||||||
|
BufferedWriter bufferedWriter = new BufferedWriter(writer);
|
||||||
|
|
||||||
|
StringJoiner header = new StringJoiner(",");
|
||||||
|
header.add("Market");
|
||||||
|
header.add("Currency");
|
||||||
|
header.add("Quantity");
|
||||||
|
header.add("Commission Paid");
|
||||||
|
header.add("Commission Currency");
|
||||||
|
header.add("Total Price");
|
||||||
|
header.add("Date Time");
|
||||||
|
header.add("Exchange");
|
||||||
|
|
||||||
|
bufferedWriter.append(header.toString());
|
||||||
|
|
||||||
|
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMdd HH:mm");
|
||||||
|
dateFormatter.setTimeZone(TimeZone.getTimeZone("UTC"));
|
||||||
|
|
||||||
|
for( CrossChainTradeLedgerEntry entry : entries ) {
|
||||||
|
StringJoiner joiner = new StringJoiner(",");
|
||||||
|
|
||||||
|
joiner.add(entry.getMarket());
|
||||||
|
joiner.add(entry.getCurrency());
|
||||||
|
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getQuantity())));
|
||||||
|
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getFeeAmount())));
|
||||||
|
joiner.add(entry.getFeeCurrency());
|
||||||
|
joiner.add(String.valueOf(Amounts.prettyAmount(entry.getTotalPrice())));
|
||||||
|
joiner.add(dateFormatter.format(new Date(entry.getTradeTimestamp())));
|
||||||
|
joiner.add(QORTAL_EXCHANGE_LABEL);
|
||||||
|
|
||||||
|
bufferedWriter.newLine();
|
||||||
|
bufferedWriter.append(joiner.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
bufferedWriter.newLine();
|
||||||
|
bufferedWriter.flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create Ledger File Name
|
||||||
|
*
|
||||||
|
* Create a file name the includes timestamp and address.
|
||||||
|
*
|
||||||
|
* @param address the address
|
||||||
|
*
|
||||||
|
* @return the file name created
|
||||||
|
*/
|
||||||
|
public static String createLedgerFileName(String address) {
|
||||||
|
DateFormat dateFormatter = new SimpleDateFormat("yyyyMMddHHmmss");
|
||||||
|
String fileName = "ledger-" + address + "-" + dateFormatter.format(new Date());
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collect Ledger Entries
|
||||||
|
*
|
||||||
|
* @param publicKey the public key for the ledger entries, buy and sell
|
||||||
|
* @param repository the data repository
|
||||||
|
* @param minimumFinalHeight the minimum block height for entries to be collected
|
||||||
|
* @param entries the ledger entries to add to
|
||||||
|
* @param codeHash code hash for the entry blockchain
|
||||||
|
* @param acct the ACCT for the entry blockchain
|
||||||
|
* @param isBuy true collecting entries for a buy, otherwise false
|
||||||
|
*
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public static void collectLedgerEntries(
|
||||||
|
byte[] publicKey,
|
||||||
|
Repository repository,
|
||||||
|
Integer minimumFinalHeight,
|
||||||
|
List<CrossChainTradeLedgerEntry> entries,
|
||||||
|
byte[] codeHash,
|
||||||
|
ACCT acct,
|
||||||
|
boolean isBuy) throws DataException {
|
||||||
|
|
||||||
|
// get all the final AT states for the code hash (foreign coin)
|
||||||
|
List<ATStateData> atStates
|
||||||
|
= repository.getATRepository().getMatchingFinalATStates(
|
||||||
|
codeHash,
|
||||||
|
isBuy ? publicKey : null,
|
||||||
|
!isBuy ? publicKey : null,
|
||||||
|
Boolean.TRUE, acct.getModeByteOffset(),
|
||||||
|
(long) AcctMode.REDEEMED.value,
|
||||||
|
minimumFinalHeight,
|
||||||
|
null, null, false
|
||||||
|
);
|
||||||
|
|
||||||
|
String foreignBlockchainCurrencyCode = acct.getBlockchain().getCurrencyCode();
|
||||||
|
|
||||||
|
// for each trade, build ledger entry, collect ledger entry
|
||||||
|
for (ATStateData atState : atStates) {
|
||||||
|
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
|
||||||
|
|
||||||
|
// We also need block timestamp for use as trade timestamp
|
||||||
|
long localTimestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
|
||||||
|
|
||||||
|
if (localTimestamp == 0) {
|
||||||
|
// Try the archive
|
||||||
|
localTimestamp = repository.getBlockArchiveRepository().getTimestampFromHeight(atState.getHeight());
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossChainTradeLedgerEntry ledgerEntry
|
||||||
|
= new CrossChainTradeLedgerEntry(
|
||||||
|
isBuy ? QORT_CURRENCY_CODE : foreignBlockchainCurrencyCode,
|
||||||
|
isBuy ? foreignBlockchainCurrencyCode : QORT_CURRENCY_CODE,
|
||||||
|
isBuy ? crossChainTradeData.qortAmount : crossChainTradeData.expectedForeignAmount,
|
||||||
|
0,
|
||||||
|
foreignBlockchainCurrencyCode,
|
||||||
|
isBuy ? crossChainTradeData.expectedForeignAmount : crossChainTradeData.qortAmount,
|
||||||
|
localTimestamp);
|
||||||
|
|
||||||
|
entries.add(ledgerEntry);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -83,6 +83,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
|||||||
return this.bitcoinjContext;
|
return this.bitcoinjContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
public String getCurrencyCode() {
|
public String getCurrencyCode() {
|
||||||
return this.currencyCode;
|
return this.currencyCode;
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@ package org.qortal.crosschain;
|
|||||||
|
|
||||||
public interface ForeignBlockchain {
|
public interface ForeignBlockchain {
|
||||||
|
|
||||||
|
public String getCurrencyCode();
|
||||||
|
|
||||||
public boolean isValidAddress(String address);
|
public boolean isValidAddress(String address);
|
||||||
|
|
||||||
public boolean isValidWalletKey(String walletKey);
|
public boolean isValidWalletKey(String walletKey);
|
||||||
|
@ -76,7 +76,7 @@ public interface ATRepository {
|
|||||||
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
* Although <tt>expectedValue</tt>, if provided, is natively an unsigned long,
|
||||||
* the data segment comparison is done via unsigned hex string.
|
* the data segment comparison is done via unsigned hex string.
|
||||||
*/
|
*/
|
||||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
|
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished,
|
||||||
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;
|
||||||
|
|
||||||
|
@ -5,6 +5,7 @@ import com.google.common.primitives.Longs;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.crypto.Crypto;
|
||||||
import org.qortal.data.at.ATData;
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.at.ATStateData;
|
import org.qortal.data.at.ATStateData;
|
||||||
import org.qortal.repository.ATRepository;
|
import org.qortal.repository.ATRepository;
|
||||||
@ -403,7 +404,7 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, String buyerAddress, String sellerAddress, Boolean isFinished,
|
public List<ATStateData> getMatchingFinalATStates(byte[] codeHash, byte[] buyerPublicKey, byte[] sellerPublicKey, Boolean isFinished,
|
||||||
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 {
|
||||||
StringBuilder sql = new StringBuilder(1024);
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
@ -426,9 +427,9 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
// Both must be the same direction (DESC) also
|
// Both must be the same direction (DESC) also
|
||||||
sql.append("ORDER BY ATStates.height DESC LIMIT 1) AS FinalATStates ");
|
sql.append("ORDER BY ATStates.height DESC LIMIT 1) AS FinalATStates ");
|
||||||
|
|
||||||
// Optional LEFT JOIN with ATTRANSACTIONS for buyerAddress
|
// Optional JOIN with ATTRANSACTIONS for buyerAddress
|
||||||
if (buyerAddress != null && !buyerAddress.isEmpty()) {
|
if (buyerPublicKey != null && buyerPublicKey.length > 0) {
|
||||||
sql.append("LEFT JOIN ATTRANSACTIONS tx ON tx.at_address = ATs.AT_address ");
|
sql.append("JOIN ATTRANSACTIONS tx ON tx.at_address = ATs.AT_address ");
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append("WHERE ATs.code_hash = ? ");
|
sql.append("WHERE ATs.code_hash = ? ");
|
||||||
@ -450,18 +451,18 @@ public class HSQLDBATRepository implements ATRepository {
|
|||||||
bindParams.add(rawExpectedValue);
|
bindParams.add(rawExpectedValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (buyerAddress != null && !buyerAddress.isEmpty()) {
|
if (buyerPublicKey != null && buyerPublicKey.length > 0 ) {
|
||||||
sql.append("AND tx.recipient = ? ");
|
// the buyer must be the recipient of the transaction and not the creator of the AT
|
||||||
bindParams.add(buyerAddress);
|
sql.append("AND tx.recipient = ? AND ATs.creator != ? ");
|
||||||
|
|
||||||
|
bindParams.add(Crypto.toAddress(buyerPublicKey));
|
||||||
|
bindParams.add(buyerPublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
if (sellerAddress != null && !sellerAddress.isEmpty()) {
|
if (sellerPublicKey != null && sellerPublicKey.length > 0) {
|
||||||
// Convert sellerAddress to publicKey (method depends on your implementation)
|
|
||||||
AccountData accountData = this.repository.getAccountRepository().getAccount(sellerAddress);
|
|
||||||
byte[] publicKey = accountData.getPublicKey();
|
|
||||||
sql.append("AND ATs.creator = ? ");
|
sql.append("AND ATs.creator = ? ");
|
||||||
bindParams.add(publicKey);
|
bindParams.add(sellerPublicKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
sql.append(" ORDER BY FinalATStates.height ");
|
sql.append(" ORDER BY FinalATStates.height ");
|
||||||
|
@ -3,10 +3,15 @@ package org.qortal.test.api;
|
|||||||
import org.json.simple.JSONObject;
|
import org.json.simple.JSONObject;
|
||||||
import org.junit.Assert;
|
import org.junit.Assert;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
import org.qortal.api.model.CrossChainTradeLedgerEntry;
|
||||||
import org.qortal.api.resource.CrossChainUtils;
|
import org.qortal.api.resource.CrossChainUtils;
|
||||||
import org.qortal.test.common.ApiCommon;
|
import org.qortal.test.common.ApiCommon;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.PrintWriter;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
public class CrossChainUtilsTests extends ApiCommon {
|
public class CrossChainUtilsTests extends ApiCommon {
|
||||||
@ -137,4 +142,53 @@ public class CrossChainUtilsTests extends ApiCommon {
|
|||||||
Assert.assertEquals(5, versionDecimal, 0.001);
|
Assert.assertEquals(5, versionDecimal, 0.001);
|
||||||
Assert.assertFalse(thrown);
|
Assert.assertFalse(thrown);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWriteToLedgerHeaderOnly() throws IOException {
|
||||||
|
CrossChainUtils.writeToLedger(new PrintWriter(System.out), new ArrayList<>());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWriteToLedgerOneRow() throws IOException {
|
||||||
|
CrossChainUtils.writeToLedger(
|
||||||
|
new PrintWriter(System.out),
|
||||||
|
List.of(
|
||||||
|
new CrossChainTradeLedgerEntry(
|
||||||
|
"QORT",
|
||||||
|
"LTC",
|
||||||
|
1000,
|
||||||
|
0,
|
||||||
|
"LTC",
|
||||||
|
1,
|
||||||
|
System.currentTimeMillis())
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testWriteToLedgerTwoRows() throws IOException {
|
||||||
|
CrossChainUtils.writeToLedger(
|
||||||
|
new PrintWriter(System.out),
|
||||||
|
List.of(
|
||||||
|
new CrossChainTradeLedgerEntry(
|
||||||
|
"QORT",
|
||||||
|
"LTC",
|
||||||
|
1000,
|
||||||
|
0,
|
||||||
|
"LTC",
|
||||||
|
1,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
),
|
||||||
|
new CrossChainTradeLedgerEntry(
|
||||||
|
"LTC",
|
||||||
|
"QORT",
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
"LTC",
|
||||||
|
1000,
|
||||||
|
System.currentTimeMillis()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user