Foreign coin trade transaction summaries

This commit is contained in:
kennycud 2024-02-12 06:31:17 -08:00
parent 587b063e6a
commit 3f29116b47
4 changed files with 579 additions and 0 deletions

View File

@ -19,11 +19,14 @@ import org.qortal.api.model.CrossChainTradeSummary;
import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crosschain.ACCT;
import org.qortal.crosschain.AcctMode;
import org.qortal.crosschain.Bitcoiny;
import org.qortal.crosschain.ForeignBlockchainException;
import org.qortal.crosschain.SupportedBlockchain;
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.TransactionSummary;
import org.qortal.data.transaction.BaseTransactionData;
import org.qortal.data.transaction.MessageTransactionData;
import org.qortal.data.transaction.TransactionData;
@ -47,6 +50,7 @@ import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.*;
import java.util.function.Supplier;
import java.util.stream.Collectors;
@Path("/crosschain")
@Tag(name = "Cross-Chain")
@ -497,6 +501,111 @@ public class CrossChainResource {
}
}
@POST
@Path("/p2sh")
@Operation(
summary = "Returns P2SH Address",
description = "Get the P2SH address to lock foreign coin in a cross chain trade for QORT",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.TEXT_PLAIN,
schema = @Schema(
type = "string",
description = "the AT address",
example = "AKFnu9yBp7tUAc5HAphhfCxRZTYoeKXgUy"
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "address"))
)
}
)
@ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA})
@SecurityRequirement(name = "apiKey")
public String getForeignP2SH(@HeaderParam(Security.API_KEY_HEADER) String apiKey, String atAddress) {
Security.checkApiCallAllowed(request);
try (final Repository repository = RepositoryManager.getRepository()) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
if( acct == null || !(acct.getBlockchain() instanceof Bitcoiny) )
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoiny bitcoiny = (Bitcoiny) acct.getBlockchain();
CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
Optional<String> p2sh
= CrossChainUtils.getP2ShAddressForAT(atAddress, repository, bitcoiny, crossChainTradeData);
if(p2sh.isPresent()){
return p2sh.get();
}
else{
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
}
}
catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
}
@POST
@Path("/txactivity")
@Operation(
summary = "Returns Foreign Transaction Activity",
description = "Get the activity related to foreign coin trading",
responses = {
@ApiResponse(
content = @Content(
array = @ArraySchema(
schema = @Schema(
implementation = TransactionSummary.class
)
)
)
)
}
)
@ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
@SecurityRequirement(name = "apiKey")
public List<TransactionSummary> getForeignTransactionActivity(@HeaderParam(Security.API_KEY_HEADER) String apiKey, @Parameter(
description = "Limit to specific blockchain",
example = "LITECOIN",
schema = @Schema(implementation = SupportedBlockchain.class)
) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
Security.checkApiCallAllowed(request);
if (!(foreignBlockchain.getInstance() instanceof Bitcoiny))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
Bitcoiny bitcoiny = (Bitcoiny) foreignBlockchain.getInstance() ;
org.bitcoinj.core.Context.propagate( bitcoiny.getBitcoinjContext() );
try (final Repository repository = RepositoryManager.getRepository()) {
// sort from last lock to first lock
return CrossChainUtils
.getForeignTradeSummaries(foreignBlockchain, repository, bitcoiny).stream()
.sorted(Comparator.comparing(TransactionSummary::getLockingTimestamp).reversed())
.collect(Collectors.toList());
}
catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
catch (ForeignBlockchainException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, e.getMessage());
}
}
private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
if (atData == null)

View File

