3
0
mirror of https://github.com/Qortal/qortal.git synced 2025-02-12 02:05:50 +00:00

Support for responding to multiple crosschain sell offers.

This commit is contained in:
kennycud 2024-07-25 15:18:11 -07:00
parent da1ea9fe2c
commit d4b0d47c90
3 changed files with 201 additions and 0 deletions

View File

@ -17,12 +17,14 @@ import org.qortal.api.ApiExceptionFactory;
import org.qortal.api.Security;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.api.model.crosschain.TradeBotRespondRequest;
import org.qortal.api.model.crosschain.TradeBotRespondRequests;
import org.qortal.asset.Asset;
import org.qortal.controller.Controller;
import org.qortal.controller.tradebot.AcctTradeBot;
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.ForeignBlockchain;
import org.qortal.crosschain.SupportedBlockchain;
import org.qortal.crypto.Crypto;
@ -42,8 +44,10 @@ import javax.servlet.http.HttpServletRequest;
import javax.ws.rs.*;
import javax.ws.rs.core.Context;
import javax.ws.rs.core.MediaType;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@Path("/crosschain/tradebot")
@ -187,6 +191,39 @@ public class CrossChainTradeBotResource {
public String tradeBotResponder(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequest tradeBotRespondRequest) {
Security.checkApiCallAllowed(request);
return createTradeBotResponse(tradeBotRespondRequest);
}
@POST
@Path("/respondmultiple")
@Operation(
summary = "Respond to multiple trade offers. NOTE: WILL SPEND FUNDS!)",
description = "Start a new trade-bot entry to respond to chosen trade offers.",
requestBody = @RequestBody(
required = true,
content = @Content(
mediaType = MediaType.APPLICATION_JSON,
schema = @Schema(
implementation = TradeBotRespondRequests.class
)
)
),
responses = {
@ApiResponse(
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
)
}
)
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
@SuppressWarnings("deprecation")
@SecurityRequirement(name = "apiKey")
public String tradeBotResponderMultiple(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequests tradeBotRespondRequest) {
Security.checkApiCallAllowed(request);
return createTradeBotResponseMultiple(tradeBotRespondRequest);
}
private String createTradeBotResponse(TradeBotRespondRequest tradeBotRespondRequest) {
final String atAddress = tradeBotRespondRequest.atAddress;
// We prefer foreignKey to deprecated xprv58
@ -257,6 +294,96 @@ public class CrossChainTradeBotResource {
}
}
private String createTradeBotResponseMultiple(TradeBotRespondRequests respondRequests) {
try (final Repository repository = RepositoryManager.getRepository()) {
if (respondRequests.foreignKey == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
List<CrossChainTradeData> crossChainTradeDataList = new ArrayList<>(respondRequests.addresses.size());
Optional<ACCT> acct = Optional.empty();
for(String atAddress : respondRequests.addresses ) {
if (atAddress == null || !Crypto.isValidAtAddress(atAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (respondRequests.receivingAddress == null || !Crypto.isValidAddress(respondRequests.receivingAddress))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
// Extract data from cross-chain trading AT
ATData atData = fetchAtDataWithChecking(repository, atAddress);
// TradeBot uses AT's code hash to map to ACCT
ACCT acctUsingAtData = TradeBot.getInstance().getAcctUsingAtData(atData);
if (acctUsingAtData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
// if the optional is empty,
// then ensure the ACCT blockchain is a Bitcoiny blockchain and fill the optional
else if( acct.isEmpty() ) {
if( !(acctUsingAtData.getBlockchain() instanceof Bitcoiny) )
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
acct = Optional.of(acctUsingAtData);
}
// if the optional is filled, then ensure it is equal to the AT in this iteration
else if( !acctUsingAtData.getCodeBytesHash().equals(acct.get().getCodeBytesHash()) )
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
if (!acctUsingAtData.getBlockchain().isValidWalletKey(respondRequests.foreignKey))
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
CrossChainTradeData crossChainTradeData = acctUsingAtData.populateTradeData(repository, atData);
if (crossChainTradeData == null)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
if (crossChainTradeData.mode != AcctMode.OFFERING)
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
// Check if there is a buy or a cancel request in progress for this trade
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
List<TransactionData> unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false);
for (TransactionData transactionData : unconfirmed) {
MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) {
// There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation.");
}
}
crossChainTradeDataList.add(crossChainTradeData);
}
AcctTradeBot.ResponseResult result
= TradeBot.getInstance().startResponseMultiple(
repository,
acct.get(),
crossChainTradeDataList,
respondRequests.receivingAddress,
respondRequests.foreignKey,
(Bitcoiny) acct.get().getBlockchain());
switch (result) {
case OK:
return "true";
case BALANCE_ISSUE:
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
case NETWORK_ISSUE:
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
default:
return "false";
}
} catch (DataException e) {
throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage());
}
}
@DELETE
@Operation(
summary = "Delete completed trade",

View File

@ -215,6 +215,41 @@ public class TradeBot implements Listener {
return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress);
}
/**
* Creates a trade-bot entries from the 'Alice' viewpoint,
* i.e. matching foreign blockchain currency to existing QORT offers.
* <p>
* Requires chosen trade offers from Bob, passed by <tt>crossChainTradeData</tt>
* and access to a foreign blockchain wallet via <tt>foreignKey</tt>.
* <p>
* @param repository
* @param crossChainTradeDataList chosen trade OFFERs that Alice wants to match
* @param receiveAddress Alice's Qortal address to receive her QORT
* @param foreignKey foreign blockchain wallet key
* @param bitcoiny
* @throws DataException
*/
public ResponseResult startResponseMultiple(
Repository repository,
ACCT acct,
List<CrossChainTradeData> crossChainTradeDataList,
String receiveAddress,
String foreignKey,
Bitcoiny bitcoiny) throws DataException {
AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
if (acctTradeBot == null) {
LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot for %s", acct.getBlockchain()));
return ResponseResult.NETWORK_ISSUE;
}
for( CrossChainTradeData tradeData : crossChainTradeDataList) {
// Check Alice doesn't already have an existing, on-going trade-bot entry for this AT.
if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(tradeData.qortalAtAddress, acctTradeBot.getEndStates()))
return ResponseResult.TRADE_ALREADY_EXISTS;
}
return TradeBotUtils.startResponseMultiple(repository, acct, crossChainTradeDataList, receiveAddress, foreignKey, bitcoiny);
}
public boolean deleteEntry(Repository repository, byte[] tradePrivateKey) throws DataException {
TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
if (tradeBotData == null)

View File

@ -343,6 +343,45 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
/**
* Returns bitcoinj transaction sending the recipient's amount to each recipient given.
*
*
* @param xprv58 the private master key
* @param amountByRecipient each amount to send indexed by the recipient to send to
* @param feePerByte the satoshis per byte
*
* @return the completed transaction, ready to broadcast
*/
public Transaction buildSpendMultiple(String xprv58, Map<String, Long> amountByRecipient, Long feePerByte) {
Context.propagate(bitcoinjContext);
Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
Transaction transaction = new Transaction(this.params);
for(Map.Entry<String, Long> amountForRecipient : amountByRecipient.entrySet()) {
Address destination = Address.fromString(this.params, amountForRecipient.getKey());
transaction.addOutput(Coin.valueOf(amountForRecipient.getValue()), destination);
}
SendRequest sendRequest = SendRequest.forTx(transaction);
if (feePerByte != null)
sendRequest.feePerKb = Coin.valueOf(feePerByte * 1000L); // Note: 1000 not 1024
else
// Allow override of default for TestNet3, etc.
sendRequest.feePerKb = this.getFeePerKb();
try {
wallet.completeTx(sendRequest);
return sendRequest.tx;
} catch (InsufficientMoneyException e) {
return null;
}
}
/**
* Get Spending Candidate Addresses
*