From f17913996770f7f04636cdff7bc328123a5f0abb Mon Sep 17 00:00:00 2001 From: catbref Date: Wed, 24 Jun 2020 17:16:04 +0100 Subject: [PATCH] WIP: trade-bot: Alice P2SH_a progress Qortal AT now includes suggested tradeTimeout again as a constant so trade partner/recipient can use that to calculate a suitable lockTimeA. CODE_HASH changed! Renamed some secret_hash to hash_of_secret. Changed TradeBotStates.trade_state back to TINYINT and adjusted values in TradeBotData.State enum to suit. Added lockTimeA to TradeBotData & repository. Added JAXB-only extra representations of Bitcoin PKHs as addresses. Fixed incorrect expected length in BTCACCT.extractOfferMessageData(). CrossChainTradeData.refundTimeout now only present in TRADE mode. Added BTC.pkhToAddress(). Added initial TradeBot.handleAliceWaitingForP2shA(). Enforce only one TradeBot thread running using 'activeFlag' atomic boolean. Replace incorrect SHA256 with HASH160 for hashOfSecretA in TradeBot.startResponse(). --- .../api/model/TradeBotCreateRequest.java | 3 + .../api/resource/CrossChainResource.java | 5 +- .../java/org/qortal/controller/TradeBot.java | 103 ++++++++++++++---- src/main/java/org/qortal/crosschain/BTC.java | 4 + .../java/org/qortal/crosschain/BTCACCT.java | 17 ++- .../data/crosschain/CrossChainTradeData.java | 26 +++++ .../qortal/data/crosschain/TradeBotData.java | 25 +++-- .../hsqldb/HSQLDBCrossChainRepository.java | 16 ++- .../hsqldb/HSQLDBDatabaseUpdates.java | 7 +- .../java/org/qortal/test/btcacct/AtTests.java | 4 +- .../org/qortal/test/btcacct/DeployAT.java | 14 ++- 11 files changed, 174 insertions(+), 50 deletions(-) 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();