diff --git a/src/main/java/org/qortal/api/ApiService.java b/src/main/java/org/qortal/api/ApiService.java
index 5baf2c5d..b88edb5a 100644
--- a/src/main/java/org/qortal/api/ApiService.java
+++ b/src/main/java/org/qortal/api/ApiService.java
@@ -43,9 +43,6 @@ import org.qortal.api.websocket.ActiveChatsWebSocket;
import org.qortal.api.websocket.AdminStatusWebSocket;
import org.qortal.api.websocket.BlocksWebSocket;
import org.qortal.api.websocket.ChatMessagesWebSocket;
-import org.qortal.api.websocket.PresenceWebSocket;
-import org.qortal.api.websocket.TradeBotWebSocket;
-import org.qortal.api.websocket.TradeOffersWebSocket;
import org.qortal.settings.Settings;
public class ApiService {
@@ -199,9 +196,6 @@ public class ApiService {
context.addServlet(BlocksWebSocket.class, "/websockets/blocks");
context.addServlet(ActiveChatsWebSocket.class, "/websockets/chat/active/*");
context.addServlet(ChatMessagesWebSocket.class, "/websockets/chat/messages");
- context.addServlet(TradeOffersWebSocket.class, "/websockets/crosschain/tradeoffers");
- context.addServlet(TradeBotWebSocket.class, "/websockets/crosschain/tradebot");
- context.addServlet(PresenceWebSocket.class, "/websockets/presence");
// Start server
this.server.start();
diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java
deleted file mode 100644
index 074fd24d..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainBitcoinRedeemRequest.java
+++ /dev/null
@@ -1,34 +0,0 @@
-package org.qortal.api.model;
-
-import java.math.BigDecimal;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainBitcoinRedeemRequest {
-
- @Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
- public byte[] refundPublicKeyHash;
-
- @Schema(description = "Bitcoin PRIVATE KEY for redeem", example = "cUvGNSnu14q6Hr1X7TESjYVTqBpFjj8GGLGjGdpJwD9NhSQKeYUk")
- public byte[] redeemPrivateKey;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- @Schema(description = "Bitcoin miner fee", example = "0.00001000")
- public BigDecimal bitcoinMinerFee;
-
- @Schema(description = "32-byte secret", example = "6gVbAXCVzJXAWwtAVGAfgAkkXpeXvPUwSciPmCfSfXJG")
- public byte[] secret;
-
- @Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
- public byte[] receivingAccountInfo;
-
- public CrossChainBitcoinRedeemRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java
deleted file mode 100644
index f2485389..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainBitcoinRefundRequest.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.qortal.api.model;
-
-import java.math.BigDecimal;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainBitcoinRefundRequest {
-
- @Schema(description = "Bitcoin PRIVATE KEY for refund", example = "cSP3zTb6bfm8GATtAcEJ8LqYtNQmzZ9jE2wQUVnZGiBzojDdrwKV")
- public byte[] refundPrivateKey;
-
- @Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
- public byte[] redeemPublicKeyHash;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- @Schema(description = "Bitcoin miner fee", example = "0.00001000")
- public BigDecimal bitcoinMinerFee;
-
- @Schema(description = "Bitcoin HASH160(public key) for receiving funds, or omit to derive from private key", example = "u17kBVKkKSp12oUzaxFwNnq1JZf")
- public byte[] receivingAccountInfo;
-
- public CrossChainBitcoinRefundRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java
deleted file mode 100644
index b7510eaa..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainBitcoinTemplateRequest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainBitcoinTemplateRequest {
-
- @Schema(description = "Bitcoin HASH160(public key) for refund", example = "2nGDBPPPFS1c9w1h33YwFk4KUJU2")
- public byte[] refundPublicKeyHash;
-
- @Schema(description = "Bitcoin HASH160(public key) for redeem", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
- public byte[] redeemPublicKeyHash;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- public CrossChainBitcoinTemplateRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java b/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java
deleted file mode 100644
index 2772eae1..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainBitcoinyHTLCStatus.java
+++ /dev/null
@@ -1,31 +0,0 @@
-package org.qortal.api.model;
-
-import java.math.BigDecimal;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainBitcoinyHTLCStatus {
-
- @Schema(description = "P2SH address", example = "3CdH27kTpV8dcFHVRYjQ8EEV5FJg9X8pSJ (mainnet), 2fMiRRXVsxhZeyfum9ifybZvaMHbQTmwdZw (testnet)")
- public String bitcoinP2shAddress;
-
- @Schema(description = "P2SH balance")
- public BigDecimal bitcoinP2shBalance;
-
- @Schema(description = "Can HTLC redeem yet?")
- public boolean canRedeem;
-
- @Schema(description = "Can HTLC refund yet?")
- public boolean canRefund;
-
- @Schema(description = "Secret used by HTLC redeemer")
- public byte[] secret;
-
- public CrossChainBitcoinyHTLCStatus() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java b/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java
deleted file mode 100644
index e8d38703..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainBuildRequest.java
+++ /dev/null
@@ -1,39 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainBuildRequest {
-
- @Schema(description = "AT creator's public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] creatorPublicKey;
-
- @Schema(description = "Final QORT amount paid out on successful trade", example = "80.40200000")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long qortAmount;
-
- @Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "123.45670000")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long fundingQortAmount;
-
- @Schema(description = "HASH160 of creator's Bitcoin public key", example = "2daMveGc5pdjRyFacbxBzMksCbyC")
- public byte[] bitcoinPublicKeyHash;
-
- @Schema(description = "HASH160 of secret", example = "43vnftqkjxrhb5kJdkU1ZFQLEnWV")
- public byte[] hashOfSecretB;
-
- @Schema(description = "Bitcoin P2SH BTC balance for release of secret", example = "0.00864200")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long bitcoinAmount;
-
- @Schema(description = "Trade time window (minutes) from trade agreement to automatic refund", example = "10080")
- public Integer tradeTimeout;
-
- public CrossChainBuildRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java b/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java
deleted file mode 100644
index 25a18952..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainCancelRequest.java
+++ /dev/null
@@ -1,20 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainCancelRequest {
-
- @Schema(description = "AT creator's public key", example = "K6wuddsBV3HzRrXFFezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] creatorPublicKey;
-
- @Schema(description = "Qortal trade AT address")
- public String atAddress;
-
- public CrossChainCancelRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java
deleted file mode 100644
index b6705d5d..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainDualSecretRequest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.qortal.api.model;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainDualSecretRequest {
-
- @Schema(description = "Public key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] partnerPublicKey;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- @Schema(description = "secret-A (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
- public byte[] secretA;
-
- @Schema(description = "secret-B (32 bytes)", example = "EN2Bgx3BcEMtxFCewmCVSMkfZjVKYhx3KEXC5A21KBGx")
- public byte[] secretB;
-
- @Schema(description = "Qortal address for receiving QORT from AT")
- public String receivingAddress;
-
- public CrossChainDualSecretRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java b/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java
deleted file mode 100644
index bf71c2d2..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainOfferSummary.java
+++ /dev/null
@@ -1,127 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import org.qortal.crosschain.AcctMode;
-import org.qortal.data.crosschain.CrossChainTradeData;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-// All properties to be converted to JSON via JAXB
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainOfferSummary {
-
- // Properties
-
- @Schema(description = "AT's Qortal address")
- private String qortalAtAddress;
-
- @Schema(description = "AT creator's Qortal address")
- private String qortalCreator;
-
- @Schema(description = "AT creator's ephemeral trading key-pair represented as Qortal address")
- private String qortalCreatorTradeAddress;
-
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long qortAmount;
-
- @Schema(description = "Bitcoin amount - DEPRECATED: use foreignAmount")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- @Deprecated
- private long btcAmount;
-
- @Schema(description = "Foreign blockchain amount")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long foreignAmount;
-
- @Schema(description = "Suggested trade timeout (minutes)", example = "10080")
- private int tradeTimeout;
-
- @Schema(description = "Current AT execution mode")
- private AcctMode mode;
-
- private long timestamp;
-
- @Schema(description = "Trade partner's Qortal receiving address")
- private String partnerQortalReceivingAddress;
-
- private String foreignBlockchain;
-
- private String acctName;
-
- protected CrossChainOfferSummary() {
- /* For JAXB */
- }
-
- public CrossChainOfferSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
- this.qortalAtAddress = crossChainTradeData.qortalAtAddress;
- this.qortalCreator = crossChainTradeData.qortalCreator;
- this.qortalCreatorTradeAddress = crossChainTradeData.qortalCreatorTradeAddress;
- this.qortAmount = crossChainTradeData.qortAmount;
- this.foreignAmount = crossChainTradeData.expectedForeignAmount;
- this.btcAmount = this.foreignAmount; // Duplicate for deprecated field
- this.tradeTimeout = crossChainTradeData.tradeTimeout;
- this.mode = crossChainTradeData.mode;
- this.timestamp = timestamp;
- this.partnerQortalReceivingAddress = crossChainTradeData.qortalPartnerReceivingAddress;
- this.foreignBlockchain = crossChainTradeData.foreignBlockchain;
- this.acctName = crossChainTradeData.acctName;
- }
-
- public String getQortalAtAddress() {
- return this.qortalAtAddress;
- }
-
- public String getQortalCreator() {
- return this.qortalCreator;
- }
-
- public String getQortalCreatorTradeAddress() {
- return this.qortalCreatorTradeAddress;
- }
-
- public long getQortAmount() {
- return this.qortAmount;
- }
-
- public long getBtcAmount() {
- return this.btcAmount;
- }
-
- public long getForeignAmount() {
- return this.foreignAmount;
- }
-
- public int getTradeTimeout() {
- return this.tradeTimeout;
- }
-
- public AcctMode getMode() {
- return this.mode;
- }
-
- public long getTimestamp() {
- return this.timestamp;
- }
-
- public String getPartnerQortalReceivingAddress() {
- return this.partnerQortalReceivingAddress;
- }
-
- public String getForeignBlockchain() {
- return this.foreignBlockchain;
- }
-
- public String getAcctName() {
- return this.acctName;
- }
-
- // For debugging mostly
-
- public String toString() {
- return String.format("%s: %s", this.qortalAtAddress, this.mode);
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java b/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java
deleted file mode 100644
index 2db475e5..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainSecretRequest.java
+++ /dev/null
@@ -1,26 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainSecretRequest {
-
- @Schema(description = "Private key to match AT's trade 'partner'", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] partnerPrivateKey;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- @Schema(description = "Secret (32 bytes)", example = "FHMzten4he9jZ4HGb4297Utj6F5g2w7serjq2EnAg2s1")
- public byte[] secret;
-
- @Schema(description = "Qortal address for receiving QORT from AT")
- public String receivingAddress;
-
- public CrossChainSecretRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java b/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java
deleted file mode 100644
index 1afd7290..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainTradeRequest.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainTradeRequest {
-
- @Schema(description = "AT creator's 'trade' public key", example = "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry")
- public byte[] tradePublicKey;
-
- @Schema(description = "Qortal AT address")
- public String atAddress;
-
- @Schema(description = "Signature of trading partner's 'offer' MESSAGE transaction")
- public byte[] messageTransactionSignature;
-
- public CrossChainTradeRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java b/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java
deleted file mode 100644
index 274dd818..00000000
--- a/src/main/java/org/qortal/api/model/CrossChainTradeSummary.java
+++ /dev/null
@@ -1,54 +0,0 @@
-package org.qortal.api.model;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import org.qortal.data.crosschain.CrossChainTradeData;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-// All properties to be converted to JSON via JAXB
-@XmlAccessorType(XmlAccessType.FIELD)
-public class CrossChainTradeSummary {
-
- private long tradeTimestamp;
-
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long qortAmount;
-
- @Deprecated
- @Schema(description = "DEPRECATED: use foreignAmount instead")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long btcAmount;
-
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- private long foreignAmount;
-
- protected CrossChainTradeSummary() {
- /* For JAXB */
- }
-
- public CrossChainTradeSummary(CrossChainTradeData crossChainTradeData, long timestamp) {
- this.tradeTimestamp = timestamp;
- this.qortAmount = crossChainTradeData.qortAmount;
- this.foreignAmount = crossChainTradeData.expectedForeignAmount;
- this.btcAmount = this.foreignAmount;
- }
-
- public long getTradeTimestamp() {
- return this.tradeTimestamp;
- }
-
- public long getQortAmount() {
- return this.qortAmount;
- }
-
- public long getBtcAmount() {
- return this.btcAmount;
- }
-
- public long getForeignAmount() {
- return this.foreignAmount;
- }
-}
diff --git a/src/main/java/org/qortal/api/model/crosschain/BitcoinSendRequest.java b/src/main/java/org/qortal/api/model/crosschain/BitcoinSendRequest.java
deleted file mode 100644
index 86d3d7c8..00000000
--- a/src/main/java/org/qortal/api/model/crosschain/BitcoinSendRequest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.qortal.api.model.crosschain;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class BitcoinSendRequest {
-
- @Schema(description = "Bitcoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
- public String xprv58;
-
- @Schema(description = "Recipient's Bitcoin address ('legacy' P2PKH only)", example = "1BitcoinEaterAddressDontSendf59kuE")
- public String receivingAddress;
-
- @Schema(description = "Amount of BTC to send", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long bitcoinAmount;
-
- @Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 BTC (100 sats) per byte", example = "0.00000100", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public Long feePerByte;
-
- public BitcoinSendRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/crosschain/LitecoinSendRequest.java b/src/main/java/org/qortal/api/model/crosschain/LitecoinSendRequest.java
deleted file mode 100644
index 5f215740..00000000
--- a/src/main/java/org/qortal/api/model/crosschain/LitecoinSendRequest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.qortal.api.model.crosschain;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class LitecoinSendRequest {
-
- @Schema(description = "Litecoin BIP32 extended private key", example = "tprv___________________________________________________________________________________________________________")
- public String xprv58;
-
- @Schema(description = "Recipient's Litecoin address ('legacy' P2PKH only)", example = "LiTecoinEaterAddressDontSendhLfzKD")
- public String receivingAddress;
-
- @Schema(description = "Amount of LTC to send", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long litecoinAmount;
-
- @Schema(description = "Transaction fee per byte (optional). Default is 0.00000100 LTC (100 sats) per byte", example = "0.00000100", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public Long feePerByte;
-
- public LitecoinSendRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/crosschain/TradeBotCreateRequest.java b/src/main/java/org/qortal/api/model/crosschain/TradeBotCreateRequest.java
deleted file mode 100644
index 1f96488e..00000000
--- a/src/main/java/org/qortal/api/model/crosschain/TradeBotCreateRequest.java
+++ /dev/null
@@ -1,46 +0,0 @@
-package org.qortal.api.model.crosschain;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter;
-
-import org.qortal.crosschain.SupportedBlockchain;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class TradeBotCreateRequest {
-
- @Schema(description = "Trade creator's public key", example = "2zR1WFsbM7akHghqSCYKBPk6LDP8aKiQSRS1FrwoLvoB")
- public byte[] creatorPublicKey;
-
- @Schema(description = "QORT amount paid out on successful trade", example = "80.40000000", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long qortAmount;
-
- @Schema(description = "QORT amount funding AT, including covering AT execution fees", example = "80.50000000", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public long fundingQortAmount;
-
- @Deprecated
- @Schema(description = "Bitcoin amount wanted in return. DEPRECATED: use foreignAmount instead", example = "0.00864200", type = "number", hidden = true)
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public Long bitcoinAmount;
-
- @Schema(description = "Foreign blockchain. Note: default (BITCOIN) to be removed in the future", example = "BITCOIN", implementation = SupportedBlockchain.class)
- public SupportedBlockchain foreignBlockchain;
-
- @Schema(description = "Foreign blockchain amount wanted in return", example = "0.00864200", type = "number")
- @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class)
- public Long foreignAmount;
-
- @Schema(description = "Suggested trade timeout (minutes)", example = "10080")
- public int tradeTimeout;
-
- @Schema(description = "Foreign blockchain address for receiving", example = "1BitcoinEaterAddressDontSendf59kuE")
- public String receivingAddress;
-
- public TradeBotCreateRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequest.java b/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequest.java
deleted file mode 100644
index ecc8ed6f..00000000
--- a/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequest.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package org.qortal.api.model.crosschain;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import io.swagger.v3.oas.annotations.media.Schema;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class TradeBotRespondRequest {
-
- @Schema(description = "Qortal AT address", example = "Aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
- public String atAddress;
-
- @Deprecated
- @Schema(description = "Bitcoin BIP32 extended private key. DEPRECATED: use foreignKey instead", hidden = true,
- example = "xprv___________________________________________________________________________________________________________")
- public String xprv58;
-
- @Schema(description = "Foreign blockchain private key, e.g. BIP32 'm' key for Bitcoin/Litecoin starting with 'xprv'",
- example = "xprv___________________________________________________________________________________________________________")
- public String foreignKey;
-
- @Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
- public String receivingAddress;
-
- public TradeBotRespondRequest() {
- }
-
-}
diff --git a/src/main/java/org/qortal/api/resource/ApiDefinition.java b/src/main/java/org/qortal/api/resource/ApiDefinition.java
index f9ec7459..fa27bfbb 100644
--- a/src/main/java/org/qortal/api/resource/ApiDefinition.java
+++ b/src/main/java/org/qortal/api/resource/ApiDefinition.java
@@ -22,7 +22,6 @@ import org.qortal.api.Security;
@Tag(name = "Automated Transactions"),
@Tag(name = "Blocks"),
@Tag(name = "Chat"),
- @Tag(name = "Cross-Chain"),
@Tag(name = "Groups"),
@Tag(name = "Names"),
@Tag(name = "Payments"),
@@ -41,4 +40,4 @@ import org.qortal.api.Security;
@SecurityScheme(name = "apiKey", type = SecuritySchemeType.APIKEY, in = SecuritySchemeIn.HEADER, paramName = Security.API_KEY_HEADER)
})
public class ApiDefinition {
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java
deleted file mode 100644
index 20a27241..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinACCTv1Resource.java
+++ /dev/null
@@ -1,363 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.util.Arrays;
-import java.util.Random;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.qortal.account.PublicKeyAccount;
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.CrossChainBuildRequest;
-import org.qortal.api.model.CrossChainDualSecretRequest;
-import org.qortal.api.model.CrossChainTradeRequest;
-import org.qortal.asset.Asset;
-import org.qortal.crosschain.BitcoinACCTv1;
-import org.qortal.crosschain.Bitcoiny;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.crypto.Crypto;
-import org.qortal.data.at.ATData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.data.transaction.BaseTransactionData;
-import org.qortal.data.transaction.DeployAtTransactionData;
-import org.qortal.data.transaction.MessageTransactionData;
-import org.qortal.data.transaction.TransactionData;
-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;
-import org.qortal.transaction.Transaction.TransactionType;
-import org.qortal.transaction.Transaction.ValidationResult;
-import org.qortal.transform.TransformationException;
-import org.qortal.transform.Transformer;
-import org.qortal.transform.transaction.DeployAtTransactionTransformer;
-import org.qortal.transform.transaction.MessageTransactionTransformer;
-import org.qortal.utils.Base58;
-import org.qortal.utils.NTP;
-
-@Path("/crosschain/BitcoinACCTv1")
-@Tag(name = "Cross-Chain (BitcoinACCTv1)")
-public class CrossChainBitcoinACCTv1Resource {
-
- @Context
- HttpServletRequest request;
-
- @POST
- @Path("/build")
- @Operation(
- summary = "Build Bitcoin cross-chain trading AT",
- description = "Returns raw, unsigned DEPLOY_AT transaction",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = CrossChainBuildRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_DATA, ApiError.INVALID_REFERENCE, ApiError.TRANSFORMATION_ERROR, ApiError.REPOSITORY_ISSUE})
- public String buildTrade(CrossChainBuildRequest tradeRequest) {
- Security.checkApiCallAllowed(request);
-
- byte[] creatorPublicKey = tradeRequest.creatorPublicKey;
-
- if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- if (tradeRequest.hashOfSecretB == null || tradeRequest.hashOfSecretB.length != Bitcoiny.HASH160_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (tradeRequest.tradeTimeout == null)
- tradeRequest.tradeTimeout = 7 * 24 * 60; // 7 days
- else
- if (tradeRequest.tradeTimeout < 10 || tradeRequest.tradeTimeout > 50000)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (tradeRequest.qortAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (tradeRequest.fundingQortAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- // funding amount must exceed initial + final
- if (tradeRequest.fundingQortAmount <= tradeRequest.qortAmount)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (tradeRequest.bitcoinAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- PublicKeyAccount creatorAccount = new PublicKeyAccount(repository, creatorPublicKey);
-
- byte[] creationBytes = BitcoinACCTv1.buildQortalAT(creatorAccount.getAddress(), tradeRequest.bitcoinPublicKeyHash, tradeRequest.hashOfSecretB,
- tradeRequest.qortAmount, tradeRequest.bitcoinAmount, tradeRequest.tradeTimeout);
-
- long txTimestamp = NTP.getTime();
- byte[] lastReference = creatorAccount.getLastReference();
- if (lastReference == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_REFERENCE);
-
- long fee = 0;
- String name = "QORT-BTC cross-chain trade";
- String description = "Qortal-Bitcoin cross-chain trade";
- String atType = "ACCT";
- String tags = "QORT-BTC ACCT";
-
- BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null);
- TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, tradeRequest.fundingQortAmount, Asset.QORT);
-
- Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
-
- fee = deployAtTransaction.calcRecommendedFee();
- deployAtTransactionData.setFee(fee);
-
- ValidationResult result = deployAtTransaction.isValidUnconfirmed();
- if (result != ValidationResult.OK)
- throw TransactionsResource.createTransactionInvalidException(request, result);
-
- byte[] bytes = DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
- return Base58.encode(bytes);
- } catch (TransformationException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @POST
- @Path("/trademessage")
- @Operation(
- summary = "Builds raw, unsigned 'trade' MESSAGE transaction that sends cross-chain trade recipient address, triggering 'trade' mode",
- description = "Specify address of cross-chain AT that needs to be messaged, and signature of 'offer' MESSAGE from trade partner. "
- + "AT needs to be in 'offer' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send! "
- + "You need to sign output with trade private key otherwise the MESSAGE transaction will be invalid.",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = CrossChainTradeRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- type = "string"
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public String buildTradeMessage(CrossChainTradeRequest tradeRequest) {
- Security.checkApiCallAllowed(request);
-
- byte[] tradePublicKey = tradeRequest.tradePublicKey;
-
- if (tradePublicKey == null || tradePublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- if (tradeRequest.atAddress == null || !Crypto.isValidAtAddress(tradeRequest.atAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (tradeRequest.messageTransactionSignature == null || !Crypto.isValidAddress(tradeRequest.messageTransactionSignature))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = fetchAtDataWithChecking(repository, tradeRequest.atAddress);
- CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-
- if (crossChainTradeData.mode != AcctMode.OFFERING)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Does supplied public key match trade public key?
- if (!Crypto.toAddress(tradePublicKey).equals(crossChainTradeData.qortalCreatorTradeAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- TransactionData transactionData = repository.getTransactionRepository().fromSignature(tradeRequest.messageTransactionSignature);
- if (transactionData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_UNKNOWN);
-
- if (transactionData.getType() != TransactionType.MESSAGE)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
-
- MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData;
- byte[] messageData = messageTransactionData.getData();
- BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData);
- if (offerMessageData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
-
- // Good to make MESSAGE
-
- byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
- byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
- int lockTimeA = (int) offerMessageData.lockTimeA;
-
- String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
- int lockTimeB = BitcoinACCTv1.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA);
-
- byte[] outgoingMessageData = BitcoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
- byte[] messageTransactionBytes = buildAtMessage(repository, tradePublicKey, tradeRequest.atAddress, outgoingMessageData);
-
- return Base58.encode(messageTransactionBytes);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @POST
- @Path("/redeemmessage")
- @Operation(
- summary = "Builds raw, unsigned 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
- description = "Specify address of cross-chain AT that needs to be messaged, both 32-byte secrets and an address for receiving QORT from AT. "
- + "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send! "
- + "You need to sign output with account the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = CrossChainDualSecretRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- type = "string"
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public String buildRedeemMessage(CrossChainDualSecretRequest secretRequest) {
- Security.checkApiCallAllowed(request);
-
- byte[] partnerPublicKey = secretRequest.partnerPublicKey;
-
- if (partnerPublicKey == null || partnerPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (secretRequest.secretA == null || secretRequest.secretA.length != BitcoinACCTv1.SECRET_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (secretRequest.secretB == null || secretRequest.secretB.length != BitcoinACCTv1.SECRET_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
- CrossChainTradeData crossChainTradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
-
- if (crossChainTradeData.mode != AcctMode.TRADING)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- String partnerAddress = Crypto.toAddress(partnerPublicKey);
-
- // MESSAGE must come from address that AT considers trade partner
- if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- // Good to make MESSAGE
-
- byte[] messageData = BitcoinACCTv1.buildRedeemMessage(secretRequest.secretA, secretRequest.secretB, secretRequest.receivingAddress);
- byte[] messageTransactionBytes = buildAtMessage(repository, partnerPublicKey, secretRequest.atAddress, messageData);
-
- return Base58.encode(messageTransactionBytes);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- // Must be correct AT - check functionality using code hash
- if (!Arrays.equals(atData.getCodeHash(), BitcoinACCTv1.CODE_BYTES_HASH))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // No point sending message to AT that's finished
- if (atData.getIsFinished())
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return atData;
- }
-
- private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
- long txTimestamp = NTP.getTime();
-
- // senderPublicKey could be ephemeral trade public key where there is no corresponding account and hence no reference
- String senderAddress = Crypto.toAddress(senderPublicKey);
- byte[] lastReference = repository.getAccountRepository().getLastReference(senderAddress);
- final boolean requiresPoW = lastReference == null;
-
- if (requiresPoW) {
- Random random = new Random();
- lastReference = new byte[Transformer.SIGNATURE_LENGTH];
- random.nextBytes(lastReference);
- }
-
- int version = 4;
- int nonce = 0;
- long amount = 0L;
- Long assetId = null; // no assetId as amount is zero
- Long fee = 0L;
-
- BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
- TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
-
- MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
-
- if (requiresPoW) {
- messageTransaction.computeNonce();
- } else {
- fee = messageTransaction.calcRecommendedFee();
- messageTransactionData.setFee(fee);
- }
-
- ValidationResult result = messageTransaction.isValidUnconfirmed();
- if (result != ValidationResult.OK)
- throw TransactionsResource.createTransactionInvalidException(request, result);
-
- try {
- return MessageTransactionTransformer.toBytes(messageTransactionData);
- } catch (TransformationException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
deleted file mode 100644
index 2c1c6991..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainBitcoinResource.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.bitcoinj.core.Transaction;
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.crosschain.BitcoinSendRequest;
-import org.qortal.crosschain.Bitcoin;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.crosschain.SimpleTransaction;
-
-@Path("/crosschain/btc")
-@Tag(name = "Cross-Chain (Bitcoin)")
-public class CrossChainBitcoinResource {
-
- @Context
- HttpServletRequest request;
-
- @POST
- @Path("/walletbalance")
- @Operation(
- summary = "Returns BTC balance for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public String getBitcoinWalletBalance(String key58) {
- Security.checkApiCallAllowed(request);
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- if (!bitcoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- Long balance = bitcoin.getWalletBalance(key58);
- if (balance == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
-
- return balance.toString();
- }
-
- @POST
- @Path("/wallettransactions")
- @Operation(
- summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public List getBitcoinWalletTransactions(String key58) {
- Security.checkApiCallAllowed(request);
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- if (!bitcoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return bitcoin.getWalletTransactions(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
- @POST
- @Path("/send")
- @Operation(
- summary = "Sends BTC from hierarchical, deterministic BIP32 wallet to specific address",
- description = "Currently only supports 'legacy' P2PKH Bitcoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = BitcoinSendRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public String sendBitcoin(BitcoinSendRequest bitcoinSendRequest) {
- Security.checkApiCallAllowed(request);
-
- if (bitcoinSendRequest.bitcoinAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- if (bitcoinSendRequest.feePerByte != null && bitcoinSendRequest.feePerByte <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- if (!bitcoin.isValidAddress(bitcoinSendRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (!bitcoin.isValidDeterministicKey(bitcoinSendRequest.xprv58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- Transaction spendTransaction = bitcoin.buildSpend(bitcoinSendRequest.xprv58,
- bitcoinSendRequest.receivingAddress,
- bitcoinSendRequest.bitcoinAmount,
- bitcoinSendRequest.feePerByte);
-
- if (spendTransaction == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
-
- try {
- bitcoin.broadcastTransaction(spendTransaction);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
-
- return spendTransaction.getTxId().toString();
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
deleted file mode 100644
index 98e9b01d..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java
+++ /dev/null
@@ -1,603 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.math.BigDecimal;
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.bitcoinj.core.*;
-import org.bitcoinj.script.Script;
-import org.qortal.api.*;
-import org.qortal.api.model.CrossChainBitcoinyHTLCStatus;
-import org.qortal.crosschain.*;
-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.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.utils.Base58;
-import org.qortal.utils.NTP;
-
-@Path("/crosschain/htlc")
-@Tag(name = "Cross-Chain (Hash time-locked contracts)")
-public class CrossChainHtlcResource {
-
- private static final Logger LOGGER = LogManager.getLogger(CrossChainHtlcResource.class);
-
- @Context
- HttpServletRequest request;
-
- @GET
- @Path("/address/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
- @Operation(
- summary = "Returns HTLC address based on trade info",
- description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_CRITERIA})
- public String deriveHtlcAddress(@PathParam("blockchain") String blockchainName,
- @PathParam("refundPKH") String refundPKH,
- @PathParam("locktime") int lockTime,
- @PathParam("redeemPKH") String redeemPKH,
- @PathParam("hashOfSecret") String hashOfSecret) {
- SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
- if (blockchain == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- byte[] refunderPubKeyHash;
- byte[] redeemerPubKeyHash;
- byte[] decodedHashOfSecret;
-
- try {
- refunderPubKeyHash = Base58.decode(refundPKH);
- redeemerPubKeyHash = Base58.decode(redeemPKH);
-
- if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
- } catch (IllegalArgumentException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
- }
-
- try {
- decodedHashOfSecret = Base58.decode(hashOfSecret);
- if (decodedHashOfSecret.length != 20)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- } catch (IllegalArgumentException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- }
-
- byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, decodedHashOfSecret);
-
- Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
-
- return bitcoiny.deriveP2shAddress(redeemScript);
- }
-
- @GET
- @Path("/status/{blockchain}/{refundPKH}/{locktime}/{redeemPKH}/{hashOfSecret}")
- @Operation(
- summary = "Checks HTLC status",
- description = "Blockchain can be BITCOIN or LITECOIN. Public key hashes (PKH) and hash of secret should be 20 bytes (base58 encoded). Locktime is seconds since epoch.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.APPLICATION_JSON, schema = @Schema(implementation = CrossChainBitcoinyHTLCStatus.class))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public CrossChainBitcoinyHTLCStatus checkHtlcStatus(@PathParam("blockchain") String blockchainName,
- @PathParam("refundPKH") String refundPKH,
- @PathParam("locktime") int lockTime,
- @PathParam("redeemPKH") String redeemPKH,
- @PathParam("hashOfSecret") String hashOfSecret) {
- Security.checkApiCallAllowed(request);
-
- SupportedBlockchain blockchain = SupportedBlockchain.valueOf(blockchainName);
- if (blockchain == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- byte[] refunderPubKeyHash;
- byte[] redeemerPubKeyHash;
- byte[] decodedHashOfSecret;
-
- try {
- refunderPubKeyHash = Base58.decode(refundPKH);
- redeemerPubKeyHash = Base58.decode(redeemPKH);
-
- if (refunderPubKeyHash.length != 20 || redeemerPubKeyHash.length != 20)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
- } catch (IllegalArgumentException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
- }
-
- try {
- decodedHashOfSecret = Base58.decode(hashOfSecret);
- if (decodedHashOfSecret.length != 20)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- } catch (IllegalArgumentException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- }
-
- byte[] redeemScript = BitcoinyHTLC.buildScript(refunderPubKeyHash, lockTime, redeemerPubKeyHash, decodedHashOfSecret);
-
- Bitcoiny bitcoiny = (Bitcoiny) blockchain.getInstance();
-
- String p2shAddress = bitcoiny.deriveP2shAddress(redeemScript);
-
- long now = NTP.getTime();
-
- try {
- int medianBlockTime = bitcoiny.getMedianBlockTime();
-
- // Check P2SH is funded
- long p2shBalance = bitcoiny.getConfirmedBalance(p2shAddress.toString());
-
- CrossChainBitcoinyHTLCStatus htlcStatus = new CrossChainBitcoinyHTLCStatus();
- htlcStatus.bitcoinP2shAddress = p2shAddress;
- htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8);
-
- List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString());
-
- if (p2shBalance > 0L && !fundingOutputs.isEmpty()) {
- htlcStatus.canRedeem = now >= medianBlockTime * 1000L;
- htlcStatus.canRefund = now >= lockTime * 1000L;
- }
-
- if (now >= medianBlockTime * 1000L) {
- // See if we can extract secret
- htlcStatus.secret = BitcoinyHTLC.findHtlcSecret(bitcoiny, htlcStatus.bitcoinP2shAddress);
- }
-
- return htlcStatus;
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
- @GET
- @Path("/redeem/LITECOIN/{ataddress}/{tradePrivateKey}/{secret}/{receivingAddress}")
- @Operation(
- summary = "Redeems HTLC associated with supplied AT, using private key, secret, and receiving address",
- description = "Secret and private key should be 32 bytes (base58 encoded). Receiving address must be a valid LTC P2PKH address. " +
- "The secret can be found in Alice's trade bot data or in the message to Bob's AT. " +
- "The trade private key and receiving address can be found in Bob's trade bot data.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public boolean redeemHtlc(@PathParam("ataddress") String atAddress,
- @PathParam("tradePrivateKey") String tradePrivateKey,
- @PathParam("secret") String secret,
- @PathParam("receivingAddress") String receivingAddress) {
- Security.checkApiCallAllowed(request);
-
- // base58 decode the trade private key
- byte[] decodedTradePrivateKey = null;
- if (tradePrivateKey != null)
- decodedTradePrivateKey = Base58.decode(tradePrivateKey);
-
- // base58 decode the secret
- byte[] decodedSecret = null;
- if (secret != null)
- decodedSecret = Base58.decode(secret);
-
- // Convert supplied Litecoin receiving address into public key hash (we only support P2PKH at this time)
- Address litecoinReceivingAddress;
- try {
- litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), receivingAddress);
- } catch (AddressFormatException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- }
- if (litecoinReceivingAddress.getOutputScriptType() != Script.ScriptType.P2PKH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
-
- return this.doRedeemHtlc(atAddress, decodedTradePrivateKey, decodedSecret, litecoinReceivingAccountInfo);
- }
-
- @GET
- @Path("/redeem/LITECOIN/{ataddress}")
- @Operation(
- summary = "Redeems HTLC associated with supplied AT",
- description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in a P2SH. " +
- "This requires Bob's trade bot data to be present in the database for this AT. " +
- "It will fail if the buyer has yet to redeem the QORT held in the AT.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public boolean redeemHtlc(@PathParam("ataddress") String atAddress) {
- Security.checkApiCallAllowed(request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- if (crossChainTradeData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Attempt to find secret from the buyer's message to AT
- byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
- if (decodedSecret == null) {
- LOGGER.info(() -> String.format("Unable to find secret-A from redeem message to AT %s", atAddress));
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
- }
-
- List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
- TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
-
- // Search for the tradePrivateKey in the tradebot data
- byte[] decodedPrivateKey = null;
- if (tradeBotData != null)
- decodedPrivateKey = tradeBotData.getTradePrivateKey();
-
- // Search for the litecoin receiving address in the tradebot data
- byte[] litecoinReceivingAccountInfo = null;
- if (tradeBotData != null)
- // Use receiving address PKH from tradebot data
- litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
-
- return this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
-
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @GET
- @Path("/redeemAll/LITECOIN")
- @Operation(
- summary = "Redeems HTLC for all applicable ATs in tradebot data",
- description = "To be used by a QORT seller (Bob) who needs to redeem LTC proceeds that are stuck in P2SH transactions. " +
- "This requires Bob's trade bot data to be present in the database for any ATs that need redeeming. " +
- "Returns true if at least one trade is redeemed. More detail is available in the log.txt.* file.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public boolean redeemAllHtlc() {
- Security.checkApiCallAllowed(request);
- boolean success = false;
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
-
- for (TradeBotData tradeBotData : allTradeBotData) {
- String atAddress = tradeBotData.getAtAddress();
- if (atAddress == null) {
- LOGGER.info("Missing AT address in tradebot data", atAddress);
- continue;
- }
-
- String tradeState = tradeBotData.getState();
- if (tradeState == null) {
- LOGGER.info("Missing trade state for AT {}", atAddress);
- continue;
- }
-
- if (tradeState.startsWith("ALICE")) {
- LOGGER.info("AT {} isn't redeemable because it is a buy order", atAddress);
- continue;
- }
-
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null) {
- LOGGER.info("Couldn't find AT with address {}", atAddress);
- continue;
- }
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null) {
- continue;
- }
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- if (crossChainTradeData == null) {
- LOGGER.info("Couldn't find crosschain trade data for AT {}", atAddress);
- continue;
- }
-
- // Attempt to find secret from the buyer's message to AT
- byte[] decodedSecret = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
- if (decodedSecret == null) {
- LOGGER.info("Unable to find secret-A from redeem message to AT {}", atAddress);
- continue;
- }
-
- // Search for the tradePrivateKey in the tradebot data
- byte[] decodedPrivateKey = tradeBotData.getTradePrivateKey();
-
- // Search for the litecoin receiving address PKH in the tradebot data
- byte[] litecoinReceivingAccountInfo = tradeBotData.getReceivingAccountInfo();
-
- try {
- LOGGER.info("Attempting to redeem P2SH balance associated with AT {}...", atAddress);
- boolean redeemed = this.doRedeemHtlc(atAddress, decodedPrivateKey, decodedSecret, litecoinReceivingAccountInfo);
- if (redeemed) {
- LOGGER.info("Redeemed P2SH balance associated with AT {}", atAddress);
- success = true;
- }
- else {
- LOGGER.info("Couldn't redeem P2SH balance associated with AT {}. Already redeemed?", atAddress);
- }
- } catch (ApiException e) {
- LOGGER.info("Couldn't redeem P2SH balance associated with AT {}. Missing data?", atAddress);
- }
- }
-
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
-
- return success;
- }
-
- private boolean doRedeemHtlc(String atAddress, byte[] decodedTradePrivateKey, byte[] decodedSecret, byte[] litecoinReceivingAccountInfo) {
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- if (crossChainTradeData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Validate trade private key
- if (decodedTradePrivateKey == null || decodedTradePrivateKey.length != 32)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Validate secret
- if (decodedSecret == null || decodedSecret.length != 32)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Validate receiving address
- if (litecoinReceivingAccountInfo == null || litecoinReceivingAccountInfo.length != 20)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Make sure the receiving address isn't a QORT address, given that we can share the same field for both QORT and LTC
- if (Crypto.isValidAddress(litecoinReceivingAccountInfo))
- if (Base58.encode(litecoinReceivingAccountInfo).startsWith("Q"))
- // This is likely a QORT address, not an LTC
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
-
- // Use secret-A to redeem P2SH-A
-
- Litecoin litecoin = Litecoin.getInstance();
-
- int lockTime = crossChainTradeData.lockTimeA;
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTime, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
- LOGGER.info(String.format("Redeeming P2SH address: %s", p2shAddressA));
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
- return false;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Double-check that we have redeemed P2SH-A...
- return false;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // Wait for AT to auto-refund
- return false;
-
- case FUNDED: {
- Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey);
- List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
-
- Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
- fundingOutputs, redeemScriptA, decodedSecret, litecoinReceivingAccountInfo);
-
- litecoin.broadcastTransaction(p2shRedeemTransaction);
- return true; // TODO: validate?
- }
- }
-
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, e);
- }
-
- return false;
- }
-
- @GET
- @Path("/refund/LITECOIN/{ataddress}")
- @Operation(
- summary = "Refunds HTLC associated with supplied AT",
- description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH. " +
- "This requires Alice's trade bot data to be present in the database for this AT. " +
- "It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public boolean refundHtlc(@PathParam("ataddress") String atAddress) {
- Security.checkApiCallAllowed(request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
- TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
- if (tradeBotData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- if (tradeBotData.getForeignKey() == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Determine LTC receive address for refund
- Litecoin litecoin = Litecoin.getInstance();
- String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
-
- return this.doRefundHtlc(atAddress, receiveAddress);
-
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, e);
- }
- }
-
- @GET
- @Path("/refund/LITECOIN/{ataddress}/{receivingAddress}")
- @Operation(
- summary = "Refunds HTLC associated with supplied AT, to the specified LTC receiving address",
- description = "To be used by a QORT buyer (Alice) who needs to refund their LTC that is stuck in a P2SH. " +
- "This requires Alice's trade bot data to be present in the database for this AT. " +
- "It will fail if it's already redeemed by the seller, or if the lockTime (60 minutes) hasn't passed yet.",
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.ADDRESS_UNKNOWN})
- public boolean refundHtlc(@PathParam("ataddress") String atAddress,
- @PathParam("receivingAddress") String receivingAddress) {
- Security.checkApiCallAllowed(request);
- return this.doRefundHtlc(atAddress, receivingAddress);
- }
-
-
- private boolean doRefundHtlc(String atAddress, String receiveAddress) {
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- if (crossChainTradeData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
- TradeBotData tradeBotData = allTradeBotData.stream().filter(tradeBotDataItem -> tradeBotDataItem.getAtAddress().equals(atAddress)).findFirst().orElse(null);
- if (tradeBotData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
-
- int lockTime = tradeBotData.getLockTimeA();
-
- // We can't refund P2SH-A until lockTime-A has passed
- if (NTP.getTime() <= lockTime * 1000L)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
-
- Litecoin litecoin = Litecoin.getInstance();
-
- // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
- int medianBlockTime = litecoin.getMedianBlockTime();
- if (medianBlockTime <= lockTime)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
-
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTime, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
- LOGGER.info(String.format("Refunding P2SH address: %s", p2shAddressA));
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestamp = calcFeeTimestamp(lockTime, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-A to be funded...
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_TOO_SOON);
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // Too late!
- return false;
-
- case FUNDED:{
- Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
-
- // Validate the destination LTC address
- Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
- if (receiving.getOutputScriptType() != Script.ScriptType.P2PKH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
- fundingOutputs, redeemScriptA, lockTime, receiving.getHash());
-
- litecoin.broadcastTransaction(p2shRefundTransaction);
- return true; // TODO: validate?
- }
- }
-
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, e);
- }
-
- return false;
- }
-
- private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
- return (lockTimeA - tradeTimeout * 60) * 1000L;
- }
-
-}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java
deleted file mode 100644
index 04923133..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinACCTv1Resource.java
+++ /dev/null
@@ -1,145 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import org.qortal.account.PrivateKeyAccount;
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.CrossChainSecretRequest;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.crosschain.LitecoinACCTv1;
-import org.qortal.crypto.Crypto;
-import org.qortal.data.at.ATData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.group.Group;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.transaction.MessageTransaction;
-import org.qortal.transaction.Transaction.ValidationResult;
-import org.qortal.transform.TransformationException;
-import org.qortal.transform.Transformer;
-import org.qortal.transform.transaction.MessageTransactionTransformer;
-import org.qortal.utils.Base58;
-import org.qortal.utils.NTP;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-import java.util.Arrays;
-import java.util.Random;
-
-@Path("/crosschain/LitecoinACCTv1")
-@Tag(name = "Cross-Chain (LitecoinACCTv1)")
-public class CrossChainLitecoinACCTv1Resource {
-
- @Context
- HttpServletRequest request;
-
- @POST
- @Path("/redeemmessage")
- @Operation(
- summary = "Signs and broadcasts a 'redeem' MESSAGE transaction that sends secrets to AT, releasing funds to partner",
- description = "Specify address of cross-chain AT that needs to be messaged, Alice's trade private key, the 32-byte secret, "
- + "and an address for receiving QORT from AT. All of these can be found in Alice's trade bot data. "
- + "AT needs to be in 'trade' mode. Messages sent to an AT in any other mode will be ignored, but still cost fees to send! "
- + "You need to use the private key that the AT considers the trade 'partner' otherwise the MESSAGE transaction will be invalid.",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = CrossChainSecretRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- type = "string"
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_DATA, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public boolean buildRedeemMessage(CrossChainSecretRequest secretRequest) {
- Security.checkApiCallAllowed(request);
-
- byte[] partnerPrivateKey = secretRequest.partnerPrivateKey;
-
- if (partnerPrivateKey == null || partnerPrivateKey.length != Transformer.PRIVATE_KEY_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- if (secretRequest.atAddress == null || !Crypto.isValidAtAddress(secretRequest.atAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (secretRequest.secret == null || secretRequest.secret.length != LitecoinACCTv1.SECRET_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_DATA);
-
- if (secretRequest.receivingAddress == null || !Crypto.isValidAddress(secretRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = fetchAtDataWithChecking(repository, secretRequest.atAddress);
- CrossChainTradeData crossChainTradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
-
- if (crossChainTradeData.mode != AcctMode.TRADING)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- byte[] partnerPublicKey = new PrivateKeyAccount(null, partnerPrivateKey).getPublicKey();
- String partnerAddress = Crypto.toAddress(partnerPublicKey);
-
- // MESSAGE must come from address that AT considers trade partner
- if (!crossChainTradeData.qortalPartnerAddress.equals(partnerAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- // Good to make MESSAGE
-
- byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secretRequest.secret, secretRequest.receivingAddress);
-
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, partnerPrivateKey);
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, secretRequest.atAddress, messageData, false, false);
-
- messageTransaction.computeNonce();
- messageTransaction.sign(sender);
-
- // reset repository state to prevent deadlock
- repository.discardChanges();
- ValidationResult result = messageTransaction.importAsUnconfirmed();
-
- if (result != ValidationResult.OK)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSACTION_INVALID);
-
- return true;
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- // Must be correct AT - check functionality using code hash
- if (!Arrays.equals(atData.getCodeHash(), LitecoinACCTv1.CODE_BYTES_HASH))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // No point sending message to AT that's finished
- if (atData.getIsFinished())
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return atData;
- }
-
-}
diff --git a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java b/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
deleted file mode 100644
index 8883f964..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainLitecoinResource.java
+++ /dev/null
@@ -1,167 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.util.List;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.bitcoinj.core.Transaction;
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.crosschain.LitecoinSendRequest;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.crosschain.Litecoin;
-import org.qortal.crosschain.SimpleTransaction;
-
-@Path("/crosschain/ltc")
-@Tag(name = "Cross-Chain (Litecoin)")
-public class CrossChainLitecoinResource {
-
- @Context
- HttpServletRequest request;
-
- @POST
- @Path("/walletbalance")
- @Operation(
- summary = "Returns LTC balance for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "balance (satoshis)"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public String getLitecoinWalletBalance(String key58) {
- Security.checkApiCallAllowed(request);
-
- Litecoin litecoin = Litecoin.getInstance();
-
- if (!litecoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- Long balance = litecoin.getWalletBalance(key58);
- if (balance == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
-
- return balance.toString();
- }
-
- @POST
- @Path("/wallettransactions")
- @Operation(
- summary = "Returns transactions for hierarchical, deterministic BIP32 wallet",
- description = "Supply BIP32 'm' private/public key in base58, starting with 'xprv'/'xpub' for mainnet, 'tprv'/'tpub' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- description = "BIP32 'm' private/public key in base58",
- example = "tpubD6NzVbkrYhZ4XTPc4btCZ6SMgn8CxmWkj6VBVZ1tfcJfMq4UwAjZbG8U74gGSypL9XBYk2R2BLbDBe8pcEyBKM1edsGQEPKXNbEskZozeZc"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(array = @ArraySchema( schema = @Schema( implementation = SimpleTransaction.class ) ) )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public List getLitecoinWalletTransactions(String key58) {
- Security.checkApiCallAllowed(request);
-
- Litecoin litecoin = Litecoin.getInstance();
-
- if (!litecoin.isValidDeterministicKey(key58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- try {
- return litecoin.getWalletTransactions(key58);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
- }
-
- @POST
- @Path("/send")
- @Operation(
- summary = "Sends LTC from hierarchical, deterministic BIP32 wallet to specific address",
- description = "Currently only supports 'legacy' P2PKH Litecoin addresses. Supply BIP32 'm' private key in base58, starting with 'xprv' for mainnet, 'tprv' for testnet",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = LitecoinSendRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string", description = "transaction hash"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_CRITERIA, ApiError.INVALID_ADDRESS, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE})
- public String sendBitcoin(LitecoinSendRequest litecoinSendRequest) {
- Security.checkApiCallAllowed(request);
-
- if (litecoinSendRequest.litecoinAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- if (litecoinSendRequest.feePerByte != null && litecoinSendRequest.feePerByte <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- Litecoin litecoin = Litecoin.getInstance();
-
- if (!litecoin.isValidAddress(litecoinSendRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (!litecoin.isValidDeterministicKey(litecoinSendRequest.xprv58))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- Transaction spendTransaction = litecoin.buildSpend(litecoinSendRequest.xprv58,
- litecoinSendRequest.receivingAddress,
- litecoinSendRequest.litecoinAmount,
- litecoinSendRequest.feePerByte);
-
- if (spendTransaction == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
-
- try {
- litecoin.broadcastTransaction(spendTransaction);
- } catch (ForeignBlockchainException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
- }
-
- return spendTransaction.getTxId().toString();
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/CrossChainResource.java b/src/main/java/org/qortal/api/resource/CrossChainResource.java
deleted file mode 100644
index fdd74b7d..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainResource.java
+++ /dev/null
@@ -1,424 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.security.SecurityRequirement;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Random;
-import java.util.function.Supplier;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.Path;
-import javax.ws.rs.PathParam;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.CrossChainCancelRequest;
-import org.qortal.api.model.CrossChainTradeSummary;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.crypto.Crypto;
-import org.qortal.data.at.ATData;
-import org.qortal.data.at.ATStateData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.data.transaction.BaseTransactionData;
-import org.qortal.data.transaction.MessageTransactionData;
-import org.qortal.data.transaction.TransactionData;
-import org.qortal.group.Group;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.transaction.MessageTransaction;
-import org.qortal.transaction.Transaction.ValidationResult;
-import org.qortal.transform.TransformationException;
-import org.qortal.transform.Transformer;
-import org.qortal.transform.transaction.MessageTransactionTransformer;
-import org.qortal.utils.Amounts;
-import org.qortal.utils.Base58;
-import org.qortal.utils.ByteArray;
-import org.qortal.utils.NTP;
-
-@Path("/crosschain")
-@Tag(name = "Cross-Chain")
-public class CrossChainResource {
-
- @Context
- HttpServletRequest request;
-
- @GET
- @Path("/tradeoffers")
- @Operation(
- summary = "Find cross-chain trade offers",
- responses = {
- @ApiResponse(
- content = @Content(
- array = @ArraySchema(
- schema = @Schema(
- implementation = CrossChainTradeData.class
- )
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public List getTradeOffers(
- @Parameter(
- description = "Limit to specific blockchain",
- example = "LITECOIN",
- schema = @Schema(implementation = SupportedBlockchain.class)
- ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain,
- @Parameter( ref = "limit") @QueryParam("limit") Integer limit,
- @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
- @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
- // Impose a limit on 'limit'
- if (limit != null && limit > 100)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- final boolean isExecutable = true;
- List crossChainTradesData = new ArrayList<>();
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- List atsData = repository.getATRepository().getATsByFunctionality(codeHash, isExecutable, limit, offset, reverse);
-
- for (ATData atData : atsData) {
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- crossChainTradesData.add(crossChainTradeData);
- }
- }
-
- return crossChainTradesData;
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @GET
- @Path("/trade/{ataddress}")
- @Operation(
- summary = "Show detailed trade info",
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- implementation = CrossChainTradeData.class
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.ADDRESS_UNKNOWN, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public CrossChainTradeData getTrade(@PathParam("ataddress") String atAddress) {
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return acct.populateTradeData(repository, atData);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @GET
- @Path("/trades")
- @Operation(
- summary = "Find completed cross-chain trades",
- description = "Returns summary info about successfully completed cross-chain trades",
- responses = {
- @ApiResponse(
- content = @Content(
- array = @ArraySchema(
- schema = @Schema(
- implementation = CrossChainTradeSummary.class
- )
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public List getCompletedTrades(
- @Parameter(
- description = "Limit to specific blockchain",
- example = "LITECOIN",
- schema = @Schema(implementation = SupportedBlockchain.class)
- ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain,
- @Parameter(
- description = "Only return trades that completed on/after this timestamp (milliseconds since epoch)",
- example = "1597310000000"
- ) @QueryParam("minimumTimestamp") Long minimumTimestamp,
- @Parameter( ref = "limit") @QueryParam("limit") Integer limit,
- @Parameter( ref = "offset" ) @QueryParam("offset") Integer offset,
- @Parameter( ref = "reverse" ) @QueryParam("reverse") Boolean reverse) {
- // Impose a limit on 'limit'
- if (limit != null && limit > 100)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // minimumTimestamp (if given) needs to be positive
- if (minimumTimestamp != null && minimumTimestamp <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- final Boolean isFinished = Boolean.TRUE;
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- Integer minimumFinalHeight = null;
-
- if (minimumTimestamp != null) {
- minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(minimumTimestamp);
-
- if (minimumFinalHeight == 0)
- // We don't have any blocks since minimumTimestamp, let alone trades, so nothing to return
- return Collections.emptyList();
-
- // height returned from repository is for block BEFORE timestamp
- // but we want trades AFTER timestamp so bump height accordingly
- minimumFinalHeight++;
- }
-
- List crossChainTrades = new ArrayList<>();
-
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- List atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
- isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumFinalHeight,
- limit, offset, reverse);
-
- for (ATStateData atState : atStates) {
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
-
- // We also need block timestamp for use as trade timestamp
- long timestamp = repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
-
- CrossChainTradeSummary crossChainTradeSummary = new CrossChainTradeSummary(crossChainTradeData, timestamp);
- crossChainTrades.add(crossChainTradeSummary);
- }
- }
-
- return crossChainTrades;
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @GET
- @Path("/price/{blockchain}")
- @Operation(
- summary = "Request current estimated trading price",
- description = "Returns price based on most recent completed trades. Price is expressed in terms of QORT per unit foreign currency.",
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- type = "number"
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- public long getTradePriceEstimate(
- @Parameter(
- description = "foreign blockchain",
- example = "LITECOIN",
- schema = @Schema(implementation = SupportedBlockchain.class)
- ) @PathParam("blockchain") SupportedBlockchain foreignBlockchain,
- @Parameter(
- description = "Maximum number of trades to include in price calculation",
- example = "10",
- schema = @Schema(type = "integer", defaultValue = "10")
- ) @QueryParam("maxtrades") Integer maxtrades) {
- // foreignBlockchain is required
- if (foreignBlockchain == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // We want both a minimum of 5 trades and enough trades to span at least 4 hours
- int minimumCount = 5;
- int maximumCount = maxtrades != null ? maxtrades : 10;
- long minimumPeriod = 4 * 60 * 60 * 1000L; // ms
- Boolean isFinished = Boolean.TRUE;
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(foreignBlockchain);
-
- long totalForeign = 0;
- long totalQort = 0;
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- List atStates = repository.getATRepository().getMatchingFinalATStatesQuorum(codeHash,
- isFinished, acct.getModeByteOffset(), (long) AcctMode.REDEEMED.value, minimumCount, maximumCount, minimumPeriod);
-
- for (ATStateData atState : atStates) {
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
- totalForeign += crossChainTradeData.expectedForeignAmount;
- totalQort += crossChainTradeData.qortAmount;
- }
- }
-
- return Amounts.scaledDivide(totalQort, totalForeign);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @DELETE
- @Path("/tradeoffer")
- @Operation(
- summary = "Builds raw, unsigned 'cancel' MESSAGE transaction that cancels cross-chain trade offer",
- description = "Specify address of cross-chain AT that needs to be cancelled. "
- + "AT needs to be in 'offer' mode. Messages sent to an AT in 'trade' mode will be ignored. "
- + "Performs MESSAGE proof-of-work. "
- + "You need to sign output with AT creator's private key otherwise the MESSAGE transaction will be invalid.",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = CrossChainCancelRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(
- schema = @Schema(
- type = "string"
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.REPOSITORY_ISSUE})
- @SecurityRequirement(name = "apiKey")
- public String cancelTrade(CrossChainCancelRequest cancelRequest) {
- Security.checkApiCallAllowed(request);
-
- byte[] creatorPublicKey = cancelRequest.creatorPublicKey;
-
- if (creatorPublicKey == null || creatorPublicKey.length != Transformer.PUBLIC_KEY_LENGTH)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- if (cancelRequest.atAddress == null || !Crypto.isValidAtAddress(cancelRequest.atAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = fetchAtDataWithChecking(repository, cancelRequest.atAddress);
-
- ACCT acct = SupportedBlockchain.getAcctByCodeHash(atData.getCodeHash());
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
-
- if (crossChainTradeData.mode != AcctMode.OFFERING)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- // Does supplied public key match AT creator's public key?
- if (!Arrays.equals(creatorPublicKey, atData.getCreatorPublicKey()))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PUBLIC_KEY);
-
- // Good to make MESSAGE
-
- String atCreatorAddress = Crypto.toAddress(creatorPublicKey);
- byte[] messageData = acct.buildCancelMessage(atCreatorAddress);
-
- byte[] messageTransactionBytes = buildAtMessage(repository, creatorPublicKey, cancelRequest.atAddress, messageData);
-
- return Base58.encode(messageTransactionBytes);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- // No point sending message to AT that's finished
- if (atData.getIsFinished())
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return atData;
- }
-
- private byte[] buildAtMessage(Repository repository, byte[] senderPublicKey, String atAddress, byte[] messageData) throws DataException {
- long txTimestamp = NTP.getTime();
-
- // senderPublicKey could be ephemeral trade public key where there is no corresponding account and hence no reference
- String senderAddress = Crypto.toAddress(senderPublicKey);
- byte[] lastReference = repository.getAccountRepository().getLastReference(senderAddress);
- final boolean requiresPoW = lastReference == null;
-
- if (requiresPoW) {
- Random random = new Random();
- lastReference = new byte[Transformer.SIGNATURE_LENGTH];
- random.nextBytes(lastReference);
- }
-
- int version = 4;
- int nonce = 0;
- long amount = 0L;
- Long assetId = null; // no assetId as amount is zero
- Long fee = 0L;
-
- BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, senderPublicKey, fee, null);
- TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, atAddress, amount, assetId, messageData, false, false);
-
- MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData);
-
- if (requiresPoW) {
- messageTransaction.computeNonce();
- } else {
- fee = messageTransaction.calcRecommendedFee();
- messageTransactionData.setFee(fee);
- }
-
- ValidationResult result = messageTransaction.isValidUnconfirmed();
- if (result != ValidationResult.OK)
- throw TransactionsResource.createTransactionInvalidException(request, result);
-
- try {
- return MessageTransactionTransformer.toBytes(messageTransactionData);
- } catch (TransformationException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.TRANSFORMATION_ERROR, e);
- }
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
deleted file mode 100644
index cd8766ca..00000000
--- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java
+++ /dev/null
@@ -1,286 +0,0 @@
-package org.qortal.api.resource;
-
-import io.swagger.v3.oas.annotations.Operation;
-import io.swagger.v3.oas.annotations.Parameter;
-import io.swagger.v3.oas.annotations.media.ArraySchema;
-import io.swagger.v3.oas.annotations.media.Content;
-import io.swagger.v3.oas.annotations.media.Schema;
-import io.swagger.v3.oas.annotations.parameters.RequestBody;
-import io.swagger.v3.oas.annotations.responses.ApiResponse;
-import io.swagger.v3.oas.annotations.tags.Tag;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.ws.rs.DELETE;
-import javax.ws.rs.GET;
-import javax.ws.rs.POST;
-import javax.ws.rs.Path;
-import javax.ws.rs.QueryParam;
-import javax.ws.rs.core.Context;
-import javax.ws.rs.core.MediaType;
-
-import org.qortal.account.Account;
-import org.qortal.account.PublicKeyAccount;
-import org.qortal.api.ApiError;
-import org.qortal.api.ApiErrors;
-import org.qortal.api.ApiExceptionFactory;
-import org.qortal.api.Security;
-import org.qortal.api.model.crosschain.TradeBotCreateRequest;
-import org.qortal.api.model.crosschain.TradeBotRespondRequest;
-import org.qortal.asset.Asset;
-import org.qortal.controller.tradebot.AcctTradeBot;
-import org.qortal.controller.tradebot.TradeBot;
-import org.qortal.crosschain.ForeignBlockchain;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.AcctMode;
-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.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.utils.Base58;
-
-@Path("/crosschain/tradebot")
-@Tag(name = "Cross-Chain (Trade-Bot)")
-public class CrossChainTradeBotResource {
-
- @Context
- HttpServletRequest request;
-
- @GET
- @Operation(
- summary = "List current trade-bot states",
- responses = {
- @ApiResponse(
- content = @Content(
- array = @ArraySchema(
- schema = @Schema(
- implementation = TradeBotData.class
- )
- )
- )
- )
- }
- )
- @ApiErrors({ApiError.REPOSITORY_ISSUE})
- public List getTradeBotStates(
- @Parameter(
- description = "Limit to specific blockchain",
- example = "LITECOIN",
- schema = @Schema(implementation = SupportedBlockchain.class)
- ) @QueryParam("foreignBlockchain") SupportedBlockchain foreignBlockchain) {
- Security.checkApiCallAllowed(request);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- List allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
-
- if (foreignBlockchain == null)
- return allTradeBotData;
-
- return allTradeBotData.stream().filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain.name())).collect(Collectors.toList());
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @POST
- @Path("/create")
- @Operation(
- summary = "Create a trade offer (trade-bot entry)",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = TradeBotCreateRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PUBLIC_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.INSUFFICIENT_BALANCE, ApiError.REPOSITORY_ISSUE})
- @SuppressWarnings("deprecation")
- public String tradeBotCreator(TradeBotCreateRequest tradeBotCreateRequest) {
- Security.checkApiCallAllowed(request);
-
- if (tradeBotCreateRequest.foreignBlockchain == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- ForeignBlockchain foreignBlockchain = tradeBotCreateRequest.foreignBlockchain.getInstance();
-
- // We prefer foreignAmount to deprecated bitcoinAmount
- if (tradeBotCreateRequest.foreignAmount == null)
- tradeBotCreateRequest.foreignAmount = tradeBotCreateRequest.bitcoinAmount;
-
- if (!foreignBlockchain.isValidAddress(tradeBotCreateRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (tradeBotCreateRequest.tradeTimeout < 60)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- if (tradeBotCreateRequest.foreignAmount == null || tradeBotCreateRequest.foreignAmount <= 0)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- if (tradeBotCreateRequest.qortAmount <= 0 || tradeBotCreateRequest.fundingQortAmount <= 0)
- 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);
-
- if (creator.getConfirmedBalance(Asset.QORT) < tradeBotCreateRequest.fundingQortAmount)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INSUFFICIENT_BALANCE);
-
- byte[] unsignedBytes = TradeBot.getInstance().createTrade(repository, tradeBotCreateRequest);
- if (unsignedBytes == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return Base58.encode(unsignedBytes);
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @POST
- @Path("/respond")
- @Operation(
- summary = "Respond to a trade offer. NOTE: WILL SPEND FUNDS!)",
- description = "Start a new trade-bot entry to respond to chosen trade offer.",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.APPLICATION_JSON,
- schema = @Schema(
- implementation = TradeBotRespondRequest.class
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
- @SuppressWarnings("deprecation")
- public String tradeBotResponder(TradeBotRespondRequest tradeBotRespondRequest) {
- Security.checkApiCallAllowed(request);
-
- final String atAddress = tradeBotRespondRequest.atAddress;
-
- // We prefer foreignKey to deprecated xprv58
- if (tradeBotRespondRequest.foreignKey == null)
- tradeBotRespondRequest.foreignKey = tradeBotRespondRequest.xprv58;
-
- if (tradeBotRespondRequest.foreignKey == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- if (atAddress == null || !Crypto.isValidAtAddress(atAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (tradeBotRespondRequest.receivingAddress == null || !Crypto.isValidAddress(tradeBotRespondRequest.receivingAddress))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- // Extract data from cross-chain trading AT
- try (final Repository repository = RepositoryManager.getRepository()) {
- ATData atData = fetchAtDataWithChecking(repository, atAddress);
-
- // TradeBot uses AT's code hash to map to ACCT
- ACCT acct = TradeBot.getInstance().getAcctUsingAtData(atData);
- if (acct == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (!acct.getBlockchain().isValidWalletKey(tradeBotRespondRequest.foreignKey))
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
-
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atData);
- if (crossChainTradeData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
-
- if (crossChainTradeData.mode != AcctMode.OFFERING)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- AcctTradeBot.ResponseResult result = TradeBot.getInstance().startResponse(repository, atData, acct, crossChainTradeData,
- tradeBotRespondRequest.foreignKey, tradeBotRespondRequest.receivingAddress);
-
- switch (result) {
- case OK:
- return "true";
-
- case BALANCE_ISSUE:
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE);
-
- case NETWORK_ISSUE:
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE);
-
- default:
- return "false";
- }
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- @DELETE
- @Operation(
- summary = "Delete completed trade",
- requestBody = @RequestBody(
- required = true,
- content = @Content(
- mediaType = MediaType.TEXT_PLAIN,
- schema = @Schema(
- type = "string",
- example = "93MB2qRDNVLxbmmPuYpLdAqn3u2x9ZhaVZK5wELHueP8"
- )
- )
- ),
- responses = {
- @ApiResponse(
- content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
- )
- }
- )
- @ApiErrors({ApiError.INVALID_ADDRESS, ApiError.REPOSITORY_ISSUE})
- public String tradeBotDelete(String tradePrivateKey58) {
- Security.checkApiCallAllowed(request);
-
- final byte[] tradePrivateKey;
- try {
- tradePrivateKey = Base58.decode(tradePrivateKey58);
-
- if (tradePrivateKey.length != 32)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
- } catch (NumberFormatException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
- }
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- // Handed off to TradeBot
- return TradeBot.getInstance().deleteEntry(repository, tradePrivateKey) ? "true" : "false";
- } catch (DataException e) {
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e);
- }
- }
-
- private ATData fetchAtDataWithChecking(Repository repository, String atAddress) throws DataException {
- ATData atData = repository.getATRepository().fromATAddress(atAddress);
- if (atData == null)
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.ADDRESS_UNKNOWN);
-
- // No point sending message to AT that's finished
- if (atData.getIsFinished())
- throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
-
- return atData;
- }
-
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/api/websocket/PresenceWebSocket.java b/src/main/java/org/qortal/api/websocket/PresenceWebSocket.java
deleted file mode 100644
index 26d131c4..00000000
--- a/src/main/java/org/qortal/api/websocket/PresenceWebSocket.java
+++ /dev/null
@@ -1,244 +0,0 @@
-package org.qortal.api.websocket;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.Collections;
-import java.util.EnumMap;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
-import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
-import org.qortal.controller.Controller;
-import org.qortal.crypto.Crypto;
-import org.qortal.data.transaction.PresenceTransactionData;
-import org.qortal.data.transaction.TransactionData;
-import org.qortal.event.Event;
-import org.qortal.event.EventBus;
-import org.qortal.event.Listener;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.transaction.PresenceTransaction.PresenceType;
-import org.qortal.transaction.Transaction.TransactionType;
-import org.qortal.utils.Base58;
-import org.qortal.utils.NTP;
-
-@WebSocket
-@SuppressWarnings("serial")
-public class PresenceWebSocket extends ApiWebSocket implements Listener {
-
- @XmlAccessorType(XmlAccessType.FIELD)
- @SuppressWarnings("unused")
- private static class PresenceInfo {
- private final PresenceType presenceType;
- private final String publicKey;
- private final long timestamp;
- private final String address;
-
- protected PresenceInfo() {
- this.presenceType = null;
- this.publicKey = null;
- this.timestamp = 0L;
- this.address = null;
- }
-
- public PresenceInfo(PresenceType presenceType, String pubKey58, long timestamp) {
- this.presenceType = presenceType;
- this.publicKey = pubKey58;
- this.timestamp = timestamp;
- this.address = Crypto.toAddress(Base58.decode(this.publicKey));
- }
-
- public PresenceType getPresenceType() {
- return this.presenceType;
- }
-
- public String getPublicKey() {
- return this.publicKey;
- }
-
- public long getTimestamp() {
- return this.timestamp;
- }
-
- public String getAddress() {
- return this.address;
- }
- }
-
- /** Outer map key is PresenceType (enum), inner map key is public key in base58, inner map value is timestamp */
- private static final Map> currentEntries = Collections.synchronizedMap(new EnumMap<>(PresenceType.class));
-
- /** (Optional) PresenceType used for filtering by that Session. */
- private static final Map sessionPresenceTypes = Collections.synchronizedMap(new HashMap<>());
-
- @Override
- public void configure(WebSocketServletFactory factory) {
- factory.register(PresenceWebSocket.class);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- populateCurrentInfo(repository);
- } catch (DataException e) {
- // How to fail properly?
- return;
- }
-
- EventBus.INSTANCE.addListener(this::listen);
- }
-
- @Override
- public void listen(Event event) {
- // We use NewBlockEvent as a proxy for 1-minute timer
- if (!(event instanceof Controller.NewTransactionEvent) && !(event instanceof Controller.NewBlockEvent))
- return;
-
- removeOldEntries();
-
- if (event instanceof Controller.NewBlockEvent)
- // We only wanted a chance to cull old entries
- return;
-
- TransactionData transactionData = ((Controller.NewTransactionEvent) event).getTransactionData();
-
- if (transactionData.getType() != TransactionType.PRESENCE)
- return;
-
- PresenceTransactionData presenceData = (PresenceTransactionData) transactionData;
- PresenceType presenceType = presenceData.getPresenceType();
-
- // Put/replace for this publickey making sure we keep newest timestamp
- String pubKey58 = Base58.encode(presenceData.getCreatorPublicKey());
- long ourTimestamp = presenceData.getTimestamp();
- long computedTimestamp = mergePresence(presenceType, pubKey58, ourTimestamp);
-
- if (computedTimestamp != ourTimestamp)
- // nothing changed
- return;
-
- List presenceInfo = Collections.singletonList(new PresenceInfo(presenceType, pubKey58, computedTimestamp));
-
- // Notify sessions
- for (Session session : getSessions()) {
- PresenceType sessionPresenceType = sessionPresenceTypes.get(session);
-
- if (sessionPresenceType == null || sessionPresenceType == presenceType)
- sendPresenceInfo(session, presenceInfo);
- }
- }
-
- @OnWebSocketConnect
- @Override
- public void onWebSocketConnect(Session session) {
- Map> queryParams = session.getUpgradeRequest().getParameterMap();
- List presenceTypes = queryParams.get("presenceType");
-
- // We only support ONE presenceType
- String presenceTypeName = presenceTypes == null || presenceTypes.isEmpty() ? null : presenceTypes.get(0);
-
- PresenceType presenceType = presenceTypeName == null ? null : PresenceType.fromString(presenceTypeName);
-
- // Make sure that if caller does give a presenceType, that it is a valid/known one.
- if (presenceTypeName != null && presenceType == null) {
- session.close(4003, "unknown presenceType: " + presenceTypeName);
- return;
- }
-
- // Save session's requested PresenceType, if given
- if (presenceType != null)
- sessionPresenceTypes.put(session, presenceType);
-
- List presenceInfo;
-
- synchronized (currentEntries) {
- presenceInfo = currentEntries.entrySet().stream()
- .filter(entry -> presenceType == null ? true : entry.getKey() == presenceType)
- .flatMap(entry -> entry.getValue().entrySet().stream().map(innerEntry -> new PresenceInfo(entry.getKey(), innerEntry.getKey(), innerEntry.getValue())))
- .collect(Collectors.toList());
- }
-
- if (!sendPresenceInfo(session, presenceInfo)) {
- session.close(4002, "websocket issue");
- return;
- }
-
- super.onWebSocketConnect(session);
- }
-
- @OnWebSocketClose
- @Override
- public void onWebSocketClose(Session session, int statusCode, String reason) {
- // clean up
- sessionPresenceTypes.remove(session);
-
- super.onWebSocketClose(session, statusCode, reason);
- }
-
- @OnWebSocketError
- public void onWebSocketError(Session session, Throwable throwable) {
- /* ignored */
- }
-
- @OnWebSocketMessage
- public void onWebSocketMessage(Session session, String message) {
- /* ignored */
- }
-
- private boolean sendPresenceInfo(Session session, List presenceInfo) {
- try {
- StringWriter stringWriter = new StringWriter();
- marshall(stringWriter, presenceInfo);
-
- String output = stringWriter.toString();
- session.getRemote().sendStringByFuture(output);
- } catch (IOException e) {
- // No output this time?
- return false;
- }
-
- return true;
- }
-
- private static void populateCurrentInfo(Repository repository) throws DataException {
- // We want ALL PRESENCE transactions
-
- List presenceTransactionsData = repository.getTransactionRepository().getUnconfirmedTransactions(TransactionType.PRESENCE, null);
-
- for (TransactionData transactionData : presenceTransactionsData) {
- PresenceTransactionData presenceData = (PresenceTransactionData) transactionData;
-
- PresenceType presenceType = presenceData.getPresenceType();
-
- // Put/replace for this publickey making sure we keep newest timestamp
- String pubKey58 = Base58.encode(presenceData.getCreatorPublicKey());
- long ourTimestamp = presenceData.getTimestamp();
-
- mergePresence(presenceType, pubKey58, ourTimestamp);
- }
- }
-
- private static long mergePresence(PresenceType presenceType, String pubKey58, long ourTimestamp) {
- Map typedPubkeyTimestamps = currentEntries.computeIfAbsent(presenceType, someType -> Collections.synchronizedMap(new HashMap<>()));
- return typedPubkeyTimestamps.compute(pubKey58, (somePubKey58, currentTimestamp) -> (currentTimestamp == null || currentTimestamp < ourTimestamp) ? ourTimestamp : currentTimestamp);
- }
-
- private static void removeOldEntries() {
- long now = NTP.getTime();
-
- currentEntries.entrySet().forEach(entry -> {
- long expiryThreshold = now - entry.getKey().getLifetime();
- entry.getValue().entrySet().removeIf(pubkeyTimestamp -> pubkeyTimestamp.getValue() < expiryThreshold);
- });
- }
-
-}
diff --git a/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
deleted file mode 100644
index 55969c6b..00000000
--- a/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java
+++ /dev/null
@@ -1,157 +0,0 @@
-package org.qortal.api.websocket;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
-import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
-import org.qortal.controller.tradebot.TradeBot;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.data.crosschain.TradeBotData;
-import org.qortal.event.Event;
-import org.qortal.event.EventBus;
-import org.qortal.event.Listener;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.utils.Base58;
-
-@WebSocket
-@SuppressWarnings("serial")
-public class TradeBotWebSocket extends ApiWebSocket implements Listener {
-
- /** Cache of trade-bot entry states, keyed by trade-bot entry's "trade private key" (base58) */
- private static final Map PREVIOUS_STATES = new HashMap<>();
-
- private static final Map sessionBlockchain = Collections.synchronizedMap(new HashMap<>());
-
- @Override
- public void configure(WebSocketServletFactory factory) {
- factory.register(TradeBotWebSocket.class);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- List tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
- if (tradeBotEntries == null)
- // How do we properly fail here?
- return;
-
- PREVIOUS_STATES.putAll(tradeBotEntries.stream().collect(Collectors.toMap(entry -> Base58.encode(entry.getTradePrivateKey()), TradeBotData::getStateValue)));
- } catch (DataException e) {
- // No output this time
- }
-
- EventBus.INSTANCE.addListener(this::listen);
- }
-
- @Override
- public void listen(Event event) {
- if (!(event instanceof TradeBot.StateChangeEvent))
- return;
-
- TradeBotData tradeBotData = ((TradeBot.StateChangeEvent) event).getTradeBotData();
- String tradePrivateKey58 = Base58.encode(tradeBotData.getTradePrivateKey());
-
- synchronized (PREVIOUS_STATES) {
- Integer previousStateValue = PREVIOUS_STATES.get(tradePrivateKey58);
- if (previousStateValue != null && previousStateValue == tradeBotData.getStateValue())
- // Not changed
- return;
-
- PREVIOUS_STATES.put(tradePrivateKey58, tradeBotData.getStateValue());
- }
-
- List tradeBotEntries = Collections.singletonList(tradeBotData);
-
- for (Session session : getSessions()) {
- // Only send if this session has this/no preferred blockchain
- String preferredBlockchain = sessionBlockchain.get(session);
-
- if (preferredBlockchain == null || preferredBlockchain.equals(tradeBotData.getForeignBlockchain()))
- sendEntries(session, tradeBotEntries);
- }
- }
-
- @OnWebSocketConnect
- @Override
- public void onWebSocketConnect(Session session) {
- Map> queryParams = session.getUpgradeRequest().getParameterMap();
-
- List foreignBlockchains = queryParams.get("foreignBlockchain");
- final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
-
- // Make sure blockchain (if any) is valid
- if (foreignBlockchain != null && SupportedBlockchain.fromString(foreignBlockchain) == null) {
- session.close(4003, "unknown blockchain: " + foreignBlockchain);
- return;
- }
-
- // save session's preferred blockchain (if any)
- sessionBlockchain.put(session, foreignBlockchain);
-
- // Send all known trade-bot entries
- try (final Repository repository = RepositoryManager.getRepository()) {
- List tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData();
-
- // Optional filtering
- if (foreignBlockchain != null)
- tradeBotEntries = tradeBotEntries.stream()
- .filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain))
- .collect(Collectors.toList());
-
- if (!sendEntries(session, tradeBotEntries)) {
- session.close(4002, "websocket issue");
- return;
- }
- } catch (DataException e) {
- session.close(4001, "repository issue fetching trade-bot entries");
- return;
- }
-
- super.onWebSocketConnect(session);
- }
-
- @OnWebSocketClose
- @Override
- public void onWebSocketClose(Session session, int statusCode, String reason) {
- // clean up
- sessionBlockchain.remove(session);
-
- super.onWebSocketClose(session, statusCode, reason);
- }
-
- @OnWebSocketError
- public void onWebSocketError(Session session, Throwable throwable) {
- /* ignored */
- }
-
- @OnWebSocketMessage
- public void onWebSocketMessage(Session session, String message) {
- /* ignored */
- }
-
- private boolean sendEntries(Session session, List tradeBotEntries) {
- try {
- StringWriter stringWriter = new StringWriter();
- marshall(stringWriter, tradeBotEntries);
-
- String output = stringWriter.toString();
- session.getRemote().sendStringByFuture(output);
- } catch (IOException e) {
- // No output this time?
- return false;
- }
-
- return true;
- }
-
-}
diff --git a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
deleted file mode 100644
index 186f79e3..00000000
--- a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java
+++ /dev/null
@@ -1,351 +0,0 @@
-package org.qortal.api.websocket;
-
-import java.io.IOException;
-import java.io.StringWriter;
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Predicate;
-import java.util.function.Supplier;
-import java.util.stream.Collectors;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.eclipse.jetty.websocket.api.Session;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketClose;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketConnect;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketError;
-import org.eclipse.jetty.websocket.api.annotations.OnWebSocketMessage;
-import org.eclipse.jetty.websocket.api.annotations.WebSocket;
-import org.eclipse.jetty.websocket.servlet.WebSocketServletFactory;
-import org.qortal.api.model.CrossChainOfferSummary;
-import org.qortal.controller.Controller;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.data.at.ATStateData;
-import org.qortal.data.block.BlockData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.event.Event;
-import org.qortal.event.EventBus;
-import org.qortal.event.Listener;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.repository.RepositoryManager;
-import org.qortal.utils.ByteArray;
-import org.qortal.utils.NTP;
-
-@WebSocket
-@SuppressWarnings("serial")
-public class TradeOffersWebSocket extends ApiWebSocket implements Listener {
-
- private static final Logger LOGGER = LogManager.getLogger(TradeOffersWebSocket.class);
-
- private static class CachedOfferInfo {
- public final Map previousAtModes = new HashMap<>();
-
- // OFFERING
- public final Map currentSummaries = new HashMap<>();
- // REDEEMED/REFUNDED/CANCELLED
- public final Map historicSummaries = new HashMap<>();
- }
- // Manual synchronization
- private static final Map cachedInfoByBlockchain = new HashMap<>();
-
- private static final Predicate isHistoric = offerSummary
- -> offerSummary.getMode() == AcctMode.REDEEMED
- || offerSummary.getMode() == AcctMode.REFUNDED
- || offerSummary.getMode() == AcctMode.CANCELLED;
-
- private static final Map sessionBlockchain = Collections.synchronizedMap(new HashMap<>());
-
- @Override
- public void configure(WebSocketServletFactory factory) {
- factory.register(TradeOffersWebSocket.class);
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- populateCurrentSummaries(repository);
-
- populateHistoricSummaries(repository);
- } catch (DataException e) {
- // How to fail properly?
- return;
- }
-
- EventBus.INSTANCE.addListener(this::listen);
- }
-
- @Override
- public void listen(Event event) {
- if (!(event instanceof Controller.NewBlockEvent))
- return;
-
- BlockData blockData = ((Controller.NewBlockEvent) event).getBlockData();
-
- // Process any new info
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- // Find any new/changed trade ATs since this block
- final Boolean isFinished = null;
- final Integer dataByteOffset = null;
- final Long expectedValue = null;
- final Integer minimumFinalHeight = blockData.getHeight();
-
- for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
-
- List crossChainOfferSummaries = new ArrayList<>();
-
- synchronized (cachedInfoByBlockchain) {
- CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- List atStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
- isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
- null, null, null);
-
- crossChainOfferSummaries.addAll(produceSummaries(repository, acct, atStates, blockData.getTimestamp()));
- }
-
- // Remove any entries unchanged from last time
- crossChainOfferSummaries.removeIf(offerSummary -> cachedInfo.previousAtModes.get(offerSummary.getQortalAtAddress()) == offerSummary.getMode());
-
- // Skip to next blockchain if nothing has changed (for this blockchain)
- if (crossChainOfferSummaries.isEmpty())
- continue;
-
- // Update
- for (CrossChainOfferSummary offerSummary : crossChainOfferSummaries) {
- String offerAtAddress = offerSummary.getQortalAtAddress();
-
- cachedInfo.previousAtModes.put(offerAtAddress, offerSummary.getMode());
- LOGGER.trace(() -> String.format("Block height: %d, AT: %s, mode: %s", blockData.getHeight(), offerAtAddress, offerSummary.getMode().name()));
-
- switch (offerSummary.getMode()) {
- case OFFERING:
- cachedInfo.currentSummaries.put(offerAtAddress, offerSummary);
- cachedInfo.historicSummaries.remove(offerAtAddress);
- break;
-
- case REDEEMED:
- case REFUNDED:
- case CANCELLED:
- cachedInfo.currentSummaries.remove(offerAtAddress);
- cachedInfo.historicSummaries.put(offerAtAddress, offerSummary);
- break;
-
- case TRADING:
- cachedInfo.currentSummaries.remove(offerAtAddress);
- cachedInfo.historicSummaries.remove(offerAtAddress);
- break;
- }
- }
-
- // Remove any historic offers that are over 24 hours old
- final long tooOldTimestamp = NTP.getTime() - 24 * 60 * 60 * 1000L;
- cachedInfo.historicSummaries.values().removeIf(historicSummary -> historicSummary.getTimestamp() < tooOldTimestamp);
- }
-
- // Notify sessions
- for (Session session : getSessions()) {
- // Only send if this session has this/no preferred blockchain
- String preferredBlockchain = sessionBlockchain.get(session);
-
- if (preferredBlockchain == null || preferredBlockchain.equals(blockchain.name()))
- sendOfferSummaries(session, crossChainOfferSummaries);
- }
-
- }
- } catch (DataException e) {
- // No output this time
- }
- }
-
- @OnWebSocketConnect
- @Override
- public void onWebSocketConnect(Session session) {
- Map> queryParams = session.getUpgradeRequest().getParameterMap();
- final boolean includeHistoric = queryParams.get("includeHistoric") != null;
-
- List foreignBlockchains = queryParams.get("foreignBlockchain");
- final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0);
-
- // Make sure blockchain (if any) is valid
- if (foreignBlockchain != null && SupportedBlockchain.fromString(foreignBlockchain) == null) {
- session.close(4003, "unknown blockchain: " + foreignBlockchain);
- return;
- }
-
- // Save session's preferred blockchain, if given
- if (foreignBlockchain != null)
- sessionBlockchain.put(session, foreignBlockchain);
-
- List crossChainOfferSummaries = new ArrayList<>();
-
- synchronized (cachedInfoByBlockchain) {
- Collection cachedInfos;
-
- if (foreignBlockchain == null)
- // No preferred blockchain, so iterate through all of them
- cachedInfos = cachedInfoByBlockchain.values();
- else
- cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo()));
-
- for (CachedOfferInfo cachedInfo : cachedInfos) {
- crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values());
-
- if (includeHistoric)
- crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values());
- }
- }
-
- if (!sendOfferSummaries(session, crossChainOfferSummaries)) {
- session.close(4002, "websocket issue");
- return;
- }
-
- super.onWebSocketConnect(session);
- }
-
- @OnWebSocketClose
- @Override
- public void onWebSocketClose(Session session, int statusCode, String reason) {
- // clean up
- sessionBlockchain.remove(session);
-
- super.onWebSocketClose(session, statusCode, reason);
- }
-
- @OnWebSocketError
- public void onWebSocketError(Session session, Throwable throwable) {
- /* ignored */
- }
-
- @OnWebSocketMessage
- public void onWebSocketMessage(Session session, String message) {
- /* ignored */
- }
-
- private boolean sendOfferSummaries(Session session, List crossChainOfferSummaries) {
- try {
- StringWriter stringWriter = new StringWriter();
- marshall(stringWriter, crossChainOfferSummaries);
-
- String output = stringWriter.toString();
- session.getRemote().sendStringByFuture(output);
- } catch (IOException e) {
- // No output this time?
- return false;
- }
-
- return true;
- }
-
- private static void populateCurrentSummaries(Repository repository) throws DataException {
- // We want ALL OFFERING trades
- Boolean isFinished = Boolean.FALSE;
- Long expectedValue = (long) AcctMode.OFFERING.value;
- Integer minimumFinalHeight = null;
-
- for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
-
- CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- Integer dataByteOffset = acct.getModeByteOffset();
- List initialAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
- isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
- null, null, null);
-
- if (initialAtStates == null)
- throw new DataException("Couldn't fetch current trades from repository");
-
- // Save initial AT modes
- cachedInfo.previousAtModes.putAll(initialAtStates.stream().collect(Collectors.toMap(ATStateData::getATAddress, atState -> AcctMode.OFFERING)));
-
- // Convert to offer summaries
- cachedInfo.currentSummaries.putAll(produceSummaries(repository, acct, initialAtStates, null).stream()
- .collect(Collectors.toMap(CrossChainOfferSummary::getQortalAtAddress, offerSummary -> offerSummary)));
- }
- }
- }
-
- private static void populateHistoricSummaries(Repository repository) throws DataException {
- // We want REDEEMED/REFUNDED/CANCELLED trades over the last 24 hours
- long timestamp = System.currentTimeMillis() - 24 * 60 * 60 * 1000L;
- int minimumFinalHeight = repository.getBlockRepository().getHeightFromTimestamp(timestamp);
-
- if (minimumFinalHeight == 0)
- throw new DataException("Couldn't fetch block timestamp from repository");
-
- Boolean isFinished = Boolean.TRUE;
- Integer dataByteOffset = null;
- Long expectedValue = null;
- ++minimumFinalHeight; // because height is just *before* timestamp
-
- for (SupportedBlockchain blockchain : SupportedBlockchain.values()) {
- Map> acctsByCodeHash = SupportedBlockchain.getFilteredAcctMap(blockchain);
-
- CachedOfferInfo cachedInfo = cachedInfoByBlockchain.computeIfAbsent(blockchain.name(), k -> new CachedOfferInfo());
-
- for (Map.Entry> acctInfo : acctsByCodeHash.entrySet()) {
- byte[] codeHash = acctInfo.getKey().value;
- ACCT acct = acctInfo.getValue().get();
-
- List historicAtStates = repository.getATRepository().getMatchingFinalATStates(codeHash,
- isFinished, dataByteOffset, expectedValue, minimumFinalHeight,
- null, null, null);
-
- if (historicAtStates == null)
- throw new DataException("Couldn't fetch historic trades from repository");
-
- for (ATStateData historicAtState : historicAtStates) {
- CrossChainOfferSummary historicOfferSummary = produceSummary(repository, acct, historicAtState, null);
-
- if (!isHistoric.test(historicOfferSummary))
- continue;
-
- // Add summary to initial burst
- cachedInfo.historicSummaries.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary);
-
- // Save initial AT mode
- cachedInfo.previousAtModes.put(historicOfferSummary.getQortalAtAddress(), historicOfferSummary.getMode());
- }
- }
- }
- }
-
- private static CrossChainOfferSummary produceSummary(Repository repository, ACCT acct, ATStateData atState, Long timestamp) throws DataException {
- CrossChainTradeData crossChainTradeData = acct.populateTradeData(repository, atState);
-
- long atStateTimestamp;
-
- if (crossChainTradeData.mode == AcctMode.OFFERING)
- // We want when trade was created, not when it was last updated
- atStateTimestamp = crossChainTradeData.creationTimestamp;
- else
- atStateTimestamp = timestamp != null ? timestamp : repository.getBlockRepository().getTimestampFromHeight(atState.getHeight());
-
- return new CrossChainOfferSummary(crossChainTradeData, atStateTimestamp);
- }
-
- private static List produceSummaries(Repository repository, ACCT acct, List atStates, Long timestamp) throws DataException {
- List offerSummaries = new ArrayList<>();
-
- for (ATStateData atState : atStates)
- offerSummaries.add(produceSummary(repository, acct, atState, timestamp));
-
- return offerSummaries;
- }
-
-}
diff --git a/src/main/java/org/qortal/at/QortalFunctionCode.java b/src/main/java/org/qortal/at/QortalFunctionCode.java
index 0d11e488..8f989e19 100644
--- a/src/main/java/org/qortal/at/QortalFunctionCode.java
+++ b/src/main/java/org/qortal/at/QortalFunctionCode.java
@@ -10,10 +10,8 @@ import org.ciyam.at.ExecutionException;
import org.ciyam.at.FunctionData;
import org.ciyam.at.IllegalFunctionCodeException;
import org.ciyam.at.MachineState;
-import org.qortal.crosschain.Bitcoin;
import org.qortal.crypto.Crypto;
import org.qortal.data.transaction.TransactionData;
-import org.qortal.settings.Settings;
/**
* Qortal-specific CIYAM-AT Functions.
@@ -100,19 +98,6 @@ public enum QortalFunctionCode {
setB(state, pkh);
}
},
- /**
- * Convert 20-byte value in LSB of B1, and all of B2 & B3 to P2SH.
- * 0x0511
- * P2SH stored in lower 25 bytes of B.
- */
- CONVERT_B_TO_P2SH(0x0511, 0, false) {
- @Override
- protected void postCheckExecute(FunctionData functionData, MachineState state, short rawFunctionCode) throws ExecutionException {
- byte addressPrefix = Settings.getInstance().getBitcoinNet() == Bitcoin.BitcoinNet.MAIN ? 0x05 : (byte) 0xc4;
-
- convertAddressInB(addressPrefix, state);
- }
- },
/**
* Convert 20-byte value in LSB of B1, and all of B2 & B3 to Qortal address.
* 0x0512
diff --git a/src/main/java/org/qortal/controller/Controller.java b/src/main/java/org/qortal/controller/Controller.java
index c7bccb73..1e74f365 100644
--- a/src/main/java/org/qortal/controller/Controller.java
+++ b/src/main/java/org/qortal/controller/Controller.java
@@ -46,7 +46,6 @@ import org.qortal.block.Block;
import org.qortal.block.BlockChain;
import org.qortal.block.BlockChain.BlockTimingByHeight;
import org.qortal.controller.Synchronizer.SynchronizationResult;
-import org.qortal.controller.tradebot.TradeBot;
import org.qortal.crypto.Crypto;
import org.qortal.data.account.MintingAccountData;
import org.qortal.data.account.RewardShareData;
@@ -483,9 +482,6 @@ public class Controller extends Thread {
blockMinter = new BlockMinter();
blockMinter.start();
- LOGGER.info("Starting trade-bot");
- TradeBot.getInstance();
-
// Arbitrary transaction data manager
// LOGGER.info("Starting arbitrary-transaction data manager");
// ArbitraryDataManager.getInstance().start();
diff --git a/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java b/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java
deleted file mode 100644
index 84a0d484..00000000
--- a/src/main/java/org/qortal/controller/tradebot/AcctTradeBot.java
+++ /dev/null
@@ -1,30 +0,0 @@
-package org.qortal.controller.tradebot;
-
-import java.util.List;
-
-import org.qortal.api.model.crosschain.TradeBotCreateRequest;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.data.at.ATData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.data.crosschain.TradeBotData;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-
-public interface AcctTradeBot {
-
- public enum ResponseResult { OK, BALANCE_ISSUE, NETWORK_ISSUE, TRADE_ALREADY_EXISTS }
-
- /** Returns list of state names for trade-bot entries that have ended, e.g. redeemed, refunded or cancelled. */
- public List getEndStates();
-
- public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException;
-
- public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct,
- CrossChainTradeData crossChainTradeData, String foreignKey, String receivingAddress) throws DataException;
-
- public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException;
-
- public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException;
-
-}
diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java
deleted file mode 100644
index ca2e2518..00000000
--- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java
+++ /dev/null
@@ -1,1273 +0,0 @@
-package org.qortal.controller.tradebot;
-
-import static java.util.Arrays.stream;
-import static java.util.stream.Collectors.toMap;
-
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.stream.Collectors;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.bitcoinj.core.Address;
-import org.bitcoinj.core.AddressFormatException;
-import org.bitcoinj.core.Coin;
-import org.bitcoinj.core.ECKey;
-import org.bitcoinj.core.Transaction;
-import org.bitcoinj.core.TransactionOutput;
-import org.bitcoinj.script.Script.ScriptType;
-import org.qortal.account.PrivateKeyAccount;
-import org.qortal.account.PublicKeyAccount;
-import org.qortal.api.model.crosschain.TradeBotCreateRequest;
-import org.qortal.asset.Asset;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.AcctMode;
-import org.qortal.crosschain.Bitcoin;
-import org.qortal.crosschain.BitcoinACCTv1;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.crosschain.SupportedBlockchain;
-import org.qortal.crosschain.BitcoinyHTLC;
-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.transaction.DeployAtTransaction;
-import org.qortal.transaction.MessageTransaction;
-import org.qortal.transaction.Transaction.ValidationResult;
-import org.qortal.transform.TransformationException;
-import org.qortal.transform.transaction.DeployAtTransactionTransformer;
-import org.qortal.utils.Base58;
-import org.qortal.utils.NTP;
-
-/**
- * Performing cross-chain trading steps on behalf of user.
- *
- * We deal with three different independent state-spaces here:
- *
- *
Qortal blockchain
- *
Foreign blockchain
- *
Trade-bot entries
- *
- */
-public class BitcoinACCTv1TradeBot implements AcctTradeBot {
-
- private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv1TradeBot.class);
-
- public enum State implements TradeBot.StateNameAndValueSupplier {
- BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
- BOB_WAITING_FOR_MESSAGE(15, true, true),
- BOB_WAITING_FOR_P2SH_B(20, true, true),
- BOB_WAITING_FOR_AT_REDEEM(25, true, true),
- BOB_DONE(30, false, false),
- BOB_REFUNDED(35, false, false),
-
- ALICE_WAITING_FOR_P2SH_A(80, true, true),
- ALICE_WAITING_FOR_AT_LOCK(85, true, true),
- ALICE_WATCH_P2SH_B(90, true, true),
- ALICE_DONE(95, false, false),
- ALICE_REFUNDING_B(100, true, true),
- ALICE_REFUNDING_A(105, true, true),
- ALICE_REFUNDED(110, false, false);
-
- private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
-
- public final int value;
- public final boolean requiresAtData;
- public final boolean requiresTradeData;
-
- State(int value, boolean requiresAtData, boolean requiresTradeData) {
- this.value = value;
- this.requiresAtData = requiresAtData;
- this.requiresTradeData = requiresTradeData;
- }
-
- public static State valueOf(int value) {
- return map.get(value);
- }
-
- @Override
- public String getState() {
- return this.name();
- }
-
- @Override
- public int getStateValue() {
- return this.value;
- }
- }
-
- /** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
- private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
-
- /** P2SH-B output amount to avoid dust threshold (3000 sats/kB). */
- private static final long P2SH_B_OUTPUT_AMOUNT = 1000L;
-
- private static BitcoinACCTv1TradeBot instance;
-
- private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDING_B, State.ALICE_REFUNDED).stream()
- .map(State::name)
- .collect(Collectors.toUnmodifiableList());
-
- private BitcoinACCTv1TradeBot() {
- }
-
- public static synchronized BitcoinACCTv1TradeBot getInstance() {
- if (instance == null)
- instance = new BitcoinACCTv1TradeBot();
-
- return instance;
- }
-
- @Override
- public List getEndStates() {
- return this.endStates;
- }
-
- /**
- * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for BTC.
- *
- * Generates:
- *
- *
new 'trade' private key
- *
secret-B
- *
- * Derives:
- *
- *
'native' (as in Qortal) public key, public key hash, address (starting with Q)
- *
'foreign' (as in Bitcoin) public key, public key hash
- *
HASH160 of secret-B
- *
- * A Qortal AT is then constructed including the following as constants in the 'data segment':
- *
- *
'native'/Qortal 'trade' address - used as a MESSAGE contact
- *
'foreign'/Bitcoin public key hash - used by Alice's P2SH scripts to allow redeem
- *
HASH160 of secret-B - used by AT and P2SH to validate a potential secret-B
- *
QORT amount on offer by Bob
- *
BTC amount expected in return by Bob (from Alice)
- *
trading timeout, in case things go wrong and everyone needs to refund
- *
- * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
- *
- * Trade-bot will wait for Bob's AT to be deployed before taking next step.
- *
- * @param repository
- * @param tradeBotCreateRequest
- * @return raw, unsigned DEPLOY_AT transaction
- * @throws DataException
- */
- public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
- byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
- byte[] secretB = TradeBot.generateSecret();
- byte[] hashOfSecretB = Crypto.hash160(secretB);
-
- byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
- byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
- String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
-
- byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
- byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
-
- // Convert Bitcoin receiving address into public key hash (we only support P2PKH at this time)
- Address bitcoinReceivingAddress;
- try {
- bitcoinReceivingAddress = Address.fromString(Bitcoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
- } catch (AddressFormatException e) {
- throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
- }
- if (bitcoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
- throw new DataException("Unsupported Bitcoin receiving address: " + tradeBotCreateRequest.receivingAddress);
-
- byte[] bitcoinReceivingAccountInfo = bitcoinReceivingAddress.getHash();
-
- PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
-
- // Deploy AT
- long timestamp = NTP.getTime();
- byte[] reference = creator.getLastReference();
- long fee = 0L;
- byte[] signature = null;
- BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
-
- String name = "QORT/BTC ACCT";
- String description = "QORT/BTC cross-chain trade";
- String aTType = "ACCT";
- String tags = "ACCT QORT BTC";
- byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, hashOfSecretB, tradeBotCreateRequest.qortAmount,
- tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
- long amount = tradeBotCreateRequest.fundingQortAmount;
-
- DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
-
- DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
- fee = deployAtTransaction.calcRecommendedFee();
- deployAtTransactionData.setFee(fee);
-
- DeployAtTransaction.ensureATAddress(deployAtTransactionData);
- String atAddress = deployAtTransactionData.getAtAddress();
-
- TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv1.NAME,
- State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
- creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
- tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
- secretB, hashOfSecretB,
- SupportedBlockchain.BITCOIN.name(),
- tradeForeignPublicKey, tradeForeignPublicKeyHash,
- tradeBotCreateRequest.foreignAmount, null, null, null, bitcoinReceivingAccountInfo);
-
- TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
-
- // Return to user for signing and broadcast as we don't have their Qortal private key
- try {
- return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
- } catch (TransformationException e) {
- throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
- }
- }
-
- /**
- * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching BTC to an existing offer.
- *
- * Requires a chosen trade offer from Bob, passed by crossChainTradeData
- * and access to a Bitcoin wallet via xprv58.
- *
- * The crossChainTradeData contains the current trade offer state
- * as extracted from the AT's data segment.
- *
- * Access to a funded wallet is via a Bitcoin BIP32 hierarchical deterministic key,
- * passed via xprv58.
- * This key will be stored in your node's database
- * to allow trade-bot to create/fund the necessary P2SH transactions!
- * However, due to the nature of BIP32 keys, it is possible to give the trade-bot
- * only a subset of wallet access (see BIP32 for more details).
- *
- * As an example, the xprv58 can be extract from a legacy, password-less
- * Electrum wallet by going to the console tab and entering:
- * wallet.keystore.xprv
- * which should result in a base58 string starting with either 'xprv' (for Bitcoin main-net)
- * or 'tprv' for (Bitcoin test-net).
- *
- * It is envisaged that the value in xprv58 will actually come from a Qortal-UI-managed wallet.
- *
- * If sufficient funds are available, this method will actually fund the P2SH-A
- * with the Bitcoin amount expected by 'Bob'.
- *
- * If the Bitcoin transaction is successfully broadcast to the network then the trade-bot entry
- * is saved to the repository and the cross-chain trading process commences.
- *
- * Trade-bot will wait for P2SH-A to confirm before taking next step.
- *
- * @param repository
- * @param crossChainTradeData chosen trade OFFER that Alice wants to match
- * @param xprv58 funded wallet xprv in base58
- * @return true if P2SH-A funding transaction successfully broadcast to Bitcoin network, false otherwise
- * @throws DataException
- */
- public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
- byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
- byte[] secretA = TradeBot.generateSecret();
- byte[] hashOfSecretA = Crypto.hash160(secretA);
-
- byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
- byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
- String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
-
- byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
- byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
- byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
-
- // We need to generate lockTime-A: add tradeTimeout to now
- long now = NTP.getTime();
- int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
-
- TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, BitcoinACCTv1.NAME,
- State.ALICE_WAITING_FOR_P2SH_A.name(), State.ALICE_WAITING_FOR_P2SH_A.value,
- receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
- tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
- secretA, hashOfSecretA,
- SupportedBlockchain.BITCOIN.name(),
- tradeForeignPublicKey, tradeForeignPublicKeyHash,
- crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
-
- // Check we have enough funds via xprv58 to fund both P2SHs to cover expectedBitcoin
- String tradeForeignAddress = Bitcoin.getInstance().pkhToAddress(tradeForeignPublicKeyHash);
-
- long p2shFee;
- try {
- p2shFee = Bitcoin.getInstance().getP2shFee(now);
- } catch (ForeignBlockchainException e) {
- LOGGER.debug("Couldn't estimate Bitcoin fees?");
- return ResponseResult.NETWORK_ISSUE;
- }
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long fundsRequiredForP2shA = p2shFee /*funding P2SH-A*/ + crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
- long fundsRequiredForP2shB = p2shFee /*funding P2SH-B*/ + P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-B*/;
- long totalFundsRequired = fundsRequiredForP2shA + fundsRequiredForP2shB;
-
- // As buildSpend also adds a fee, this is more pessimistic than required
- Transaction fundingCheckTransaction = Bitcoin.getInstance().buildSpend(xprv58, tradeForeignAddress, totalFundsRequired);
- if (fundingCheckTransaction == null)
- return ResponseResult.BALANCE_ISSUE;
-
- // P2SH-A to be funded
- byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
- String p2shAddress = Bitcoin.getInstance().deriveP2shAddress(redeemScriptBytes);
-
- // Fund P2SH-A
-
- // Do not include fee for funding transaction as this is covered by buildSpend()
- long amountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFee /*redeeming/refunding P2SH-A*/;
-
- Transaction p2shFundingTransaction = Bitcoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
- if (p2shFundingTransaction == null) {
- LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
- return ResponseResult.BALANCE_ISSUE;
- }
-
- try {
- Bitcoin.getInstance().broadcastTransaction(p2shFundingTransaction);
- } catch (ForeignBlockchainException e) {
- // We couldn't fund P2SH-A at this time
- LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
- return ResponseResult.NETWORK_ISSUE;
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Waiting for confirmation", p2shAddress));
-
- return ResponseResult.OK;
- }
-
- @Override
- public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
- State tradeBotState = State.valueOf(tradeBotData.getStateValue());
- if (tradeBotState == null)
- return true;
-
- // If the AT doesn't exist then we might as well let the user tidy up
- if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
- return true;
-
- switch (tradeBotState) {
- case BOB_WAITING_FOR_AT_CONFIRM:
- case ALICE_DONE:
- case BOB_DONE:
- case ALICE_REFUNDED:
- case BOB_REFUNDED:
- return true;
-
- default:
- return false;
- }
- }
-
- @Override
- public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
- State tradeBotState = State.valueOf(tradeBotData.getStateValue());
- if (tradeBotState == null) {
- LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
- return;
- }
-
- ATData atData = null;
- CrossChainTradeData tradeData = null;
-
- if (tradeBotState.requiresAtData) {
- // Attempt to fetch AT data
- atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
- if (atData == null) {
- LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
- return;
- }
-
- if (tradeBotState.requiresTradeData) {
- tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData);
- if (tradeData == null) {
- LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
- return;
- }
- }
- }
-
- switch (tradeBotState) {
- case BOB_WAITING_FOR_AT_CONFIRM:
- handleBobWaitingForAtConfirm(repository, tradeBotData);
- break;
-
- case ALICE_WAITING_FOR_P2SH_A:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceWaitingForP2shA(repository, tradeBotData, atData, tradeData);
- break;
-
- case BOB_WAITING_FOR_MESSAGE:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_WAITING_FOR_AT_LOCK:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
- break;
-
- case BOB_WAITING_FOR_P2SH_B:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleBobWaitingForP2shB(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_WATCH_P2SH_B:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceWatchingP2shB(repository, tradeBotData, atData, tradeData);
- break;
-
- case BOB_WAITING_FOR_AT_REDEEM:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_DONE:
- case BOB_DONE:
- break;
-
- case ALICE_REFUNDING_B:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceRefundingP2shB(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_REFUNDING_A:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_REFUNDED:
- case BOB_REFUNDED:
- break;
- }
- }
-
- /**
- * Trade-bot is waiting for Bob's AT to deploy.
- *
- * If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
- */
- private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
- if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
- if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
- return;
-
- // We've waited ages for AT to be confirmed into a block but something has gone awry.
- // After this long we assume transaction loss so give up with trade-bot entry too.
- tradeBotData.setState(State.BOB_REFUNDED.name());
- tradeBotData.setStateValue(State.BOB_REFUNDED.value);
- tradeBotData.setTimestamp(NTP.getTime());
- // We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
- repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
- repository.saveChanges();
-
- LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
- TradeBot.notifyStateChange(tradeBotData);
- return;
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
- () -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
- }
-
- /**
- * Trade-bot is waiting for Alice's P2SH-A to confirm.
- *
- * If P2SH-A is confirmed, then trade-bot's next step is to MESSAGE Bob's trade address with Alice's trade info.
- *
- * It is possible between broadcast and confirmation of P2SH-A funding transaction, that Bob has cancelled his trade offer.
- * If this is detected then trade-bot's next step is to wait until P2SH-A can refund back to Alice.
- *
- * In normal operation, trade-bot send a zero-fee, PoW MESSAGE on Alice's behalf containing:
- *
- *
Alice's 'foreign'/Bitcoin public key hash - so Bob's trade-bot can derive P2SH-A address and check balance
- *
HASH160 of Alice's secret-A - also used to derive P2SH-A address
- *
lockTime of P2SH-A - also used to derive P2SH-A address, but also for other use later in the trading process
- *
- * If MESSAGE transaction is successfully broadcast, trade-bot's next step is to wait until Bob's AT has locked trade to Alice only.
- * @throws ForeignBlockchainException
- */
- private void handleAliceWaitingForP2shA(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getLockTimeA(), crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestampA = calcP2shAFeeTimestamp(tradeBotData.getLockTimeA(), crossChainTradeData.tradeTimeout);
- long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // This shouldn't occur, but defensively check P2SH-B in case we haven't redeemed the AT
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WATCH_P2SH_B,
- () -> String.format("P2SH-A %s already spent? Defensively checking P2SH-B next", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
- () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
- return;
-
- case FUNDED:
- // Fall-through out of switch...
- break;
- }
-
- // P2SH-A funding confirmed
-
- // Attempt to send MESSAGE to Bob's Qortal trade address
- byte[] messageData = BitcoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
- String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, 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 Bob's trade-bot %s: %s", messageRecipient, result.name()));
- return;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WAITING_FOR_AT_LOCK,
- () -> String.format("P2SH-A %s funding confirmed. Messaged %s. Waiting for AT %s to lock to us",
- p2shAddressA, messageRecipient, tradeBotData.getAtAddress()));
- }
-
- /**
- * Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
- *
- * It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
- * in which case trade-bot is done with this specific trade and finalizes on refunded state.
- *
- * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
- *
- * Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
- *
- * Assuming P2SH-A has at least expected Bitcoin balance,
- * Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
- *
- * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
- *
- * Trade-bot's next step is to wait for P2SH-B, which will allow Bob to reveal his secret-B,
- * needed by Alice to progress her side of the trade.
- * @throws ForeignBlockchainException
- */
- private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- // If AT has finished then Bob likely cancelled his trade offer
- if (atData.getIsFinished()) {
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
- () -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
- return;
- }
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- String address = tradeBotData.getTradeNativeAddress();
- List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
-
- final byte[] originalLastTransactionSignature = tradeBotData.getLastTransactionSignature();
-
- // Skip past previously processed messages
- if (originalLastTransactionSignature != null)
- for (int i = 0; i < messageTransactionsData.size(); ++i)
- if (Arrays.equals(messageTransactionsData.get(i).getSignature(), originalLastTransactionSignature)) {
- messageTransactionsData.subList(0, i + 1).clear();
- break;
- }
-
- while (!messageTransactionsData.isEmpty()) {
- MessageTransactionData messageTransactionData = messageTransactionsData.remove(0);
- tradeBotData.setLastTransactionSignature(messageTransactionData.getSignature());
-
- if (messageTransactionData.isText())
- continue;
-
- // We're expecting: HASH160(secret-A), Alice's Bitcoin pubkeyhash and lockTime-A
- byte[] messageData = messageTransactionData.getData();
- BitcoinACCTv1.OfferMessageData offerMessageData = BitcoinACCTv1.extractOfferMessageData(messageData);
- if (offerMessageData == null)
- continue;
-
- byte[] aliceForeignPublicKeyHash = offerMessageData.partnerBitcoinPKH;
- byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
- int lockTimeA = (int) offerMessageData.lockTimeA;
-
- // Determine P2SH-A address and confirm funded
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
- String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
-
- long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
- final long minimumAmountA = tradeBotData.getForeignAmount() - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
-
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // There might be another MESSAGE from someone else with an actually funded P2SH-A...
- continue;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // This shouldn't occur, but defensively bump to next state
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_P2SH_B,
- () -> String.format("P2SH-A %s already spent? Defensively checking P2SH-B next", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
- continue;
-
- case FUNDED:
- // Fall-through out of switch...
- break;
- }
-
- // Good to go - send MESSAGE to AT
-
- String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
- int lockTimeB = BitcoinACCTv1.calcLockTimeB(messageTransactionData.getTimestamp(), lockTimeA);
-
- // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
- byte[] outgoingMessageData = BitcoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB);
- String messageRecipient = tradeBotData.getAtAddress();
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
-
- 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", messageRecipient, result.name()));
- return;
- }
- }
-
- byte[] redeemScriptB = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeB, tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret());
- String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_P2SH_B,
- () -> String.format("Locked AT %s to %s. Waiting for P2SH-B %s", tradeBotData.getAtAddress(), aliceNativeAddress, p2shAddressB));
-
- return;
- }
-
- // Don't resave/notify if we don't need to
- if (tradeBotData.getLastTransactionSignature() != originalLastTransactionSignature)
- TradeBot.updateTradeBotState(repository, tradeBotData, null);
- }
-
- /**
- * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
- *
- * It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
- * this process has taken so long that we've reached P2SH-A's locktime, or that someone else
- * has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
- *
- * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
- *
- * If all is well, trade-bot then uses Bitcoin wallet to (token) fund P2SH-B.
- *
- * If P2SH-B funding transaction is successfully broadcast to the Bitcoin network, trade-bot's next
- * step is to watch for Bob revealing secret-B by redeeming P2SH-B.
- * @throws ForeignBlockchainException
- */
- private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
- int lockTimeA = tradeBotData.getLockTimeA();
-
- // Refund P2SH-A if we've passed lockTime-A
- if (NTP.getTime() >= tradeBotData.getLockTimeA() * 1000L) {
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
-
- long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // This shouldn't occur, but defensively revert back to waiting for P2SH-A
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WAITING_FOR_P2SH_A,
- () -> String.format("P2SH-A %s no longer funded? Defensively checking P2SH-A next", p2shAddressA));
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // This shouldn't occur, but defensively bump to next state
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WATCH_P2SH_B,
- () -> String.format("P2SH-A %s already spent? Defensively checking P2SH-B next", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
- () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
- return;
-
- case FUNDED:
- // Fall-through out of switch...
- break;
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> atData.getIsFinished()
- ? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
- : String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
-
- return;
- }
-
- // We're waiting for AT to be in TRADE mode
- if (crossChainTradeData.mode != AcctMode.TRADING)
- return;
-
- // AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
-
- // Alice needs to fund P2SH-B here
-
- // Find our MESSAGE to AT from previous state
- List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
- crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
- if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
- LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
- return;
- }
-
- long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
- int lockTimeB = BitcoinACCTv1.calcLockTimeB(recipientMessageTimestamp, lockTimeA);
-
- // Our calculated lockTime-B should match AT's calculated lockTime-B
- if (lockTimeB != crossChainTradeData.lockTimeB) {
- LOGGER.debug(() -> String.format("Trade AT lockTime-B '%d' doesn't match our lockTime-B '%d'", crossChainTradeData.lockTimeB, lockTimeB));
- // We'll eventually refund
- return;
- }
-
- byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
- String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
-
- long feeTimestampB = calcP2shBFeeTimestamp(lockTimeA, lockTimeB);
- long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
-
- // Have we funded P2SH-B already?
- final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
-
- BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
-
- switch (htlcStatusB) {
- case UNFUNDED: {
- // Do not include fee for funding transaction as this is covered by buildSpend()
- long amountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB /*redeeming/refunding P2SH-B*/;
-
- Transaction p2shFundingTransaction = bitcoin.buildSpend(tradeBotData.getForeignKey(), p2shAddressB, amountB);
- if (p2shFundingTransaction == null) {
- LOGGER.debug("Unable to build P2SH-B funding transaction - lack of funds?");
- return;
- }
-
- bitcoin.broadcastTransaction(p2shFundingTransaction);
- break;
- }
-
- case FUNDING_IN_PROGRESS:
- case FUNDED:
- break;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // This shouldn't occur, but defensively bump to next state
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WATCH_P2SH_B,
- () -> String.format("P2SH-B %s already spent? Defensively checking P2SH-B next", p2shAddressB));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> String.format("P2SH-B %s already refunded. Refunding P2SH-A next", p2shAddressB));
- return;
- }
-
- // P2SH-B funded, now we wait for Bob to redeem it
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_WATCH_P2SH_B,
- () -> String.format("AT %s locked to us (%s). P2SH-B %s funded. Watching P2SH-B for secret-B",
- tradeBotData.getAtAddress(), tradeBotData.getTradeNativeAddress(), p2shAddressB));
- }
-
- /**
- * Trade-bot is waiting for P2SH-B to funded.
- *
- * It's possible than Bob's AT has reached it's trading timeout and automatically refunded QORT back to Bob.
- * In which case, trade-bot is done with this specific trade and finalizes on refunded state.
- *
- * Assuming P2SH-B is funded, trade-bot 'redeems' this P2SH using secret-B, thus revealing it to Alice.
- *
- * Trade-bot's next step is to wait for Alice to use secret-B, and her secret-A, to redeem Bob's AT.
- * @throws ForeignBlockchainException
- */
- private void handleBobWaitingForP2shB(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- // If we've passed AT refund timestamp then AT will have finished after auto-refunding
- if (atData.getIsFinished()) {
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
- () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
-
- return;
- }
-
- // It's possible AT hasn't processed our previous MESSAGE yet and so lockTimeB won't be set
- if (crossChainTradeData.lockTimeB == null)
- // AT yet to process MESSAGE
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- byte[] redeemScriptB = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, crossChainTradeData.lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
- String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
-
- long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
- long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
-
- final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
-
- BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
-
- switch (htlcStatusB) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-B to be funded...
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // This shouldn't occur, but defensively bump to next state
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
- () -> String.format("P2SH-B %s already spent (exposing secret-B)? Checking AT %s for secret-A", p2shAddressB, tradeBotData.getAtAddress()));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // AT should auto-refund - we don't need to do anything here
- return;
-
- case FUNDED:
- break;
- }
-
- // Redeem P2SH-B using secret-B
- Coin redeemAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees. The real funds are in P2SH-A.
- ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
- byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
-
- Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
- fundingOutputs, redeemScriptB, tradeBotData.getSecret(), receivingAccountInfo);
-
- bitcoin.broadcastTransaction(p2shRedeemTransaction);
-
- // P2SH-B redeemed, now we wait for Alice to use secret-A to redeem AT
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
- () -> String.format("P2SH-B %s redeemed (exposing secret-B). Watching AT %s for secret-A", p2shAddressB, tradeBotData.getAtAddress()));
- }
-
- /**
- * Trade-bot is waiting for Bob to redeem P2SH-B thus revealing secret-B to Alice.
- *
- * It's possible that this process has taken so long that we've reached P2SH-B's locktime.
- * In which case, trade-bot switches to begin the refund process.
- *
- * If trade-bot can extract a valid secret-B from the spend of P2SH-B, then it creates a
- * zero-fee, PoW MESSAGE to send to Bob's AT, including both secret-B and also Alice's secret-A.
- *
- * Both secrets are needed to release the QORT funds from Bob's AT to Alice's 'native'/Qortal
- * trade address.
- *
- * In revealing a valid secret-A, Bob can then redeem the BTC funds from P2SH-A.
- *
- * If trade-bot successfully broadcasts the MESSAGE transaction, then this specific trade is done.
- * @throws ForeignBlockchainException
- */
- private void handleAliceWatchingP2shB(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), crossChainTradeData.lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
- String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
-
- long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, crossChainTradeData.lockTimeB);
- long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
- final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
-
- BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
-
- switch (htlcStatusB) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- case FUNDED:
- case REDEEM_IN_PROGRESS:
- // Still waiting for P2SH-B to be funded/redeemed...
- return;
-
- case REDEEMED:
- // Bob has redeemed P2SH-B, so double-check that we have redeemed AT...
- break;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // We've refunded P2SH-B? Bump to refunding P2SH-A then
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> String.format("P2SH-B %s already refunded. Refunding P2SH-A next", p2shAddressB));
- return;
- }
-
- byte[] secretB = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddressB);
- if (secretB == null)
- // Secret not revealed at this time
- return;
-
- // Send 'redeem' MESSAGE to AT using both secrets
- byte[] secretA = tradeBotData.getSecret();
- String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
- byte[] messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, qortalReceivingAddress);
- String messageRecipient = tradeBotData.getAtAddress();
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, 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", messageRecipient, result.name()));
- return;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
- () -> String.format("P2SH-B %s redeemed, using secrets to redeem AT %s. Funds should arrive at %s",
- p2shAddressB, tradeBotData.getAtAddress(), qortalReceivingAddress));
- }
-
- /**
- * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the BTC funds from P2SH-A.
- *
- * It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
- * trade-bot is done with this specific trade and finalizes in refunded state.
- *
- * Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the BTC funds from P2SH-A
- * to Bob's 'foreign'/Bitcoin trade legacy-format address, as derived from trade private key.
- *
- * (This could potentially be 'improved' to send BTC to any address of Bob's choosing by changing the transaction output).
- *
- * If trade-bot successfully broadcasts the transaction, then this specific trade is done.
- * @throws ForeignBlockchainException
- */
- private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- // AT should be 'finished' once Alice has redeemed QORT funds
- if (!atData.getIsFinished())
- // Not finished yet
- return;
-
- // If AT is not REDEEMED then something has gone wrong
- if (crossChainTradeData.mode != AcctMode.REDEEMED) {
- // Not redeemed so must be refunded/cancelled
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
- () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
-
- return;
- }
-
- byte[] secretA = BitcoinACCTv1.findSecretA(repository, crossChainTradeData);
- if (secretA == null) {
- LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
- return;
- }
-
- // Use secret-A to redeem P2SH-A
-
- Bitcoin bitcoin = Bitcoin.getInstance();
- int lockTimeA = crossChainTradeData.lockTimeA;
-
- byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
- String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Double-check that we have redeemed P2SH-A...
- break;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // Wait for AT to auto-refund
- return;
-
- case FUNDED: {
- Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
- ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
-
- Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
- fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
-
- bitcoin.broadcastTransaction(p2shRedeemTransaction);
- break;
- }
- }
-
- String receivingAddress = bitcoin.pkhToAddress(receivingAccountInfo);
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
- () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
- }
-
- /**
- * Trade-bot is attempting to refund P2SH-B.
- *
- * We could potentially skip this step as P2SH-B is only funded with a token amount to cover the mining fee should Bob redeem P2SH-B.
- *
- * Upon successful broadcast of P2SH-B refunding transaction, trade-bot's next step is to begin refunding of P2SH-A.
- * @throws ForeignBlockchainException
- */
- private void handleAliceRefundingP2shB(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- int lockTimeB = crossChainTradeData.lockTimeB;
-
- // We can't refund P2SH-B until lockTime-B has passed
- if (NTP.getTime() <= lockTimeB * 1000L)
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- // We can't refund P2SH-B until median block time has passed lockTime-B (see BIP113)
- int medianBlockTime = bitcoin.getMedianBlockTime();
- if (medianBlockTime <= lockTimeB)
- return;
-
- byte[] redeemScriptB = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeB, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretB);
- String p2shAddressB = bitcoin.deriveP2shAddress(redeemScriptB);
-
- long feeTimestampB = calcP2shBFeeTimestamp(crossChainTradeData.lockTimeA, lockTimeB);
- long p2shFeeB = bitcoin.getP2shFee(feeTimestampB);
- final long minimumAmountB = P2SH_B_OUTPUT_AMOUNT + p2shFeeB;
-
- BitcoinyHTLC.Status htlcStatusB = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressB, minimumAmountB);
-
- switch (htlcStatusB) {
- case UNFUNDED:
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> String.format("P2SH-B %s never funded?. Refunding P2SH-A next", p2shAddressB));
- return;
-
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-B to be funded...
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // We must be very close to trade timeout. Defensively try to refund P2SH-A
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> String.format("P2SH-B %s already spent?. Refunding P2SH-A next", p2shAddressB));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- break;
-
- case FUNDED:{
- Coin refundAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees.
- ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
-
- // Determine receive address for refund
- String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
- Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress);
-
- Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
- fundingOutputs, redeemScriptB, lockTimeB, receiving.getHash());
-
- bitcoin.broadcastTransaction(p2shRefundTransaction);
- break;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> String.format("Refunded P2SH-B %s. Waiting for LockTime-A", p2shAddressB));
- }
-
- /**
- * Trade-bot is attempting to refund P2SH-A.
- * @throws ForeignBlockchainException
- */
- private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- int lockTimeA = tradeBotData.getLockTimeA();
-
- // We can't refund P2SH-A until lockTime-A has passed
- if (NTP.getTime() <= lockTimeA * 1000L)
- return;
-
- Bitcoin bitcoin = Bitcoin.getInstance();
-
- // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
- int medianBlockTime = bitcoin.getMedianBlockTime();
- if (medianBlockTime <= lockTimeA)
- return;
-
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = bitcoin.deriveP2shAddress(redeemScriptA);
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestampA = calcP2shAFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFeeA = bitcoin.getP2shFee(feeTimestampA);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT + p2shFeeA;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-A to be funded...
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Too late!
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
- () -> String.format("P2SH-A %s already spent!", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- break;
-
- case FUNDED:{
- Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
- ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
-
- // Determine receive address for refund
- String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
- Address receiving = Address.fromString(bitcoin.getNetworkParameters(), receiveAddress);
-
- Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoin.getNetworkParameters(), refundAmount, refundKey,
- fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
-
- bitcoin.broadcastTransaction(p2shRefundTransaction);
- break;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
- () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
- }
-
- /**
- * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
- *
- * We deal with three different independent state-spaces here:
- *
- *
Qortal blockchain
- *
Foreign blockchain
- *
Trade-bot entries
- *
- */
-public class LitecoinACCTv1TradeBot implements AcctTradeBot {
-
- private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv1TradeBot.class);
-
- public enum State implements TradeBot.StateNameAndValueSupplier {
- BOB_WAITING_FOR_AT_CONFIRM(10, false, false),
- BOB_WAITING_FOR_MESSAGE(15, true, true),
- BOB_WAITING_FOR_AT_REDEEM(25, true, true),
- BOB_DONE(30, false, false),
- BOB_REFUNDED(35, false, false),
-
- ALICE_WAITING_FOR_AT_LOCK(85, true, true),
- ALICE_DONE(95, false, false),
- ALICE_REFUNDING_A(105, true, true),
- ALICE_REFUNDED(110, false, false);
-
- private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
-
- public final int value;
- public final boolean requiresAtData;
- public final boolean requiresTradeData;
-
- State(int value, boolean requiresAtData, boolean requiresTradeData) {
- this.value = value;
- this.requiresAtData = requiresAtData;
- this.requiresTradeData = requiresTradeData;
- }
-
- public static State valueOf(int value) {
- return map.get(value);
- }
-
- @Override
- public String getState() {
- return this.name();
- }
-
- @Override
- public int getStateValue() {
- return this.value;
- }
- }
-
- /** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
- private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
-
- private static LitecoinACCTv1TradeBot instance;
-
- private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream()
- .map(State::name)
- .collect(Collectors.toUnmodifiableList());
-
- private LitecoinACCTv1TradeBot() {
- }
-
- public static synchronized LitecoinACCTv1TradeBot getInstance() {
- if (instance == null)
- instance = new LitecoinACCTv1TradeBot();
-
- return instance;
- }
-
- @Override
- public List getEndStates() {
- return this.endStates;
- }
-
- /**
- * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC.
- *
- * Generates:
- *
- *
new 'trade' private key
- *
- * Derives:
- *
- *
'native' (as in Qortal) public key, public key hash, address (starting with Q)
- *
'foreign' (as in Litecoin) public key, public key hash
- *
- * A Qortal AT is then constructed including the following as constants in the 'data segment':
- *
- *
'native'/Qortal 'trade' address - used as a MESSAGE contact
- *
'foreign'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem
- *
QORT amount on offer by Bob
- *
LTC amount expected in return by Bob (from Alice)
- *
trading timeout, in case things go wrong and everyone needs to refund
- *
- * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
- *
- * Trade-bot will wait for Bob's AT to be deployed before taking next step.
- *
- * @param repository
- * @param tradeBotCreateRequest
- * @return raw, unsigned DEPLOY_AT transaction
- * @throws DataException
- */
- public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
- byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
-
- byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
- byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
- String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
-
- byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
- byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
-
- // Convert Litecoin receiving address into public key hash (we only support P2PKH at this time)
- Address litecoinReceivingAddress;
- try {
- litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
- } catch (AddressFormatException e) {
- throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
- }
- if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
- throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
-
- byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.getHash();
-
- PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
-
- // Deploy AT
- long timestamp = NTP.getTime();
- byte[] reference = creator.getLastReference();
- long fee = 0L;
- byte[] signature = null;
- BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
-
- String name = "QORT/LTC ACCT";
- String description = "QORT/LTC cross-chain trade";
- String aTType = "ACCT";
- String tags = "ACCT QORT LTC";
- byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
- tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
- long amount = tradeBotCreateRequest.fundingQortAmount;
-
- DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
-
- DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
- fee = deployAtTransaction.calcRecommendedFee();
- deployAtTransactionData.setFee(fee);
-
- DeployAtTransaction.ensureATAddress(deployAtTransactionData);
- String atAddress = deployAtTransactionData.getAtAddress();
-
- TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv1.NAME,
- State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
- creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
- tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
- null, null,
- SupportedBlockchain.LITECOIN.name(),
- tradeForeignPublicKey, tradeForeignPublicKeyHash,
- tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo);
-
- TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
-
- // Attempt to backup the trade bot data
- TradeBot.backupTradeBotData(repository);
-
- // Return to user for signing and broadcast as we don't have their Qortal private key
- try {
- return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
- } catch (TransformationException e) {
- throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
- }
- }
-
- /**
- * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching LTC to an existing offer.
- *
- * Requires a chosen trade offer from Bob, passed by crossChainTradeData
- * and access to a Litecoin wallet via xprv58.
- *
- * The crossChainTradeData contains the current trade offer state
- * as extracted from the AT's data segment.
- *
- * Access to a funded wallet is via a Litecoin BIP32 hierarchical deterministic key,
- * passed via xprv58.
- * This key will be stored in your node's database
- * to allow trade-bot to create/fund the necessary P2SH transactions!
- * However, due to the nature of BIP32 keys, it is possible to give the trade-bot
- * only a subset of wallet access (see BIP32 for more details).
- *
- * As an example, the xprv58 can be extract from a legacy, password-less
- * Electrum wallet by going to the console tab and entering:
- * wallet.keystore.xprv
- * which should result in a base58 string starting with either 'xprv' (for Litecoin main-net)
- * or 'tprv' for (Litecoin test-net).
- *
- * It is envisaged that the value in xprv58 will actually come from a Qortal-UI-managed wallet.
- *
- * If sufficient funds are available, this method will actually fund the P2SH-A
- * with the Litecoin amount expected by 'Bob'.
- *
- * If the Litecoin transaction is successfully broadcast to the network then
- * we also send a MESSAGE to Bob's trade-bot to let them know.
- *
- * The trade-bot entry is saved to the repository and the cross-chain trading process commences.
- *
- * @param repository
- * @param crossChainTradeData chosen trade OFFER that Alice wants to match
- * @param xprv58 funded wallet xprv in base58
- * @return true if P2SH-A funding transaction successfully broadcast to Litecoin network, false otherwise
- * @throws DataException
- */
- public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
- byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
- byte[] secretA = TradeBot.generateSecret();
- byte[] hashOfSecretA = Crypto.hash160(secretA);
-
- byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
- byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
- String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
-
- byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
- byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
- byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
-
- // We need to generate lockTime-A: add tradeTimeout to now
- long now = NTP.getTime();
- int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
-
- TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, LitecoinACCTv1.NAME,
- State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
- receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
- tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
- secretA, hashOfSecretA,
- SupportedBlockchain.LITECOIN.name(),
- tradeForeignPublicKey, tradeForeignPublicKeyHash,
- crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
-
- // Attempt to backup the trade bot data
- TradeBot.backupTradeBotData(repository);
-
- // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
- long p2shFee;
- try {
- p2shFee = Litecoin.getInstance().getP2shFee(now);
- } catch (ForeignBlockchainException e) {
- LOGGER.debug("Couldn't estimate Litecoin fees?");
- return ResponseResult.NETWORK_ISSUE;
- }
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- // Do not include fee for funding transaction as this is covered by buildSpend()
- long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
-
- // P2SH-A to be funded
- byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
- String p2shAddress = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
-
- // Build transaction for funding P2SH-A
- Transaction p2shFundingTransaction = Litecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
- if (p2shFundingTransaction == null) {
- LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
- return ResponseResult.BALANCE_ISSUE;
- }
-
- try {
- Litecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
- } catch (ForeignBlockchainException e) {
- // We couldn't fund P2SH-A at this time
- LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
- return ResponseResult.NETWORK_ISSUE;
- }
-
- // Attempt to send MESSAGE to Bob's Qortal trade address
- byte[] messageData = LitecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
- String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, 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 Bob's trade-bot %s: %s", messageRecipient, result.name()));
- return ResponseResult.NETWORK_ISSUE;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
-
- return ResponseResult.OK;
- }
-
- @Override
- public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
- State tradeBotState = State.valueOf(tradeBotData.getStateValue());
- if (tradeBotState == null)
- return true;
-
- // If the AT doesn't exist then we might as well let the user tidy up
- if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
- return true;
-
- switch (tradeBotState) {
- case BOB_WAITING_FOR_AT_CONFIRM:
- case ALICE_DONE:
- case BOB_DONE:
- case ALICE_REFUNDED:
- case BOB_REFUNDED:
- return true;
-
- default:
- return false;
- }
- }
-
- @Override
- public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
- State tradeBotState = State.valueOf(tradeBotData.getStateValue());
- if (tradeBotState == null) {
- LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
- return;
- }
-
- ATData atData = null;
- CrossChainTradeData tradeData = null;
-
- if (tradeBotState.requiresAtData) {
- // Attempt to fetch AT data
- atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
- if (atData == null) {
- LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
- return;
- }
-
- if (tradeBotState.requiresTradeData) {
- tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData);
- if (tradeData == null) {
- LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
- return;
- }
- }
- }
-
- switch (tradeBotState) {
- case BOB_WAITING_FOR_AT_CONFIRM:
- handleBobWaitingForAtConfirm(repository, tradeBotData);
- break;
-
- case BOB_WAITING_FOR_MESSAGE:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_WAITING_FOR_AT_LOCK:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
- break;
-
- case BOB_WAITING_FOR_AT_REDEEM:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_DONE:
- case BOB_DONE:
- break;
-
- case ALICE_REFUNDING_A:
- TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
- handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
- break;
-
- case ALICE_REFUNDED:
- case BOB_REFUNDED:
- break;
- }
- }
-
- /**
- * Trade-bot is waiting for Bob's AT to deploy.
- *
- * If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
- */
- private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
- if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
- if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
- return;
-
- // We've waited ages for AT to be confirmed into a block but something has gone awry.
- // After this long we assume transaction loss so give up with trade-bot entry too.
- tradeBotData.setState(State.BOB_REFUNDED.name());
- tradeBotData.setStateValue(State.BOB_REFUNDED.value);
- tradeBotData.setTimestamp(NTP.getTime());
- // We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
- repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
- repository.saveChanges();
-
- LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
- TradeBot.notifyStateChange(tradeBotData);
- return;
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
- () -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
- }
-
- /**
- * Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
- *
- * It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
- * in which case trade-bot is done with this specific trade and finalizes on refunded state.
- *
- * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
- *
- * Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
- *
- * Assuming P2SH-A has at least expected Litecoin balance,
- * Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
- *
- * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
- *
- * Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
- * extract secret-A needed to redeem Alice's P2SH.
- * @throws ForeignBlockchainException
- */
- private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- // If AT has finished then Bob likely cancelled his trade offer
- if (atData.getIsFinished()) {
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
- () -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
- return;
- }
-
- Litecoin litecoin = Litecoin.getInstance();
-
- String address = tradeBotData.getTradeNativeAddress();
- List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
-
- for (MessageTransactionData messageTransactionData : messageTransactionsData) {
- if (messageTransactionData.isText())
- continue;
-
- // We're expecting: HASH160(secret-A), Alice's Litecoin pubkeyhash and lockTime-A
- byte[] messageData = messageTransactionData.getData();
- LitecoinACCTv1.OfferMessageData offerMessageData = LitecoinACCTv1.extractOfferMessageData(messageData);
- if (offerMessageData == null)
- continue;
-
- byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH;
- byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
- int lockTimeA = (int) offerMessageData.lockTimeA;
- long messageTimestamp = messageTransactionData.getTimestamp();
- int refundTimeout = LitecoinACCTv1.calcRefundTimeout(messageTimestamp, lockTimeA);
-
- // Determine P2SH-A address and confirm funded
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
-
- long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
-
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // There might be another MESSAGE from someone else with an actually funded P2SH-A...
- continue;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // We've already redeemed this?
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
- () -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
- continue;
-
- case FUNDED:
- // Fall-through out of switch...
- break;
- }
-
- // Good to go - send MESSAGE to AT
-
- String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
-
- // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
- byte[] outgoingMessageData = LitecoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
- String messageRecipient = tradeBotData.getAtAddress();
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
-
- 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", messageRecipient, result.name()));
- return;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
- () -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
-
- return;
- }
- }
-
- /**
- * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
- *
- * It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
- * this process has taken so long that we've reached P2SH-A's locktime, or that someone else
- * has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
- *
- * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
- *
- * If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
- *
- * In revealing a valid secret-A, Bob can then redeem the LTC funds from P2SH-A.
- *
- * @throws ForeignBlockchainException
- */
- private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
- return;
-
- Litecoin litecoin = Litecoin.getInstance();
- int lockTimeA = tradeBotData.getLockTimeA();
-
- // Refund P2SH-A if we've passed lockTime-A
- if (NTP.getTime() >= lockTimeA * 1000L) {
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
-
- long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
-
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- case FUNDED:
- break;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Already redeemed?
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
- () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
- () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
- return;
-
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
- () -> atData.getIsFinished()
- ? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
- : String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
-
- return;
- }
-
- // We're waiting for AT to be in TRADE mode
- if (crossChainTradeData.mode != AcctMode.TRADING)
- return;
-
- // AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
-
- // Find our MESSAGE to AT from previous state
- List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
- crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
- if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
- LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
- return;
- }
-
- long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
- int refundTimeout = LitecoinACCTv1.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
-
- // Our calculated refundTimeout should match AT's refundTimeout
- if (refundTimeout != crossChainTradeData.refundTimeout) {
- LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
- // We'll eventually refund
- return;
- }
-
- // We're good to redeem AT
-
- // Send 'redeem' MESSAGE to AT using both secret
- byte[] secretA = tradeBotData.getSecret();
- String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
- byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secretA, qortalReceivingAddress);
- String messageRecipient = tradeBotData.getAtAddress();
-
- boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
- if (!isMessageAlreadySent) {
- PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, 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", messageRecipient, result.name()));
- return;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
- () -> String.format("Redeeming AT %s. Funds should arrive at %s",
- tradeBotData.getAtAddress(), qortalReceivingAddress));
- }
-
- /**
- * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the LTC funds from P2SH-A.
- *
- * It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
- * trade-bot is done with this specific trade and finalizes in refunded state.
- *
- * Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the LTC funds from P2SH-A
- * to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key.
- *
- * (This could potentially be 'improved' to send LTC to any address of Bob's choosing by changing the transaction output).
- *
- * If trade-bot successfully broadcasts the transaction, then this specific trade is done.
- * @throws ForeignBlockchainException
- */
- private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- // AT should be 'finished' once Alice has redeemed QORT funds
- if (!atData.getIsFinished())
- // Not finished yet
- return;
-
- // If AT is REFUNDED or CANCELLED then something has gone wrong
- if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
- // Alice hasn't redeemed the QORT, so there is no point in trying to redeem the LTC
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
- () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
-
- return;
- }
-
- byte[] secretA = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
- if (secretA == null) {
- LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
- return;
- }
-
- // Use secret-A to redeem P2SH-A
-
- Litecoin litecoin = Litecoin.getInstance();
-
- byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
- int lockTimeA = crossChainTradeData.lockTimeA;
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Double-check that we have redeemed P2SH-A...
- break;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- // Wait for AT to auto-refund
- return;
-
- case FUNDED: {
- Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
-
- Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
- fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
-
- litecoin.broadcastTransaction(p2shRedeemTransaction);
- break;
- }
- }
-
- String receivingAddress = litecoin.pkhToAddress(receivingAccountInfo);
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
- () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
- }
-
- /**
- * Trade-bot is attempting to refund P2SH-A.
- * @throws ForeignBlockchainException
- */
- private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
- ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
- int lockTimeA = tradeBotData.getLockTimeA();
-
- // We can't refund P2SH-A until lockTime-A has passed
- if (NTP.getTime() <= lockTimeA * 1000L)
- return;
-
- Litecoin litecoin = Litecoin.getInstance();
-
- // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
- int medianBlockTime = litecoin.getMedianBlockTime();
- if (medianBlockTime <= lockTimeA)
- return;
-
- byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
- String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA);
-
- // Fee for redeem/refund is subtracted from P2SH-A balance.
- long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
- long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp);
- long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
- BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
-
- switch (htlcStatusA) {
- case UNFUNDED:
- case FUNDING_IN_PROGRESS:
- // Still waiting for P2SH-A to be funded...
- return;
-
- case REDEEM_IN_PROGRESS:
- case REDEEMED:
- // Too late!
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
- () -> String.format("P2SH-A %s already spent!", p2shAddressA));
- return;
-
- case REFUND_IN_PROGRESS:
- case REFUNDED:
- break;
-
- case FUNDED:{
- Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
- ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
- List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
-
- // Determine receive address for refund
- String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
- Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress);
-
- Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey,
- fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
-
- litecoin.broadcastTransaction(p2shRefundTransaction);
- break;
- }
- }
-
- TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
- () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
- }
-
- /**
- * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
- *
- * We deal with three different independent state-spaces here:
- *
- *
Qortal blockchain
- *
Foreign blockchain
- *
Trade-bot entries
- *
- */
-public class TradeBot implements Listener {
-
- private static final Logger LOGGER = LogManager.getLogger(TradeBot.class);
- private static final Random RANDOM = new SecureRandom();
-
- public interface StateNameAndValueSupplier {
- public String getState();
- public int getStateValue();
- }
-
- public static class StateChangeEvent implements Event {
- private final TradeBotData tradeBotData;
-
- public StateChangeEvent(TradeBotData tradeBotData) {
- this.tradeBotData = tradeBotData;
- }
-
- public TradeBotData getTradeBotData() {
- return this.tradeBotData;
- }
- }
-
- private static final Map, Supplier> acctTradeBotSuppliers = new HashMap<>();
- static {
- acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
- acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
- }
-
- private static TradeBot instance;
-
- private final Map presenceTimestampsByAtAddress = Collections.synchronizedMap(new HashMap<>());
-
- private TradeBot() {
- EventBus.INSTANCE.addListener(event -> TradeBot.getInstance().listen(event));
- }
-
- public static synchronized TradeBot getInstance() {
- if (instance == null)
- instance = new TradeBot();
-
- return instance;
- }
-
- public ACCT getAcctUsingAtData(ATData atData) {
- byte[] codeHash = atData.getCodeHash();
- if (codeHash == null)
- return null;
-
- return SupportedBlockchain.getAcctByCodeHash(codeHash);
- }
-
- public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
- ACCT acct = this.getAcctUsingAtData(atData);
- if (acct == null)
- return null;
-
- return acct.populateTradeData(repository, atData);
- }
-
- /**
- * Creates a new trade-bot entry from the "Bob" viewpoint,
- * i.e. OFFERing QORT in exchange for foreign blockchain currency.
- *
- * Generates:
- *
- *
new 'trade' private key
- *
secret(s)
- *
- * Derives:
- *
- *
'native' (as in Qortal) public key, public key hash, address (starting with Q)
- *
'foreign' public key, public key hash
- *
hash(es) of secret(s)
- *
- * A Qortal AT is then constructed including the following as constants in the 'data segment':
- *
- *
'native' (Qortal) 'trade' address - used to MESSAGE AT
- *
'foreign' public key hash - used by Alice's to allow redeem of currency on foreign blockchain
- *
hash(es) of secret(s) - used by AT (optional) and foreign blockchain as needed
- *
QORT amount on offer by Bob
- *
foreign currency amount expected in return by Bob (from Alice)
- *
trading timeout, in case things go wrong and everyone needs to refund
- *
- * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
- *
- * Trade-bot will wait for Bob's AT to be deployed before taking next step.
- *
- * @param repository
- * @param tradeBotCreateRequest
- * @return raw, unsigned DEPLOY_AT transaction
- * @throws DataException
- */
- public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
- // Fetch latest ACCT version for requested foreign blockchain
- ACCT acct = tradeBotCreateRequest.foreignBlockchain.getLatestAcct();
-
- AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
- if (acctTradeBot == null)
- return null;
-
- return acctTradeBot.createTrade(repository, tradeBotCreateRequest);
- }
-
- /**
- * Creates a trade-bot entry from the 'Alice' viewpoint,
- * i.e. matching foreign blockchain currency to an existing QORT offer.
- *
- * Requires a chosen trade offer from Bob, passed by crossChainTradeData
- * and access to a foreign blockchain wallet via foreignKey.
- *
- * @param repository
- * @param crossChainTradeData chosen trade OFFER that Alice wants to match
- * @param foreignKey foreign blockchain wallet key
- * @throws DataException
- */
- public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct,
- CrossChainTradeData crossChainTradeData, String foreignKey, String receivingAddress) throws DataException {
- AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
- if (acctTradeBot == null) {
- LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot for AT %s", atData.getATAddress()));
- return ResponseResult.NETWORK_ISSUE;
- }
-
- // Check Alice doesn't already have an existing, on-going trade-bot entry for this AT.
- if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(atData.getATAddress(), acctTradeBot.getEndStates()))
- return ResponseResult.TRADE_ALREADY_EXISTS;
-
- return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress);
- }
-
- public boolean deleteEntry(Repository repository, byte[] tradePrivateKey) throws DataException {
- TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey);
- if (tradeBotData == null)
- // Can't delete what we don't have!
- return false;
-
- boolean canDelete = false;
-
- ACCT acct = SupportedBlockchain.getAcctByName(tradeBotData.getAcctName());
- if (acct == null)
- // We can't/no longer support this ACCT
- canDelete = true;
- else {
- AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
- canDelete = acctTradeBot == null || acctTradeBot.canDelete(repository, tradeBotData);
- }
-
- if (canDelete) {
- repository.getCrossChainRepository().delete(tradePrivateKey);
- repository.saveChanges();
- }
-
- return canDelete;
- }
-
- @Override
- public void listen(Event event) {
- if (!(event instanceof Controller.NewBlockEvent))
- return;
-
- synchronized (this) {
- List allTradeBotData;
-
- try (final Repository repository = RepositoryManager.getRepository()) {
- allTradeBotData = repository.getCrossChainRepository().getAllTradeBotData();
- } catch (DataException e) {
- LOGGER.error("Couldn't run trade bot due to repository issue", e);
- return;
- }
-
- for (TradeBotData tradeBotData : allTradeBotData)
- try (final Repository repository = RepositoryManager.getRepository()) {
- // Find ACCT-specific trade-bot for this entry
- ACCT acct = SupportedBlockchain.getAcctByName(tradeBotData.getAcctName());
- if (acct == null) {
- LOGGER.debug(() -> String.format("Couldn't find ACCT matching name %s", tradeBotData.getAcctName()));
- continue;
- }
-
- AcctTradeBot acctTradeBot = findTradeBotForAcct(acct);
- if (acctTradeBot == null) {
- LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot matching name %s", tradeBotData.getAcctName()));
- continue;
- }
-
- acctTradeBot.progress(repository, tradeBotData);
- } catch (DataException e) {
- LOGGER.error("Couldn't run trade bot due to repository issue", e);
- } catch (ForeignBlockchainException e) {
- LOGGER.warn(() -> String.format("Foreign blockchain issue processing trade-bot entry for AT %s: %s", tradeBotData.getAtAddress(), e.getMessage()));
- }
- }
- }
-
- /*package*/ static byte[] generateTradePrivateKey() {
- // The private key is used for both Curve25519 and secp256k1 so needs to be valid for both.
- // Curve25519 accepts any seed, so generate a valid secp256k1 key and use that.
- return new ECKey().getPrivKeyBytes();
- }
-
- /*package*/ static byte[] deriveTradeNativePublicKey(byte[] privateKey) {
- return PrivateKeyAccount.toPublicKey(privateKey);
- }
-
- /*package*/ static byte[] deriveTradeForeignPublicKey(byte[] privateKey) {
- return ECKey.fromPrivate(privateKey).getPubKey();
- }
-
- /*package*/ static byte[] generateSecret() {
- byte[] secret = new byte[32];
- RANDOM.nextBytes(secret);
- return secret;
- }
-
- /*package*/ static void backupTradeBotData(Repository repository) {
- // Attempt to backup the trade bot data. This an optional step and doesn't impact trading, so don't throw an exception on failure
- try {
- LOGGER.info("About to backup trade bot data...");
- repository.exportNodeLocalData();
- } catch (DataException e) {
- LOGGER.info(String.format("Repository issue when exporting trade bot data: %s", e.getMessage()));
- }
- }
-
- /** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
- /*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData,
- String newState, int newStateValue, Supplier logMessageSupplier) throws DataException {
- tradeBotData.setState(newState);
- tradeBotData.setStateValue(newStateValue);
- tradeBotData.setTimestamp(NTP.getTime());
- repository.getCrossChainRepository().save(tradeBotData);
- repository.saveChanges();
-
- if (Settings.getInstance().isTradebotSystrayEnabled())
- SysTray.getInstance().showMessage("Trade-Bot", String.format("%s: %s", tradeBotData.getAtAddress(), newState), MessageType.INFO);
-
- if (logMessageSupplier != null)
- LOGGER.info(logMessageSupplier);
-
- LOGGER.debug(() -> String.format("new state for trade-bot entry based on AT %s: %s", tradeBotData.getAtAddress(), newState));
-
- notifyStateChange(tradeBotData);
- }
-
- /** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
- /*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData, StateNameAndValueSupplier newStateSupplier, Supplier logMessageSupplier) throws DataException {
- updateTradeBotState(repository, tradeBotData, newStateSupplier.getState(), newStateSupplier.getStateValue(), logMessageSupplier);
- }
-
- /** Updates trade-bot entry to new state, with current timestamp, logs message and notifies state-change listeners. */
- /*package*/ static void updateTradeBotState(Repository repository, TradeBotData tradeBotData, Supplier logMessageSupplier) throws DataException {
- updateTradeBotState(repository, tradeBotData, tradeBotData.getState(), tradeBotData.getStateValue(), logMessageSupplier);
- }
-
- /*package*/ static void notifyStateChange(TradeBotData tradeBotData) {
- StateChangeEvent stateChangeEvent = new StateChangeEvent(tradeBotData);
- EventBus.INSTANCE.notify(stateChangeEvent);
- }
-
- /*package*/ static AcctTradeBot findTradeBotForAcct(ACCT acct) {
- Supplier acctTradeBotSupplier = acctTradeBotSuppliers.get(acct.getClass());
- if (acctTradeBotSupplier == null)
- return null;
-
- return acctTradeBotSupplier.get();
- }
-
- // PRESENCE-related
- /*package*/ void updatePresence(Repository repository, TradeBotData tradeBotData, CrossChainTradeData tradeData)
- throws DataException {
- String atAddress = tradeBotData.getAtAddress();
-
- PrivateKeyAccount tradeNativeAccount = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
- String signerAddress = tradeNativeAccount.getAddress();
-
- /*
- * There's no point in Alice trying to build a PRESENCE transaction
- * for an AT that isn't locked to her, as other peers won't be able
- * to validate the PRESENCE transaction as signing public key won't
- * be visible.
- */
- if (!signerAddress.equals(tradeData.qortalCreatorTradeAddress) && !signerAddress.equals(tradeData.qortalPartnerAddress))
- // Signer is neither Bob, nor Alice, or trade not yet locked to Alice
- return;
-
- long now = NTP.getTime();
- long threshold = now - PresenceType.TRADE_BOT.getLifetime();
-
- long timestamp = presenceTimestampsByAtAddress.compute(atAddress, (k, v) -> (v == null || v < threshold) ? now : v);
-
- // If timestamp hasn't been updated then nothing to do
- if (timestamp != now)
- return;
-
- 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/crosschain/ACCT.java b/src/main/java/org/qortal/crosschain/ACCT.java
deleted file mode 100644
index e557a3e2..00000000
--- a/src/main/java/org/qortal/crosschain/ACCT.java
+++ /dev/null
@@ -1,23 +0,0 @@
-package org.qortal.crosschain;
-
-import org.qortal.data.at.ATData;
-import org.qortal.data.at.ATStateData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-
-public interface ACCT {
-
- public byte[] getCodeBytesHash();
-
- public int getModeByteOffset();
-
- public ForeignBlockchain getBlockchain();
-
- public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException;
-
- public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException;
-
- public byte[] buildCancelMessage(String creatorQortalAddress);
-
-}
diff --git a/src/main/java/org/qortal/crosschain/AcctMode.java b/src/main/java/org/qortal/crosschain/AcctMode.java
deleted file mode 100644
index 21496032..00000000
--- a/src/main/java/org/qortal/crosschain/AcctMode.java
+++ /dev/null
@@ -1,21 +0,0 @@
-package org.qortal.crosschain;
-
-import static java.util.Arrays.stream;
-import static java.util.stream.Collectors.toMap;
-
-import java.util.Map;
-
-public enum AcctMode {
- OFFERING(0), TRADING(1), CANCELLED(2), REFUNDED(3), REDEEMED(4);
-
- public final int value;
- private static final Map map = stream(AcctMode.values()).collect(toMap(mode -> mode.value, mode -> mode));
-
- AcctMode(int value) {
- this.value = value;
- }
-
- public static AcctMode valueOf(int value) {
- return map.get(value);
- }
-}
\ No newline at end of file
diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java
deleted file mode 100644
index 28275d6a..00000000
--- a/src/main/java/org/qortal/crosschain/Bitcoin.java
+++ /dev/null
@@ -1,190 +0,0 @@
-package org.qortal.crosschain;
-
-import java.util.Arrays;
-import java.util.Collection;
-import java.util.EnumMap;
-import java.util.Map;
-
-import org.bitcoinj.core.Context;
-import org.bitcoinj.core.NetworkParameters;
-import org.bitcoinj.params.MainNetParams;
-import org.bitcoinj.params.RegTestParams;
-import org.bitcoinj.params.TestNet3Params;
-import org.qortal.crosschain.ElectrumX.Server;
-import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
-import org.qortal.settings.Settings;
-
-public class Bitcoin extends Bitcoiny {
-
- public static final String CURRENCY_CODE = "BTC";
-
- // Temporary values until a dynamic fee system is written.
- private static final long OLD_FEE_AMOUNT = 4_000L; // Not 5000 so that existing P2SH-B can output 1000, avoiding dust issue, leaving 4000 for fees.
- private static final long NEW_FEE_TIMESTAMP = 1598280000000L; // milliseconds since epoch
- private static final long NEW_FEE_AMOUNT = 10_000L;
-
- private static final long NON_MAINNET_FEE = 1000L; // enough for TESTNET3 and should be OK for REGTEST
-
- private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ElectrumX.Server.ConnectionType.class);
- static {
- DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
- DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
- }
-
- public enum BitcoinNet {
- MAIN {
- @Override
- public NetworkParameters getParams() {
- return MainNetParams.get();
- }
-
- @Override
- public Collection getServers() {
- return Arrays.asList(
- // Servers chosen on NO BASIS WHATSOEVER from various sources!
- new Server("128.0.190.26", Server.ConnectionType.SSL, 50002),
- new Server("hodlers.beer", Server.ConnectionType.SSL, 50002),
- new Server("electrumx.erbium.eu", Server.ConnectionType.TCP, 50001),
- new Server("electrumx.erbium.eu", Server.ConnectionType.SSL, 50002),
- new Server("btc.lastingcoin.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum.bitaroo.net", Server.ConnectionType.SSL, 50002),
- new Server("bitcoin.grey.pw", Server.ConnectionType.SSL, 50002),
- new Server("2electrumx.hopto.me", Server.ConnectionType.SSL, 56022),
- new Server("185.64.116.15", Server.ConnectionType.SSL, 50002),
- new Server("kirsche.emzy.de", Server.ConnectionType.SSL, 50002),
- new Server("alviss.coinjoined.com", Server.ConnectionType.SSL, 50002),
- new Server("electrum.emzy.de", Server.ConnectionType.SSL, 50002),
- new Server("electrum.emzy.de", Server.ConnectionType.TCP, 50001),
- new Server("vmd71287.contaboserver.net", Server.ConnectionType.SSL, 50002),
- new Server("btc.litepay.ch", Server.ConnectionType.SSL, 50002),
- new Server("electrum.stippy.com", Server.ConnectionType.SSL, 50002),
- new Server("xtrum.com", Server.ConnectionType.SSL, 50002),
- new Server("electrum.acinq.co", Server.ConnectionType.SSL, 50002),
- new Server("electrum2.taborsky.cz", Server.ConnectionType.SSL, 50002),
- new Server("vmd63185.contaboserver.net", Server.ConnectionType.SSL, 50002),
- new Server("electrum2.privateservers.network", Server.ConnectionType.SSL, 50002),
- new Server("electrumx.alexridevski.net", Server.ConnectionType.SSL, 50002),
- new Server("192.166.219.200", Server.ConnectionType.SSL, 50002),
- new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
- new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002),
- new Server("caleb.vegas", Server.ConnectionType.SSL, 50002));
- }
-
- @Override
- public String getGenesisHash() {
- return "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f";
- }
-
- @Override
- public long getP2shFee(Long timestamp) {
- // TODO: This will need to be replaced with something better in the near future!
- if (timestamp != null && timestamp < NEW_FEE_TIMESTAMP)
- return OLD_FEE_AMOUNT;
-
- return NEW_FEE_AMOUNT;
- }
- },
- TEST3 {
- @Override
- public NetworkParameters getParams() {
- return TestNet3Params.get();
- }
-
- @Override
- public Collection getServers() {
- return Arrays.asList(
- new Server("tn.not.fyi", Server.ConnectionType.SSL, 55002),
- new Server("electrumx-test.1209k.com", Server.ConnectionType.SSL, 50002),
- new Server("testnet.qtornado.com", Server.ConnectionType.SSL, 51002),
- new Server("testnet.aranguren.org", Server.ConnectionType.TCP, 51001),
- new Server("testnet.aranguren.org", Server.ConnectionType.SSL, 51002),
- new Server("testnet.hsmiths.com", Server.ConnectionType.SSL, 53012));
- }
-
- @Override
- public String getGenesisHash() {
- return "000000000933ea01ad0ee984209779baaec3ced90fa3f408719526f8d77f4943";
- }
-
- @Override
- public long getP2shFee(Long timestamp) {
- return NON_MAINNET_FEE;
- }
- },
- REGTEST {
- @Override
- public NetworkParameters getParams() {
- return RegTestParams.get();
- }
-
- @Override
- public Collection getServers() {
- return Arrays.asList(
- new Server("localhost", Server.ConnectionType.TCP, 50001),
- new Server("localhost", Server.ConnectionType.SSL, 50002));
- }
-
- @Override
- public String getGenesisHash() {
- // This is unique to each regtest instance
- return null;
- }
-
- @Override
- public long getP2shFee(Long timestamp) {
- return NON_MAINNET_FEE;
- }
- };
-
- public abstract NetworkParameters getParams();
- public abstract Collection getServers();
- public abstract String getGenesisHash();
- public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
- }
-
- private static Bitcoin instance;
-
- private final BitcoinNet bitcoinNet;
-
- // Constructors and instance
-
- private Bitcoin(BitcoinNet bitcoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- super(blockchain, bitcoinjContext, currencyCode);
- this.bitcoinNet = bitcoinNet;
-
- LOGGER.info(() -> String.format("Starting Bitcoin support using %s", this.bitcoinNet.name()));
- }
-
- public static synchronized Bitcoin getInstance() {
- if (instance == null) {
- BitcoinNet bitcoinNet = Settings.getInstance().getBitcoinNet();
-
- BitcoinyBlockchainProvider electrumX = new ElectrumX("Bitcoin-" + bitcoinNet.name(), bitcoinNet.getGenesisHash(), bitcoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
- Context bitcoinjContext = new Context(bitcoinNet.getParams());
-
- instance = new Bitcoin(bitcoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
- }
-
- return instance;
- }
-
- // Getters & setters
-
- public static synchronized void resetForTesting() {
- instance = null;
- }
-
- // Actual useful methods for use by other classes
-
- /**
- * Returns estimated BTC fee, in sats per 1000bytes, optionally for historic timestamp.
- *
- * @param timestamp optional milliseconds since epoch, or null for 'now'
- * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
- */
- @Override
- public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
- return this.bitcoinNet.getP2shFee(timestamp);
- }
-
-}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
deleted file mode 100644
index 5118e103..00000000
--- a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
+++ /dev/null
@@ -1,921 +0,0 @@
-package org.qortal.crosschain;
-
-import static org.ciyam.at.OpCode.calcOffset;
-
-import java.nio.ByteBuffer;
-import java.util.Arrays;
-import java.util.List;
-
-import org.ciyam.at.API;
-import org.ciyam.at.CompilationException;
-import org.ciyam.at.FunctionCode;
-import org.ciyam.at.MachineState;
-import org.ciyam.at.OpCode;
-import org.ciyam.at.Timestamp;
-import org.qortal.account.Account;
-import org.qortal.asset.Asset;
-import org.qortal.at.QortalFunctionCode;
-import org.qortal.crypto.Crypto;
-import org.qortal.data.at.ATData;
-import org.qortal.data.at.ATStateData;
-import org.qortal.data.crosschain.CrossChainTradeData;
-import org.qortal.data.transaction.MessageTransactionData;
-import org.qortal.repository.DataException;
-import org.qortal.repository.Repository;
-import org.qortal.utils.Base58;
-import org.qortal.utils.BitTwiddling;
-
-import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-
-/**
- * Cross-chain trade AT
- *
- *
- *
- *
Bob generates Bitcoin & Qortal 'trade' keys, and secret-b
- *
- *
private key required to sign P2SH redeem tx
- *
private key could be used to create 'secret' (e.g. double-SHA256)
- *
encrypted private key could be stored in Qortal AT for access by Bob from any node
- *
- *
- *
Bob deploys Qortal AT
- *
- *
- *
- *
Alice finds Qortal AT and wants to trade
- *
- *
Alice generates Bitcoin & Qortal 'trade' keys
- *
Alice funds Bitcoin P2SH-A
- *
Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
- *
- *
hash-of-secret-A
- *
her 'trade' Bitcoin PKH
- *
- *
- *
- *
- *
Bob receives "offer" MESSAGE
- *
- *
Checks Alice's P2SH-A
- *
Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
- *
- *
Alice's trade Qortal address
- *
Alice's trade Bitcoin PKH
- *
hash-of-secret-A
- *
- *
- *
- *
- *
Alice checks Qortal AT to confirm it's locked to her
- *
- *
Alice creates/funds Bitcoin P2SH-B
- *
- *
- *
Bob checks P2SH-B is funded
- *
- *
Bob redeems P2SH-B using his Bitcoin trade key and secret-B
- *
- *
- *
Alice scans P2SH-B redeem transaction to extract secret-B
- *
- *
Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
- *
- *
secret-A
- *
secret-B
- *
Qortal receiving address of her chosing
- *
- *
- *
AT's QORT funds are sent to Qortal receiving address
- *
- *
- *
Bob checks AT, extracts secret-A
- *
- *
Bob redeems P2SH-A using his Bitcoin trade key and secret-A
- *
P2SH-A BTC funds end up at Bitcoin address determined by redeem transaction output(s)
- *
- *
- *
- */
-public class BitcoinACCTv1 implements ACCT {
-
- public static final String NAME = BitcoinACCTv1.class.getSimpleName();
- public static final byte[] CODE_BYTES_HASH = HashCode.fromString("f7f419522a9aaa3c671149878f8c1374dfc59d4fd86ca43ff2a4d913cfbc9e89").asBytes(); // SHA256 of AT code bytes
-
- public static final int SECRET_LENGTH = 32;
-
- /** Value offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
- private static final int MODE_VALUE_OFFSET = 68;
- /** Byte offset into AT state data where 'mode' variable (long) is stored. */
- public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
-
- public static class OfferMessageData {
- public byte[] partnerBitcoinPKH;
- public byte[] hashOfSecretA;
- public long lockTimeA;
- }
- public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerBitcoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
- public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
- + 24 /*partner's Bitcoin PKH (padded from 20 to 24)*/
- + 8 /*lockTimeB*/
- + 24 /*hash of secret-A (padded from 20 to 24)*/
- + 8 /*lockTimeA*/;
- public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret*/ + 32 /*secret*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
- public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
-
- private static BitcoinACCTv1 instance;
-
- private BitcoinACCTv1() {
- }
-
- public static synchronized BitcoinACCTv1 getInstance() {
- if (instance == null)
- instance = new BitcoinACCTv1();
-
- return instance;
- }
-
- @Override
- public byte[] getCodeBytesHash() {
- return CODE_BYTES_HASH;
- }
-
- @Override
- public int getModeByteOffset() {
- return MODE_BYTE_OFFSET;
- }
-
- @Override
- public ForeignBlockchain getBlockchain() {
- return Bitcoin.getInstance();
- }
-
- /**
- * Returns Qortal AT creation bytes for cross-chain trading AT.
- *
- * tradeTimeout (minutes) is the time window for the trade partner to send the
- * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
- *
- * @param creatorTradeAddress AT creator's trade Qortal address, also used for refunds
- * @param bitcoinPublicKeyHash 20-byte HASH160 of creator's trade Bitcoin public key
- * @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
- */
- public static byte[] buildQortalAT(String creatorTradeAddress, byte[] bitcoinPublicKeyHash, byte[] hashOfSecretB, long qortAmount, long bitcoinAmount, int tradeTimeout) {
- // Labels for data segment addresses
- int addrCounter = 0;
-
- // Constants (with corresponding dataByteBuffer.put*() calls below)
-
- final int addrCreatorTradeAddress1 = addrCounter++;
- final int addrCreatorTradeAddress2 = addrCounter++;
- final int addrCreatorTradeAddress3 = addrCounter++;
- final int addrCreatorTradeAddress4 = addrCounter++;
-
- final int addrBitcoinPublicKeyHash = addrCounter;
- addrCounter += 4;
-
- final int addrHashOfSecretB = addrCounter;
- addrCounter += 4;
-
- final int addrQortAmount = addrCounter++;
- final int addrBitcoinAmount = addrCounter++;
- final int addrTradeTimeout = addrCounter++;
-
- final int addrMessageTxnType = addrCounter++;
- final int addrExpectedTradeMessageLength = addrCounter++;
- final int addrExpectedRedeemMessageLength = addrCounter++;
-
- final int addrCreatorAddressPointer = addrCounter++;
- final int addrHashOfSecretBPointer = addrCounter++;
- final int addrQortalPartnerAddressPointer = addrCounter++;
- final int addrMessageSenderPointer = addrCounter++;
-
- final int addrTradeMessagePartnerBitcoinPKHOffset = addrCounter++;
- final int addrPartnerBitcoinPKHPointer = addrCounter++;
- final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
- final int addrHashOfSecretAPointer = addrCounter++;
-
- final int addrRedeemMessageSecretBOffset = addrCounter++;
- final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
-
- final int addrMessageDataPointer = addrCounter++;
- final int addrMessageDataLength = addrCounter++;
-
- final int addrPartnerReceivingAddressPointer = addrCounter++;
-
- final int addrEndOfConstants = addrCounter;
-
- // Variables
-
- final int addrCreatorAddress1 = addrCounter++;
- final int addrCreatorAddress2 = addrCounter++;
- final int addrCreatorAddress3 = addrCounter++;
- final int addrCreatorAddress4 = addrCounter++;
-
- final int addrQortalPartnerAddress1 = addrCounter++;
- final int addrQortalPartnerAddress2 = addrCounter++;
- final int addrQortalPartnerAddress3 = addrCounter++;
- final int addrQortalPartnerAddress4 = addrCounter++;
-
- final int addrLockTimeA = addrCounter++;
- final int addrLockTimeB = addrCounter++;
- final int addrRefundTimeout = addrCounter++;
- final int addrRefundTimestamp = addrCounter++;
- final int addrLastTxnTimestamp = addrCounter++;
- final int addrBlockTimestamp = addrCounter++;
- final int addrTxnType = addrCounter++;
- final int addrResult = addrCounter++;
-
- final int addrMessageSender1 = addrCounter++;
- final int addrMessageSender2 = addrCounter++;
- final int addrMessageSender3 = addrCounter++;
- final int addrMessageSender4 = addrCounter++;
-
- final int addrMessageLength = addrCounter++;
-
- final int addrMessageData = addrCounter;
- addrCounter += 4;
-
- final int addrHashOfSecretA = addrCounter;
- addrCounter += 4;
-
- final int addrPartnerBitcoinPKH = addrCounter;
- addrCounter += 4;
-
- final int addrPartnerReceivingAddress = addrCounter;
- addrCounter += 4;
-
- final int addrMode = addrCounter++;
- assert addrMode == MODE_VALUE_OFFSET : "MODE_VALUE_OFFSET does not match addrMode";
-
- // Data segment
- ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
-
- // AT creator's trade Qortal address, decoded from Base58
- assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
- byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
- dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
-
- // Bitcoin public key hash
- assert dataByteBuffer.position() == addrBitcoinPublicKeyHash * MachineState.VALUE_SIZE : "addrBitcoinPublicKeyHash incorrect";
- dataByteBuffer.put(Bytes.ensureCapacity(bitcoinPublicKeyHash, 32, 0));
-
- // Hash of secret-B
- assert dataByteBuffer.position() == addrHashOfSecretB * MachineState.VALUE_SIZE : "addrHashOfSecretB incorrect";
- dataByteBuffer.put(Bytes.ensureCapacity(hashOfSecretB, 32, 0));
-
- // Redeem Qort amount
- assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
- dataByteBuffer.putLong(qortAmount);
-
- // Expected Bitcoin amount
- 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() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
- dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
-
- // Expected length of 'trade' MESSAGE data from AT creator
- assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
- dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
-
- // Expected length of 'redeem' MESSAGE data from trade partner
- assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
- dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
-
- // Index into data segment of AT creator's address, used by GET_B_IND
- assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
- dataByteBuffer.putLong(addrCreatorAddress1);
-
- // Index into data segment of hash of secret B, used by GET_B_IND
- assert dataByteBuffer.position() == addrHashOfSecretBPointer * MachineState.VALUE_SIZE : "addrHashOfSecretBPointer incorrect";
- dataByteBuffer.putLong(addrHashOfSecretB);
-
- // Index into data segment of partner's Qortal address, used by SET_B_IND
- assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
- dataByteBuffer.putLong(addrQortalPartnerAddress1);
-
- // Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
- assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
- dataByteBuffer.putLong(addrMessageSender1);
-
- // Offset into 'trade' MESSAGE data payload for extracting partner's Bitcoin PKH
- assert dataByteBuffer.position() == addrTradeMessagePartnerBitcoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerBitcoinPKHOffset incorrect";
- dataByteBuffer.putLong(32L);
-
- // Index into data segment of partner's Bitcoin PKH, used by GET_B_IND
- assert dataByteBuffer.position() == addrPartnerBitcoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerBitcoinPKHPointer incorrect";
- dataByteBuffer.putLong(addrPartnerBitcoinPKH);
-
- // Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
- assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
- dataByteBuffer.putLong(64L);
-
- // Index into data segment to hash of secret A, used by GET_B_IND
- assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
- dataByteBuffer.putLong(addrHashOfSecretA);
-
- // Offset into 'redeem' MESSAGE data payload for extracting secret-B
- assert dataByteBuffer.position() == addrRedeemMessageSecretBOffset * MachineState.VALUE_SIZE : "addrRedeemMessageSecretBOffset incorrect";
- dataByteBuffer.putLong(32L);
-
- // Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
- assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
- dataByteBuffer.putLong(64L);
-
- // Source location and length for hashing any passed secret
- assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
- dataByteBuffer.putLong(addrMessageData);
- assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
- dataByteBuffer.putLong(32L);
-
- // Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
- assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
- dataByteBuffer.putLong(addrPartnerReceivingAddress);
-
- assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
-
- // Code labels
- Integer labelRefund = null;
-
- Integer labelTradeTxnLoop = null;
- Integer labelCheckTradeTxn = null;
- Integer labelCheckCancelTxn = null;
- Integer labelNotTradeNorCancelTxn = null;
- Integer labelCheckNonRefundTradeTxn = null;
- Integer labelTradeTxnExtract = null;
- Integer labelRedeemTxnLoop = null;
- Integer labelCheckRedeemTxn = null;
- Integer labelCheckRedeemTxnSender = null;
- Integer labelCheckSecretB = null;
- Integer labelPayout = null;
-
- ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
-
- // Two-pass version
- for (int pass = 0; pass < 2; ++pass) {
- codeByteBuffer.clear();
-
- try {
- /* Initialization */
-
- // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
-
- // Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
- codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
-
- // Set restart position to after this opcode
- codeByteBuffer.put(OpCode.SET_PCS.compile());
-
- /* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
-
- /* Transaction processing loop */
- labelTradeTxnLoop = codeByteBuffer.position();
-
- // Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
- // If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
- // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
- codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
- // Stop and wait for next block
- codeByteBuffer.put(OpCode.STP_IMD.compile());
-
- /* Check transaction */
- labelCheckTradeTxn = codeByteBuffer.position();
-
- // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
- // Extract transaction type (message/payment) from transaction and save type in addrTxnType
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
- // If transaction type is not MESSAGE type then go look for another transaction
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
-
- /* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
-
- // Extract sender address from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
- // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
- // Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
- // Message sender's address matches AT creator's trade address so go process 'trade' message
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
-
- /* Checking message sender for possible cancel message */
- labelCheckCancelTxn = codeByteBuffer.position();
-
- // Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
- // Partner address is AT creator's address, so cancel offer and finish.
- codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
- // We're finished forever (finishing auto-refunds remaining balance to AT creator)
- codeByteBuffer.put(OpCode.FIN_IMD.compile());
-
- /* Not trade nor cancel message */
- labelNotTradeNorCancelTxn = codeByteBuffer.position();
-
- // Loop to find another transaction
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
-
- /* Possible switch-to-trade-mode message */
- labelCheckNonRefundTradeTxn = codeByteBuffer.position();
-
- // Check 'trade' message we received has expected number of message bytes
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
- // If message length matches, branch to info extraction code
- codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
- // Message length didn't match - go back to finding another 'trade' MESSAGE transaction
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
-
- /* Extracting info from 'trade' MESSAGE transaction */
- labelTradeTxnExtract = codeByteBuffer.position();
-
- // Extract message from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
- // Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
-
- // Extract trade partner's Bitcoin public key hash (PKH)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerBitcoinPKHOffset));
- // Extract partner's Bitcoin PKH (we only really use values from B1-B3)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerBitcoinPKHPointer));
- // Also extract lockTimeB
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeB));
-
- // Grab next 32 bytes
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
-
- // Extract hash-of-secret-a (we only really use values from B1-B3)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
- // Extract lockTimeA (from B4)
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
-
- // Calculate trade refund timeout: (lockTimeA - lockTimeB) / 2 / 60
- codeByteBuffer.put(OpCode.SET_DAT.compile(addrRefundTimeout, addrLockTimeA)); // refundTimeout = lockTimeA
- codeByteBuffer.put(OpCode.SUB_DAT.compile(addrRefundTimeout, addrLockTimeB)); // refundTimeout -= lockTimeB
- codeByteBuffer.put(OpCode.DIV_VAL.compile(addrRefundTimeout, 2L * 60L)); // refundTimeout /= 2 * 60
- // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
- codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
-
- /* We are in 'trade mode' */
- codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
-
- // Set restart position to after this opcode
- codeByteBuffer.put(OpCode.SET_PCS.compile());
-
- /* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
-
- // Fetch current block 'timestamp'
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
- // If we're not past refund 'timestamp' then look for next transaction
- codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
- // We're past refund 'timestamp' so go refund everything back to AT creator
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
-
- /* Transaction processing loop */
- labelRedeemTxnLoop = codeByteBuffer.position();
-
- // Find next transaction to this AT since the last one (if any)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
- // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
- // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
- codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
- // Stop and wait for next block
- codeByteBuffer.put(OpCode.STP_IMD.compile());
-
- /* Check transaction */
- labelCheckRedeemTxn = codeByteBuffer.position();
-
- // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
- // Extract transaction type (message/payment) from transaction and save type in addrTxnType
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
- // If transaction type is not MESSAGE type then go look for another transaction
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
-
- /* Check message payload length */
- codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
- // If message length matches, branch to sender checking code
- codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
- // Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
-
- /* Check transaction's sender */
- labelCheckRedeemTxnSender = codeByteBuffer.position();
-
- // Extract sender address from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
- // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
- // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
- codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
-
- /* Check 'secret-A' in transaction's message */
-
- // Extract secret-A from first 32 bytes of message from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
- // Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
- // Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
- // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
- // Save the equality result (1 if they match, 0 otherwise) into addrResult.
- codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
- // If hashes don't match, addrResult will be zero so go find another transaction
- codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckSecretB)));
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
-
- /* Check 'secret-B' in transaction's message */
-
- labelCheckSecretB = codeByteBuffer.position();
-
- // Extract secret-B from next 32 bytes of message from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageSecretBOffset));
- // Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
- // Load B register with expected hash result (as pointed to by addrHashOfSecretBPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretBPointer));
- // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
- // Save the equality result (1 if they match, 0 otherwise) into addrResult.
- codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
- // If hashes don't match, addrResult will be zero so go find another transaction
- codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
- codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
-
- /* Success! Pay arranged amount to receiving address */
- labelPayout = codeByteBuffer.position();
-
- // Extract Qortal receiving address from next 32 bytes of message from transaction into B register
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
- // Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
- // Pay AT's balance to receiving address
- codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
- // Set redeemed mode
- codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
- // We're finished forever (finishing auto-refunds remaining balance to AT creator)
- codeByteBuffer.put(OpCode.FIN_IMD.compile());
-
- // Fall-through to refunding any remaining balance back to AT creator
-
- /* Refund balance back to AT creator */
- labelRefund = codeByteBuffer.position();
-
- // Set refunded mode
- codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
- // We're finished forever (finishing auto-refunds remaining balance to AT creator)
- codeByteBuffer.put(OpCode.FIN_IMD.compile());
- } catch (CompilationException e) {
- throw new IllegalStateException("Unable to compile BTC-QORT ACCT?", e);
- }
- }
-
- codeByteBuffer.flip();
-
- byte[] codeBytes = new byte[codeByteBuffer.limit()];
- codeByteBuffer.get(codeBytes);
-
- assert Arrays.equals(Crypto.digest(codeBytes), BitcoinACCTv1.CODE_BYTES_HASH)
- : String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
-
- final short ciyamAtVersion = 2;
- final short numCallStackPages = 0;
- final short numUserStackPages = 0;
- final long minActivationAmount = 0L;
-
- return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
- }
-
- /**
- * Returns CrossChainTradeData with useful info extracted from AT.
- */
- @Override
- public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
- ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
- return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
- }
-
- /**
- * Returns CrossChainTradeData with useful info extracted from AT.
- */
- @Override
- public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
- ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
- return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
- }
-
- /**
- * Returns CrossChainTradeData with useful info extracted from AT.
- */
- public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
- byte[] addressBytes = new byte[25]; // for general use
- String atAddress = atStateData.getATAddress();
-
- CrossChainTradeData tradeData = new CrossChainTradeData();
-
- tradeData.foreignBlockchain = SupportedBlockchain.BITCOIN.name();
- tradeData.acctName = NAME;
-
- tradeData.qortalAtAddress = atAddress;
- tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
- tradeData.creationTimestamp = creationTimestamp;
-
- Account atAccount = new Account(repository, atAddress);
- tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
-
- byte[] stateData = atStateData.getStateData();
- ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
- dataByteBuffer.position(MachineState.HEADER_LENGTH);
-
- /* Constants */
-
- // Skip creator's trade address
- dataByteBuffer.get(addressBytes);
- tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
-
- // Creator's Bitcoin/foreign public key hash
- tradeData.creatorForeignPKH = new byte[20];
- dataByteBuffer.get(tradeData.creatorForeignPKH);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
-
- // Hash of secret B
- tradeData.hashOfSecretB = new byte[20];
- dataByteBuffer.get(tradeData.hashOfSecretB);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.hashOfSecretB.length); // skip to 32 bytes
-
- // Redeem payout
- tradeData.qortAmount = dataByteBuffer.getLong();
-
- // Expected BTC amount
- tradeData.expectedForeignAmount = dataByteBuffer.getLong();
-
- // Trade timeout
- tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
-
- // Skip MESSAGE transaction type
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip expected 'trade' message length
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip expected 'redeem' message length
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to creator's address
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to hash-of-secret-B
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to partner's Qortal trade address
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to message sender
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip 'trade' message data offset for partner's bitcoin PKH
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to partner's bitcoin PKH
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip 'trade' message data offset for hash-of-secret-A
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to hash-of-secret-A
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip 'redeem' message data offset for secret-B
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip 'redeem' message data offset for partner's Qortal receiving address
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to message data
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip message data length
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip pointer to partner's receiving address
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- /* End of constants / begin variables */
-
- // Skip AT creator's address
- dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
-
- // Partner's trade address (if present)
- dataByteBuffer.get(addressBytes);
- String qortalRecipient = Base58.encode(addressBytes);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
-
- // Potential lockTimeA (if in trade mode)
- int lockTimeA = (int) dataByteBuffer.getLong();
-
- // Potential lockTimeB (if in trade mode)
- int lockTimeB = (int) dataByteBuffer.getLong();
-
- // AT refund timeout (probably only useful for debugging)
- int refundTimeout = (int) dataByteBuffer.getLong();
-
- // Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
- long tradeRefundTimestamp = dataByteBuffer.getLong();
-
- // Skip last transaction timestamp
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip block timestamp
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip transaction type
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip temporary result
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip temporary message sender
- dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
-
- // Skip message length
- dataByteBuffer.position(dataByteBuffer.position() + 8);
-
- // Skip temporary message data
- dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
-
- // Potential hash160 of secret A
- byte[] hashOfSecretA = new byte[20];
- dataByteBuffer.get(hashOfSecretA);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
-
- // Potential partner's Bitcoin PKH
- byte[] partnerBitcoinPKH = new byte[20];
- dataByteBuffer.get(partnerBitcoinPKH);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerBitcoinPKH.length); // skip to 32 bytes
-
- // Partner's receiving address (if present)
- byte[] partnerReceivingAddress = new byte[25];
- dataByteBuffer.get(partnerReceivingAddress);
- dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
-
- // Trade AT's 'mode'
- long modeValue = dataByteBuffer.getLong();
- AcctMode acctMode = AcctMode.valueOf((int) (modeValue & 0xffL));
-
- /* End of variables */
-
- if (acctMode != null && acctMode != AcctMode.OFFERING) {
- tradeData.mode = acctMode;
- tradeData.refundTimeout = refundTimeout;
- tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
- tradeData.qortalPartnerAddress = qortalRecipient;
- tradeData.hashOfSecretA = hashOfSecretA;
- tradeData.partnerForeignPKH = partnerBitcoinPKH;
- tradeData.lockTimeA = lockTimeA;
- tradeData.lockTimeB = lockTimeB;
-
- if (acctMode == AcctMode.REDEEMED)
- tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
- } else {
- tradeData.mode = AcctMode.OFFERING;
- }
-
- tradeData.duplicateDeprecated();
-
- return tradeData;
- }
-
- /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
- public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
- byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
- return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
- }
-
- /** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
- public static OfferMessageData extractOfferMessageData(byte[] messageData) {
- if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
- return null;
-
- OfferMessageData offerMessageData = new OfferMessageData();
- offerMessageData.partnerBitcoinPKH = Arrays.copyOfRange(messageData, 0, 20);
- offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
- offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
-
- return offerMessageData;
- }
-
- /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
- public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int lockTimeB) {
- byte[] data = new byte[TRADE_MESSAGE_LENGTH];
- byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
- byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
- byte[] lockTimeBBytes = BitTwiddling.toBEByteArray((long) lockTimeB);
-
- System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
- System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
- System.arraycopy(lockTimeBBytes, 0, data, 56, lockTimeBBytes.length);
- System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
- System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
-
- return data;
- }
-
- /** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
- @Override
- public byte[] buildCancelMessage(String creatorQortalAddress) {
- byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
- byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
-
- System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
-
- return data;
- }
-
- /** Returns 'redeem' MESSAGE payload for trade partner/ to send to AT. */
- public static byte[] buildRedeemMessage(byte[] secretA, byte[] secretB, String qortalReceivingAddress) {
- byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
- byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
-
- System.arraycopy(secretA, 0, data, 0, secretA.length);
- System.arraycopy(secretB, 0, data, 32, secretB.length);
- System.arraycopy(qortalReceivingAddressBytes, 0, data, 64, qortalReceivingAddressBytes.length);
-
- return data;
- }
-
- /** Returns P2SH-B lockTime (epoch seconds) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
- public static int calcLockTimeB(long offerMessageTimestamp, int lockTimeA) {
- // lockTimeB is halfway between offerMessageTimestamp and lockTimeA
- return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
- }
-
- public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
- String atAddress = crossChainTradeData.qortalAtAddress;
- String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
-
- // We don't have partner's public key so we check every message to AT
- List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
- if (messageTransactionsData == null)
- return null;
-
- // Find 'redeem' message
- for (MessageTransactionData messageTransactionData : messageTransactionsData) {
- // Check message payload type/encryption
- if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
- continue;
-
- // Check message payload size
- byte[] messageData = messageTransactionData.getData();
- if (messageData.length != REDEEM_MESSAGE_LENGTH)
- // Wrong payload length
- continue;
-
- // Check sender
- if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
- // Wrong sender;
- continue;
-
- // Extract both secretA & secretB
- byte[] secretA = new byte[32];
- System.arraycopy(messageData, 0, secretA, 0, secretA.length);
- byte[] secretB = new byte[32];
- System.arraycopy(messageData, 32, secretB, 0, secretB.length);
-
- byte[] hashOfSecretA = Crypto.hash160(secretA);
- if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
- continue;
-
- byte[] hashOfSecretB = Crypto.hash160(secretB);
- if (!Arrays.equals(hashOfSecretB, crossChainTradeData.hashOfSecretB))
- continue;
-
- return secretA;
- }
-
- return null;
- }
-
-}
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
deleted file mode 100644
index fc98f959..00000000
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ /dev/null
@@ -1,740 +0,0 @@
-package org.qortal.crosschain;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
-
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.bitcoinj.core.Address;
-import org.bitcoinj.core.AddressFormatException;
-import org.bitcoinj.core.Coin;
-import org.bitcoinj.core.Context;
-import org.bitcoinj.core.ECKey;
-import org.bitcoinj.core.InsufficientMoneyException;
-import org.bitcoinj.core.LegacyAddress;
-import org.bitcoinj.core.NetworkParameters;
-import org.bitcoinj.core.Sha256Hash;
-import org.bitcoinj.core.Transaction;
-import org.bitcoinj.core.TransactionOutput;
-import org.bitcoinj.core.UTXO;
-import org.bitcoinj.core.UTXOProvider;
-import org.bitcoinj.core.UTXOProviderException;
-import org.bitcoinj.crypto.ChildNumber;
-import org.bitcoinj.crypto.DeterministicHierarchy;
-import org.bitcoinj.crypto.DeterministicKey;
-import org.bitcoinj.script.Script.ScriptType;
-import org.bitcoinj.script.ScriptBuilder;
-import org.bitcoinj.wallet.DeterministicKeyChain;
-import org.bitcoinj.wallet.SendRequest;
-import org.bitcoinj.wallet.Wallet;
-import org.qortal.api.model.SimpleForeignTransaction;
-import org.qortal.crypto.Crypto;
-import org.qortal.utils.Amounts;
-import org.qortal.utils.BitTwiddling;
-
-import com.google.common.hash.HashCode;
-
-/** Bitcoin-like (Bitcoin, Litecoin, etc.) support */
-public abstract class Bitcoiny implements ForeignBlockchain {
-
- protected static final Logger LOGGER = LogManager.getLogger(Bitcoiny.class);
-
- public static final int HASH160_LENGTH = 20;
-
- protected final BitcoinyBlockchainProvider blockchain;
- protected final Context bitcoinjContext;
- protected final String currencyCode;
-
- protected final NetworkParameters params;
-
- /** Keys that have been previously marked as fully spent,
- * i.e. keys with transactions but with no unspent outputs. */
- protected final Set spentKeys = Collections.synchronizedSet(new HashSet<>());
-
- /** How many bitcoinj wallet keys to generate in each batch. */
- private static final int WALLET_KEY_LOOKAHEAD_INCREMENT = 3;
-
- /** Byte offset into raw block headers to block timestamp. */
- private static final int TIMESTAMP_OFFSET = 4 + 32 + 32;
-
- // Constructors and instance
-
- protected Bitcoiny(BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
- this.blockchain = blockchain;
- this.bitcoinjContext = bitcoinjContext;
- this.currencyCode = currencyCode;
-
- this.params = this.bitcoinjContext.getParams();
- }
-
- // Getters & setters
-
- public BitcoinyBlockchainProvider getBlockchainProvider() {
- return this.blockchain;
- }
-
- public Context getBitcoinjContext() {
- return this.bitcoinjContext;
- }
-
- public String getCurrencyCode() {
- return this.currencyCode;
- }
-
- public NetworkParameters getNetworkParameters() {
- return this.params;
- }
-
- // Interface obligations
-
- @Override
- public boolean isValidAddress(String address) {
- try {
- ScriptType addressType = Address.fromString(this.params, address).getOutputScriptType();
-
- return addressType == ScriptType.P2PKH || addressType == ScriptType.P2SH;
- } catch (AddressFormatException e) {
- return false;
- }
- }
-
- @Override
- public boolean isValidWalletKey(String walletKey) {
- return this.isValidDeterministicKey(walletKey);
- }
-
- // Actual useful methods for use by other classes
-
- public String format(Coin amount) {
- return this.format(amount.value);
- }
-
- public String format(long amount) {
- return Amounts.prettyAmount(amount) + " " + this.currencyCode;
- }
-
- public boolean isValidDeterministicKey(String key58) {
- try {
- Context.propagate(this.bitcoinjContext);
- DeterministicKey.deserializeB58(null, key58, this.params);
- return true;
- } catch (IllegalArgumentException e) {
- return false;
- }
- }
-
- /** Returns P2PKH address using passed public key hash. */
- public String pkhToAddress(byte[] publicKeyHash) {
- Context.propagate(this.bitcoinjContext);
- return LegacyAddress.fromPubKeyHash(this.params, publicKeyHash).toString();
- }
-
- /** Returns P2SH address using passed redeem script. */
- public String deriveP2shAddress(byte[] redeemScriptBytes) {
- Context.propagate(bitcoinjContext);
- byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
- return LegacyAddress.fromScriptHash(this.params, redeemScriptHash).toString();
- }
-
- /**
- * Returns median timestamp from latest 11 blocks, in seconds.
- *
- * @throws ForeignBlockchainException if error occurs
- */
- public int getMedianBlockTime() throws ForeignBlockchainException {
- int height = this.blockchain.getCurrentHeight();
-
- // Grab latest 11 blocks
- List blockHeaders = this.blockchain.getRawBlockHeaders(height - 11, 11);
- if (blockHeaders.size() < 11)
- throw new ForeignBlockchainException("Not enough blocks to determine median block time");
-
- List blockTimestamps = blockHeaders.stream().map(blockHeader -> BitTwiddling.intFromLEBytes(blockHeader, TIMESTAMP_OFFSET)).collect(Collectors.toList());
-
- // Descending order
- blockTimestamps.sort((a, b) -> Integer.compare(b, a));
-
- // Pick median
- return blockTimestamps.get(5);
- }
-
- /** Returns fee per transaction KB. To be overridden for testnet/regtest. */
- public Coin getFeePerKb() {
- return this.bitcoinjContext.getFeePerKb();
- }
-
- /**
- * Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp.
- *
- * @param timestamp optional milliseconds since epoch, or null for 'now'
- * @return sats per 1000bytes
- * @throws ForeignBlockchainException if something went wrong
- */
- public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
-
- /**
- * Returns confirmed balance, based on passed payment script.
- *
- * @return confirmed balance, or zero if script unknown
- * @throws ForeignBlockchainException if there was an error
- */
- public long getConfirmedBalance(String base58Address) throws ForeignBlockchainException {
- return this.blockchain.getConfirmedBalance(addressToScriptPubKey(base58Address));
- }
-
- /**
- * Returns list of unspent outputs pertaining to passed address.
- *
- * @return list of unspent outputs, or empty list if address unknown
- * @throws ForeignBlockchainException if there was an error.
- */
- // TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
- public List getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
- List unspentOutputs = this.blockchain.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
-
- List unspentTransactionOutputs = new ArrayList<>();
- for (UnspentOutput unspentOutput : unspentOutputs) {
- List transactionOutputs = this.getOutputs(unspentOutput.hash);
-
- unspentTransactionOutputs.add(transactionOutputs.get(unspentOutput.index));
- }
-
- return unspentTransactionOutputs;
- }
-
- /**
- * Returns list of outputs pertaining to passed transaction hash.
- *
- * @return list of outputs, or empty list if transaction unknown
- * @throws ForeignBlockchainException if there was an error.
- */
- // TODO: don't return bitcoinj-based objects like TransactionOutput, use BitcoinyTransaction.Output instead
- public List getOutputs(byte[] txHash) throws ForeignBlockchainException {
- byte[] rawTransactionBytes = this.blockchain.getRawTransaction(txHash);
-
- Context.propagate(bitcoinjContext);
- Transaction transaction = new Transaction(this.params, rawTransactionBytes);
- return transaction.getOutputs();
- }
-
- /**
- * Returns list of transaction hashes pertaining to passed address.
- *
- * @return list of unspent outputs, or empty list if script unknown
- * @throws ForeignBlockchainException if there was an error.
- */
- public List getAddressTransactions(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
- return this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), includeUnconfirmed);
- }
-
- /**
- * Returns list of raw, confirmed transactions involving given address.
- *
- * @throws ForeignBlockchainException if there was an error
- */
- public List getAddressTransactions(String base58Address) throws ForeignBlockchainException {
- List transactionHashes = this.blockchain.getAddressTransactions(addressToScriptPubKey(base58Address), false);
-
- List rawTransactions = new ArrayList<>();
- for (TransactionHash transactionInfo : transactionHashes) {
- byte[] rawTransaction = this.blockchain.getRawTransaction(HashCode.fromString(transactionInfo.txHash).asBytes());
- rawTransactions.add(rawTransaction);
- }
-
- return rawTransactions;
- }
-
- /**
- * Returns transaction info for passed transaction hash.
- *
- * @throws ForeignBlockchainException.NotFoundException if transaction unknown
- * @throws ForeignBlockchainException if error occurs
- */
- public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException {
- return this.blockchain.getTransaction(txHash);
- }
-
- /**
- * Broadcasts raw transaction to network.
- *
- * @throws ForeignBlockchainException if error occurs
- */
- public void broadcastTransaction(Transaction transaction) throws ForeignBlockchainException {
- this.blockchain.broadcastTransaction(transaction.bitcoinSerialize());
- }
-
- /**
- * Returns bitcoinj transaction sending amount to recipient.
- *
- * @param xprv58 BIP32 private key
- * @param recipient P2PKH address
- * @param amount unscaled amount
- * @param feePerByte unscaled fee per byte, or null to use default fees
- * @return transaction, or null if insufficient funds
- */
- public Transaction buildSpend(String xprv58, String recipient, long amount, Long feePerByte) {
- Context.propagate(bitcoinjContext);
-
- Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
- wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
-
- Address destination = Address.fromString(this.params, recipient);
- SendRequest sendRequest = SendRequest.to(destination, Coin.valueOf(amount));
-
- if (feePerByte != null)
- sendRequest.feePerKb = Coin.valueOf(feePerByte * 1000L); // Note: 1000 not 1024
- else
- // Allow override of default for TestNet3, etc.
- sendRequest.feePerKb = this.getFeePerKb();
-
- try {
- wallet.completeTx(sendRequest);
- return sendRequest.tx;
- } catch (InsufficientMoneyException e) {
- return null;
- }
- }
-
- /**
- * Returns bitcoinj transaction sending amount to recipient using default fees.
- *
- * @param xprv58 BIP32 private key
- * @param recipient P2PKH address
- * @param amount unscaled amount
- * @return transaction, or null if insufficient funds
- */
- public Transaction buildSpend(String xprv58, String recipient, long amount) {
- return buildSpend(xprv58, recipient, amount, null);
- }
-
- /**
- * Returns unspent Bitcoin balance given 'm' BIP32 key.
- *
- * @param key58 BIP32/HD extended Bitcoin private/public key
- * @return unspent BTC balance, or null if unable to determine balance
- */
- public Long getWalletBalance(String key58) {
- Context.propagate(bitcoinjContext);
-
- Wallet wallet = walletFromDeterministicKey58(key58);
- wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet));
-
- Coin balance = wallet.getBalance();
- if (balance == null)
- return null;
-
- return balance.value;
- }
-
- public List getWalletTransactions(String key58) throws ForeignBlockchainException {
- Context.propagate(bitcoinjContext);
-
- Wallet wallet = walletFromDeterministicKey58(key58);
- DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
-
- keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
- keyChain.maybeLookAhead();
-
- List keys = new ArrayList<>(keyChain.getLeafKeys());
-
- Set walletTransactions = new HashSet<>();
- Set keySet = new HashSet<>();
-
- int ki = 0;
- do {
- boolean areAllKeysUnused = true;
-
- for (; ki < keys.size(); ++ki) {
- DeterministicKey dKey = keys.get(ki);
-
- // Check for transactions
- Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
- keySet.add(address.toString());
- byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
-
- // Ask for transaction history - if it's empty then key has never been used
- List historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
-
- if (!historicTransactionHashes.isEmpty()) {
- areAllKeysUnused = false;
-
- for (TransactionHash transactionHash : historicTransactionHashes)
- walletTransactions.add(this.getTransaction(transactionHash.txHash));
- }
- }
-
- if (areAllKeysUnused)
- // No transactions for this batch of keys so assume we're done searching.
- break;
-
- // Generate some more keys
- keys.addAll(generateMoreKeys(keyChain));
-
- // Process new keys
- } while (true);
-
- Comparator newestTimestampFirstComparator = Comparator.comparingInt(SimpleTransaction::getTimestamp).reversed();
-
- return walletTransactions.stream().map(t -> convertToSimpleTransaction(t, keySet)).sorted(newestTimestampFirstComparator).collect(Collectors.toList());
- }
-
- protected SimpleTransaction convertToSimpleTransaction(BitcoinyTransaction t, Set keySet) {
- long amount = 0;
- long total = 0L;
- for (BitcoinyTransaction.Input input : t.inputs) {
- try {
- BitcoinyTransaction t2 = getTransaction(input.outputTxHash);
- List senders = t2.outputs.get(input.outputVout).addresses;
- for (String sender : senders) {
- if (keySet.contains(sender)) {
- total += t2.outputs.get(input.outputVout).value;
- }
- }
- } catch (ForeignBlockchainException e) {
- LOGGER.trace("Failed to retrieve transaction information {}", input.outputTxHash);
- }
- }
- if (t.outputs != null && !t.outputs.isEmpty()) {
- for (BitcoinyTransaction.Output output : t.outputs) {
- for (String address : output.addresses) {
- if (keySet.contains(address)) {
- if (total > 0L) {
- amount -= (total - output.value);
- } else {
- amount += output.value;
- }
- }
- }
- }
- }
- return new SimpleTransaction(t.txHash, t.timestamp, amount);
- }
-
- /**
- * Returns first unused receive address given 'm' BIP32 key.
- *
- * @param key58 BIP32/HD extended Bitcoin private/public key
- * @return P2PKH address
- * @throws ForeignBlockchainException if something went wrong
- */
- public String getUnusedReceiveAddress(String key58) throws ForeignBlockchainException {
- Context.propagate(bitcoinjContext);
-
- Wallet wallet = walletFromDeterministicKey58(key58);
- DeterministicKeyChain keyChain = wallet.getActiveKeyChain();
-
- keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
- keyChain.maybeLookAhead();
-
- final int keyChainPathSize = keyChain.getAccountPath().size();
- List keys = new ArrayList<>(keyChain.getLeafKeys());
-
- int ki = 0;
- do {
- for (; ki < keys.size(); ++ki) {
- DeterministicKey dKey = keys.get(ki);
- List dKeyPath = dKey.getPath();
-
- // If keyChain is based on 'm', then make sure dKey is m/0/ki - i.e. a 'receive' address, not 'change' (m/1/ki)
- if (dKeyPath.size() != keyChainPathSize + 2 || dKeyPath.get(dKeyPath.size() - 2) != ChildNumber.ZERO)
- continue;
-
- // Check unspent
- Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH);
- byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
-
- List unspentOutputs = this.blockchain.getUnspentOutputs(script, false);
-
- /*
- * If there are no unspent outputs then either:
- * a) all the outputs have been spent
- * b) address has never been used
- *
- * For case (a) we want to remember not to check this address (key) again.
- */
-
- if (unspentOutputs.isEmpty()) {
- // If this is a known key that has been spent before, then we can skip asking for transaction history
- if (this.spentKeys.contains(dKey)) {
- wallet.getActiveKeyChain().markKeyAsUsed(dKey);
- continue;
- }
-
- // Ask for transaction history - if it's empty then key has never been used
- List historicTransactionHashes = this.blockchain.getAddressTransactions(script, false);
-
- if (!historicTransactionHashes.isEmpty()) {
- // Fully spent key - case (a)
- this.spentKeys.add(dKey);
- wallet.getActiveKeyChain().markKeyAsUsed(dKey);
- continue;
- }
-
- // Key never been used - case (b)
- return address.toString();
- }
-
- // Key has unspent outputs, hence used, so no good to us
- this.spentKeys.remove(dKey);
- }
-
- // Generate some more keys
- keys.addAll(generateMoreKeys(keyChain));
-
- // Process new keys
- } while (true);
- }
-
- // UTXOProvider support
-
- static class WalletAwareUTXOProvider implements UTXOProvider {
- private final Bitcoiny bitcoiny;
- private final Wallet wallet;
-
- private final DeterministicKeyChain keyChain;
-
- public WalletAwareUTXOProvider(Bitcoiny bitcoiny, Wallet wallet) {
- this.bitcoiny = bitcoiny;
- this.wallet = wallet;
- this.keyChain = this.wallet.getActiveKeyChain();
-
- // Set up wallet's key chain
- this.keyChain.setLookaheadSize(Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
- this.keyChain.maybeLookAhead();
- }
-
- @Override
- public List getOpenTransactionOutputs(List keys) throws UTXOProviderException {
- List allUnspentOutputs = new ArrayList<>();
- final boolean coinbase = false;
-
- int ki = 0;
- do {
- boolean areAllKeysUnspent = true;
-
- for (; ki < keys.size(); ++ki) {
- ECKey key = keys.get(ki);
-
- Address address = Address.fromKey(this.bitcoiny.params, key, ScriptType.P2PKH);
- byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
-
- List unspentOutputs;
- try {
- unspentOutputs = this.bitcoiny.blockchain.getUnspentOutputs(script, false);
- } catch (ForeignBlockchainException e) {
- throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
- }
-
- /*
- * If there are no unspent outputs then either:
- * a) all the outputs have been spent
- * b) address has never been used
- *
- * For case (a) we want to remember not to check this address (key) again.
- */
-
- if (unspentOutputs.isEmpty()) {
- // If this is a known key that has been spent before, then we can skip asking for transaction history
- if (this.bitcoiny.spentKeys.contains(key)) {
- this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
- areAllKeysUnspent = false;
- continue;
- }
-
- // Ask for transaction history - if it's empty then key has never been used
- List historicTransactionHashes;
- try {
- historicTransactionHashes = this.bitcoiny.blockchain.getAddressTransactions(script, false);
- } catch (ForeignBlockchainException e) {
- throw new UTXOProviderException(String.format("Unable to fetch transaction history for %s", address));
- }
-
- if (!historicTransactionHashes.isEmpty()) {
- // Fully spent key - case (a)
- this.bitcoiny.spentKeys.add(key);
- this.wallet.getActiveKeyChain().markKeyAsUsed((DeterministicKey) key);
- areAllKeysUnspent = false;
- } else {
- // Key never been used - case (b)
- }
-
- continue;
- }
-
- // If we reach here, then there's definitely at least one unspent key
- this.bitcoiny.spentKeys.remove(key);
-
- for (UnspentOutput unspentOutput : unspentOutputs) {
- List transactionOutputs;
- try {
- transactionOutputs = this.bitcoiny.getOutputs(unspentOutput.hash);
- } catch (ForeignBlockchainException e) {
- throw new UTXOProviderException(String.format("Unable to fetch outputs for TX %s",
- HashCode.fromBytes(unspentOutput.hash)));
- }
-
- TransactionOutput transactionOutput = transactionOutputs.get(unspentOutput.index);
-
- UTXO utxo = new UTXO(Sha256Hash.wrap(unspentOutput.hash), unspentOutput.index,
- Coin.valueOf(unspentOutput.value), unspentOutput.height, coinbase,
- transactionOutput.getScriptPubKey());
-
- allUnspentOutputs.add(utxo);
- }
- }
-
- if (areAllKeysUnspent)
- // No transactions for this batch of keys so assume we're done searching.
- return allUnspentOutputs;
-
- // Generate some more keys
- keys.addAll(Bitcoiny.generateMoreKeys(this.keyChain));
-
- // Process new keys
- } while (true);
- }
-
- @Override
- public int getChainHeadHeight() throws UTXOProviderException {
- try {
- return this.bitcoiny.blockchain.getCurrentHeight();
- } catch (ForeignBlockchainException e) {
- throw new UTXOProviderException("Unable to determine Bitcoiny chain height");
- }
- }
-
- @Override
- public NetworkParameters getParams() {
- return this.bitcoiny.params;
- }
- }
-
- // Utility methods for others
-
- public static List simplifyWalletTransactions(List transactions) {
- // Sort by oldest timestamp first
- transactions.sort(Comparator.comparingInt(t -> t.timestamp));
-
- // Manual 2nd-level sort same-timestamp transactions so that a transaction's input comes first
- int fromIndex = 0;
- do {
- int timestamp = transactions.get(fromIndex).timestamp;
-
- int toIndex;
- for (toIndex = fromIndex + 1; toIndex < transactions.size(); ++toIndex)
- if (transactions.get(toIndex).timestamp != timestamp)
- break;
-
- // Process same-timestamp sub-list
- List subList = transactions.subList(fromIndex, toIndex);
-
- // Only if necessary
- if (subList.size() > 1) {
- // Quick index lookup
- Map indexByTxHash = subList.stream().collect(Collectors.toMap(t -> t.txHash, t -> t.timestamp));
-
- int restartIndex = 0;
- boolean isSorted;
- do {
- isSorted = true;
-
- for (int ourIndex = restartIndex; ourIndex < subList.size(); ++ourIndex) {
- BitcoinyTransaction ourTx = subList.get(ourIndex);
-
- for (BitcoinyTransaction.Input input : ourTx.inputs) {
- Integer inputIndex = indexByTxHash.get(input.outputTxHash);
-
- if (inputIndex != null && inputIndex > ourIndex) {
- // Input tx is currently after current tx, so swap
- BitcoinyTransaction tmpTx = subList.get(inputIndex);
- subList.set(inputIndex, ourTx);
- subList.set(ourIndex, tmpTx);
-
- // Update index lookup too
- indexByTxHash.put(ourTx.txHash, inputIndex);
- indexByTxHash.put(tmpTx.txHash, ourIndex);
-
- if (isSorted)
- restartIndex = Math.max(restartIndex, ourIndex);
-
- isSorted = false;
- break;
- }
- }
- }
- } while (!isSorted);
- }
-
- fromIndex = toIndex;
- } while (fromIndex < transactions.size());
-
- // Simplify
- List simpleTransactions = new ArrayList<>();
-
- // Quick lookup of txs in our wallet
- Set walletTxHashes = transactions.stream().map(t -> t.txHash).collect(Collectors.toSet());
-
- for (BitcoinyTransaction transaction : transactions) {
- SimpleForeignTransaction.Builder builder = new SimpleForeignTransaction.Builder();
- builder.txHash(transaction.txHash);
- builder.timestamp(transaction.timestamp);
-
- builder.isSentNotReceived(false);
-
- for (BitcoinyTransaction.Input input : transaction.inputs) {
- // TODO: add input via builder
-
- if (walletTxHashes.contains(input.outputTxHash))
- builder.isSentNotReceived(true);
- }
-
- for (BitcoinyTransaction.Output output : transaction.outputs)
- builder.output(output.addresses, output.value);
-
- simpleTransactions.add(builder.build());
- }
-
- return simpleTransactions;
- }
-
- // Utility methods for us
-
- protected static List generateMoreKeys(DeterministicKeyChain keyChain) {
- int existingLeafKeyCount = keyChain.getLeafKeys().size();
-
- // Increase lookahead size...
- keyChain.setLookaheadSize(keyChain.getLookaheadSize() + Bitcoiny.WALLET_KEY_LOOKAHEAD_INCREMENT);
- // ...and lookahead threshold (minimum number of keys to generate)...
- keyChain.setLookaheadThreshold(0);
- // ...so that this call will generate more keys
- keyChain.maybeLookAhead();
-
- // This returns *all* keys
- List allLeafKeys = keyChain.getLeafKeys();
-
- // Only return newly generated keys
- return allLeafKeys.subList(existingLeafKeyCount, allLeafKeys.size());
- }
-
- protected byte[] addressToScriptPubKey(String base58Address) {
- Context.propagate(this.bitcoinjContext);
- Address address = Address.fromString(this.params, base58Address);
- return ScriptBuilder.createOutputScript(address).getProgram();
- }
-
- protected Wallet walletFromDeterministicKey58(String key58) {
- DeterministicKey dKey = DeterministicKey.deserializeB58(null, key58, this.params);
-
- if (dKey.hasPrivKey())
- return Wallet.fromSpendingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
- else
- return Wallet.fromWatchingKeyB58(this.params, key58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS);
- }
-
-}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
deleted file mode 100644
index 7691efb1..00000000
--- a/src/main/java/org/qortal/crosschain/BitcoinyBlockchainProvider.java
+++ /dev/null
@@ -1,40 +0,0 @@
-package org.qortal.crosschain;
-
-import java.util.List;
-
-public abstract class BitcoinyBlockchainProvider {
-
- public static final boolean INCLUDE_UNCONFIRMED = true;
- public static final boolean EXCLUDE_UNCONFIRMED = false;
-
- /** Returns ID unique to bitcoiny network (e.g. "Litecoin-TEST3") */
- public abstract String getNetId();
-
- /** Returns current blockchain height. */
- public abstract int getCurrentHeight() throws ForeignBlockchainException;
-
- /** Returns a list of raw block headers, starting at startHeight (inclusive), up to count max. */
- public abstract List getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException;
-
- /** Returns balance of address represented by scriptPubKey. */
- public abstract long getConfirmedBalance(byte[] scriptPubKey) throws ForeignBlockchainException;
-
- /** Returns raw, serialized, transaction bytes given txHash. */
- public abstract byte[] getRawTransaction(String txHash) throws ForeignBlockchainException;
-
- /** Returns raw, serialized, transaction bytes given txHash. */
- public abstract byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException;
-
- /** Returns unpacked transaction given txHash. */
- public abstract BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException;
-
- /** Returns list of transaction hashes (and heights) for address represented by scriptPubKey, optionally including unconfirmed transactions. */
- public abstract List getAddressTransactions(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
-
- /** Returns list of unspent transaction outputs for address represented by scriptPubKey, optionally including unconfirmed transactions. */
- public abstract List getUnspentOutputs(byte[] scriptPubKey, boolean includeUnconfirmed) throws ForeignBlockchainException;
-
- /** Broadcasts raw, serialized, transaction bytes to network, returning success/failure. */
- public abstract void broadcastTransaction(byte[] rawTransaction) throws ForeignBlockchainException;
-
-}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java b/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java
deleted file mode 100644
index 8ebfffa2..00000000
--- a/src/main/java/org/qortal/crosschain/BitcoinyHTLC.java
+++ /dev/null
@@ -1,438 +0,0 @@
-package org.qortal.crosschain;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.function.Function;
-
-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.bitcoinj.core.Transaction;
-import org.bitcoinj.core.Transaction.SigHash;
-import org.bitcoinj.core.TransactionInput;
-import org.bitcoinj.core.TransactionOutput;
-import org.bitcoinj.crypto.TransactionSignature;
-import org.bitcoinj.script.Script;
-import org.bitcoinj.script.ScriptBuilder;
-import org.bitcoinj.script.ScriptChunk;
-import org.bitcoinj.script.ScriptOpCodes;
-import org.qortal.crypto.Crypto;
-import org.qortal.utils.Base58;
-import org.qortal.utils.BitTwiddling;
-
-import com.google.common.hash.HashCode;
-import com.google.common.primitives.Bytes;
-
-public class BitcoinyHTLC {
-
- public enum Status {
- UNFUNDED, FUNDING_IN_PROGRESS, FUNDED, REDEEM_IN_PROGRESS, REDEEMED, REFUND_IN_PROGRESS, REFUNDED
- }
-
- public static final int SECRET_LENGTH = 32;
- public static final int MIN_LOCKTIME = 1500000000;
-
- public static final long NO_LOCKTIME_NO_RBF_SEQUENCE = 0xFFFFFFFFL;
- public static final long LOCKTIME_NO_RBF_SEQUENCE = NO_LOCKTIME_NO_RBF_SEQUENCE - 1;
-
- // Assuming node's trade-bot has no more than 100 entries?
- private static final int MAX_CACHE_ENTRIES = 100;
-
- // Max time-to-live for cache entries (milliseconds)
- private static final long CACHE_TIMEOUT = 30_000L;
-
- @SuppressWarnings("serial")
- private static final Map SECRET_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
- // This method is called just after a new entry has been added
- @Override
- public boolean removeEldestEntry(Map.Entry eldest) {
- return size() > MAX_CACHE_ENTRIES;
- }
- };
- private static final byte[] NO_SECRET_CACHE_ENTRY = new byte[0];
-
- @SuppressWarnings("serial")
- private static final Map STATUS_CACHE = new LinkedHashMap<>(MAX_CACHE_ENTRIES + 1, 0.75F, true) {
- // This method is called just after a new entry has been added
- @Override
- public boolean removeEldestEntry(Map.Entry eldest) {
- return size() > MAX_CACHE_ENTRIES;
- }
- };
-
- /*
- * OP_TUCK (to copy public key to before signature)
- * OP_CHECKSIGVERIFY (sig & pubkey must verify or script fails)
- * OP_HASH160 (convert public key to PKH)
- * OP_DUP (duplicate PKH)
- * OP_EQUAL (does PKH match refund PKH?)
- * OP_IF
- * OP_DROP (no need for duplicate PKH)
- *
- * OP_CHECKLOCKTIMEVERIFY (if this passes, leftover stack is so script passes)
- * OP_ELSE
- * OP_EQUALVERIFY (duplicate PKH must match redeem PKH or script fails)
- * OP_HASH160 (hash secret)
- * OP_EQUAL (do hashes of secrets match? if true, script passes else script fails)
- * OP_ENDIF
- */
-
- private static final byte[] redeemScript1 = HashCode.fromString("7dada97614").asBytes(); // OP_TUCK OP_CHECKSIGVERIFY OP_HASH160 OP_DUP push(0x14 bytes)
- private static final byte[] redeemScript2 = HashCode.fromString("87637504").asBytes(); // OP_EQUAL OP_IF OP_DROP push(0x4 bytes)
- private static final byte[] redeemScript3 = HashCode.fromString("b16714").asBytes(); // OP_CHECKLOCKTIMEVERIFY OP_ELSE push(0x14 bytes)
- private static final byte[] redeemScript4 = HashCode.fromString("88a914").asBytes(); // OP_EQUALVERIFY OP_HASH160 push(0x14 bytes)
- private static final byte[] redeemScript5 = HashCode.fromString("8768").asBytes(); // OP_EQUAL OP_ENDIF
-
- /**
- * Returns redeemScript used for cross-chain trading.
- *
- * See comments in {@link BitcoinyHTLC} for more details.
- *
- * @param refunderPubKeyHash 20-byte HASH160 of P2SH funder's public key, for refunding purposes
- * @param lockTime seconds-since-epoch threshold, after which P2SH funder can claim refund
- * @param redeemerPubKeyHash 20-byte HASH160 of P2SH redeemer's public key
- * @param hashOfSecret 20-byte HASH160 of secret, used by P2SH redeemer to claim funds
- */
- public static byte[] buildScript(byte[] refunderPubKeyHash, int lockTime, byte[] redeemerPubKeyHash, byte[] hashOfSecret) {
- return Bytes.concat(redeemScript1, refunderPubKeyHash, redeemScript2, BitTwiddling.toLEByteArray((int) (lockTime & 0xffffffffL)),
- redeemScript3, redeemerPubKeyHash, redeemScript4, hashOfSecret, redeemScript5);
- }
-
- /**
- * Builds a custom transaction to spend HTLC P2SH.
- *
- * @param params blockchain network parameters
- * @param amount output amount, should be total of input amounts, less miner fees
- * @param spendKey key for signing transaction, and also where funds are 'sent' (output)
- * @param fundingOutput output from transaction that funded P2SH address
- * @param redeemScriptBytes the redeemScript itself, in byte[] form
- * @param lockTime (optional) transaction nLockTime, used in refund scenario
- * @param scriptSigBuilder function for building scriptSig using transaction input signature
- * @param outputPublicKeyHash PKH used to create P2PKH output
- * @return Signed transaction for spending P2SH
- */
- public static Transaction buildP2shTransaction(NetworkParameters params, Coin amount, ECKey spendKey,
- List fundingOutputs, byte[] redeemScriptBytes,
- Long lockTime, Function scriptSigBuilder, byte[] outputPublicKeyHash) {
- Transaction transaction = new Transaction(params);
- transaction.setVersion(2);
-
- // Output is back to P2SH funder
- transaction.addOutput(amount, ScriptBuilder.createP2PKHOutputScript(outputPublicKeyHash));
-
- for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
- TransactionOutput fundingOutput = fundingOutputs.get(inputIndex);
-
- // Input (without scriptSig prior to signing)
- TransactionInput input = new TransactionInput(params, null, redeemScriptBytes, fundingOutput.getOutPointFor());
- if (lockTime != null)
- input.setSequenceNumber(LOCKTIME_NO_RBF_SEQUENCE); // Use max-value - 1, so lockTime can be used but not RBF
- else
- input.setSequenceNumber(NO_LOCKTIME_NO_RBF_SEQUENCE); // Use max-value, so no lockTime and no RBF
- transaction.addInput(input);
- }
-
- // Set locktime after inputs added but before input signatures are generated
- if (lockTime != null)
- transaction.setLockTime(lockTime);
-
- for (int inputIndex = 0; inputIndex < fundingOutputs.size(); ++inputIndex) {
- // Generate transaction signature for input
- final boolean anyoneCanPay = false;
- TransactionSignature txSig = transaction.calculateSignature(inputIndex, spendKey, redeemScriptBytes, SigHash.ALL, anyoneCanPay);
-
- // Calculate transaction signature
- byte[] txSigBytes = txSig.encodeToBitcoin();
-
- // Build scriptSig using lambda and tx signature
- Script scriptSig = scriptSigBuilder.apply(txSigBytes);
-
- // Set input scriptSig
- transaction.getInput(inputIndex).setScriptSig(scriptSig);
- }
-
- return transaction;
- }
-
- /**
- * Returns signed transaction claiming refund from HTLC P2SH.
- *
- * @param params blockchain network parameters
- * @param refundAmount refund amount, should be total of input amounts, less miner fees
- * @param refundKey key for signing transaction
- * @param fundingOutputs outputs from transaction that funded P2SH address
- * @param redeemScriptBytes the redeemScript itself, in byte[] form
- * @param lockTime transaction nLockTime - must be at least locktime used in redeemScript
- * @param receivingAccountInfo public-key-hash used for P2PKH output
- * @return Signed transaction for refunding P2SH
- */
- public static Transaction buildRefundTransaction(NetworkParameters params, Coin refundAmount, ECKey refundKey,
- List fundingOutputs, byte[] redeemScriptBytes, long lockTime, byte[] receivingAccountInfo) {
- Function refundSigScriptBuilder = (txSigBytes) -> {
- // Build scriptSig with...
- ScriptBuilder scriptBuilder = new ScriptBuilder();
-
- // transaction signature
- scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
-
- // redeem public key
- byte[] refundPubKey = refundKey.getPubKey();
- scriptBuilder.addChunk(new ScriptChunk(refundPubKey.length, refundPubKey));
-
- // redeem script
- scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
-
- return scriptBuilder.build();
- };
-
- // Send funds back to funding address
- return buildP2shTransaction(params, refundAmount, refundKey, fundingOutputs, redeemScriptBytes, lockTime, refundSigScriptBuilder, receivingAccountInfo);
- }
-
- /**
- * Returns signed transaction redeeming funds from P2SH address.
- *
- * @param params blockchain network parameters
- * @param redeemAmount redeem amount, should be total of input amounts, less miner fees
- * @param redeemKey key for signing transaction
- * @param fundingOutputs outputs from transaction that funded P2SH address
- * @param redeemScriptBytes the redeemScript itself, in byte[] form
- * @param secret actual 32-byte secret used when building redeemScript
- * @param receivingAccountInfo Bitcoin PKH used for output
- * @return Signed transaction for redeeming P2SH
- */
- public static Transaction buildRedeemTransaction(NetworkParameters params, Coin redeemAmount, ECKey redeemKey,
- List fundingOutputs, byte[] redeemScriptBytes, byte[] secret, byte[] receivingAccountInfo) {
- Function redeemSigScriptBuilder = (txSigBytes) -> {
- // Build scriptSig with...
- ScriptBuilder scriptBuilder = new ScriptBuilder();
-
- // secret
- scriptBuilder.addChunk(new ScriptChunk(secret.length, secret));
-
- // transaction signature
- scriptBuilder.addChunk(new ScriptChunk(txSigBytes.length, txSigBytes));
-
- // redeem public key
- byte[] redeemPubKey = redeemKey.getPubKey();
- scriptBuilder.addChunk(new ScriptChunk(redeemPubKey.length, redeemPubKey));
-
- // redeem script
- scriptBuilder.addChunk(new ScriptChunk(ScriptOpCodes.OP_PUSHDATA1, redeemScriptBytes));
-
- return scriptBuilder.build();
- };
-
- return buildP2shTransaction(params, redeemAmount, redeemKey, fundingOutputs, redeemScriptBytes, null, redeemSigScriptBuilder, receivingAccountInfo);
- }
-
- /**
- * Returns 'secret', if any, given HTLC's P2SH address.
- *
- * @throws ForeignBlockchainException
- */
- public static byte[] findHtlcSecret(Bitcoiny bitcoiny, String p2shAddress) throws ForeignBlockchainException {
- NetworkParameters params = bitcoiny.getNetworkParameters();
- String compoundKey = String.format("%s-%s-%d", params.getId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
-
- byte[] secret = SECRET_CACHE.getOrDefault(compoundKey, NO_SECRET_CACHE_ENTRY);
- if (secret != NO_SECRET_CACHE_ENTRY)
- return secret;
-
- List rawTransactions = bitcoiny.getAddressTransactions(p2shAddress);
-
- for (byte[] rawTransaction : rawTransactions) {
- Transaction transaction = new Transaction(params, rawTransaction);
-
- // Cycle through inputs, looking for one that spends our HTLC
- for (TransactionInput input : transaction.getInputs()) {
- Script scriptSig = input.getScriptSig();
- List scriptChunks = scriptSig.getChunks();
-
- // Expected number of script chunks for redeem. Refund might not have the same number.
- int expectedChunkCount = 1 /*secret*/ + 1 /*sig*/ + 1 /*pubkey*/ + 1 /*redeemScript*/;
- if (scriptChunks.size() != expectedChunkCount)
- continue;
-
- // We're expecting last chunk to contain the actual redeemScript
- ScriptChunk lastChunk = scriptChunks.get(scriptChunks.size() - 1);
- byte[] redeemScriptBytes = lastChunk.data;
-
- // If non-push scripts, redeemScript will be null
- if (redeemScriptBytes == null)
- continue;
-
- byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
- Address inputAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash);
-
- if (!inputAddress.toString().equals(p2shAddress))
- // Input isn't spending our HTLC
- continue;
-
- secret = scriptChunks.get(0).data;
- if (secret.length != BitcoinyHTLC.SECRET_LENGTH)
- continue;
-
- // Cache secret for a while
- SECRET_CACHE.put(compoundKey, secret);
-
- return secret;
- }
- }
-
- // Cache negative result
- SECRET_CACHE.put(compoundKey, null);
-
- return null;
- }
-
- /**
- * Returns HTLC status, given P2SH address and expected redeem/refund amount
- *
- * @throws ForeignBlockchainException if error occurs
- */
- public static Status determineHtlcStatus(BitcoinyBlockchainProvider blockchain, String p2shAddress, long minimumAmount) throws ForeignBlockchainException {
- String compoundKey = String.format("%s-%s-%d", blockchain.getNetId(), p2shAddress, System.currentTimeMillis() / CACHE_TIMEOUT);
-
- Status cachedStatus = STATUS_CACHE.getOrDefault(compoundKey, null);
- if (cachedStatus != null)
- return cachedStatus;
-
- byte[] ourScriptPubKey = addressToScriptPubKey(p2shAddress);
- List transactionHashes = blockchain.getAddressTransactions(ourScriptPubKey, BitcoinyBlockchainProvider.INCLUDE_UNCONFIRMED);
-
- // Sort by confirmed first, followed by ascending height
- transactionHashes.sort(TransactionHash.CONFIRMED_FIRST.thenComparing(TransactionHash::getHeight));
-
- // Transaction cache
- Map transactionsByHash = new HashMap<>();
- // HASH160(redeem script) for this p2shAddress
- byte[] ourRedeemScriptHash = addressToRedeemScriptHash(p2shAddress);
-
- // Check for spends first, caching full transaction info as we progress just in case we don't return in this loop
- for (TransactionHash transactionInfo : transactionHashes) {
- BitcoinyTransaction bitcoinyTransaction = blockchain.getTransaction(transactionInfo.txHash);
-
- // Cache for possible later reuse
- transactionsByHash.put(transactionInfo.txHash, bitcoinyTransaction);
-
- // Acceptable funding is one transaction output, so we're expecting only one input
- if (bitcoinyTransaction.inputs.size() != 1)
- // Wrong number of inputs
- continue;
-
- String scriptSig = bitcoinyTransaction.inputs.get(0).scriptSig;
-
- List scriptSigChunks = extractScriptSigChunks(HashCode.fromString(scriptSig).asBytes());
- if (scriptSigChunks.size() < 3 || scriptSigChunks.size() > 4)
- // Not valid chunks for our form of HTLC
- continue;
-
- // Last chunk is redeem script
- byte[] redeemScriptBytes = scriptSigChunks.get(scriptSigChunks.size() - 1);
- byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes);
- if (!Arrays.equals(redeemScriptHash, ourRedeemScriptHash))
- // Not spending our specific HTLC redeem script
- continue;
-
- if (scriptSigChunks.size() == 4)
- // If we have 4 chunks, then secret is present, hence redeem
- cachedStatus = transactionInfo.height == 0 ? Status.REDEEM_IN_PROGRESS : Status.REDEEMED;
- else
- cachedStatus = transactionInfo.height == 0 ? Status.REFUND_IN_PROGRESS : Status.REFUNDED;
-
- STATUS_CACHE.put(compoundKey, cachedStatus);
- return cachedStatus;
- }
-
- String ourScriptPubKeyHex = HashCode.fromBytes(ourScriptPubKey).toString();
-
- // Check for funding
- for (TransactionHash transactionInfo : transactionHashes) {
- BitcoinyTransaction bitcoinyTransaction = transactionsByHash.get(transactionInfo.txHash);
- if (bitcoinyTransaction == null)
- // Should be present in map!
- throw new ForeignBlockchainException("Cached Bitcoin transaction now missing?");
-
- // Check outputs for our specific P2SH
- for (BitcoinyTransaction.Output output : bitcoinyTransaction.outputs) {
- // Check amount
- if (output.value < minimumAmount)
- // Output amount too small (not taking fees into account)
- continue;
-
- String scriptPubKeyHex = output.scriptPubKey;
- if (!scriptPubKeyHex.equals(ourScriptPubKeyHex))
- // Not funding our specific P2SH
- continue;
-
- cachedStatus = transactionInfo.height == 0 ? Status.FUNDING_IN_PROGRESS : Status.FUNDED;
- STATUS_CACHE.put(compoundKey, cachedStatus);
- return cachedStatus;
- }
- }
-
- cachedStatus = Status.UNFUNDED;
- STATUS_CACHE.put(compoundKey, cachedStatus);
- return cachedStatus;
- }
-
- private static List extractScriptSigChunks(byte[] scriptSigBytes) {
- List chunks = new ArrayList<>();
-
- int offset = 0;
- int previousOffset = 0;
- while (offset < scriptSigBytes.length) {
- byte pushOp = scriptSigBytes[offset++];
-
- if (pushOp < 0 || pushOp > 0x4c)
- // Unacceptable OP
- return Collections.emptyList();
-
- // Special treatment for OP_PUSHDATA1
- if (pushOp == 0x4c) {
- if (offset >= scriptSigBytes.length)
- // Run out of scriptSig bytes?
- return Collections.emptyList();
-
- pushOp = scriptSigBytes[offset++];
- }
-
- previousOffset = offset;
- offset += Byte.toUnsignedInt(pushOp);
-
- byte[] chunk = Arrays.copyOfRange(scriptSigBytes, previousOffset, offset);
- chunks.add(chunk);
- }
-
- return chunks;
- }
-
- private static byte[] addressToScriptPubKey(String p2shAddress) {
- // We want the HASH160 part of the P2SH address
- byte[] p2shAddressBytes = Base58.decode(p2shAddress);
-
- byte[] scriptPubKey = new byte[1 + 1 + 20 + 1];
- scriptPubKey[0x00] = (byte) 0xa9; /* OP_HASH160 */
- scriptPubKey[0x01] = (byte) 0x14; /* PUSH 0x14 bytes */
- System.arraycopy(p2shAddressBytes, 1, scriptPubKey, 2, 0x14);
- scriptPubKey[0x16] = (byte) 0x87; /* OP_EQUAL */
-
- return scriptPubKey;
- }
-
- private static byte[] addressToRedeemScriptHash(String p2shAddress) {
- // We want the HASH160 part of the P2SH address
- byte[] p2shAddressBytes = Base58.decode(p2shAddress);
-
- return Arrays.copyOfRange(p2shAddressBytes, 1, 1 + 20);
- }
-
-}
diff --git a/src/main/java/org/qortal/crosschain/BitcoinyTransaction.java b/src/main/java/org/qortal/crosschain/BitcoinyTransaction.java
deleted file mode 100644
index caf0b36d..00000000
--- a/src/main/java/org/qortal/crosschain/BitcoinyTransaction.java
+++ /dev/null
@@ -1,146 +0,0 @@
-package org.qortal.crosschain;
-
-import java.util.List;
-import java.util.stream.Collectors;
-
-import javax.xml.bind.annotation.XmlAccessType;
-import javax.xml.bind.annotation.XmlAccessorType;
-import javax.xml.bind.annotation.XmlTransient;
-
-@XmlAccessorType(XmlAccessType.FIELD)
-public class BitcoinyTransaction {
-
- public final String txHash;
-
- @XmlTransient
- public final int size;
-
- @XmlTransient
- public final int locktime;
-
- // Not present if transaction is unconfirmed
- public final Integer timestamp;
-
- public static class Input {
- @XmlTransient
- public final String scriptSig;
-
- @XmlTransient
- public final int sequence;
-
- public final String outputTxHash;
-
- public final int outputVout;
-
- // For JAXB
- protected Input() {
- this.scriptSig = null;
- this.sequence = 0;
- this.outputTxHash = null;
- this.outputVout = 0;
- }
-
- public Input(String scriptSig, int sequence, String outputTxHash, int outputVout) {
- this.scriptSig = scriptSig;
- this.sequence = sequence;
- this.outputTxHash = outputTxHash;
- this.outputVout = outputVout;
- }
-
- public String toString() {
- return String.format("{output %s:%d, sequence %d, scriptSig %s}",
- this.outputTxHash, this.outputVout, this.sequence, this.scriptSig);
- }
- }
- @XmlTransient
- public final List inputs;
-
- public static class Output {
- @XmlTransient
- public final String scriptPubKey;
-
- public final long value;
-
- public final List addresses;
-
- // For JAXB
- protected Output() {
- this.scriptPubKey = null;
- this.value = 0;
- this.addresses = null;
- }
-
- public Output(String scriptPubKey, long value) {
- this.scriptPubKey = scriptPubKey;
- this.value = value;
- this.addresses = null;
- }
-
- public Output(String scriptPubKey, long value, List addresses) {
- this.scriptPubKey = scriptPubKey;
- this.value = value;
- this.addresses = addresses;
- }
-
- public String toString() {
- return String.format("{value %d, scriptPubKey %s}", this.value, this.scriptPubKey);
- }
- }
- public final List