@ -2,12 +2,28 @@ package org.qortal.api.resource;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
import org.bitcoinj.script.Script;
import org.bitcoinj.script.ScriptBuilder;
import org.qortal.crosschain.*;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.AtomicTransactionData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
import org.qortal.data.crosschain.TransactionSummary;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Optional;
import java.util.HashMap;
import java.util.Map;
import java.util.stream.Collectors;
public class CrossChainUtils {
private static final Logger LOGGER = LogManager.getLogger(CrossChainUtils.class);
@ -85,4 +101,320 @@ public class CrossChainUtils {
return String.valueOf(bitcoiny.getFeeCeiling());
}
/**
* Get P2Sh Address For AT
*
* @param atAddress the AT address
* @param repository the repository
* @param bitcoiny the blockchain data
* @param crossChainTradeData the trade data
*
* @return the p2sh address for the trade, if there is one
*
* @throws DataException
*/
public static Optional<String> getP2ShAddressForAT(
String atAddress,
Repository repository,
Bitcoiny bitcoiny,
CrossChainTradeData crossChainTradeData) throws DataException {
// get the trade bot data for the AT address
Optional<TradeBotData> tradeBotDataOptional
= repository.getCrossChainRepository()
.getAllTradeBotData().stream()
.filter(data -> data.getAtAddress().equals(atAddress))
.findFirst();
if( tradeBotDataOptional.isEmpty() )
return Optional.empty();
TradeBotData tradeBotData = tradeBotDataOptional.get();
// return the p2sh address from the trade bot
return getP2ShFromTradeBot(bitcoiny, crossChainTradeData, tradeBotData);
}
/**
* Get Foreign Trade Summaries
*
* @param foreignBlockchain the blockchain traded on
* @param repository the repository
* @param bitcoiny data for the blockchain trade on
* @return
* @throws DataException
* @throws ForeignBlockchainException
*/
public static List<TransactionSummary> getForeignTradeSummaries(
SupportedBlockchain foreignBlockchain,
Repository repository,
Bitcoiny bitcoiny) throws DataException, ForeignBlockchainException {
// get all the AT address for the given blockchain
List<String> atAddresses
= repository.getCrossChainRepository().getAllTradeBotData().stream()
.filter(data -> foreignBlockchain.name().toLowerCase().equals(data.getForeignBlockchain().toLowerCase()))
//.filter( data -> data.getForeignKey().equals( xpriv )) // TODO
.map(data -> data.getAtAddress())
.collect(Collectors.toList());
List<TransactionSummary> summaries = new ArrayList<>( atAddresses.size() * 2 );
// for each AT address, gather the data and get foreign trade summary
for( String atAddress: atAddresses) {
ATData atData = repository.getATRepository().fromATAddress(atAddress);
CrossChainTradeData crossChainTradeData = foreignBlockchain.getLatestAcct().populateTradeData(repository, atData);
Optional<String> address = getP2ShAddressForAT(atAddress,repository, bitcoiny, crossChainTradeData);
if( address.isPresent()){
summaries.add( getForeignTradeSummary( bitcoiny, address.get(), atAddress ) );
}
}
return summaries;
}
/**
* Get P2Sh From Trade Bot
*
* Get P2Sh address from the trade bot
*
* @param bitcoiny the blockchain for the trade
* @param crossChainTradeData the cross cahin data for the trade
* @param tradeBotData the data from the trade bot
*
* @return the address, original format
*/
private static Optional<String> getP2ShFromTradeBot(
Bitcoiny bitcoiny,
CrossChainTradeData crossChainTradeData,
TradeBotData tradeBotData) {
// Pirate Chain does not support this
if( SupportedBlockchain.PIRATECHAIN.name().equals(tradeBotData.getForeignBlockchain())) return Optional.empty();
// need to get the trade PKH from the trade bot
if( tradeBotData.getTradeForeignPublicKeyHash() == null ) return Optional.empty();
// need to get the lock time from the trade bot
if( tradeBotData.getLockTimeA() == null ) return Optional.empty();
// need to get the creator PKH from the trade bot
if( crossChainTradeData.creatorForeignPKH == null ) return Optional.empty();
// need to get the secret from the trade bot
if( tradeBotData.getHashOfSecret() == null ) return Optional.empty();
// if we have the necessary data from the trade bot,
// then build the redeem script necessary to facilitate the trade
byte[] redeemScriptBytes
= BitcoinyHTLC.buildScript(
tradeBotData.getTradeForeignPublicKeyHash(),
tradeBotData.getLockTimeA(),
crossChainTradeData.creatorForeignPKH,
tradeBotData.getHashOfSecret()
);
String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes);
return Optional.of(p2shAddress);
}
/**
* Get Foreign Trade Summary
*
* @param bitcoiny the blockchain the trade occurred on
* @param p2shAddress the p2sh address
* @param atAddress the AT address the p2sh address is derived from
*
* @return the summary
*
* @throws ForeignBlockchainException
*/
public static TransactionSummary getForeignTradeSummary(Bitcoiny bitcoiny, String p2shAddress, String atAddress)
throws ForeignBlockchainException {
Script outputScript = ScriptBuilder.createOutputScript(
Address.fromString(bitcoiny.getNetworkParameters(), p2shAddress));
List<TransactionHash> hashes
= bitcoiny.getAddressTransactions( outputScript.getProgram(), true);
TransactionSummary summary;
if(hashes.isEmpty()){
summary
= new TransactionSummary(
atAddress,
p2shAddress,
"N/A",
"N/A",
0,
0,
0,
0,
"N/A",
0,
0,
0,
0);
}
else if( hashes.size() == 1) {
AtomicTransactionData data = buildTransactionData(bitcoiny, hashes.get(0));
summary = new TransactionSummary(
atAddress,
p2shAddress,
"N/A",
data.hash.txHash,
data.timestamp,
data.totalAmount,
getTotalInput(bitcoiny, data.inputs) - data.totalAmount,
data.size,
"N/A",
0,
0,
0,
0);
}
// otherwise assuming there is 2 and only 2 hashes
else {
List<AtomicTransactionData> atomicTransactionDataList = new ArrayList<>(2);
// hashes -> data
for( TransactionHash hash : hashes){
atomicTransactionDataList.add(buildTransactionData(bitcoiny,hash));
}
// sort the transaction data by time
List<AtomicTransactionData> sorted
= atomicTransactionDataList.stream()
.sorted((data1, data2) -> data1.timestamp.compareTo(data2.timestamp))
.collect(Collectors.toList());
// build the summary using the first 2 transactions
summary = buildForeignTradeSummary(atAddress, p2shAddress, sorted.get(0), sorted.get(1), bitcoiny);
}
return summary;
}
/**
* Build Foreign Trade Summary
*
* @param p2shValue the p2sh address, original format
* @param lockingTransaction the transaction lock the foreighn coin
* @param unlockingTransaction the transaction to unlock the foreign coin
* @param bitcoiny the blockchain the trade occurred on
*
* @return
*
* @throws ForeignBlockchainException
*/
private static TransactionSummary buildForeignTradeSummary(
String atAddress,
String p2shValue,
AtomicTransactionData lockingTransaction,
AtomicTransactionData unlockingTransaction,
Bitcoiny bitcoiny) throws ForeignBlockchainException {
// get sum of the relevant inputs for each transaction
long lockingTotalInput = getTotalInput(bitcoiny, lockingTransaction.inputs);
long unlockingTotalInput = getTotalInput(bitcoiny, unlockingTransaction.inputs);
// find the address that has output that matches the total input
Optional<Map.Entry<List<String>, Long>> addressValue
= lockingTransaction.valueByAddress.entrySet().stream()
.filter(entry -> entry.getValue() == unlockingTotalInput).findFirst();
// set that matching address, if found
String p2shAddress;
if( addressValue.isPresent() && addressValue.get().getKey().size() == 1 ){
p2shAddress = addressValue.get().getKey().get(0);
}
else {
p2shAddress = "N/A";
}
// build summaries with prepared values
// the fees are the total amount subtracted by the total transaction input
return new TransactionSummary(
atAddress,
p2shValue,
p2shAddress,
lockingTransaction.hash.txHash,
lockingTransaction.timestamp,
lockingTransaction.totalAmount,
lockingTotalInput - lockingTransaction.totalAmount,
lockingTransaction.size,
unlockingTransaction.hash.txHash,
unlockingTransaction.timestamp,
unlockingTransaction.totalAmount,
unlockingTotalInput - unlockingTransaction.totalAmount,
unlockingTransaction.size
);
}
/**
* Build Transaction Data
*
* @param bitcoiny the coin for the transaction
* @param hash the hash for the transaction
*
* @return the data for the transaction
*
* @throws ForeignBlockchainException
*/
private static AtomicTransactionData buildTransactionData( Bitcoiny bitcoiny, TransactionHash hash)
throws ForeignBlockchainException {
BitcoinyTransaction transaction = bitcoiny.getTransaction(hash.txHash);
// destination address list -> value
Map<List<String>, Long> valueByAddress = new HashMap<>();
// for each output in the transaction, index by address list
for( BitcoinyTransaction.Output output : transaction.outputs) {
valueByAddress.put(output.addresses, output.value);
}
return new AtomicTransactionData(
hash,
transaction.timestamp,
transaction.inputs,
valueByAddress,
transaction.totalAmount,
transaction.size);
}
/**
* Get Total Input
*
* Get the sum of all the inputs used in a list of inputs.
*
* @param bitcoiny the coin the inputs belong to
* @param inputs the inputs
*
* @return the sum
*
* @throws ForeignBlockchainException
*/
private static long getTotalInput(Bitcoiny bitcoiny, List<BitcoinyTransaction.Input> inputs)
throws ForeignBlockchainException {
long totalInputOut = 0;
// for each input, add to total input,
// get the indexed transaction output value and add to total value
for( BitcoinyTransaction.Input input : inputs){
BitcoinyTransaction inputOut = bitcoiny.getTransaction(input.outputTxHash);
BitcoinyTransaction.Output output = inputOut.outputs.get(input.outputVout);
totalInputOut += output.value;
}
return totalInputOut;
}
}

