diff --git a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java index c1db35e7..1898a989 100644 --- a/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java +++ b/src/main/java/org/qortal/api/model/TradeBotCreateRequest.java @@ -24,6 +24,9 @@ public class TradeBotCreateRequest { @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public long bitcoinAmount; + @Schema(description = "Suggested trade timeout (minutes)", example = "10080") + public int tradeTimeout; + public TradeBotCreateRequest() { } diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java index 477a3ef9..92cf4096 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainResource.java @@ -183,7 +183,7 @@ public class CrossChainResource { PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey); byte[] creationBytes = BTCACCT.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB, - tradeRequest.qortAmount, tradeRequest.bitcoinAmount); + tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout); long txTimestamp = NTP.getTime(); byte[] lastReference = creatorAccount.getLastReference(); @@ -866,6 +866,9 @@ public class CrossChainResource { ) @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE}) public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) { + if (tradeBotCreateRequest.tradeTimeout < 600) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + try (final Repository repository = RepositoryManager.getRepository()) { // Do some simple checking first Account creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey); diff --git a/src/main/java/org/qortal/controller/TradeBot.java b/src/main/java/org/qortal/controller/TradeBot.java index 8c8369e4..38c85c31 100644 --- a/src/main/java/org/qortal/controller/TradeBot.java +++ b/src/main/java/org/qortal/controller/TradeBot.java @@ -4,13 +4,11 @@ import java.security.SecureRandom; import java.util.Arrays; import java.util.List; import java.util.Random; +import java.util.concurrent.atomic.AtomicBoolean; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.bitcoinj.core.Address; 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; @@ -43,8 +41,10 @@ public class TradeBot { private static TradeBot instance; + /** To help ensure only TradeBot is only active on one thread. */ + private AtomicBoolean activeFlag = new AtomicBoolean(false); + private TradeBot() { - } public static synchronized TradeBot getInstance() { @@ -79,7 +79,7 @@ public class TradeBot { String description = "QORT/BTC cross-chain trade"; String aTType = "ACCT"; String tags = "ACCT QORT BTC"; - byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(tradeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount, tradeBotCreateRequest.bitcoinAmount, tradeBotCreateRequest.tradeTimeout); long amount = tradeBotCreateRequest.fundingQortAmount; DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT); @@ -95,7 +95,7 @@ public class TradeBot { atAddress, tradeNativePublicKey, tradeNativePublicKeyHash, secretB, hashOfSecretB, tradeForeignPublicKey, tradeForeignPublicKeyHash, - tradeBotCreateRequest.bitcoinAmount, null); + tradeBotCreateRequest.bitcoinAmount, null, null); repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); @@ -108,12 +108,9 @@ public class TradeBot { } public static String startResponse(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException { - BTC btc = BTC.getInstance(); - NetworkParameters params = btc.getNetworkParameters(); - byte[] tradePrivateKey = generateTradePrivateKey(); - byte[] secret = generateSecret(); - byte[] secretHash = Crypto.digest(secret); + byte[] secretA = generateSecret(); + byte[] hashOfSecretA = Crypto.hash160(secretA); byte[] tradeNativePublicKey = deriveTradeNativePublicKey(tradePrivateKey); byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey); @@ -121,20 +118,20 @@ public class TradeBot { byte[] tradeForeignPublicKey = deriveTradeForeignPublicKey(tradePrivateKey); byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey); + // We need to generate lockTimeA: halfway of refundTimeout from now + int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (NTP.getTime() / 1000L); + TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, TradeBotData.State.ALICE_WAITING_FOR_P2SH_A, crossChainTradeData.qortalAtAddress, - tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, + tradeNativePublicKey, tradeNativePublicKeyHash, secretA, hashOfSecretA, tradeForeignPublicKey, tradeForeignPublicKeyHash, - crossChainTradeData.expectedBitcoin, null); + crossChainTradeData.expectedBitcoin, null, lockTimeA); repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); // P2SH_a to be funded - byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, crossChainTradeData.lockTimeA, crossChainTradeData.creatorBitcoinPKH, secretHash); - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - - Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - return p2shAddress.toString(); + byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorBitcoinPKH, hashOfSecretA); + return BTC.getInstance().deriveP2shAddress(redeemScriptBytes); } private static byte[] generateTradePrivateKey() { @@ -158,11 +155,17 @@ public class TradeBot { } public void onChainTipChange() { + if (!activeFlag.compareAndSet(false, true)) + // Trade bot already active on another thread + return; + // Get repo for trade situations try (final Repository repository = RepositoryManager.getRepository()) { List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData(); - for (TradeBotData tradeBotData : allTradeBotData) + for (TradeBotData tradeBotData : allTradeBotData) { + repository.discardChanges(); + switch (tradeBotData.getState()) { case BOB_WAITING_FOR_AT_CONFIRM: handleBobWaitingForAtConfirm(repository, tradeBotData); @@ -172,11 +175,18 @@ public class TradeBot { handleBobWaitingForMessage(repository, tradeBotData); break; + case ALICE_WAITING_FOR_P2SH_A: + handleAliceWaitingForP2shA(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); + } finally { + activeFlag.set(false); } } @@ -200,10 +210,12 @@ public class TradeBot { String address = Crypto.toAddress(tradeBotData.getTradeNativePublicKey()); List messageTransactionsData = repository.getTransactionRepository().getMessagesByRecipient(address, null, null, null); + final byte[] originalLastTransactionSignature = tradeBotData.getLastTransactionSignature(); + // Skip past previously processed messages - if (tradeBotData.getLastTransactionSignature() != null) + if (originalLastTransactionSignature != null) for (int i = 0; i < messageTransactionsData.size(); ++i) - if (Arrays.equals(messageTransactionsData.get(i).getSignature(), tradeBotData.getLastTransactionSignature())) { + if (Arrays.equals(messageTransactionsData.get(i).getSignature(), originalLastTransactionSignature)) { messageTransactionsData.subList(0, i + 1).clear(); break; } @@ -248,17 +260,62 @@ public class TradeBot { outgoingMessageTransaction.computeNonce(); outgoingMessageTransaction.sign(sender); + // reset repository state to prevent deadlock + repository.discardChanges(); ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed(); if (result != ValidationResult.OK) { - LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT '%s': %s", tradeBotData.getAtAddress(), result.name())); + LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT '%s': %s", outgoingMessageTransaction.getRecipient(), result.name())); return; } tradeBotData.setState(TradeBotData.State.BOB_WAITING_FOR_P2SH_B); - break; + repository.getCrossChainRepository().save(tradeBotData); + repository.saveChanges(); + return; + } + + // Don't resave if we don't need to + if (tradeBotData.getLastTransactionSignature() != originalLastTransactionSignature) { + repository.getCrossChainRepository().save(tradeBotData); + repository.saveChanges(); + } + } + + private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData) throws DataException { + ATData atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress()); + if (atData == null) { + LOGGER.warn(() -> String.format("Unable to fetch trade AT '%s' from repository", tradeBotData.getAtAddress())); + return; + } + CrossChainTradeData crossChainTradeData = BTCACCT.populateTradeData(repository, atData); + + byte[] redeemScriptBytes = BTCP2SH.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorBitcoinPKH, tradeBotData.getHashOfSecret()); + String p2shAddress = BTC.getInstance().deriveP2shAddress(redeemScriptBytes); + + Long balance = BTC.getInstance().getBalance(p2shAddress); + if (balance == null || balance < crossChainTradeData.expectedBitcoin) + return; + + // Attempt to send MESSAGE to Bob's Qortal trade address + byte[] messageData = BTCACCT.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + + PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey()); + MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, crossChainTradeData.qortalCreatorTradeAddress, messageData, false, false); + + messageTransaction.computeNonce(); + messageTransaction.sign(sender); + + // reset repository state to prevent deadlock + repository.discardChanges(); + ValidationResult result = messageTransaction.importAsUnconfirmed(); + + if (result != ValidationResult.OK) { + LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT '%s': %s", messageTransaction.getRecipient(), result.name())); + return; } + tradeBotData.setState(TradeBotData.State.ALICE_WAITING_FOR_AT_LOCK); repository.getCrossChainRepository().save(tradeBotData); repository.saveChanges(); } diff --git a/src/main/java/org/qortal/crosschain/BTC.java b/src/main/java/org/qortal/crosschain/BTC.java index 65af8781..6bf00073 100644 --- a/src/main/java/org/qortal/crosschain/BTC.java +++ b/src/main/java/org/qortal/crosschain/BTC.java @@ -98,6 +98,10 @@ public class BTC { return format(Coin.valueOf(amount)); } + public String pkhToAddress(byte[] publicKeyHash) { + return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString(); + } + public String deriveP2shAddress(byte[] redeemScriptBytes) { byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); Address p2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); diff --git a/src/main/java/org/qortal/crosschain/BTCACCT.java b/src/main/java/org/qortal/crosschain/BTCACCT.java index 172d24f9..ad185d87 100644 --- a/src/main/java/org/qortal/crosschain/BTCACCT.java +++ b/src/main/java/org/qortal/crosschain/BTCACCT.java @@ -88,7 +88,7 @@ public class BTCACCT { public static final int SECRET_LENGTH = 32; public static final int MIN_LOCKTIME = 1500000000; - public static final byte[] CODE_BYTES_HASH = HashCode.fromString("ca0dc643fdaba4d12cd5550800a8353746f40a0d9824d8c10d8b4bd0324eac0d").asBytes(); // SHA256 of AT code bytes + public static final byte[] CODE_BYTES_HASH = HashCode.fromString("14ee2cb9899f582037901c384bab9ccdd41e48d8c98bf7df5cf79f4e8c236286").asBytes(); // SHA256 of AT code bytes public static class OfferMessageData { public byte[] recipientBitcoinPKH; @@ -110,9 +110,10 @@ public class BTCACCT { * @param hashOfSecretB 20-byte HASH160 of 32-byte secret-B * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT * @param bitcoinAmount how much BTC the AT creator is expecting to trade + * @param tradeTimeout suggested timeout for entire trade * @return */ - public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount) { + public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) { // Labels for data segment addresses int addrCounter = 0; @@ -131,6 +132,7 @@ public class BTCACCT { final int addrQortAmount = addrCounter++; final int addrBitcoinAmount = addrCounter++; + final int addrTradeTimeout = addrCounter++; final int addrMessageTxType = addrCounter++; final int addrExpectedOfferMessageLength = addrCounter++; @@ -216,6 +218,10 @@ public class BTCACCT { assert dataByteBuffer.position() == addrBitcoinAmount * MachineState.VALUE_SIZE : "addrBitcoinAmount incorrect"; dataByteBuffer.putLong(bitcoinAmount); + // Suggested trade timeout (minutes) + assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect"; + dataByteBuffer.putLong(tradeTimeout); + // We're only interested in MESSAGE transactions assert dataByteBuffer.position() == addrMessageTxType * MachineState.VALUE_SIZE : "addrMessageTxType incorrect"; dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value); @@ -565,6 +571,8 @@ public class BTCACCT { // Expected BTC amount tradeData.expectedBitcoin = dataByteBuffer.getLong(); + tradeData.tradeTimeout = (int) dataByteBuffer.getLong(); + // Skip MESSAGE transaction type dataByteBuffer.position(dataByteBuffer.position() + 8); @@ -624,7 +632,7 @@ public class BTCACCT { int lockTimeB = (int) dataByteBuffer.getLong(); // AT refund timeout (probably only useful for debugging) - tradeData.refundTimeout = (int) dataByteBuffer.getLong(); + int refundTimeout = (int) dataByteBuffer.getLong(); // Trade offer timeout (AT 'timestamp' converted to Qortal block height) long tradeRefundTimestamp = dataByteBuffer.getLong(); @@ -664,6 +672,7 @@ public class BTCACCT { if (mode != 0) { tradeData.mode = CrossChainTradeData.Mode.TRADE; + tradeData.refundTimeout = refundTimeout; tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; tradeData.qortalRecipient = qortalRecipient; tradeData.hashOfSecretA = hashOfSecretA; @@ -685,7 +694,7 @@ public class BTCACCT { /** Returns trade-info extracted from MESSAGE payload sent by trade partner/recipient, or null if not valid. */ public static OfferMessageData extractOfferMessageData(byte[] messageData) { - if (messageData == null || messageData.length != 32 + 32 + 8) + if (messageData == null || messageData.length != 20 + 20 + 8) return null; OfferMessageData offerMessageData = new OfferMessageData(); diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java index 1c047c13..99a7f5e5 100644 --- a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java +++ b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java @@ -2,8 +2,11 @@ package org.qortal.data.crosschain; import javax.xml.bind.annotation.XmlAccessType; import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; +import org.qortal.crosschain.BTC; + import io.swagger.v3.oas.annotations.media.Schema; // All properties to be converted to JSON via JAXB @@ -29,6 +32,9 @@ public class CrossChainTradeData { @Schema(description = "Timestamp when AT was created (milliseconds since epoch)") public long creationTimestamp; + @Schema(description = "Suggested trade timeout (minutes)", example = "10080") + public int tradeTimeout; + @Schema(description = "AT's current QORT balance") @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) public long qortBalance; @@ -76,4 +82,24 @@ public class CrossChainTradeData { public CrossChainTradeData() { } + // We can represent BitcoinPKH as an address + @XmlElement(name = "creatorBitcoinAddress") + @Schema(description = "AT creator's Bitcoin PKH in address form") + public String getCreatorBitcoinAddress() { + if (this.creatorBitcoinPKH == null) + return null; + + return BTC.getInstance().pkhToAddress(this.creatorBitcoinPKH); + } + + // We can represent BitcoinPKH as an address + @XmlElement(name = "recipientBitcoinAddress") + @Schema(description = "Trade partner's Bitcoin PKH in address form") + public String getRecipientBitcoinAddress() { + if (this.recipientBitcoinPKH == null) + return null; + + return BTC.getInstance().pkhToAddress(this.recipientBitcoinPKH); + } + } diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java index c149ead0..4441212c 100644 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ b/src/main/java/org/qortal/data/crosschain/TradeBotData.java @@ -22,7 +22,7 @@ public class TradeBotData { public enum State { 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(80), ALICE_WAITING_FOR_AT_LOCK(90), ALICE_WATCH_P2SH_B(100); public final int value; private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state)); @@ -43,7 +43,7 @@ public class TradeBotData { private byte[] tradeNativePublicKeyHash; private byte[] secret; - private byte[] secretHash; + private byte[] hashOfSecret; private byte[] tradeForeignPublicKey; private byte[] tradeForeignPublicKeyHash; @@ -52,21 +52,24 @@ public class TradeBotData { private byte[] lastTransactionSignature; + private Integer lockTimeA; + public TradeBotData(byte[] tradePrivateKey, State tradeState, String atAddress, - byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] secretHash, + byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, byte[] secret, byte[] hashOfSecret, byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, - long bitcoinAmount, byte[] lastTransactionSignature) { + long bitcoinAmount, byte[] lastTransactionSignature, Integer lockTimeA) { this.tradePrivateKey = tradePrivateKey; this.tradeState = tradeState; this.atAddress = atAddress; this.tradeNativePublicKey = tradeNativePublicKey; this.tradeNativePublicKeyHash = tradeNativePublicKeyHash; this.secret = secret; - this.secretHash = secretHash; + this.hashOfSecret = hashOfSecret; this.tradeForeignPublicKey = tradeForeignPublicKey; this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash; this.bitcoinAmount = bitcoinAmount; this.lastTransactionSignature = lastTransactionSignature; + this.lockTimeA = lockTimeA; } public byte[] getTradePrivateKey() { @@ -101,8 +104,8 @@ public class TradeBotData { return this.secret; } - public byte[] getSecretHash() { - return this.secretHash; + public byte[] getHashOfSecret() { + return this.hashOfSecret; } public byte[] getTradeForeignPublicKey() { @@ -125,4 +128,12 @@ public class TradeBotData { this.lastTransactionSignature = lastTransactionSignature; } + public Integer getLockTimeA() { + return this.lockTimeA; + } + + public void setLockTimeA(Integer lockTimeA) { + this.lockTimeA = lockTimeA; + } + } diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java index 2debbc67..392f42b1 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java @@ -21,9 +21,9 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { public List getAllTradeBotData() throws DataException { String sql = "SELECT trade_private_key, trade_state, at_address, " + "trade_native_public_key, trade_native_public_key_hash, " - + "secret, secret_hash, " + + "secret, hash_of_secret, " + "trade_foreign_public_key, trade_foreign_public_key_hash, " - + "bitcoin_amount, last_transaction_signature " + + "bitcoin_amount, last_transaction_signature, locktime_a " + "FROM TradeBotStates"; List allTradeBotData = new ArrayList<>(); @@ -43,17 +43,20 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { byte[] tradeNativePublicKey = resultSet.getBytes(4); byte[] tradeNativePublicKeyHash = resultSet.getBytes(5); byte[] secret = resultSet.getBytes(6); - byte[] secretHash = resultSet.getBytes(7); + byte[] hashOfSecret = resultSet.getBytes(7); byte[] tradeForeignPublicKey = resultSet.getBytes(8); byte[] tradeForeignPublicKeyHash = resultSet.getBytes(9); long bitcoinAmount = resultSet.getLong(10); byte[] lastTransactionSignature = resultSet.getBytes(11); + Integer lockTimeA = resultSet.getInt(12); + if (lockTimeA == 0 && resultSet.wasNull()) + lockTimeA = null; TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, tradeState, atAddress, - tradeNativePublicKey, tradeNativePublicKeyHash, secret, secretHash, + tradeNativePublicKey, tradeNativePublicKeyHash, secret, hashOfSecret, tradeForeignPublicKey, tradeForeignPublicKeyHash, - bitcoinAmount, lastTransactionSignature); + bitcoinAmount, lastTransactionSignature, lockTimeA); allTradeBotData.add(tradeBotData); } while (resultSet.next()); @@ -70,9 +73,10 @@ public class HSQLDBCrossChainRepository implements CrossChainRepository { saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey()) .bind("trade_state", tradeBotData.getState().value) .bind("at_address", tradeBotData.getAtAddress()) + .bind("locktime_a", tradeBotData.getLockTimeA()) .bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey()) .bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash()) - .bind("secret", tradeBotData.getSecret()).bind("secret_hash", tradeBotData.getSecretHash()) + .bind("secret", tradeBotData.getSecret()).bind("hash_of_secret", tradeBotData.getHashOfSecret()) .bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey()) .bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash()) .bind("bitcoin_amount", tradeBotData.getBitcoinAmount()) diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index 346a1daa..df08efcb 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -620,12 +620,13 @@ public class HSQLDBDatabaseUpdates { case 20: // Trade bot - stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state SMALLINT NOT NULL, " + stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, " + "at_address QortalAddress, " + "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, " - + "secret VARBINARY(32) NOT NULL, secret_hash VARBINARY(32) NOT NULL, " + + "secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, " + "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " - + "bitcoin_amount BIGINT NOT NULL, last_transaction_signature Signature, PRIMARY KEY (trade_private_key))"); + + "bitcoin_amount BIGINT NOT NULL, last_transaction_signature Signature, locktime_a BIGINT, " + + "PRIMARY KEY (trade_private_key))"); break; default: diff --git a/src/test/java/org/qortal/test/btcacct/AtTests.java b/src/test/java/org/qortal/test/btcacct/AtTests.java index 5d5b5f2c..3f0fe919 100644 --- a/src/test/java/org/qortal/test/btcacct/AtTests.java +++ b/src/test/java/org/qortal/test/btcacct/AtTests.java @@ -63,7 +63,7 @@ public class AtTests extends Common { public void testCompile() { Account deployer = Common.getTestAccount(null, "chloe"); - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); } @@ -526,7 +526,7 @@ public class AtTests extends Common { } private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer) throws DataException { - byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount); + byte[] creationBytes = BTCACCT.buildQortalAT(deployer.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); long txTimestamp = System.currentTimeMillis(); byte[] lastReference = deployer.getLastReference(); diff --git a/src/test/java/org/qortal/test/btcacct/DeployAT.java b/src/test/java/org/qortal/test/btcacct/DeployAT.java index 74233e25..56e75150 100644 --- a/src/test/java/org/qortal/test/btcacct/DeployAT.java +++ b/src/test/java/org/qortal/test/btcacct/DeployAT.java @@ -34,19 +34,20 @@ public class DeployAT { if (error != null) System.err.println(error); - System.err.println(String.format("usage: DeployAT ")); + System.err.println(String.format("usage: DeployAT 50000) + usage("Trade timeout (minutes) must be between 60 and 50000"); } catch (IllegalArgumentException e) { usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); } @@ -108,7 +114,7 @@ public class DeployAT { System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(secretHash))); // Deploy AT - byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin); + byte[] creationBytes = BTCACCT.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, secretHash, redeemAmount, expectedBitcoin, tradeTimeout); System.out.println("CIYAM AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); long txTimestamp = System.currentTimeMillis();