forked from Qortal/qortal
WIP: trade-bot MESSAGE support
This commit is contained in:
parent
593b61ea4b
commit
a6fa4fc613
@ -1,31 +1,39 @@
|
||||
package org.qortal.controller;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
import java.util.Random;
|
||||
|
||||
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.core.ECKey;
|
||||
import org.bitcoinj.core.LegacyAddress;
|
||||
import org.bitcoinj.core.NetworkParameters;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.TradeBotCreateRequest;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.BTC;
|
||||
import org.qortal.crosschain.BTCACCT;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
import org.qortal.data.crosschain.CrossChainTradeData;
|
||||
import org.qortal.data.crosschain.TradeBotData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.DeployAtTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.group.Group;
|
||||
import org.qortal.repository.DataException;
|
||||
import org.qortal.repository.Repository;
|
||||
import org.qortal.repository.RepositoryManager;
|
||||
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.utils.NTP;
|
||||
|
||||
@ -141,20 +149,112 @@ public class TradeBot {
|
||||
// Get repo for trade situations
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
List<TradeBotData> allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
|
||||
|
||||
|
||||
for (TradeBotData tradeBotData : allTradeBotData)
|
||||
switch (tradeBotData.getState()) {
|
||||
case BOB_WAITING_FOR_AT_CONFIRM:
|
||||
handleBobWaitingForAtConfirm(repository, tradeBotData);
|
||||
break;
|
||||
|
||||
case BOB_WAITING_FOR_MESSAGE:
|
||||
handleBobWaitingForMessage(repository, tradeBotData);
|
||||
break;
|
||||
|
||||
default:
|
||||
LOGGER.warn(() -> String.format("Unhandled trade-bot state %s", tradeBotData.getState().name()));
|
||||
}
|
||||
} catch (DataException e) {
|
||||
LOGGER.error("Couldn't run trade bot due to repository issue", e);
|
||||
}
|
||||
}
|
||||
|
||||
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 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);
|
||||
|
||||
public final int value;
|
||||
|
@ -6,6 +6,7 @@ import java.util.Map;
|
||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
import org.qortal.transaction.Transaction.TransactionType;
|
||||
@ -107,6 +108,18 @@ public interface TransactionRepository {
|
||||
*/
|
||||
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.
|
||||
*
|
||||
|
@ -19,6 +19,7 @@ import org.qortal.data.PaymentData;
|
||||
import org.qortal.data.group.GroupApprovalData;
|
||||
import org.qortal.data.transaction.BaseTransactionData;
|
||||
import org.qortal.data.transaction.GroupApprovalTransactionData;
|
||||
import org.qortal.data.transaction.MessageTransactionData;
|
||||
import org.qortal.data.transaction.TransactionData;
|
||||
import org.qortal.data.transaction.TransferAssetTransactionData;
|
||||
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
|
||||
public List<TransactionData> getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse)
|
||||
throws DataException {
|
||||
|
Loading…
Reference in New Issue
Block a user