View File

@ -0,0 +1,32 @@
package org.qortal.data.crosschain;
import org.qortal.crosschain.BitcoinyTransaction;
import org.qortal.crosschain.TransactionHash;
import java.util.List;
import java.util.Map;
public class AtomicTransactionData {
public final TransactionHash hash;
public final Integer timestamp;
public final List<BitcoinyTransaction.Input> inputs;
public final Map<List<String>, Long> valueByAddress;
public final long totalAmount;
public final int size;
public AtomicTransactionData(
TransactionHash hash,
Integer timestamp,
List<BitcoinyTransaction.Input> inputs,
Map<List<String>, Long> valueByAddress,
long totalAmount,
int size) {
this.hash = hash;
this.timestamp = timestamp;
this.inputs = inputs;
this.valueByAddress = valueByAddress;
this.totalAmount = totalAmount;
this.size = size;
}
}

View File

@ -0,0 +1,106 @@
package org.qortal.data.crosschain;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
@XmlAccessorType(XmlAccessType.FIELD)
public class TransactionSummary {
private String atAddress;
private String p2shValue;
private String p2shAddress;
private String lockingHash;
private Integer lockingTimestamp;
private long lockingTotalAmount;
private long lockingFee;
private int lockingSize;
private String unlockingHash;
private Integer unlockingTimestamp;
private long unlockingTotalAmount;
private long unlockingFee;
private int unlockingSize;
public TransactionSummary(){}
public TransactionSummary(
String atAddress,
String p2shValue,
String p2shAddress,
String lockingHash,
Integer lockingTimestamp,
long lockingTotalAmount,
long lockingFee,
int lockingSize,
String unlockingHash,
Integer unlockingTimestamp,
long unlockingTotalAmount,
long unlockingFee,
int unlockingSize) {
this.atAddress = atAddress;
this.p2shValue = p2shValue;
this.p2shAddress = p2shAddress;
this.lockingHash = lockingHash;
this.lockingTimestamp = lockingTimestamp;
this.lockingTotalAmount = lockingTotalAmount;
this.lockingFee = lockingFee;
this.lockingSize = lockingSize;
this.unlockingHash = unlockingHash;
this.unlockingTimestamp = unlockingTimestamp;
this.unlockingTotalAmount = unlockingTotalAmount;
this.unlockingFee = unlockingFee;
this.unlockingSize = unlockingSize;
}
public String getAtAddress() {
return atAddress;
}
public String getP2shValue() {
return p2shValue;
}
public String getP2shAddress() {
return p2shAddress;
}
public String getLockingHash() {
return lockingHash;
}
public Integer getLockingTimestamp() {
return lockingTimestamp;
}
public long getLockingTotalAmount() {
return lockingTotalAmount;
}
public long getLockingFee() {
return lockingFee;
}
public int getLockingSize() {
return lockingSize;
}
public String getUnlockingHash() {
return unlockingHash;
}
public Integer getUnlockingTimestamp() {
return unlockingTimestamp;
}
public long getUnlockingTotalAmount() {
return unlockingTotalAmount;
}
public long getUnlockingFee() {
return unlockingFee;
}
public int getUnlockingSize() {
return unlockingSize;
}
}