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:
parent
da1ea9fe2c
commit
d4b0d47c90
@ -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",
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
*
|
||||
|
Loading…
x
Reference in New Issue
Block a user