From a6fa4fc6134e5f0f33b90994d36418f73e3acd23 Mon Sep 17 00:00:00 2001 From: catbref Date: Thu, 11 Jun 2020 14:54:41 +0100 Subject: [PATCH] WIP: trade-bot MESSAGE support --- .../java/org/qortal/controller/TradeBot.java | 104 +++++++++++++++++- .../qortal/data/crosschain/TradeBotData.java | 2 +- .../repository/TransactionRepository.java | 13 +++ .../HSQLDBTransactionRepository.java | 38 +++++++ 4 files changed, 154 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 460b29c7..4965ddae 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -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 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 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); } } diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index c6887060..3c8b4f63 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -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; diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java index 56e51be1..38856d24 100644 --- a/src/main/java/org/qortal/repository/TransactionRepository.java +++ b/src/main/java/org/qortal/repository/TransactionRepository.java @@ -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 getMessagesByRecipient(String recipient, + Integer limit, Integer offset, Boolean reverse) throws DataException; + /** * Returns list of transactions relating to specific asset ID. * diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java index 0ab6ed94..bf9c88aa 100644 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java @@ -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 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 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 getAssetTransactions(long assetId, ConfirmationStatus confirmationStatus, Integer limit, Integer offset, Boolean reverse) throws DataException {