transactions = repository.getTransactionRepository().getUnconfirmedTransactions();
- for (TransactionData transactionData : transactions)
- if (now >= Transaction.getDeadline(transactionData)) {
- LOGGER.info(String.format("Deleting expired, unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
+ for (TransactionData transactionData : transactions) {
+ Transaction transaction = Transaction.fromData(repository, transactionData);
+
+ if (now >= transaction.getDeadline()) {
+ LOGGER.info(() -> String.format("Deleting expired, unconfirmed transaction %s", Base58.encode(transactionData.getSignature())));
repository.getTransactionRepository().delete(transactionData);
}
+ }
repository.saveChanges();
} catch (DataException e) {
diff --git a/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java b/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java
index e9b846fc..b32a2b06 100644
--- a/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java
+++ b/src/main/java/org/qortal/controller/OnlineAccountsSignaturesTrimmer.java
@@ -23,6 +23,8 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
// Don't even start trimming until initial rush has ended
Thread.sleep(INITIAL_SLEEP_PERIOD);
+ int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
+
while (!Controller.isStopping()) {
repository.discardChanges();
@@ -40,8 +42,6 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
long upperTrimmableTimestamp = NTP.getTime() - BlockChain.getInstance().getOnlineAccountSignaturesMaxLifetime();
int upperTrimmableHeight = repository.getBlockRepository().getHeightFromTimestamp(upperTrimmableTimestamp);
- int trimStartHeight = repository.getBlockRepository().getOnlineAccountsSignaturesTrimHeight();
-
int upperBatchHeight = trimStartHeight + Settings.getInstance().getOnlineSignaturesTrimBatchSize();
int upperTrimHeight = Math.min(upperBatchHeight, upperTrimmableHeight);
@@ -52,16 +52,20 @@ public class OnlineAccountsSignaturesTrimmer implements Runnable {
repository.saveChanges();
if (numSigsTrimmed > 0) {
+ final int finalTrimStartHeight = trimStartHeight;
LOGGER.debug(() -> String.format("Trimmed %d online accounts signature%s between blocks %d and %d",
numSigsTrimmed, (numSigsTrimmed != 1 ? "s" : ""),
- trimStartHeight, upperTrimHeight));
+ finalTrimStartHeight, upperTrimHeight));
} else {
// Can we move onto next batch?
if (upperTrimmableHeight > upperBatchHeight) {
- repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(upperBatchHeight);
+ trimStartHeight = upperBatchHeight;
+
+ repository.getBlockRepository().setOnlineAccountsSignaturesTrimHeight(trimStartHeight);
repository.saveChanges();
- LOGGER.debug(() -> String.format("Bumping online accounts signatures trim height to %d", upperBatchHeight));
+ final int finalTrimStartHeight = trimStartHeight;
+ LOGGER.debug(() -> String.format("Bumping online accounts signatures base trim height to %d", finalTrimStartHeight));
}
}
}
diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java
index cba61360..60ca14b7 100644
--- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java
@@ -387,26 +387,32 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
break;
case ALICE_WAITING_FOR_P2SH_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceWaitingForP2shA(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_MESSAGE:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
break;
case ALICE_WAITING_FOR_AT_LOCK:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_P2SH_B:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleBobWaitingForP2shB(repository, tradeBotData, atData, tradeData);
break;
case ALICE_WATCH_P2SH_B:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceWatchingP2shB(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_AT_REDEEM:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
break;
@@ -415,10 +421,12 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
break;
case ALICE_REFUNDING_B:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceRefundingP2shB(repository, tradeBotData, atData, tradeData);
break;
case ALICE_REFUNDING_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
break;
diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
index 9e10cb7f..5fc8b3ab 100644
--- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
@@ -384,14 +384,17 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
break;
case BOB_WAITING_FOR_MESSAGE:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
break;
case ALICE_WAITING_FOR_AT_LOCK:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
break;
case BOB_WAITING_FOR_AT_REDEEM:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
break;
@@ -400,6 +403,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
break;
case ALICE_REFUNDING_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData);
handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
break;
diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
index 558d0e35..68cb7ed6 100644
--- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
@@ -2,6 +2,7 @@ package org.qortal.controller.tradebot;
import java.awt.TrayIcon.MessageType;
import java.security.SecureRandom;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -23,16 +24,25 @@ import org.qortal.crosschain.SupportedBlockchain;
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.PresenceTransactionData;
import org.qortal.event.Event;
import org.qortal.event.EventBus;
import org.qortal.event.Listener;
+import org.qortal.group.Group;
import org.qortal.gui.SysTray;
import org.qortal.repository.DataException;
import org.qortal.repository.Repository;
import org.qortal.repository.RepositoryManager;
import org.qortal.settings.Settings;
+import org.qortal.transaction.PresenceTransaction;
+import org.qortal.transaction.PresenceTransaction.PresenceType;
+import org.qortal.transaction.Transaction.ValidationResult;
+import org.qortal.transform.transaction.TransactionTransformer;
import org.qortal.utils.NTP;
+import com.google.common.primitives.Longs;
+
/**
* Performing cross-chain trading steps on behalf of user.
*
@@ -73,6 +83,8 @@ public class TradeBot implements Listener {
private static TradeBot instance;
+ private final Map presenceTimestampsByAtAddress = Collections.synchronizedMap(new HashMap<>());
+
private TradeBot() {
EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
}
@@ -292,4 +304,41 @@ public class TradeBot implements Listener {
return acctTradeBotSupplier.get();
}
+ // PRESENCE-related
+ /*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData) throws DataException {
+ String key = tradeBotData.getAtAddress();
+
+ long now = NTP.getTime();
+ long threshold = now - PresenceType.TRADE_BOT.getLifetime();
+
+ long timestamp = presenceTimestampsByAtAddress.compute(key, (k, v) -> (v == null || v < threshold) ? now : v);
+
+ // If timestamp hasn't been updated then nothing to do
+ if (timestamp != now)
+ return;
+
+ PrivateKeyAccount tradeNativeAccount = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+
+ int txGroupId = Group.NO_GROUP;
+ byte[] reference = new byte[TransactionTransformer.SIGNATURE_LENGTH];
+ byte[] creatorPublicKey = tradeNativeAccount.getPublicKey();
+ long fee = 0L;
+
+ BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null);
+
+ int nonce = 0;
+ byte[] timestampSignature = tradeNativeAccount.sign(Longs.toByteArray(timestamp));
+
+ PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature);
+
+ PresenceTransaction presenceTransaction = new PresenceTransaction(repository, transactionData);
+ presenceTransaction.computeNonce();
+
+ presenceTransaction.sign(tradeNativeAccount);
+
+ ValidationResult result = presenceTransaction.importAsUnconfirmed();
+ if (result != ValidationResult.OK)
+ LOGGER.debug(() -> String.format("Unable to build trade-bot PRESENCE transaction for %s: %s", tradeBotData.getAtAddress(), result.name()));
+ }
+
}
diff --git a/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java b/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java
new file mode 100644
index 00000000..001bd5b4
--- /dev/null
+++ b/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java
@@ -0,0 +1,73 @@
+package org.qortal.data.transaction;
+
+import javax.xml.bind.Unmarshaller;
+import javax.xml.bind.annotation.XmlAccessType;
+import javax.xml.bind.annotation.XmlAccessorType;
+
+import org.qortal.transaction.PresenceTransaction.PresenceType;
+import org.qortal.transaction.Transaction.TransactionType;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
+
+// All properties to be converted to JSON via JAXB
+@XmlAccessorType(XmlAccessType.FIELD)
+@Schema(allOf = { TransactionData.class })
+public class PresenceTransactionData extends TransactionData {
+
+ // Properties
+ @Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP")
+ private byte[] senderPublicKey;
+
+ @Schema(accessMode = AccessMode.READ_ONLY)
+ private int nonce;
+
+ private PresenceType presenceType;
+
+ @Schema(description = "timestamp signature", example = "2yGEbwRFyhPZZckKA")
+ private byte[] timestampSignature;
+
+ // Constructors
+
+ // For JAXB
+ protected PresenceTransactionData() {
+ super(TransactionType.PRESENCE);
+ }
+
+ public void afterUnmarshal(Unmarshaller u, Object parent) {
+ this.creatorPublicKey = this.senderPublicKey;
+ }
+
+ public PresenceTransactionData(BaseTransactionData baseTransactionData,
+ int nonce, PresenceType presenceType, byte[] timestampSignature) {
+ super(TransactionType.PRESENCE, baseTransactionData);
+
+ this.senderPublicKey = baseTransactionData.creatorPublicKey;
+ this.nonce = nonce;
+ this.presenceType = presenceType;
+ this.timestampSignature = timestampSignature;
+ }
+
+ // Getters/Setters
+
+ public byte[] getSenderPublicKey() {
+ return this.senderPublicKey;
+ }
+
+ public int getNonce() {
+ return this.nonce;
+ }
+
+ public void setNonce(int nonce) {
+ this.nonce = nonce;
+ }
+
+ public PresenceType getPresenceType() {
+ return this.presenceType;
+ }
+
+ public byte[] getTimestampSignature() {
+ return this.timestampSignature;
+ }
+
+}
diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java
index 397693b8..060901f2 100644
--- a/src/main/java/org/qortal/data/transaction/TransactionData.java
+++ b/src/main/java/org/qortal/data/transaction/TransactionData.java
@@ -40,7 +40,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode;
GroupApprovalTransactionData.class, SetGroupTransactionData.class,
UpdateAssetTransactionData.class,
AccountFlagsTransactionData.class, RewardShareTransactionData.class,
- AccountLevelTransactionData.class, ChatTransactionData.class
+ AccountLevelTransactionData.class, ChatTransactionData.class, PresenceTransactionData.class
})
//All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
diff --git a/src/main/java/org/qortal/repository/RepositoryManager.java b/src/main/java/org/qortal/repository/RepositoryManager.java
index 9f5cf239..2b6e637b 100644
--- a/src/main/java/org/qortal/repository/RepositoryManager.java
+++ b/src/main/java/org/qortal/repository/RepositoryManager.java
@@ -4,6 +4,10 @@ public abstract class RepositoryManager {
private static RepositoryFactory repositoryFactory = null;
+ public static RepositoryFactory getRepositoryFactory() {
+ return repositoryFactory;
+ }
+
public static void setRepositoryFactory(RepositoryFactory newRepositoryFactory) {
repositoryFactory = newRepositoryFactory;
}
diff --git a/src/main/java/org/qortal/repository/TransactionRepository.java b/src/main/java/org/qortal/repository/TransactionRepository.java
index 68d0cdac..acde78df 100644
--- a/src/main/java/org/qortal/repository/TransactionRepository.java
+++ b/src/main/java/org/qortal/repository/TransactionRepository.java
@@ -239,6 +239,18 @@ public interface TransactionRepository {
return getUnconfirmedTransactions(null, null, null);
}
+ /**
+ * Returns list of unconfirmed transactions with specified type and/or creator.
+ *
+ * At least one of txType or creatorPublicKey must be non-null.
+ *
+ * @param txType optional
+ * @param creatorPublicKey optional
+ * @return list of transactions, or empty if none.
+ * @throws DataException
+ */
+ public List getUnconfirmedTransactions(TransactionType txType, byte[] creatorPublicKey) throws DataException;
+
/**
* Remove transaction from unconfirmed transactions pile.
*
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
index bffc0d5a..3203c954 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java
@@ -807,6 +807,13 @@ public class HSQLDBDatabaseUpdates {
stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN hash_of_secret SET NULL");
break;
+ case 32:
+ // PRESENCE transactions
+ stmt.execute("CREATE TABLE IF NOT EXISTS PresenceTransactions ("
+ + "signature Signature, nonce INT NOT NULL, presence_type INT NOT NULL, "
+ + "timestamp_signature Signature NOT NULL, " + TRANSACTION_KEYS + ")");
+ break;
+
default:
// nothing to do
return false;
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
index 7c694b53..a282614e 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
@@ -60,7 +60,8 @@ public class HSQLDBRepository implements Repository {
protected List sqlStatements;
protected long sessionId;
protected final Map preparedStatementCache = new HashMap<>();
- protected final Object trimHeightsLock = new Object();
+ // We want the same object corresponding to the actual DB
+ protected final Object trimHeightsLock = RepositoryManager.getRepositoryFactory();
private final ATRepository atRepository = new HSQLDBATRepository(this);
private final AccountRepository accountRepository = new HSQLDBAccountRepository(this);
diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java
new file mode 100644
index 00000000..309ffcad
--- /dev/null
+++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java
@@ -0,0 +1,57 @@
+package org.qortal.repository.hsqldb.transaction;
+
+import java.sql.ResultSet;
+import java.sql.SQLException;
+
+import org.qortal.data.transaction.BaseTransactionData;
+import org.qortal.data.transaction.PresenceTransactionData;
+import org.qortal.data.transaction.TransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.hsqldb.HSQLDBRepository;
+import org.qortal.repository.hsqldb.HSQLDBSaver;
+import org.qortal.transaction.PresenceTransaction.PresenceType;
+
+public class HSQLDBPresenceTransactionRepository extends HSQLDBTransactionRepository {
+
+ public HSQLDBPresenceTransactionRepository(HSQLDBRepository repository) {
+ this.repository = repository;
+ }
+
+ TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException {
+ String sql = "SELECT nonce, presence_type, timestamp_signature FROM PresenceTransactions WHERE signature = ?";
+
+ try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) {
+ if (resultSet == null)
+ return null;
+
+ int nonce = resultSet.getInt(1);
+ int presenceTypeValue = resultSet.getInt(2);
+ PresenceType presenceType = PresenceType.valueOf(presenceTypeValue);
+
+ byte[] timestampSignature = resultSet.getBytes(3);
+
+ return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature);
+ } catch (SQLException e) {
+ throw new DataException("Unable to fetch presence transaction from repository", e);
+ }
+ }
+
+ @Override
+ public void save(TransactionData transactionData) throws DataException {
+ PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData;
+
+ HSQLDBSaver saveHelper = new HSQLDBSaver("PresenceTransactions");
+
+ saveHelper.bind("signature", presenceTransactionData.getSignature())
+ .bind("nonce", presenceTransactionData.getNonce())
+ .bind("presence_type", presenceTransactionData.getPresenceType().value)
+ .bind("timestamp_signature", presenceTransactionData.getTimestampSignature());
+
+ try {
+ saveHelper.execute(this.repository);
+ } catch (SQLException e) {
+ throw new DataException("Unable to save chat transaction into repository", e);
+ }
+ }
+
+}
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 a603a916..83eeba72 100644
--- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBTransactionRepository.java
@@ -1124,6 +1124,63 @@ public class HSQLDBTransactionRepository implements TransactionRepository {
}
}
+ @Override
+ public List getUnconfirmedTransactions(TransactionType txType, byte[] creatorPublicKey) throws DataException {
+ if (txType == null && creatorPublicKey == null)
+ throw new IllegalArgumentException("At least one of txType or creatorPublicKey must be non-null");
+
+ StringBuilder sql = new StringBuilder(1024);
+ sql.append("SELECT signature FROM UnconfirmedTransactions ");
+ sql.append("JOIN Transactions USING (signature) ");
+ sql.append("WHERE ");
+
+ List whereClauses = new ArrayList<>();
+ List