mirror of
https://github.com/Qortal/qortal.git
synced 2025-05-05 17:27:52 +00:00
WIP: trade-bot MESSAGE support
This commit is contained in:
parent
593b61ea4b
commit
a6fa4fc613
@ -1,31 +1,39 @@
|
|||||||
package org.qortal.controller;
|
package org.qortal.controller;
|
||||||
|
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Random;
|
import java.util.Random;
|
||||||
|
|
||||||
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.bitcoinj.core.Address;
|
import org.bitcoinj.core.Address;
|
||||||
|
import org.bitcoinj.core.Coin;
|
||||||
import org.bitcoinj.core.ECKey;
|
import org.bitcoinj.core.ECKey;
|
||||||
import org.bitcoinj.core.LegacyAddress;
|
import org.bitcoinj.core.LegacyAddress;
|
||||||
import org.bitcoinj.core.NetworkParameters;
|
import org.bitcoinj.core.NetworkParameters;
|
||||||
import org.qortal.account.PrivateKeyAccount;
|
import org.qortal.account.PrivateKeyAccount;
|
||||||
import org.qortal.account.PublicKeyAccount;
|
import org.qortal.account.PublicKeyAccount;
|
||||||
import org.qortal.api.model.TradeBotCreateRequest;
|
import org.qortal.api.model.TradeBotCreateRequest;
|
||||||
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
import org.qortal.asset.Asset;
|
import org.qortal.asset.Asset;
|
||||||
import org.qortal.crosschain.BTC;
|
import org.qortal.crosschain.BTC;
|
||||||
import org.qortal.crosschain.BTCACCT;
|
import org.qortal.crosschain.BTCACCT;
|
||||||
import org.qortal.crypto.Crypto;
|
import org.qortal.crypto.Crypto;
|
||||||
|
import org.qortal.data.at.ATData;
|
||||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||||
import org.qortal.data.crosschain.TradeBotData;
|
import org.qortal.data.crosschain.TradeBotData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.group.Group;
|
import org.qortal.group.Group;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
import org.qortal.repository.Repository;
|
import org.qortal.repository.Repository;
|
||||||
import org.qortal.repository.RepositoryManager;
|
import org.qortal.repository.RepositoryManager;
|
||||||
import org.qortal.transaction.DeployAtTransaction;
|
import org.qortal.transaction.DeployAtTransaction;
|
||||||
|
import org.qortal.transaction.MessageTransaction;
|
||||||
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
|
import org.qortal.transaction.Transaction.ValidationResult;
|
||||||
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
import org.qortal.transform.transaction.DeployAtTransactionTransformer;
|
||||||
import org.qortal.utils.NTP;
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
@ -144,17 +152,109 @@ public class TradeBot {
|
|||||||
|
|
||||||
for (TradeBotData tradeBotData : allTradeBotData)
|
for (TradeBotData tradeBotData : allTradeBotData)
|
||||||
switch (tradeBotData.getState()) {
|
switch (tradeBotData.getState()) {
|
||||||
|
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||||
|
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||||
|
break;
|
||||||
|
|
||||||
case BOB_WAITING_FOR_MESSAGE:
|
case BOB_WAITING_FOR_MESSAGE:
|
||||||
handleBobWaitingForMessage(repository, tradeBotData);
|
handleBobWaitingForMessage(repository, tradeBotData);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
LOGGER.warn(() -> String.format("Unhandled trade-bot state %s", tradeBotData.getState().name()));
|
||||||
}
|
}
|
||||||
} catch (DataException e) {
|
} catch (DataException e) {
|
||||||
LOGGER.error("Couldn't run trade bot due to repository issue", e);
|
LOGGER.error("Couldn't run trade bot due to repository issue", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) {
|
private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
|
||||||
|
if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
|
||||||
|
return;
|
||||||
|
|
||||||
|
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_MESSAGE);
|
||||||
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData) {
|
||||||
|
// Fetch AT so we can determine trade start timestamp
|
||||||
|
ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
|
||||||
|
if (atData == null) {
|
||||||
|
LOGGER.error(String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
long tradeStartTimestamp = atData.getCreation();
|
||||||
|
|
||||||
|
String address = Crypto.toAddress(tradeBotData.getTradeNativePublicKey());
|
||||||
|
List<MessageTransactionData> messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(address, null, null, null);
|
||||||
|
|
||||||
|
// Skip past previously processed messages
|
||||||
|
if (tradeBotData.getLastTransactionSignature() != null)
|
||||||
|
for (int i = 0; i < messageTransactionsData.size(); ++i)
|
||||||
|
if (Arrays.equals(messageTransactionsData.get(i).getSignature(), tradeBotData.getLastTransactionSignature())) {
|
||||||
|
messageTransactionsData.subList(0, i + 1).clear();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!messageTransactionsData.isEmpty()) {
|
||||||
|
MessageTransactionData messageTransactionData = messageTransactionsData.remove(0);
|
||||||
|
tradeBotData.setLastTransactionSignature(messageTransactionData.getSignature());
|
||||||
|
|
||||||
|
if (messageTransactionData.isText())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Could enforce encryption here
|
||||||
|
|
||||||
|
// We're expecting: HASH160(secret) + Alice's Bitcoin pubkeyhash
|
||||||
|
byte[] messageData = messageTransactionData.getData();
|
||||||
|
|
||||||
|
if (messageData.length != 40)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
byte[] aliceSecretHash = new byte[20];
|
||||||
|
System.arraycopy(messageData, 0, aliceSecretHash, 0, 20);
|
||||||
|
|
||||||
|
byte[] aliceForeignPublicKeyHash = new byte[20];
|
||||||
|
System.arraycopy(messageData, 20, aliceForeignPublicKeyHash, 0, 20);
|
||||||
|
|
||||||
|
// Determine P2SH address and confirm funded
|
||||||
|
int lockTime = (int) (tradeStartTimestamp / 1000L + tradeBotData.getTradeTimeout() / 4 * 60); // First P2SH locktime is ¼ of timeout period
|
||||||
|
byte[] redeemScript = BTCACCT.buildScript(aliceForeignPublicKeyHash, lockTime, tradeBotData.getTradeForeignPublicKeyHash(), aliceSecretHash);
|
||||||
|
String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScript);
|
||||||
|
|
||||||
|
Long balance = BTC.getInstance().getBalance(p2shAddress);
|
||||||
|
if (balance == null || balance < tradeBotData.getBitcoinAmount())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Good to go - send MESSAGE to AT
|
||||||
|
|
||||||
|
byte[] aliceNativePublicKeyHash = Crypto.hash160(messageTransactionData.getCreatorPublicKey());
|
||||||
|
|
||||||
|
// Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
|
||||||
|
byte[] outgoingMessageData = new byte[96];
|
||||||
|
System.arraycopy(aliceSecretHash, 0, outgoingMessageData, 0, 20);
|
||||||
|
System.arraycopy(aliceForeignPublicKeyHash, 0, outgoingMessageData, 32, 20);
|
||||||
|
System.arraycopy(aliceNativePublicKeyHash, 0, outgoingMessageData, 64, 20);
|
||||||
|
|
||||||
|
PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
|
||||||
|
MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, tradeBotData.getAtAddress(), outgoingMessageData, false, false);
|
||||||
|
|
||||||
|
outgoingMessageTransaction.computeNonce();
|
||||||
|
outgoingMessageTransaction.sign(sender);
|
||||||
|
|
||||||
|
ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
|
||||||
|
|
||||||
|
if (result != ValidationResult.OK) {
|
||||||
|
LOGGER.error(String.format("Unable to send MESSAGE to AT '%s': %s", tradeBotData.getAtAddress(), result.name()));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_P2SH_B);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.getCrossChainRepository().save(tradeBotData);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -16,7 +16,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
|
|||||||
public class TradeBotData {
|
public class TradeBotData {
|
||||||
|
|
||||||
public enum State {
|
public enum State {
|
||||||
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_WAITING_FOR_P2SH_A(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50),
|
BOB_WAITING_FOR_AT_CONFIRM(10), BOB_WAITING_FOR_MESSAGE(20), BOB_SENDING_MESSAGE_TO_AT(30), BOB_WAITING_FOR_P2SH_B(40), BOB_WAITING_FOR_AT_REDEEM(50),
|
||||||
ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
|
ALICE_WAITING_FOR_P2SH_A(110), ALICE_WAITING_FOR_AT_LOCK(120), ALICE_WATCH_P2SH_B(130);
|
||||||
|
|
||||||
public final int value;
|
public final int value;
|
||||||
|
@ -6,6 +6,7 @@ import java.util.Map;
|
|||||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
import org.qortal.data.group.GroupApprovalData;
|
import org.qortal.data.group.GroupApprovalData;
|
||||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||||
import org.qortal.transaction.Transaction.TransactionType;
|
import org.qortal.transaction.Transaction.TransactionType;
|
||||||
@ -107,6 +108,18 @@ public interface TransactionRepository {
|
|||||||
*/
|
*/
|
||||||
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
public byte[] getLatestAutoUpdateTransaction(TransactionType txType, int txGroupId, Integer service) throws DataException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns list of MESSAGE transaction data matching recipient.
|
||||||
|
* @param recipient
|
||||||
|
* @param limit
|
||||||
|
* @param offset
|
||||||
|
* @param reverse
|
||||||
|
* @return
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||||
|
Integer limit, Integer offset, Boolean reverse) throws DataException;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns list of transactions relating to specific asset ID.
|
* Returns list of transactions relating to specific asset ID.
|
||||||
*
|
*
|
||||||
|
@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
|
|||||||
import org.qortal.data.group.GroupApprovalData;
|
import org.qortal.data.group.GroupApprovalData;
|
||||||
import org.qortal.data.transaction.BaseTransactionData;
|
import org.qortal.data.transaction.BaseTransactionData;
|
||||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||||
|
import org.qortal.data.transaction.MessageTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
@ -630,6 +631,43 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<MessageTransactionData> getMessagesByRecipient(String recipient,
|
||||||
|
Integer limit, Integer offset, Boolean reverse) throws DataException {
|
||||||
|
StringBuilder sql = new StringBuilder(1024);
|
||||||
|
sql.append("SELECT signature from MessageTransactions "
|
||||||
|
+ "JOIN Transactions USING (signature) "
|
||||||
|
+ "JOIN BlockTransactions ON transaction_signature = signature "
|
||||||
|
+ "WHERE recipient = ?");
|
||||||
|
|
||||||
|
sql.append("ORDER BY Transactions.created_when");
|
||||||
|
sql.append((reverse == null || !reverse) ? " ASC" : " DESC");
|
||||||
|
|
||||||
|
HSQLDBRepository.limitOffsetSql(sql, limit, offset);
|
||||||
|
|
||||||
|
List<MessageTransactionData> messageTransactionsData = new ArrayList<>();
|
||||||
|
|
||||||
|
try (ResultSet resultSet = this.repository.checkedExecute(sql.toString(), recipient)) {
|
||||||
|
if (resultSet == null)
|
||||||
|
return messageTransactionsData;
|
||||||
|
|
||||||
|
do {
|
||||||
|
byte[] signature = resultSet.getBytes(1);
|
||||||
|
|
||||||
|
TransactionData transactionData = this.fromSignature(signature);
|
||||||
|
if (transactionData == null || transactionData.getType() != TransactionType.MESSAGE)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
messageTransactionsData.add((MessageTransactionData) transactionData);
|
||||||
|
} while (resultSet.next());
|
||||||
|
|
||||||
|
return messageTransactionsData;
|
||||||
|
} catch (SQLException e) {
|
||||||
|
throw new DataException("Unable to fetch trade-bot messages from repository", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||||
throws DataException {
|
throws DataException {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user