mirror of
https://github.com/Qortal/qortal.git
synced 2025-04-23 19:37:51 +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.Security;
|
||||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||||
import org.qortal.api.model.crosschain.TradeBotRespondRequest;
|
import org.qortal.api.model.crosschain.TradeBotRespondRequest;
|
||||||
|
import org.qortal.api.model.crosschain.TradeBotRespondRequests;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.controller.Controller;
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.controller.tradebot.AcctTradeBot;
|
import org.qortal.controller.tradebot.AcctTradeBot;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
import org.qortal.crosschain.ACCT;
|
import org.qortal.crosschain.ACCT;
|
||||||
import org.qortal.crosschain.AcctMode;
|
import org.qortal.crosschain.AcctMode;
|
||||||
|
import org.qortal.crosschain.Bitcoiny;
|
||||||
import org.qortal.crosschain.ForeignBlockchain;
|
import org.qortal.crosschain.ForeignBlockchain;
|
||||||
import org.qortal.crosschain.SupportedBlockchain;
|
import org.qortal.crosschain.SupportedBlockchain;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
@ -42,8 +44,10 @@ import javax.servlet.http.HttpServletRequest;
|
|||||||
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.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Optional;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Path("/crosschain/tradebot")
|
@Path("/crosschain/tradebot")
|
||||||
@ -187,6 +191,39 @@ public class CrossChainTradeBotResource {
|
|||||||
public String tradeBotResponder(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequest tradeBotRespondRequest) {
|
public String tradeBotResponder(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequest tradeBotRespondRequest) {
|
||||||
Security.checkApiCallAllowed(request);
|
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;
|
final String atAddress = tradeBotRespondRequest.atAddress;
|
||||||
|
|
||||||
// We prefer foreignKey to deprecated xprv58
|
// 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
|
@DELETE
|
||||||
@Operation(
|
@Operation(
|
||||||
summary = "Delete completed trade",
|
summary = "Delete completed trade",
|
||||||
|
@ -215,6 +215,41 @@ public class TradeBot implements Listener {
|
|||||||
return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress);
|
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 {
|
public boolean deleteEntry(Repository repository, byte[] tradePrivateKey) throws DataException {
|
||||||
TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
|
TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
|
||||||
if (tradeBotData == null)
|
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
|
* Get Spending Candidate Addresses
|
||||||
*
|
*
|
||||||
|
Loading…
x
Reference in New Issue
Block a user