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. - *

- * Will automatically update trade-bot state to ALICE_REFUNDING_B or ALICE_DONE as necessary. - * - * @throws DataException - * @throws ForeignBlockchainException - */ - private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData, - ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { - // This is OK - if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING) - return false; - - boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress); - - if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING && isAtLockedToUs) - return false; - - if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) { - // We've redeemed already? - TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, - () -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress())); - } else { - // Any other state is not good, so start defensive refund - TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_B, - () -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress())); - } - - return true; - } - - private long calcP2shAFeeTimestamp(int lockTimeA, int tradeTimeout) { - return (lockTimeA - tradeTimeout * 60) * 1000L; - } - - private long calcP2shBFeeTimestamp(int lockTimeA, int lockTimeB) { - // lockTimeB is halfway between offerMessageTimestamp and lockTimeA - return (lockTimeA - (lockTimeA - lockTimeB) * 2) * 1000L; - } - -} diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java deleted file mode 100644 index 0bd2972b..00000000 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java +++ /dev/null @@ -1,894 +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.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.LitecoinACCTv1; -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 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. - *

- * Will automatically update trade-bot state to ALICE_REFUNDING_A or ALICE_DONE as necessary. - * - * @throws DataException - * @throws ForeignBlockchainException - */ - private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData, - ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException { - // This is OK - if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING) - return false; - - boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress); - - if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING) - if (isAtLockedToUs) { - // AT is trading with us - OK - return false; - } else { - TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, - () -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress)); - - return true; - } - - if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) { - // We've redeemed already? - TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE, - () -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress())); - } else { - // Any other state is not good, so start defensive refund - TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A, - () -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress())); - } - - return true; - } - - private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) { - return (lockTimeA - tradeTimeout * 60) * 1000L; - } - -} diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java deleted file mode 100644 index fa3b599e..00000000 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ /dev/null @@ -1,373 +0,0 @@ -package org.qortal.controller.tradebot; - -import java.awt.TrayIcon.MessageType; -import java.security.SecureRandom; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Random; -import java.util.concurrent.locks.ReentrantLock; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.apache.logging.log4j.util.Supplier; -import org.bitcoinj.core.ECKey; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.api.model.crosschain.TradeBotCreateRequest; -import org.qortal.controller.Controller; -import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult; -import org.qortal.crosschain.ACCT; -import org.qortal.crosschain.BitcoinACCTv1; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.LitecoinACCTv1; -import org.qortal.crosschain.SupportedBlockchain; -import org.qortal.data.at.ATData; -import org.qortal.data.crosschain.CrossChainTradeData; -import org.qortal.data.crosschain.TradeBotData; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.PresenceTransactionData; -import org.qortal.event.Event; -import org.qortal.event.EventBus; -import org.qortal.event.Listener; -import org.qortal.group.Group; -import org.qortal.gui.SysTray; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryManager; -import org.qortal.settings.Settings; -import org.qortal.transaction.PresenceTransaction; -import org.qortal.transaction.PresenceTransaction.PresenceType; -import org.qortal.transaction.Transaction.ValidationResult; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.NTP; - -import com.google.common.primitives.Longs; - -/** - * Performing cross-chain trading steps on behalf of user. - *

- * 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 outputs; - - public final long totalAmount; - - // For JAXB - protected BitcoinyTransaction() { - this.txHash = null; - this.size = 0; - this.locktime = 0; - this.timestamp = 0; - this.inputs = null; - this.outputs = null; - this.totalAmount = 0; - } - - public BitcoinyTransaction(String txHash, int size, int locktime, Integer timestamp, - List inputs, List outputs) { - this.txHash = txHash; - this.size = size; - this.locktime = locktime; - this.timestamp = timestamp; - this.inputs = inputs; - this.outputs = outputs; - - this.totalAmount = outputs.stream().map(output -> output.value).reduce(0L, Long::sum); - } - - public String toString() { - return String.format("txHash %s, size %d, locktime %d, timestamp %d\n" - + "\tinputs: [%s]\n" - + "\toutputs: [%s]\n", - this.txHash, - this.size, - this.locktime, - this.timestamp, - this.inputs.stream().map(Input::toString).collect(Collectors.joining(",\n\t\t")), - this.outputs.stream().map(Output::toString).collect(Collectors.joining(",\n\t\t"))); - } - - @Override - public boolean equals(Object other) { - if (other == this) - return true; - - if (!(other instanceof BitcoinyTransaction)) - return false; - - BitcoinyTransaction otherTransaction = (BitcoinyTransaction) other; - - return this.txHash.equals(otherTransaction.txHash); - } - - @Override - public int hashCode() { - return this.txHash.hashCode(); - } - -} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java deleted file mode 100644 index b34aa199..00000000 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ /dev/null @@ -1,688 +0,0 @@ -package org.qortal.crosschain; - -import java.io.IOException; -import java.math.BigDecimal; -import java.net.InetSocketAddress; -import java.net.Socket; -import java.net.SocketAddress; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.EnumMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.NoSuchElementException; -import java.util.Random; -import java.util.Scanner; -import java.util.Set; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import javax.net.ssl.SSLSocketFactory; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.json.simple.JSONArray; -import org.json.simple.JSONObject; -import org.json.simple.JSONValue; -import org.qortal.crypto.Crypto; -import org.qortal.crypto.TrustlessSSLSocketFactory; - -import com.google.common.hash.HashCode; -import com.google.common.primitives.Bytes; - -/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */ -public class ElectrumX extends BitcoinyBlockchainProvider { - - private static final Logger LOGGER = LogManager.getLogger(ElectrumX.class); - private static final Random RANDOM = new Random(); - - private static final double MIN_PROTOCOL_VERSION = 1.2; - private static final int BLOCK_HEADER_LENGTH = 80; - - // "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})" - private static final Pattern DAEMON_ERROR_REGEX = Pattern.compile("DaemonError\\(\\{.*'code': ?(-?[0-9]+).*\\}\\)\\z"); // Capture 'code' inside curly-brace content - - /** Error message sent by some ElectrumX servers when they don't support returning verbose transactions. */ - private static final String VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE = "verbose transactions are currently unsupported"; - - public static class Server { - String hostname; - - public enum ConnectionType { TCP, SSL } - ConnectionType connectionType; - - int port; - - public Server(String hostname, ConnectionType connectionType, int port) { - this.hostname = hostname; - this.connectionType = connectionType; - this.port = port; - } - - @Override - public boolean equals(Object other) { - if (other == this) - return true; - - if (!(other instanceof Server)) - return false; - - Server otherServer = (Server) other; - - return this.connectionType == otherServer.connectionType - && this.port == otherServer.port - && this.hostname.equals(otherServer.hostname); - } - - @Override - public int hashCode() { - return this.hostname.hashCode() ^ this.port; - } - - @Override - public String toString() { - return String.format("%s:%s:%d", this.connectionType.name(), this.hostname, this.port); - } - } - private Set servers = new HashSet<>(); - private List remainingServers = new ArrayList<>(); - private Set uselessServers = Collections.synchronizedSet(new HashSet<>()); - - private final String netId; - private final String expectedGenesisHash; - private final Map defaultPorts = new EnumMap<>(Server.ConnectionType.class); - - private final Object serverLock = new Object(); - private Server currentServer; - private Socket socket; - private Scanner scanner; - private int nextId = 1; - - private static final int TX_CACHE_SIZE = 200; - @SuppressWarnings("serial") - private final Map transactionCache = Collections.synchronizedMap(new LinkedHashMap<>(TX_CACHE_SIZE + 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() > TX_CACHE_SIZE; - } - }); - - // Constructors - - public ElectrumX(String netId, String genesisHash, Collection initialServerList, Map defaultPorts) { - this.netId = netId; - this.expectedGenesisHash = genesisHash; - this.servers.addAll(initialServerList); - this.defaultPorts.putAll(defaultPorts); - } - - // Methods for use by other classes - - @Override - public String getNetId() { - return this.netId; - } - - /** - * Returns current blockchain height. - *

- * @throws ForeignBlockchainException if error occurs - */ - @Override - public int getCurrentHeight() throws ForeignBlockchainException { - Object blockObj = this.rpc("blockchain.headers.subscribe"); - if (!(blockObj instanceof JSONObject)) - throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.headers.subscribe RPC"); - - JSONObject blockJson = (JSONObject) blockObj; - - Object heightObj = blockJson.get("height"); - - if (!(heightObj instanceof Long)) - throw new ForeignBlockchainException.NetworkException("Missing/invalid 'height' in JSON from ElectrumX blockchain.headers.subscribe RPC"); - - return ((Long) heightObj).intValue(); - } - - /** - * Returns list of raw block headers, starting from startHeight inclusive. - *

- * @throws ForeignBlockchainException if error occurs - */ - @Override - public List getRawBlockHeaders(int startHeight, int count) throws ForeignBlockchainException { - Object blockObj = this.rpc("blockchain.block.headers", startHeight, count); - if (!(blockObj instanceof JSONObject)) - throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.block.headers RPC"); - - JSONObject blockJson = (JSONObject) blockObj; - - Object countObj = blockJson.get("count"); - Object hexObj = blockJson.get("hex"); - - if (!(countObj instanceof Long) || !(hexObj instanceof String)) - throw new ForeignBlockchainException.NetworkException("Missing/invalid 'count' or 'hex' entries in JSON from ElectrumX blockchain.block.headers RPC"); - - Long returnedCount = (Long) countObj; - String hex = (String) hexObj; - - byte[] raw = HashCode.fromString(hex).asBytes(); - if (raw.length != returnedCount * BLOCK_HEADER_LENGTH) - throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC"); - - List rawBlockHeaders = new ArrayList<>(returnedCount.intValue()); - for (int i = 0; i < returnedCount; ++i) - rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH)); - - return rawBlockHeaders; - } - - /** - * Returns confirmed balance, based on passed payment script. - *

- * @return confirmed balance, or zero if script unknown - * @throws ForeignBlockchainException if there was an error - */ - @Override - public long getConfirmedBalance(byte[] script) throws ForeignBlockchainException { - byte[] scriptHash = Crypto.digest(script); - Bytes.reverse(scriptHash); - - Object balanceObj = this.rpc("blockchain.scripthash.get_balance", HashCode.fromBytes(scriptHash).toString()); - if (!(balanceObj instanceof JSONObject)) - throw new ForeignBlockchainException.NetworkException("Unexpected output from ElectrumX blockchain.scripthash.get_balance RPC"); - - JSONObject balanceJson = (JSONObject) balanceObj; - - Object confirmedBalanceObj = balanceJson.get("confirmed"); - - if (!(confirmedBalanceObj instanceof Long)) - throw new ForeignBlockchainException.NetworkException("Missing confirmed balance from ElectrumX blockchain.scripthash.get_balance RPC"); - - return (Long) balanceJson.get("confirmed"); - } - - /** - * Returns list of unspent outputs pertaining to passed payment script. - *

- * @return list of unspent outputs, or empty list if script unknown - * @throws ForeignBlockchainException if there was an error. - */ - @Override - public List getUnspentOutputs(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException { - byte[] scriptHash = Crypto.digest(script); - Bytes.reverse(scriptHash); - - Object unspentJson = this.rpc("blockchain.scripthash.listunspent", HashCode.fromBytes(scriptHash).toString()); - if (!(unspentJson instanceof JSONArray)) - throw new ForeignBlockchainException("Expected array output from ElectrumX blockchain.scripthash.listunspent RPC"); - - List unspentOutputs = new ArrayList<>(); - for (Object rawUnspent : (JSONArray) unspentJson) { - JSONObject unspent = (JSONObject) rawUnspent; - - int height = ((Long) unspent.get("height")).intValue(); - // We only want unspent outputs from confirmed transactions (and definitely not mempool duplicates with height 0) - if (!includeUnconfirmed && height <= 0) - continue; - - byte[] txHash = HashCode.fromString((String) unspent.get("tx_hash")).asBytes(); - int outputIndex = ((Long) unspent.get("tx_pos")).intValue(); - long value = (Long) unspent.get("value"); - - unspentOutputs.add(new UnspentOutput(txHash, outputIndex, height, value)); - } - - return unspentOutputs; - } - - /** - * Returns raw transaction for passed transaction hash. - *

- * NOTE: Do not mutate returned byte[]! - * - * @throws ForeignBlockchainException.NotFoundException if transaction not found - * @throws ForeignBlockchainException if error occurs - */ - @Override - public byte[] getRawTransaction(String txHash) throws ForeignBlockchainException { - Object rawTransactionHex; - try { - rawTransactionHex = this.rpc("blockchain.transaction.get", txHash, false); - } catch (ForeignBlockchainException.NetworkException e) { - // DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'}) - if (Integer.valueOf(-5).equals(e.getDaemonErrorCode())) - throw new ForeignBlockchainException.NotFoundException(e.getMessage()); - - throw e; - } - - if (!(rawTransactionHex instanceof String)) - throw new ForeignBlockchainException.NetworkException("Expected hex string as raw transaction from ElectrumX blockchain.transaction.get RPC"); - - return HashCode.fromString((String) rawTransactionHex).asBytes(); - } - - /** - * Returns raw transaction for passed transaction hash. - *

- * NOTE: Do not mutate returned byte[]! - * - * @throws ForeignBlockchainException.NotFoundException if transaction not found - * @throws ForeignBlockchainException if error occurs - */ - @Override - public byte[] getRawTransaction(byte[] txHash) throws ForeignBlockchainException { - return getRawTransaction(HashCode.fromBytes(txHash).toString()); - } - - /** - * Returns transaction info for passed transaction hash. - *

- * @throws ForeignBlockchainException.NotFoundException if transaction not found - * @throws ForeignBlockchainException if error occurs - */ - @Override - public BitcoinyTransaction getTransaction(String txHash) throws ForeignBlockchainException { - // Check cache first - BitcoinyTransaction transaction = transactionCache.get(txHash); - if (transaction != null) - return transaction; - - Object transactionObj = null; - - do { - try { - transactionObj = this.rpc("blockchain.transaction.get", txHash, true); - } catch (ForeignBlockchainException.NetworkException e) { - // DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'}) - if (Integer.valueOf(-5).equals(e.getDaemonErrorCode())) - throw new ForeignBlockchainException.NotFoundException(e.getMessage()); - - // Some servers also return non-standard responses like this: - // {"error":"verbose transactions are currently unsupported","id":3,"jsonrpc":"2.0"} - // We should probably not use this server any more - if (e.getServer() != null && e.getMessage() != null && e.getMessage().contains(VERBOSE_TRANSACTIONS_UNSUPPORTED_MESSAGE)) { - Server uselessServer = (Server) e.getServer(); - LOGGER.trace(() -> String.format("Server %s doesn't support verbose transactions - barring use of that server", uselessServer)); - this.uselessServers.add(uselessServer); - this.closeServer(uselessServer); - continue; - } - - throw e; - } - } while (transactionObj == null); - - if (!(transactionObj instanceof JSONObject)) - throw new ForeignBlockchainException.NetworkException("Expected JSONObject as response from ElectrumX blockchain.transaction.get RPC"); - - JSONObject transactionJson = (JSONObject) transactionObj; - - Object inputsObj = transactionJson.get("vin"); - if (!(inputsObj instanceof JSONArray)) - throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vin' from ElectrumX blockchain.transaction.get RPC"); - - Object outputsObj = transactionJson.get("vout"); - if (!(outputsObj instanceof JSONArray)) - throw new ForeignBlockchainException.NetworkException("Expected JSONArray for 'vout' from ElectrumX blockchain.transaction.get RPC"); - - try { - int size = ((Long) transactionJson.get("size")).intValue(); - int locktime = ((Long) transactionJson.get("locktime")).intValue(); - - // Timestamp might not be present, e.g. for unconfirmed transaction - Object timeObj = transactionJson.get("time"); - Integer timestamp = timeObj != null - ? ((Long) timeObj).intValue() - : null; - - List inputs = new ArrayList<>(); - for (Object inputObj : (JSONArray) inputsObj) { - JSONObject inputJson = (JSONObject) inputObj; - - String scriptSig = (String) ((JSONObject) inputJson.get("scriptSig")).get("hex"); - int sequence = ((Long) inputJson.get("sequence")).intValue(); - String outputTxHash = (String) inputJson.get("txid"); - int outputVout = ((Long) inputJson.get("vout")).intValue(); - - inputs.add(new BitcoinyTransaction.Input(scriptSig, sequence, outputTxHash, outputVout)); - } - - List outputs = new ArrayList<>(); - for (Object outputObj : (JSONArray) outputsObj) { - JSONObject outputJson = (JSONObject) outputObj; - - String scriptPubKey = (String) ((JSONObject) outputJson.get("scriptPubKey")).get("hex"); - long value = BigDecimal.valueOf((Double) outputJson.get("value")).setScale(8).unscaledValue().longValue(); - - // address too, if present - List addresses = null; - Object addressesObj = ((JSONObject) outputJson.get("scriptPubKey")).get("addresses"); - if (addressesObj instanceof JSONArray) { - addresses = new ArrayList<>(); - for (Object addressObj : (JSONArray) addressesObj) - addresses.add((String) addressObj); - } - - outputs.add(new BitcoinyTransaction.Output(scriptPubKey, value, addresses)); - } - - transaction = new BitcoinyTransaction(txHash, size, locktime, timestamp, inputs, outputs); - - // Save into cache - transactionCache.put(txHash, transaction); - - return transaction; - } catch (NullPointerException | ClassCastException e) { - // Unexpected / invalid response from ElectrumX server - } - - throw new ForeignBlockchainException.NetworkException("Unexpected JSON format from ElectrumX blockchain.transaction.get RPC"); - } - - /** - * Returns list of transactions, relating to passed payment script. - *

- * @return list of related transactions, or empty list if script unknown - * @throws ForeignBlockchainException if error occurs - */ - @Override - public List getAddressTransactions(byte[] script, boolean includeUnconfirmed) throws ForeignBlockchainException { - byte[] scriptHash = Crypto.digest(script); - Bytes.reverse(scriptHash); - - Object transactionsJson = this.rpc("blockchain.scripthash.get_history", HashCode.fromBytes(scriptHash).toString()); - if (!(transactionsJson instanceof JSONArray)) - throw new ForeignBlockchainException.NetworkException("Expected array output from ElectrumX blockchain.scripthash.get_history RPC"); - - List transactionHashes = new ArrayList<>(); - - for (Object rawTransactionInfo : (JSONArray) transactionsJson) { - JSONObject transactionInfo = (JSONObject) rawTransactionInfo; - - Long height = (Long) transactionInfo.get("height"); - if (!includeUnconfirmed && (height == null || height == 0)) - // We only want confirmed transactions - continue; - - String txHash = (String) transactionInfo.get("tx_hash"); - - transactionHashes.add(new TransactionHash(height.intValue(), txHash)); - } - - return transactionHashes; - } - - /** - * Broadcasts raw transaction to network. - *

- * @throws ForeignBlockchainException if error occurs - */ - @Override - public void broadcastTransaction(byte[] transactionBytes) throws ForeignBlockchainException { - Object rawBroadcastResult = this.rpc("blockchain.transaction.broadcast", HashCode.fromBytes(transactionBytes).toString()); - - // We're expecting a simple string that is the transaction hash - if (!(rawBroadcastResult instanceof String)) - throw new ForeignBlockchainException.NetworkException("Unexpected response from ElectrumX blockchain.transaction.broadcast RPC"); - } - - // Class-private utility methods - - /** - * Query current server for its list of peer servers, and return those we can parse. - *

- * @throws ForeignBlockchainException - * @throws ClassCastException to be handled by caller - */ - private Set serverPeersSubscribe() throws ForeignBlockchainException { - Set newServers = new HashSet<>(); - - Object peers = this.connectedRpc("server.peers.subscribe"); - - for (Object rawPeer : (JSONArray) peers) { - JSONArray peer = (JSONArray) rawPeer; - if (peer.size() < 3) - // We're expecting at least 3 fields for each peer entry: IP, hostname, features - continue; - - String hostname = (String) peer.get(1); - JSONArray features = (JSONArray) peer.get(2); - - for (Object rawFeature : features) { - String feature = (String) rawFeature; - Server.ConnectionType connectionType = null; - Integer port = null; - - switch (feature.charAt(0)) { - case 's': - connectionType = Server.ConnectionType.SSL; - port = this.defaultPorts.get(connectionType); - break; - - case 't': - connectionType = Server.ConnectionType.TCP; - port = this.defaultPorts.get(connectionType); - break; - - default: - // e.g. could be 'v' for protocol version, or 'p' for pruning limit - break; - } - - if (connectionType == null || port == null) - // We couldn't extract any peer connection info? - continue; - - // Possible non-default port? - if (feature.length() > 1) - try { - port = Integer.parseInt(feature.substring(1)); - } catch (NumberFormatException e) { - // no good - continue; // for-loop above - } - - Server newServer = new Server(hostname, connectionType, port); - newServers.add(newServer); - } - } - - return newServers; - } - - /** - * Performs RPC call, with automatic reconnection to different server if needed. - *

- * @return "result" object from within JSON output - * @throws ForeignBlockchainException if server returns error or something goes wrong - */ - private Object rpc(String method, Object...params) throws ForeignBlockchainException { - synchronized (this.serverLock) { - if (this.remainingServers.isEmpty()) - this.remainingServers.addAll(this.servers); - - while (haveConnection()) { - Object response = connectedRpc(method, params); - if (response != null) - return response; - - // Didn't work, try another server... - this.closeServer(); - } - - // Failed to perform RPC - maybe lack of servers? - throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method)); - } - } - - /** Returns true if we have, or create, a connection to an ElectrumX server. */ - private boolean haveConnection() throws ForeignBlockchainException { - if (this.currentServer != null) - return true; - - while (!this.remainingServers.isEmpty()) { - Server server = this.remainingServers.remove(RANDOM.nextInt(this.remainingServers.size())); - LOGGER.trace(() -> String.format("Connecting to %s", server)); - - try { - SocketAddress endpoint = new InetSocketAddress(server.hostname, server.port); - int timeout = 5000; // ms - - this.socket = new Socket(); - this.socket.connect(endpoint, timeout); - this.socket.setTcpNoDelay(true); - - if (server.connectionType == Server.ConnectionType.SSL) { - SSLSocketFactory factory = TrustlessSSLSocketFactory.getSocketFactory(); - this.socket = factory.createSocket(this.socket, server.hostname, server.port, true); - } - - this.scanner = new Scanner(this.socket.getInputStream()); - this.scanner.useDelimiter("\n"); - - // Check connection is suitable by asking for server features, including genesis block hash - JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features"); - - if (featuresJson == null || Double.valueOf((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION) - continue; - - if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash)) - continue; - - // Ask for more servers - Set moreServers = serverPeersSubscribe(); - // Discard duplicate servers we already know - moreServers.removeAll(this.servers); - // Add to both lists - this.remainingServers.addAll(moreServers); - this.servers.addAll(moreServers); - - LOGGER.debug(() -> String.format("Connected to %s", server)); - this.currentServer = server; - return true; - } catch (IOException | ForeignBlockchainException | ClassCastException | NullPointerException e) { - // Didn't work, try another server... - closeServer(); - } - } - - return false; - } - - /** - * Perform RPC using currently connected server. - *

- * @param method - * @param params - * @return response Object, or null if server fails to respond - * @throws ForeignBlockchainException if server returns error - */ - @SuppressWarnings("unchecked") - private Object connectedRpc(String method, Object...params) throws ForeignBlockchainException { - JSONObject requestJson = new JSONObject(); - requestJson.put("id", this.nextId++); - requestJson.put("method", method); - requestJson.put("jsonrpc", "2.0"); - - JSONArray requestParams = new JSONArray(); - requestParams.addAll(Arrays.asList(params)); - requestJson.put("params", requestParams); - - String request = requestJson.toJSONString() + "\n"; - LOGGER.trace(() -> String.format("Request: %s", request)); - - final String response; - - try { - this.socket.getOutputStream().write(request.getBytes()); - response = scanner.next(); - } catch (IOException | NoSuchElementException e) { - // Unable to send, or receive -- try another server? - return null; - } - - LOGGER.trace(() -> String.format("Response: %s", response)); - - if (response.isEmpty()) - // Empty response - try another server? - return null; - - Object responseObj = JSONValue.parse(response); - if (!(responseObj instanceof JSONObject)) - // Unexpected response - try another server? - return null; - - JSONObject responseJson = (JSONObject) responseObj; - - Object errorObj = responseJson.get("error"); - if (errorObj != null) { - if (errorObj instanceof String) - throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error message from ElectrumX RPC %s: %s", method, (String) errorObj), this.currentServer); - - if (!(errorObj instanceof JSONObject)) - throw new ForeignBlockchainException.NetworkException(String.format("Unexpected error response from ElectrumX RPC %s", method), this.currentServer); - - JSONObject errorJson = (JSONObject) errorObj; - - Object messageObj = errorJson.get("message"); - - if (!(messageObj instanceof String)) - throw new ForeignBlockchainException.NetworkException(String.format("Missing/invalid message in error response from ElectrumX RPC %s", method), this.currentServer); - - String message = (String) messageObj; - - // Some error 'messages' are actually wrapped upstream bitcoind errors: - // "message": "daemon error: DaemonError({'code': -5, 'message': 'No such mempool or blockchain transaction. Use gettransaction for wallet transactions.'})" - // We want to detect these and extract the upstream error code for caller's use - Matcher messageMatcher = DAEMON_ERROR_REGEX.matcher(message); - if (messageMatcher.find()) - try { - int daemonErrorCode = Integer.parseInt(messageMatcher.group(1)); - throw new ForeignBlockchainException.NetworkException(daemonErrorCode, message, this.currentServer); - } catch (NumberFormatException e) { - // We couldn't parse the error code integer? Fall-through to generic exception... - } - - throw new ForeignBlockchainException.NetworkException(message, this.currentServer); - } - - return responseJson.get("result"); - } - - /** - * Closes connection to server if it is currently connected server. - * @param server - */ - private void closeServer(Server server) { - synchronized (this.serverLock) { - if (this.currentServer == null || !this.currentServer.equals(server)) - return; - - if (this.socket != null && !this.socket.isClosed()) - try { - this.socket.close(); - } catch (IOException e) { - // We did try... - } - - this.socket = null; - this.scanner = null; - this.currentServer = null; - } - } - - /** Closes connection to currently connected server (if any). */ - private void closeServer() { - synchronized (this.serverLock) { - this.closeServer(this.currentServer); - } - } - -} diff --git a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java b/src/main/java/org/qortal/crosschain/ForeignBlockchain.java deleted file mode 100644 index 0a71e9d9..00000000 --- a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java +++ /dev/null @@ -1,9 +0,0 @@ -package org.qortal.crosschain; - -public interface ForeignBlockchain { - - public boolean isValidAddress(String address); - - public boolean isValidWalletKey(String walletKey); - -} diff --git a/src/main/java/org/qortal/crosschain/ForeignBlockchainException.java b/src/main/java/org/qortal/crosschain/ForeignBlockchainException.java deleted file mode 100644 index 1e658621..00000000 --- a/src/main/java/org/qortal/crosschain/ForeignBlockchainException.java +++ /dev/null @@ -1,77 +0,0 @@ -package org.qortal.crosschain; - -@SuppressWarnings("serial") -public class ForeignBlockchainException extends Exception { - - public ForeignBlockchainException() { - super(); - } - - public ForeignBlockchainException(String message) { - super(message); - } - - public static class NetworkException extends ForeignBlockchainException { - private final Integer daemonErrorCode; - private final transient Object server; - - public NetworkException() { - super(); - this.daemonErrorCode = null; - this.server = null; - } - - public NetworkException(String message) { - super(message); - this.daemonErrorCode = null; - this.server = null; - } - - public NetworkException(int errorCode, String message) { - super(message); - this.daemonErrorCode = errorCode; - this.server = null; - } - - public NetworkException(String message, Object server) { - super(message); - this.daemonErrorCode = null; - this.server = server; - } - - public NetworkException(int errorCode, String message, Object server) { - super(message); - this.daemonErrorCode = errorCode; - this.server = server; - } - - public Integer getDaemonErrorCode() { - return this.daemonErrorCode; - } - - public Object getServer() { - return this.server; - } - } - - public static class NotFoundException extends ForeignBlockchainException { - public NotFoundException() { - super(); - } - - public NotFoundException(String message) { - super(message); - } - } - - public static class InsufficientFundsException extends ForeignBlockchainException { - public InsufficientFundsException() { - super(); - } - - public InsufficientFundsException(String message) { - super(message); - } - } - -} diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java deleted file mode 100644 index 5cbe4044..00000000 --- a/src/main/java/org/qortal/crosschain/Litecoin.java +++ /dev/null @@ -1,175 +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.Coin; -import org.bitcoinj.core.Context; -import org.bitcoinj.core.NetworkParameters; -import org.libdohj.params.LitecoinMainNetParams; -import org.libdohj.params.LitecoinRegTestParams; -import org.libdohj.params.LitecoinTestNet3Params; -import org.qortal.crosschain.ElectrumX.Server; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; -import org.qortal.settings.Settings; - -public class Litecoin extends Bitcoiny { - - public static final String CURRENCY_CODE = "LTC"; - - private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(10000); // 0.0001 LTC per 1000 bytes - - // Temporary values until a dynamic fee system is written. - private static final long MAINNET_FEE = 1000L; - 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 LitecoinNet { - MAIN { - @Override - public NetworkParameters getParams() { - return LitecoinMainNetParams.get(); - } - - @Override - public Collection getServers() { - return Arrays.asList( - // Servers chosen on NO BASIS WHATSOEVER from various sources! - new Server("electrum-ltc.someguy123.net", Server.ConnectionType.SSL, 50002), - new Server("backup.electrum-ltc.org", Server.ConnectionType.TCP, 50001), - new Server("backup.electrum-ltc.org", Server.ConnectionType.SSL, 443), - new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 50001), - new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 50002), - new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 50002), - new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001), - new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002), - new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002), - new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022)); - } - - @Override - public String getGenesisHash() { - return "12a765e31ffd4059bada1e25190f6e98c99d9714d334efa41a195a7e7e04bfe2"; - } - - @Override - public long getP2shFee(Long timestamp) { - // TODO: This will need to be replaced with something better in the near future! - return MAINNET_FEE; - } - }, - TEST3 { - @Override - public NetworkParameters getParams() { - return LitecoinTestNet3Params.get(); - } - - @Override - public Collection getServers() { - return Arrays.asList( - new Server("electrum-ltc.bysh.me", Server.ConnectionType.TCP, 51001), - new Server("electrum-ltc.bysh.me", Server.ConnectionType.SSL, 51002), - new Server("electrum.ltc.xurious.com", Server.ConnectionType.TCP, 51001), - new Server("electrum.ltc.xurious.com", Server.ConnectionType.SSL, 51002)); - } - - @Override - public String getGenesisHash() { - return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0"; - } - - @Override - public long getP2shFee(Long timestamp) { - return NON_MAINNET_FEE; - } - }, - REGTEST { - @Override - public NetworkParameters getParams() { - return LitecoinRegTestParams.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 Litecoin instance; - - private final LitecoinNet litecoinNet; - - // Constructors and instance - - private Litecoin(LitecoinNet litecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) { - super(blockchain, bitcoinjContext, currencyCode); - this.litecoinNet = litecoinNet; - - LOGGER.info(() -> String.format("Starting Litecoin support using %s", this.litecoinNet.name())); - } - - public static synchronized Litecoin getInstance() { - if (instance == null) { - LitecoinNet litecoinNet = Settings.getInstance().getLitecoinNet(); - - BitcoinyBlockchainProvider electrumX = new ElectrumX("Litecoin-" + litecoinNet.name(), litecoinNet.getGenesisHash(), litecoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS); - Context bitcoinjContext = new Context(litecoinNet.getParams()); - - instance = new Litecoin(litecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE); - } - - return instance; - } - - // Getters & setters - - public static synchronized void resetForTesting() { - instance = null; - } - - // Actual useful methods for use by other classes - - /** Default Litecoin fee is lower than Bitcoin: only 10sats/byte. */ - @Override - public Coin getFeePerKb() { - return DEFAULT_FEE_PER_KB; - } - - /** - * Returns estimated LTC 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.litecoinNet.getP2shFee(timestamp); - } - -} diff --git a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java deleted file mode 100644 index 454e80c2..00000000 --- a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java +++ /dev/null @@ -1,853 +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 Litecoin & Qortal 'trade' keys - *
      - *
    • 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 Litecoin & Qortal 'trade' keys
    • - *
    • Alice funds Litecoin P2SH-A
    • - *
    • Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing: - *
        - *
      • hash-of-secret-A
      • - *
      • her 'trade' Litecoin 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 Litecoin PKH
      • - *
      • hash-of-secret-A
      • - *
      - *
    • - *
    - *
  • - *
  • Alice checks Qortal AT to confirm it's locked to her - *
      - *
    • Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing: - *
        - *
      • secret-A
      • - *
      • 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 Litecoin trade key and secret-A
    • - *
    • P2SH-A LTC funds end up at Litecoin address determined by redeem transaction output(s)
    • - *
    - *
  • - *
- */ -public class LitecoinACCTv1 implements ACCT { - - public static final String NAME = LitecoinACCTv1.class.getSimpleName(); - public static final byte[] CODE_BYTES_HASH = HashCode.fromString("0fb15ad9ad1867dfbcafa51155481aa15d984ff9506f2b428eca4e2a2feac2b3").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 = 61; - /** 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[] partnerLitecoinPKH; - public byte[] hashOfSecretA; - public long lockTimeA; - } - public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerLitecoinPKH*/ + 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 Litecoin PKH (padded from 20 to 24)*/ - + 8 /*AT trade timeout (minutes)*/ - + 24 /*hash of secret-A (padded from 20 to 24)*/ - + 8 /*lockTimeA*/; - public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 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 LitecoinACCTv1 instance; - - private LitecoinACCTv1() { - } - - public static synchronized LitecoinACCTv1 getInstance() { - if (instance == null) - instance = new LitecoinACCTv1(); - - return instance; - } - - @Override - public byte[] getCodeBytesHash() { - return CODE_BYTES_HASH; - } - - @Override - public int getModeByteOffset() { - return MODE_BYTE_OFFSET; - } - - @Override - public ForeignBlockchain getBlockchain() { - return Litecoin.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 - * @param litecoinPublicKeyHash 20-byte HASH160 of creator's trade Litecoin public key - * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT - * @param litecoinAmount how much LTC the AT creator is expecting to trade - * @param tradeTimeout suggested timeout for entire trade - */ - public static byte[] buildQortalAT(String creatorTradeAddress, byte[] litecoinPublicKeyHash, long qortAmount, long litecoinAmount, int tradeTimeout) { - if (litecoinPublicKeyHash.length != 20) - throw new IllegalArgumentException("Litecoin public key hash should be 20 bytes"); - - // 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 addrLitecoinPublicKeyHash = addrCounter; - addrCounter += 4; - - final int addrQortAmount = addrCounter++; - final int addrLitecoinAmount = addrCounter++; - final int addrTradeTimeout = addrCounter++; - - final int addrMessageTxnType = addrCounter++; - final int addrExpectedTradeMessageLength = addrCounter++; - final int addrExpectedRedeemMessageLength = addrCounter++; - - final int addrCreatorAddressPointer = addrCounter++; - final int addrQortalPartnerAddressPointer = addrCounter++; - final int addrMessageSenderPointer = addrCounter++; - - final int addrTradeMessagePartnerLitecoinPKHOffset = addrCounter++; - final int addrPartnerLitecoinPKHPointer = addrCounter++; - final int addrTradeMessageHashOfSecretAOffset = addrCounter++; - final int addrHashOfSecretAPointer = 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 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 addrPartnerLitecoinPKH = addrCounter; - addrCounter += 4; - - final int addrPartnerReceivingAddress = addrCounter; - addrCounter += 4; - - final int addrMode = addrCounter++; - assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET); - - // 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)); - - // Litecoin public key hash - assert dataByteBuffer.position() == addrLitecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrLitecoinPublicKeyHash incorrect"; - dataByteBuffer.put(Bytes.ensureCapacity(litecoinPublicKeyHash, 32, 0)); - - // Redeem Qort amount - assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; - dataByteBuffer.putLong(qortAmount); - - // Expected Litecoin amount - assert dataByteBuffer.position() == addrLitecoinAmount * MachineState.VALUE_SIZE : "addrLitecoinAmount incorrect"; - dataByteBuffer.putLong(litecoinAmount); - - // 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 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 Litecoin PKH - assert dataByteBuffer.position() == addrTradeMessagePartnerLitecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerLitecoinPKHOffset incorrect"; - dataByteBuffer.putLong(32L); - - // Index into data segment of partner's Litecoin PKH, used by GET_B_IND - assert dataByteBuffer.position() == addrPartnerLitecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerLitecoinPKHPointer incorrect"; - dataByteBuffer.putLong(addrPartnerLitecoinPKH); - - // 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 Qortal receiving address - assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect"; - dataByteBuffer.putLong(32L); - - // 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 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 Litecoin public key hash (PKH) from message into B - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerLitecoinPKHOffset)); - // Store partner's Litecoin PKH (we only really use values from B1-B3) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerLitecoinPKHPointer)); - // Extract AT trade timeout (minutes) (from B4) - codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout)); - - // 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 lockTime-A (from B4) - codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA)); - - // 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, 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 LTC-QORT ACCT?", e); - } - } - - codeByteBuffer.flip(); - - byte[] codeBytes = new byte[codeByteBuffer.limit()]; - codeByteBuffer.get(codeBytes); - - assert Arrays.equals(Crypto.digest(codeBytes), LitecoinACCTv1.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.LITECOIN.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 Litecoin/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 - - // We don't use secret-B - tradeData.hashOfSecretB = null; - - // Redeem payout - tradeData.qortAmount = dataByteBuffer.getLong(); - - // Expected LTC 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 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 Litecoin PKH - dataByteBuffer.position(dataByteBuffer.position() + 8); - - // Skip pointer to partner's Litecoin 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 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(); - - // 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 Litecoin PKH - byte[] partnerLitecoinPKH = new byte[20]; - dataByteBuffer.get(partnerLitecoinPKH); - dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerLitecoinPKH.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 mode = AcctMode.valueOf((int) (modeValue & 0xffL)); - - /* End of variables */ - - if (mode != null && mode != AcctMode.OFFERING) { - tradeData.mode = mode; - tradeData.refundTimeout = refundTimeout; - tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight; - tradeData.qortalPartnerAddress = qortalRecipient; - tradeData.hashOfSecretA = hashOfSecretA; - tradeData.partnerForeignPKH = partnerLitecoinPKH; - tradeData.lockTimeA = lockTimeA; - - if (mode == 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.partnerLitecoinPKH = 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 refundTimeout) { - byte[] data = new byte[TRADE_MESSAGE_LENGTH]; - byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress); - byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); - byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout); - - System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length); - System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length); - System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.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, String qortalReceivingAddress) { - byte[] data = new byte[REDEEM_MESSAGE_LENGTH]; - byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress); - - System.arraycopy(secretA, 0, data, 0, secretA.length); - System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length); - - return data; - } - - /** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */ - public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) { - // refund should be triggered halfway between offerMessageTimestamp and lockTimeA - return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L); - } - - 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 secretA - byte[] secretA = new byte[32]; - System.arraycopy(messageData, 0, secretA, 0, secretA.length); - - byte[] hashOfSecretA = Crypto.hash160(secretA); - if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA)) - continue; - - return secretA; - } - - return null; - } - -} diff --git a/src/main/java/org/qortal/crosschain/SimpleTransaction.java b/src/main/java/org/qortal/crosschain/SimpleTransaction.java deleted file mode 100644 index 0fae20a5..00000000 --- a/src/main/java/org/qortal/crosschain/SimpleTransaction.java +++ /dev/null @@ -1,32 +0,0 @@ -package org.qortal.crosschain; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; - -@XmlAccessorType(XmlAccessType.FIELD) -public class SimpleTransaction { - private String txHash; - private Integer timestamp; - private long totalAmount; - - public SimpleTransaction() { - } - - public SimpleTransaction(String txHash, Integer timestamp, long totalAmount) { - this.txHash = txHash; - this.timestamp = timestamp; - this.totalAmount = totalAmount; - } - - public String getTxHash() { - return txHash; - } - - public Integer getTimestamp() { - return timestamp; - } - - public long getTotalAmount() { - return totalAmount; - } -} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java deleted file mode 100644 index 7b6f91f5..00000000 --- a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java +++ /dev/null @@ -1,113 +0,0 @@ -package org.qortal.crosschain; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.function.Supplier; -import java.util.stream.Collectors; - -import org.qortal.utils.ByteArray; -import org.qortal.utils.Triple; - -public enum SupportedBlockchain { - - BITCOIN(Arrays.asList( - Triple.valueOf(BitcoinACCTv1.NAME, BitcoinACCTv1.CODE_BYTES_HASH, BitcoinACCTv1::getInstance) - // Could add improved BitcoinACCTv2 here in the future - )) { - @Override - public ForeignBlockchain getInstance() { - return Bitcoin.getInstance(); - } - - @Override - public ACCT getLatestAcct() { - return BitcoinACCTv1.getInstance(); - } - }, - - LITECOIN(Arrays.asList( - Triple.valueOf(LitecoinACCTv1.NAME, LitecoinACCTv1.CODE_BYTES_HASH, LitecoinACCTv1::getInstance) - )) { - @Override - public ForeignBlockchain getInstance() { - return Litecoin.getInstance(); - } - - @Override - public ACCT getLatestAcct() { - return LitecoinACCTv1.getInstance(); - } - }; - - private static final Map> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values()) - .map(supportedBlockchain -> supportedBlockchain.supportedAccts) - .flatMap(List::stream) - .collect(Collectors.toUnmodifiableMap(triple -> new ByteArray(triple.getB()), Triple::getC)); - - private static final Map> supportedAcctsByName = Arrays.stream(SupportedBlockchain.values()) - .map(supportedBlockchain -> supportedBlockchain.supportedAccts) - .flatMap(List::stream) - .collect(Collectors.toUnmodifiableMap(Triple::getA, Triple::getC)); - - private static final Map blockchainsByName = Arrays.stream(SupportedBlockchain.values()) - .collect(Collectors.toUnmodifiableMap(Enum::name, blockchain -> blockchain)); - - private final List>> supportedAccts; - - SupportedBlockchain(List>> supportedAccts) { - this.supportedAccts = supportedAccts; - } - - public abstract ForeignBlockchain getInstance(); - public abstract ACCT getLatestAcct(); - - public static Map> getAcctMap() { - return supportedAcctsByCodeHash; - } - - public static SupportedBlockchain fromString(String name) { - return blockchainsByName.get(name); - } - - public static Map> getFilteredAcctMap(SupportedBlockchain blockchain) { - if (blockchain == null) - return getAcctMap(); - - return blockchain.supportedAccts.stream() - .collect(Collectors.toUnmodifiableMap(triple -> new ByteArray(triple.getB()), Triple::getC)); - } - - public static Map> getFilteredAcctMap(String specificBlockchain) { - if (specificBlockchain == null) - return getAcctMap(); - - SupportedBlockchain blockchain = blockchainsByName.get(specificBlockchain); - if (blockchain == null) - return Collections.emptyMap(); - - return getFilteredAcctMap(blockchain); - } - - public static ACCT getAcctByCodeHash(byte[] codeHash) { - ByteArray wrappedCodeHash = new ByteArray(codeHash); - - Supplier acctInstanceSupplier = supportedAcctsByCodeHash.get(wrappedCodeHash); - - if (acctInstanceSupplier == null) - return null; - - return acctInstanceSupplier.get(); - } - - public static ACCT getAcctByName(String acctName) { - Supplier acctInstanceSupplier = supportedAcctsByName.get(acctName); - - if (acctInstanceSupplier == null) - return null; - - return acctInstanceSupplier.get(); - } - -} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/TransactionHash.java b/src/main/java/org/qortal/crosschain/TransactionHash.java deleted file mode 100644 index c002ae80..00000000 --- a/src/main/java/org/qortal/crosschain/TransactionHash.java +++ /dev/null @@ -1,31 +0,0 @@ -package org.qortal.crosschain; - -import java.util.Comparator; - -public class TransactionHash { - - public static final Comparator CONFIRMED_FIRST = (a, b) -> Boolean.compare(a.height != 0, b.height != 0); - - public final int height; - public final String txHash; - - public TransactionHash(int height, String txHash) { - this.height = height; - this.txHash = txHash; - } - - public int getHeight() { - return this.height; - } - - public String getTxHash() { - return this.txHash; - } - - public String toString() { - return this.height == 0 - ? String.format("txHash %s (unconfirmed)", this.txHash) - : String.format("txHash %s (height %d)", this.txHash, this.height); - } - -} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/UnspentOutput.java b/src/main/java/org/qortal/crosschain/UnspentOutput.java deleted file mode 100644 index 86aa533d..00000000 --- a/src/main/java/org/qortal/crosschain/UnspentOutput.java +++ /dev/null @@ -1,16 +0,0 @@ -package org.qortal.crosschain; - -/** Unspent output info as returned by ElectrumX network. */ -public class UnspentOutput { - public final byte[] hash; - public final int index; - public final int height; - public final long value; - - public UnspentOutput(byte[] hash, int index, int height, long value) { - this.hash = hash; - this.index = index; - this.height = height; - this.value = value; - } -} \ No newline at end of file diff --git a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java b/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java deleted file mode 100644 index 69250e54..00000000 --- a/src/main/java/org/qortal/data/crosschain/CrossChainTradeData.java +++ /dev/null @@ -1,109 +0,0 @@ -package org.qortal.data.crosschain; - -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 io.swagger.v3.oas.annotations.media.Schema; - -// All properties to be converted to JSON via JAXB -@XmlAccessorType(XmlAccessType.FIELD) -public class CrossChainTradeData { - - // Properties - - @Schema(description = "AT's Qortal address") - public String qortalAtAddress; - - @Schema(description = "AT creator's Qortal address") - public String qortalCreator; - - @Schema(description = "AT creator's ephemeral trading key-pair represented as Qortal address") - public String qortalCreatorTradeAddress; - - @Deprecated - @Schema(description = "DEPRECATED: use creatorForeignPKH instead") - public byte[] creatorBitcoinPKH; - - @Schema(description = "AT creator's foreign blockchain trade public-key-hash (PKH)") - public byte[] creatorForeignPKH; - - @Schema(description = "Timestamp when AT was created (milliseconds since epoch)") - public long creationTimestamp; - - @Schema(description = "Suggested trade timeout (minutes)", example = "10080") - public int tradeTimeout; - - @Schema(description = "AT's current QORT balance") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long qortBalance; - - @Schema(description = "HASH160 of 32-byte secret-A") - public byte[] hashOfSecretA; - - @Schema(description = "HASH160 of 32-byte secret-B") - public byte[] hashOfSecretB; - - @Schema(description = "Final QORT payment that will be sent to Qortal trade partner") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long qortAmount; - - @Schema(description = "Trade partner's Qortal address (trade begins when this is set)") - public String qortalPartnerAddress; - - @Schema(description = "Timestamp when AT switched to trade mode") - public Long tradeModeTimestamp; - - @Schema(description = "How long from AT creation until AT triggers automatic refund to AT creator (minutes)") - public Integer refundTimeout; - - @Schema(description = "Actual Qortal block height when AT will automatically refund to AT creator (after trade begins)") - public Integer tradeRefundHeight; - - @Deprecated - @Schema(description = "DEPRECATED: use expectedForeignAmount instread") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long expectedBitcoin; - - @Schema(description = "Amount, in foreign blockchain currency, that AT creator expects trade partner to pay out (excluding miner fees)") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - public long expectedForeignAmount; - - @Schema(description = "Current AT execution mode") - public AcctMode mode; - - @Schema(description = "Suggested P2SH-A nLockTime based on trade timeout") - public Integer lockTimeA; - - @Schema(description = "Suggested P2SH-B nLockTime based on trade timeout") - public Integer lockTimeB; - - @Deprecated - @Schema(description = "DEPRECATED: use partnerForeignPKH instead") - public byte[] partnerBitcoinPKH; - - @Schema(description = "Trade partner's foreign blockchain public-key-hash (PKH)") - public byte[] partnerForeignPKH; - - @Schema(description = "Trade partner's Qortal receiving address") - public String qortalPartnerReceivingAddress; - - public String foreignBlockchain; - - public String acctName; - - // Constructors - - // Necessary for JAXB - public CrossChainTradeData() { - } - - public void duplicateDeprecated() { - this.creatorBitcoinPKH = this.creatorForeignPKH; - this.expectedBitcoin = this.expectedForeignAmount; - this.partnerBitcoinPKH = this.partnerForeignPKH; - } - -} diff --git a/src/main/java/org/qortal/data/crosschain/TradeBotData.java b/src/main/java/org/qortal/data/crosschain/TradeBotData.java deleted file mode 100644 index 19481466..00000000 --- a/src/main/java/org/qortal/data/crosschain/TradeBotData.java +++ /dev/null @@ -1,268 +0,0 @@ -package org.qortal.data.crosschain; - -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; -import javax.xml.bind.annotation.XmlTransient; -import javax.xml.bind.annotation.adapters.XmlJavaTypeAdapter; - -import io.swagger.v3.oas.annotations.media.Schema; -import org.json.JSONObject; - -import org.qortal.utils.Base58; - -// All properties to be converted to JSON via JAXB -@XmlAccessorType(XmlAccessType.FIELD) -public class TradeBotData { - - private byte[] tradePrivateKey; - - private String acctName; - private String tradeState; - - // Internal use - not shown via API - @XmlTransient - @Schema(hidden = true) - private int tradeStateValue; - - private String creatorAddress; - private String atAddress; - - private long timestamp; - - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private long qortAmount; - - private byte[] tradeNativePublicKey; - private byte[] tradeNativePublicKeyHash; - String tradeNativeAddress; - - private byte[] secret; - private byte[] hashOfSecret; - - private String foreignBlockchain; - private byte[] tradeForeignPublicKey; - private byte[] tradeForeignPublicKeyHash; - - @Deprecated - @Schema(description = "DEPRECATED: use foreignAmount instead", type = "number") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private long bitcoinAmount; - - @Schema(description = "amount in foreign blockchain currency", type = "number") - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private long foreignAmount; - - // Never expose this via API - @XmlTransient - @Schema(hidden = true) - private String foreignKey; - - private byte[] lastTransactionSignature; - private Integer lockTimeA; - - // Could be Bitcoin or Qortal... - private byte[] receivingAccountInfo; - - protected TradeBotData() { - /* JAXB */ - } - - public TradeBotData(byte[] tradePrivateKey, String acctName, String tradeState, int tradeStateValue, - String creatorAddress, String atAddress, - long timestamp, long qortAmount, - byte[] tradeNativePublicKey, byte[] tradeNativePublicKeyHash, String tradeNativeAddress, - byte[] secret, byte[] hashOfSecret, - String foreignBlockchain, byte[] tradeForeignPublicKey, byte[] tradeForeignPublicKeyHash, - long foreignAmount, String foreignKey, - byte[] lastTransactionSignature, Integer lockTimeA, byte[] receivingAccountInfo) { - this.tradePrivateKey = tradePrivateKey; - this.acctName = acctName; - this.tradeState = tradeState; - this.tradeStateValue = tradeStateValue; - this.creatorAddress = creatorAddress; - this.atAddress = atAddress; - this.timestamp = timestamp; - this.qortAmount = qortAmount; - this.tradeNativePublicKey = tradeNativePublicKey; - this.tradeNativePublicKeyHash = tradeNativePublicKeyHash; - this.tradeNativeAddress = tradeNativeAddress; - this.secret = secret; - this.hashOfSecret = hashOfSecret; - this.foreignBlockchain = foreignBlockchain; - this.tradeForeignPublicKey = tradeForeignPublicKey; - this.tradeForeignPublicKeyHash = tradeForeignPublicKeyHash; - // deprecated copy - this.bitcoinAmount = foreignAmount; - this.foreignAmount = foreignAmount; - this.foreignKey = foreignKey; - this.lastTransactionSignature = lastTransactionSignature; - this.lockTimeA = lockTimeA; - this.receivingAccountInfo = receivingAccountInfo; - } - - public byte[] getTradePrivateKey() { - return this.tradePrivateKey; - } - - public String getAcctName() { - return this.acctName; - } - - public String getState() { - return this.tradeState; - } - - public void setState(String state) { - this.tradeState = state; - } - - public int getStateValue() { - return this.tradeStateValue; - } - - public void setStateValue(int stateValue) { - this.tradeStateValue = stateValue; - } - - public String getCreatorAddress() { - return this.creatorAddress; - } - - public String getAtAddress() { - return this.atAddress; - } - - public void setAtAddress(String atAddress) { - this.atAddress = atAddress; - } - - public long getTimestamp() { - return this.timestamp; - } - - public void setTimestamp(long timestamp) { - this.timestamp = timestamp; - } - - public long getQortAmount() { - return this.qortAmount; - } - - public byte[] getTradeNativePublicKey() { - return this.tradeNativePublicKey; - } - - public byte[] getTradeNativePublicKeyHash() { - return this.tradeNativePublicKeyHash; - } - - public String getTradeNativeAddress() { - return this.tradeNativeAddress; - } - - public byte[] getSecret() { - return this.secret; - } - - public byte[] getHashOfSecret() { - return this.hashOfSecret; - } - - public String getForeignBlockchain() { - return this.foreignBlockchain; - } - - public byte[] getTradeForeignPublicKey() { - return this.tradeForeignPublicKey; - } - - public byte[] getTradeForeignPublicKeyHash() { - return this.tradeForeignPublicKeyHash; - } - - public long getForeignAmount() { - return this.foreignAmount; - } - - public String getForeignKey() { - return this.foreignKey; - } - - public byte[] getLastTransactionSignature() { - return this.lastTransactionSignature; - } - - public void setLastTransactionSignature(byte[] lastTransactionSignature) { - this.lastTransactionSignature = lastTransactionSignature; - } - - public Integer getLockTimeA() { - return this.lockTimeA; - } - - public void setLockTimeA(Integer lockTimeA) { - this.lockTimeA = lockTimeA; - } - - public byte[] getReceivingAccountInfo() { - return this.receivingAccountInfo; - } - - public JSONObject toJson() { - JSONObject jsonObject = new JSONObject(); - jsonObject.put("tradePrivateKey", Base58.encode(this.getTradePrivateKey())); - jsonObject.put("acctName", this.getAcctName()); - jsonObject.put("tradeState", this.getState()); - jsonObject.put("tradeStateValue", this.getStateValue()); - jsonObject.put("creatorAddress", this.getCreatorAddress()); - jsonObject.put("atAddress", this.getAtAddress()); - jsonObject.put("timestamp", this.getTimestamp()); - jsonObject.put("qortAmount", this.getQortAmount()); - if (this.getTradeNativePublicKey() != null) jsonObject.put("tradeNativePublicKey", Base58.encode(this.getTradeNativePublicKey())); - if (this.getTradeNativePublicKeyHash() != null) jsonObject.put("tradeNativePublicKeyHash", Base58.encode(this.getTradeNativePublicKeyHash())); - jsonObject.put("tradeNativeAddress", this.getTradeNativeAddress()); - if (this.getSecret() != null) jsonObject.put("secret", Base58.encode(this.getSecret())); - if (this.getHashOfSecret() != null) jsonObject.put("hashOfSecret", Base58.encode(this.getHashOfSecret())); - jsonObject.put("foreignBlockchain", this.getForeignBlockchain()); - if (this.getTradeForeignPublicKey() != null) jsonObject.put("tradeForeignPublicKey", Base58.encode(this.getTradeForeignPublicKey())); - if (this.getTradeForeignPublicKeyHash() != null) jsonObject.put("tradeForeignPublicKeyHash", Base58.encode(this.getTradeForeignPublicKeyHash())); - jsonObject.put("foreignKey", this.getForeignKey()); - jsonObject.put("foreignAmount", this.getForeignAmount()); - if (this.getLastTransactionSignature() != null) jsonObject.put("lastTransactionSignature", Base58.encode(this.getLastTransactionSignature())); - jsonObject.put("lockTimeA", this.getLockTimeA()); - if (this.getReceivingAccountInfo() != null) jsonObject.put("receivingAccountInfo", Base58.encode(this.getReceivingAccountInfo())); - return jsonObject; - } - - public static TradeBotData fromJson(JSONObject json) { - return new TradeBotData( - json.isNull("tradePrivateKey") ? null : Base58.decode(json.getString("tradePrivateKey")), - json.isNull("acctName") ? null : json.getString("acctName"), - json.isNull("tradeState") ? null : json.getString("tradeState"), - json.isNull("tradeStateValue") ? null : json.getInt("tradeStateValue"), - json.isNull("creatorAddress") ? null : json.getString("creatorAddress"), - json.isNull("atAddress") ? null : json.getString("atAddress"), - json.isNull("timestamp") ? null : json.getLong("timestamp"), - json.isNull("qortAmount") ? null : json.getLong("qortAmount"), - json.isNull("tradeNativePublicKey") ? null : Base58.decode(json.getString("tradeNativePublicKey")), - json.isNull("tradeNativePublicKeyHash") ? null : Base58.decode(json.getString("tradeNativePublicKeyHash")), - json.isNull("tradeNativeAddress") ? null : json.getString("tradeNativeAddress"), - json.isNull("secret") ? null : Base58.decode(json.getString("secret")), - json.isNull("hashOfSecret") ? null : Base58.decode(json.getString("hashOfSecret")), - json.isNull("foreignBlockchain") ? null : json.getString("foreignBlockchain"), - json.isNull("tradeForeignPublicKey") ? null : Base58.decode(json.getString("tradeForeignPublicKey")), - json.isNull("tradeForeignPublicKeyHash") ? null : Base58.decode(json.getString("tradeForeignPublicKeyHash")), - json.isNull("foreignAmount") ? null : json.getLong("foreignAmount"), - json.isNull("foreignKey") ? null : json.getString("foreignKey"), - json.isNull("lastTransactionSignature") ? null : Base58.decode(json.getString("lastTransactionSignature")), - json.isNull("lockTimeA") ? null : json.getInt("lockTimeA"), - json.isNull("receivingAccountInfo") ? null : Base58.decode(json.getString("receivingAccountInfo")) - ); - } - - // Mostly for debugging - public String toString() { - return String.format("%s: %s (%d)", this.atAddress, this.tradeState, this.tradeStateValue); - } - -} diff --git a/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java b/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java deleted file mode 100644 index 001bd5b4..00000000 --- a/src/main/java/org/qortal/data/transaction/PresenceTransactionData.java +++ /dev/null @@ -1,73 +0,0 @@ -package org.qortal.data.transaction; - -import javax.xml.bind.Unmarshaller; -import javax.xml.bind.annotation.XmlAccessType; -import javax.xml.bind.annotation.XmlAccessorType; - -import org.qortal.transaction.PresenceTransaction.PresenceType; -import org.qortal.transaction.Transaction.TransactionType; - -import io.swagger.v3.oas.annotations.media.Schema; -import io.swagger.v3.oas.annotations.media.Schema.AccessMode; - -// All properties to be converted to JSON via JAXB -@XmlAccessorType(XmlAccessType.FIELD) -@Schema(allOf = { TransactionData.class }) -public class PresenceTransactionData extends TransactionData { - - // Properties - @Schema(description = "sender's public key", example = "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP") - private byte[] senderPublicKey; - - @Schema(accessMode = AccessMode.READ_ONLY) - private int nonce; - - private PresenceType presenceType; - - @Schema(description = "timestamp signature", example = "2yGEbwRFyhPZZckKA") - private byte[] timestampSignature; - - // Constructors - - // For JAXB - protected PresenceTransactionData() { - super(TransactionType.PRESENCE); - } - - public void afterUnmarshal(Unmarshaller u, Object parent) { - this.creatorPublicKey = this.senderPublicKey; - } - - public PresenceTransactionData(BaseTransactionData baseTransactionData, - int nonce, PresenceType presenceType, byte[] timestampSignature) { - super(TransactionType.PRESENCE, baseTransactionData); - - this.senderPublicKey = baseTransactionData.creatorPublicKey; - this.nonce = nonce; - this.presenceType = presenceType; - this.timestampSignature = timestampSignature; - } - - // Getters/Setters - - public byte[] getSenderPublicKey() { - return this.senderPublicKey; - } - - public int getNonce() { - return this.nonce; - } - - public void setNonce(int nonce) { - this.nonce = nonce; - } - - public PresenceType getPresenceType() { - return this.presenceType; - } - - public byte[] getTimestampSignature() { - return this.timestampSignature; - } - -} diff --git a/src/main/java/org/qortal/data/transaction/TransactionData.java b/src/main/java/org/qortal/data/transaction/TransactionData.java index 060901f2..397693b8 100644 --- a/src/main/java/org/qortal/data/transaction/TransactionData.java +++ b/src/main/java/org/qortal/data/transaction/TransactionData.java @@ -40,7 +40,7 @@ import io.swagger.v3.oas.annotations.media.Schema.AccessMode; GroupApprovalTransactionData.class, SetGroupTransactionData.class, UpdateAssetTransactionData.class, AccountFlagsTransactionData.class, RewardShareTransactionData.class, - AccountLevelTransactionData.class, ChatTransactionData.class, PresenceTransactionData.class + AccountLevelTransactionData.class, ChatTransactionData.class }) //All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) diff --git a/src/main/java/org/qortal/repository/CrossChainRepository.java b/src/main/java/org/qortal/repository/CrossChainRepository.java deleted file mode 100644 index 70ebdbf9..00000000 --- a/src/main/java/org/qortal/repository/CrossChainRepository.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.qortal.repository; - -import java.util.List; - -import org.qortal.data.crosschain.TradeBotData; - -public interface CrossChainRepository { - - public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException; - - /** Returns true if there is an existing trade-bot entry relating to given AT address, excluding trade-bot entries with given states. */ - public boolean existsTradeWithAtExcludingStates(String atAddress, List excludeStates) throws DataException; - - public List getAllTradeBotData() throws DataException; - - public void save(TradeBotData tradeBotData) throws DataException; - - /** Delete trade-bot states using passed private key. */ - public int delete(byte[] tradePrivateKey) throws DataException; - -} diff --git a/src/main/java/org/qortal/repository/Repository.java b/src/main/java/org/qortal/repository/Repository.java index 656e6e1e..9cdfe26c 100644 --- a/src/main/java/org/qortal/repository/Repository.java +++ b/src/main/java/org/qortal/repository/Repository.java @@ -14,8 +14,6 @@ public interface Repository extends AutoCloseable { public ChatRepository getChatRepository(); - public CrossChainRepository getCrossChainRepository(); - public GroupRepository getGroupRepository(); public MessageRepository getMessageRepository(); diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java deleted file mode 100644 index 29f2994c..00000000 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBCrossChainRepository.java +++ /dev/null @@ -1,202 +0,0 @@ -package org.qortal.repository.hsqldb; - -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; - -import org.qortal.data.crosschain.TradeBotData; -import org.qortal.repository.CrossChainRepository; -import org.qortal.repository.DataException; - -public class HSQLDBCrossChainRepository implements CrossChainRepository { - - protected HSQLDBRepository repository; - - public HSQLDBCrossChainRepository(HSQLDBRepository repository) { - this.repository = repository; - } - - @Override - public TradeBotData getTradeBotData(byte[] tradePrivateKey) throws DataException { - String sql = "SELECT acct_name, trade_state, trade_state_value, " - + "creator_address, at_address, " - + "updated_when, qort_amount, " - + "trade_native_public_key, trade_native_public_key_hash, " - + "trade_native_address, secret, hash_of_secret, " - + "foreign_blockchain, trade_foreign_public_key, trade_foreign_public_key_hash, " - + "foreign_amount, foreign_key, last_transaction_signature, locktime_a, receiving_account_info " - + "FROM TradeBotStates " - + "WHERE trade_private_key = ?"; - - try (ResultSet resultSet = this.repository.checkedExecute(sql, tradePrivateKey)) { - if (resultSet == null) - return null; - - String acctName = resultSet.getString(1); - String tradeState = resultSet.getString(2); - int tradeStateValue = resultSet.getInt(3); - String creatorAddress = resultSet.getString(4); - String atAddress = resultSet.getString(5); - long timestamp = resultSet.getLong(6); - long qortAmount = resultSet.getLong(7); - byte[] tradeNativePublicKey = resultSet.getBytes(8); - byte[] tradeNativePublicKeyHash = resultSet.getBytes(9); - String tradeNativeAddress = resultSet.getString(10); - byte[] secret = resultSet.getBytes(11); - byte[] hashOfSecret = resultSet.getBytes(12); - String foreignBlockchain = resultSet.getString(13); - byte[] tradeForeignPublicKey = resultSet.getBytes(14); - byte[] tradeForeignPublicKeyHash = resultSet.getBytes(15); - long foreignAmount = resultSet.getLong(16); - String foreignKey = resultSet.getString(17); - byte[] lastTransactionSignature = resultSet.getBytes(18); - Integer lockTimeA = resultSet.getInt(19); - if (lockTimeA == 0 && resultSet.wasNull()) - lockTimeA = null; - byte[] receivingAccountInfo = resultSet.getBytes(20); - - return new TradeBotData(tradePrivateKey, acctName, - tradeState, tradeStateValue, - creatorAddress, atAddress, timestamp, qortAmount, - tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, - secret, hashOfSecret, - foreignBlockchain, tradeForeignPublicKey, tradeForeignPublicKeyHash, - foreignAmount, foreignKey, lastTransactionSignature, lockTimeA, receivingAccountInfo); - } catch (SQLException e) { - throw new DataException("Unable to fetch trade-bot trading state from repository", e); - } - } - - @Override - public boolean existsTradeWithAtExcludingStates(String atAddress, List excludeStates) throws DataException { - if (excludeStates == null) - excludeStates = Collections.emptyList(); - - StringBuilder whereClause = new StringBuilder(256); - whereClause.append("at_address = ?"); - - Object[] bindParams = new Object[1 + excludeStates.size()]; - bindParams[0] = atAddress; - - if (!excludeStates.isEmpty()) { - whereClause.append(" AND trade_state NOT IN (?"); - bindParams[1] = excludeStates.get(0); - - for (int i = 1; i < excludeStates.size(); ++i) { - whereClause.append(", ?"); - bindParams[1 + i] = excludeStates.get(i); - } - - whereClause.append(")"); - } - - try { - return this.repository.exists("TradeBotStates", whereClause.toString(), bindParams); - } catch (SQLException e) { - throw new DataException("Unable to check for trade-bot state in repository", e); - } - } - - @Override - public List getAllTradeBotData() throws DataException { - String sql = "SELECT trade_private_key, acct_name, trade_state, trade_state_value, " - + "creator_address, at_address, " - + "updated_when, qort_amount, " - + "trade_native_public_key, trade_native_public_key_hash, " - + "trade_native_address, secret, hash_of_secret, " - + "foreign_blockchain, trade_foreign_public_key, trade_foreign_public_key_hash, " - + "foreign_amount, foreign_key, last_transaction_signature, locktime_a, receiving_account_info " - + "FROM TradeBotStates"; - - List allTradeBotData = new ArrayList<>(); - - try (ResultSet resultSet = this.repository.checkedExecute(sql)) { - if (resultSet == null) - return allTradeBotData; - - do { - byte[] tradePrivateKey = resultSet.getBytes(1); - String acctName = resultSet.getString(2); - String tradeState = resultSet.getString(3); - int tradeStateValue = resultSet.getInt(4); - String creatorAddress = resultSet.getString(5); - String atAddress = resultSet.getString(6); - long timestamp = resultSet.getLong(7); - long qortAmount = resultSet.getLong(8); - byte[] tradeNativePublicKey = resultSet.getBytes(9); - byte[] tradeNativePublicKeyHash = resultSet.getBytes(10); - String tradeNativeAddress = resultSet.getString(11); - byte[] secret = resultSet.getBytes(12); - byte[] hashOfSecret = resultSet.getBytes(13); - String foreignBlockchain = resultSet.getString(14); - byte[] tradeForeignPublicKey = resultSet.getBytes(15); - byte[] tradeForeignPublicKeyHash = resultSet.getBytes(16); - long foreignAmount = resultSet.getLong(17); - String foreignKey = resultSet.getString(18); - byte[] lastTransactionSignature = resultSet.getBytes(19); - Integer lockTimeA = resultSet.getInt(20); - if (lockTimeA == 0 && resultSet.wasNull()) - lockTimeA = null; - byte[] receivingAccountInfo = resultSet.getBytes(21); - - TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, acctName, - tradeState, tradeStateValue, - creatorAddress, atAddress, timestamp, qortAmount, - tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, - secret, hashOfSecret, - foreignBlockchain, tradeForeignPublicKey, tradeForeignPublicKeyHash, - foreignAmount, foreignKey, lastTransactionSignature, lockTimeA, receivingAccountInfo); - allTradeBotData.add(tradeBotData); - } while (resultSet.next()); - - return allTradeBotData; - } catch (SQLException e) { - throw new DataException("Unable to fetch trade-bot trading states from repository", e); - } - } - - @Override - public void save(TradeBotData tradeBotData) throws DataException { - HSQLDBSaver saveHelper = new HSQLDBSaver("TradeBotStates"); - - saveHelper.bind("trade_private_key", tradeBotData.getTradePrivateKey()) - .bind("acct_name", tradeBotData.getAcctName()) - .bind("trade_state", tradeBotData.getState()) - .bind("trade_state_value", tradeBotData.getStateValue()) - .bind("creator_address", tradeBotData.getCreatorAddress()) - .bind("at_address", tradeBotData.getAtAddress()) - .bind("updated_when", tradeBotData.getTimestamp()) - .bind("qort_amount", tradeBotData.getQortAmount()) - .bind("trade_native_public_key", tradeBotData.getTradeNativePublicKey()) - .bind("trade_native_public_key_hash", tradeBotData.getTradeNativePublicKeyHash()) - .bind("trade_native_address", tradeBotData.getTradeNativeAddress()) - .bind("secret", tradeBotData.getSecret()) - .bind("hash_of_secret", tradeBotData.getHashOfSecret()) - .bind("foreign_blockchain", tradeBotData.getForeignBlockchain()) - .bind("trade_foreign_public_key", tradeBotData.getTradeForeignPublicKey()) - .bind("trade_foreign_public_key_hash", tradeBotData.getTradeForeignPublicKeyHash()) - .bind("foreign_amount", tradeBotData.getForeignAmount()) - .bind("foreign_key", tradeBotData.getForeignKey()) - .bind("last_transaction_signature", tradeBotData.getLastTransactionSignature()) - .bind("locktime_a", tradeBotData.getLockTimeA()) - .bind("receiving_account_info", tradeBotData.getReceivingAccountInfo()); - - try { - saveHelper.execute(this.repository); - } catch (SQLException e) { - throw new DataException("Unable to save trade bot data into repository", e); - } - } - - @Override - public int delete(byte[] tradePrivateKey) throws DataException { - try { - return this.repository.delete("TradeBotStates", "trade_private_key = ?", tradePrivateKey); - } catch (SQLException e) { - throw new DataException("Unable to delete trade-bot states from repository", e); - } - } - -} \ No newline at end of file diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java index b82f55c3..1dbac289 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBDatabaseUpdates.java @@ -9,7 +9,6 @@ import java.util.stream.Collectors; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; -import org.qortal.controller.tradebot.BitcoinACCTv1TradeBot; public class HSQLDBDatabaseUpdates { @@ -620,17 +619,6 @@ public class HSQLDBDatabaseUpdates { break; case 20: - // Trade bot - // See case 25 below for changes - stmt.execute("CREATE TABLE TradeBotStates (trade_private_key QortalKeySeed NOT NULL, trade_state TINYINT NOT NULL, " - + "creator_address QortalAddress NOT NULL, at_address QortalAddress, updated_when BIGINT NOT NULL, qort_amount QortalAmount NOT NULL, " - + "trade_native_public_key QortalPublicKey NOT NULL, trade_native_public_key_hash VARBINARY(32) NOT NULL, " - + "trade_native_address QortalAddress NOT NULL, secret VARBINARY(32) NOT NULL, hash_of_secret VARBINARY(32) NOT NULL, " - + "trade_foreign_public_key VARBINARY(33) NOT NULL, trade_foreign_public_key_hash VARBINARY(32) NOT NULL, " - + "bitcoin_amount BIGINT NOT NULL, xprv58 VARCHAR(200), last_transaction_signature Signature, locktime_a BIGINT, " - + "receiving_account_info VARBINARY(32) NOT NULL, PRIMARY KEY (trade_private_key))"); - break; - case 21: // AT functionality index stmt.execute("CREATE INDEX IF NOT EXISTS ATCodeHashIndex ON ATs (code_hash, is_finished)"); @@ -712,14 +700,6 @@ public class HSQLDBDatabaseUpdates { } } - try (ResultSet resultSet = stmt.executeQuery("SELECT COUNT(*) FROM TradeBotStates")) { - int rowCount = resultSet.next() ? resultSet.getInt(1) : 0; - if (rowCount > 0) { - stmt.execute("PERFORM EXPORT SCRIPT FOR TABLE TradeBotStates DATA TO 'TradeBotStates.script'"); - LOGGER.info("Exported sensitive/node-local trade-bot states into TradeBotStates.script"); - } - } - LOGGER.info("If following reshape takes too long, use bootstrap and import node-local data using API's POST /admin/repository/data"); } @@ -784,37 +764,6 @@ public class HSQLDBDatabaseUpdates { break; case 32: - // Multiple blockchains, ACCTs and trade-bots - stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN acct_name VARCHAR(40) BEFORE trade_state"); - stmt.execute("UPDATE TradeBotStates SET acct_name = 'BitcoinACCTv1' WHERE acct_name IS NULL"); - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN acct_name SET NOT NULL"); - - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN trade_state RENAME TO trade_state_value"); - - stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN trade_state VARCHAR(40) BEFORE trade_state_value"); - // Any existing values will be BitcoinACCTv1 - StringBuilder updateTradeBotStatesSql = new StringBuilder(1024); - updateTradeBotStatesSql.append("UPDATE TradeBotStates SET (trade_state) = (") - .append("SELECT state_name FROM (VALUES ") - .append( - Arrays.stream(BitcoinACCTv1TradeBot.State.values()) - .map(state -> String.format("(%d, '%s')", state.value, state.name())) - .collect(Collectors.joining(", "))) - .append(") AS BitcoinACCTv1States (state_value, state_name) ") - .append("WHERE state_value = trade_state_value)"); - stmt.execute(updateTradeBotStatesSql.toString()); - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN trade_state SET NOT NULL"); - - stmt.execute("ALTER TABLE TradeBotStates ADD COLUMN foreign_blockchain VARCHAR(40) BEFORE trade_foreign_public_key"); - - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN bitcoin_amount RENAME TO foreign_amount"); - - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN xprv58 RENAME TO foreign_key"); - - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN secret SET NULL"); - stmt.execute("ALTER TABLE TradeBotStates ALTER COLUMN hash_of_secret SET NULL"); - break; - case 33: // PRESENCE transactions stmt.execute("CREATE TABLE IF NOT EXISTS PresenceTransactions (" diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java index 09c6a6d4..02de9f5f 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java @@ -28,7 +28,6 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.qortal.account.PrivateKeyAccount; import org.qortal.crypto.Crypto; -import org.qortal.data.crosschain.TradeBotData; import org.qortal.globalization.Translator; import org.qortal.gui.SysTray; import org.qortal.repository.ATRepository; @@ -37,7 +36,6 @@ import org.qortal.repository.ArbitraryRepository; import org.qortal.repository.AssetRepository; import org.qortal.repository.BlockRepository; import org.qortal.repository.ChatRepository; -import org.qortal.repository.CrossChainRepository; import org.qortal.repository.DataException; import org.qortal.repository.GroupRepository; import org.qortal.repository.MessageRepository; @@ -76,7 +74,6 @@ public class HSQLDBRepository implements Repository { private final AssetRepository assetRepository = new HSQLDBAssetRepository(this); private final BlockRepository blockRepository = new HSQLDBBlockRepository(this); private final ChatRepository chatRepository = new HSQLDBChatRepository(this); - private final CrossChainRepository crossChainRepository = new HSQLDBCrossChainRepository(this); private final GroupRepository groupRepository = new HSQLDBGroupRepository(this); private final MessageRepository messageRepository = new HSQLDBMessageRepository(this); private final NameRepository nameRepository = new HSQLDBNameRepository(this); @@ -147,11 +144,6 @@ public class HSQLDBRepository implements Repository { return this.chatRepository; } - @Override - public CrossChainRepository getCrossChainRepository() { - return this.crossChainRepository; - } - @Override public GroupRepository getGroupRepository() { return this.groupRepository; @@ -458,68 +450,12 @@ public class HSQLDBRepository implements Repository { @Override public void exportNodeLocalData() throws DataException { - // Create the qortal-backup folder if it doesn't exist - Path backupPath = Paths.get("qortal-backup"); - try { - Files.createDirectories(backupPath); - } catch (IOException e) { - LOGGER.info("Unable to create backup folder"); - throw new DataException("Unable to create backup folder"); - } - - try { - // Load trade bot data - List allTradeBotData = this.getCrossChainRepository().getAllTradeBotData(); - JSONArray allTradeBotDataJson = new JSONArray(); - for (TradeBotData tradeBotData : allTradeBotData) { - JSONObject tradeBotDataJson = tradeBotData.toJson(); - allTradeBotDataJson.put(tradeBotDataJson); - } - - // We need to combine existing TradeBotStates data before overwriting - String fileName = "qortal-backup/TradeBotStates.json"; - File tradeBotStatesBackupFile = new File(fileName); - if (tradeBotStatesBackupFile.exists()) { - String jsonString = new String(Files.readAllBytes(Paths.get(fileName))); - JSONArray allExistingTradeBotData = new JSONArray(jsonString); - Iterator iterator = allExistingTradeBotData.iterator(); - while(iterator.hasNext()) { - JSONObject existingTradeBotData = (JSONObject)iterator.next(); - String existingTradePrivateKey = (String) existingTradeBotData.get("tradePrivateKey"); - // Check if we already have an entry for this trade - boolean found = allTradeBotData.stream().anyMatch(tradeBotData -> Base58.encode(tradeBotData.getTradePrivateKey()).equals(existingTradePrivateKey)); - if (found == false) - // We need to add this to our list - allTradeBotDataJson.put(existingTradeBotData); - } - } - - FileWriter writer = new FileWriter(fileName); - writer.write(allTradeBotDataJson.toString()); - writer.close(); - LOGGER.info("Exported sensitive/node-local data: trade bot states"); - - } catch (DataException | IOException e) { - throw new DataException("Unable to export trade bot states from repository"); - } + // TODO } @Override public void importDataFromFile(String filename) throws DataException { - LOGGER.info(() -> String.format("Importing data into repository from %s", filename)); - try { - String jsonString = new String(Files.readAllBytes(Paths.get(filename))); - JSONArray tradeBotDataToImport = new JSONArray(jsonString); - Iterator iterator = tradeBotDataToImport.iterator(); - while(iterator.hasNext()) { - JSONObject tradeBotDataJson = (JSONObject)iterator.next(); - TradeBotData tradeBotData = TradeBotData.fromJson(tradeBotDataJson); - this.getCrossChainRepository().save(tradeBotData); - } - } catch (IOException e) { - throw new DataException("Unable to import sensitive/node-local trade bot states to repository: " + e.getMessage()); - } - LOGGER.info(() -> String.format("Imported trade bot states into repository from %s", filename)); + // TODO } @Override @@ -1056,4 +992,4 @@ public class HSQLDBRepository implements Repository { return DEADLOCK_ERROR_CODE.equals(e.getErrorCode()); } -} \ No newline at end of file +} diff --git a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java b/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java deleted file mode 100644 index 309ffcad..00000000 --- a/src/main/java/org/qortal/repository/hsqldb/transaction/HSQLDBPresenceTransactionRepository.java +++ /dev/null @@ -1,57 +0,0 @@ -package org.qortal.repository.hsqldb.transaction; - -import java.sql.ResultSet; -import java.sql.SQLException; - -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.PresenceTransactionData; -import org.qortal.data.transaction.TransactionData; -import org.qortal.repository.DataException; -import org.qortal.repository.hsqldb.HSQLDBRepository; -import org.qortal.repository.hsqldb.HSQLDBSaver; -import org.qortal.transaction.PresenceTransaction.PresenceType; - -public class HSQLDBPresenceTransactionRepository extends HSQLDBTransactionRepository { - - public HSQLDBPresenceTransactionRepository(HSQLDBRepository repository) { - this.repository = repository; - } - - TransactionData fromBase(BaseTransactionData baseTransactionData) throws DataException { - String sql = "SELECT nonce, presence_type, timestamp_signature FROM PresenceTransactions WHERE signature = ?"; - - try (ResultSet resultSet = this.repository.checkedExecute(sql, baseTransactionData.getSignature())) { - if (resultSet == null) - return null; - - int nonce = resultSet.getInt(1); - int presenceTypeValue = resultSet.getInt(2); - PresenceType presenceType = PresenceType.valueOf(presenceTypeValue); - - byte[] timestampSignature = resultSet.getBytes(3); - - return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature); - } catch (SQLException e) { - throw new DataException("Unable to fetch presence transaction from repository", e); - } - } - - @Override - public void save(TransactionData transactionData) throws DataException { - PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData; - - HSQLDBSaver saveHelper = new HSQLDBSaver("PresenceTransactions"); - - saveHelper.bind("signature", presenceTransactionData.getSignature()) - .bind("nonce", presenceTransactionData.getNonce()) - .bind("presence_type", presenceTransactionData.getPresenceType().value) - .bind("timestamp_signature", presenceTransactionData.getTimestampSignature()); - - try { - saveHelper.execute(this.repository); - } catch (SQLException e) { - throw new DataException("Unable to save chat transaction into repository", e); - } - } - -} diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 4743c9f4..f17324df 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -22,8 +22,6 @@ import org.eclipse.persistence.exceptions.XMLMarshalException; import org.eclipse.persistence.jaxb.JAXBContextFactory; import org.eclipse.persistence.jaxb.UnmarshallerProperties; import org.qortal.block.BlockChain; -import org.qortal.crosschain.Bitcoin.BitcoinNet; -import org.qortal.crosschain.Litecoin.LitecoinNet; // All properties to be converted to JSON via JAXB @XmlAccessorType(XmlAccessType.FIELD) @@ -146,8 +144,6 @@ public class Settings { // Which blockchains this node is running private String blockchainConfig = null; // use default from resources - private BitcoinNet bitcoinNet = BitcoinNet.MAIN; - private LitecoinNet litecoinNet = LitecoinNet.MAIN; // Also crosschain-related: /** Whether to show SysTray pop-up notifications when trade-bot entries change state */ private boolean tradebotSystrayEnabled = false; @@ -440,14 +436,6 @@ public class Settings { return this.blockchainConfig; } - public BitcoinNet getBitcoinNet() { - return this.bitcoinNet; - } - - public LitecoinNet getLitecoinNet() { - return this.litecoinNet; - } - public boolean isTradebotSystrayEnabled() { return this.tradebotSystrayEnabled; } diff --git a/src/main/java/org/qortal/transaction/PresenceTransaction.java b/src/main/java/org/qortal/transaction/PresenceTransaction.java deleted file mode 100644 index 729270e0..00000000 --- a/src/main/java/org/qortal/transaction/PresenceTransaction.java +++ /dev/null @@ -1,256 +0,0 @@ -package org.qortal.transaction; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.function.Supplier; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.qortal.account.Account; -import org.qortal.controller.Controller; -import org.qortal.crosschain.ACCT; -import org.qortal.crosschain.SupportedBlockchain; -import org.qortal.crypto.Crypto; -import org.qortal.crypto.MemoryPoW; -import org.qortal.data.at.ATData; -import org.qortal.data.crosschain.CrossChainTradeData; -import org.qortal.data.transaction.PresenceTransactionData; -import org.qortal.data.transaction.TransactionData; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.PresenceTransactionTransformer; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Base58; -import org.qortal.utils.ByteArray; - -import com.google.common.primitives.Longs; - -public class PresenceTransaction extends Transaction { - - private static final Logger LOGGER = LogManager.getLogger(PresenceTransaction.class); - - // Properties - private PresenceTransactionData presenceTransactionData; - - // Other useful constants - public static final int POW_BUFFER_SIZE = 8 * 1024 * 1024; // bytes - public static final int POW_DIFFICULTY = 8; // leading zero bits - - public enum PresenceType { - REWARD_SHARE(0) { - @Override - public long getLifetime() { - return Controller.ONLINE_TIMESTAMP_MODULUS; - } - }, - TRADE_BOT(1) { - @Override - public long getLifetime() { - return 30 * 60 * 1000L; // 30 minutes in milliseconds - } - }; - - public final int value; - private static final Map map = stream(PresenceType.values()).collect(toMap(type -> type.value, type -> type)); - - PresenceType(int value) { - this.value = value; - } - - public abstract long getLifetime(); - - public static PresenceType valueOf(int value) { - return map.get(value); - } - - /** Returns PresenceType with matching name or null (instead of throwing IllegalArgumentException). */ - public static PresenceType fromString(String name) { - try { - return PresenceType.valueOf(name); - } catch (IllegalArgumentException e) { - return null; - } - } - } - - // Constructors - - public PresenceTransaction(Repository repository, TransactionData transactionData) { - super(repository, transactionData); - - this.presenceTransactionData = (PresenceTransactionData) this.transactionData; - } - - // More information - - @Override - public long getDeadline() { - return this.transactionData.getTimestamp() + this.presenceTransactionData.getPresenceType().getLifetime(); - } - - @Override - public List getRecipientAddresses() throws DataException { - return Collections.emptyList(); - } - - // Navigation - - public Account getSender() { - return this.getCreator(); - } - - // Processing - - public void computeNonce() throws DataException { - byte[] transactionBytes; - - try { - transactionBytes = TransactionTransformer.toBytesForSigning(this.transactionData); - } catch (TransformationException e) { - throw new RuntimeException("Unable to transform transaction to byte array for verification", e); - } - - // Clear nonce from transactionBytes - PresenceTransactionTransformer.clearNonce(transactionBytes); - - // Calculate nonce - this.presenceTransactionData.setNonce(MemoryPoW.compute2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY)); - } - - /** - * Returns whether PRESENCE transaction has valid txGroupId. - *

- * We insist on NO_GROUP. - */ - @Override - protected boolean isValidTxGroupId() throws DataException { - int txGroupId = this.transactionData.getTxGroupId(); - - return txGroupId == Group.NO_GROUP; - } - - @Override - public ValidationResult isFeeValid() throws DataException { - if (this.transactionData.getFee() < 0) - return ValidationResult.NEGATIVE_FEE; - - return ValidationResult.OK; - } - - @Override - public boolean hasValidReference() throws DataException { - return true; - } - - @Override - public ValidationResult isValid() throws DataException { - // Nonce checking is done via isSignatureValid() as that method is only called once per import - - // If we exist in the repository then we've been imported as unconfirmed, - // but we don't want to make it into a block, so return fake non-OK result. - if (this.repository.getTransactionRepository().exists(this.presenceTransactionData.getSignature())) - return ValidationResult.INVALID_BUT_OK; - - // We only support TRADE_BOT-type PRESENCE at this time - if (PresenceType.TRADE_BOT != this.presenceTransactionData.getPresenceType()) - return ValidationResult.NOT_YET_RELEASED; - - // Check timestamp signature - byte[] timestampSignature = this.presenceTransactionData.getTimestampSignature(); - byte[] timestampBytes = Longs.toByteArray(this.presenceTransactionData.getTimestamp()); - if (!Crypto.verify(this.transactionData.getCreatorPublicKey(), timestampSignature, timestampBytes)) - return ValidationResult.INVALID_TIMESTAMP_SIGNATURE; - - Map> acctSuppliersByCodeHash = SupportedBlockchain.getAcctMap(); - Set codeHashes = acctSuppliersByCodeHash.keySet(); - boolean isExecutable = true; - - List atsData = repository.getATRepository().getAllATsByFunctionality(codeHashes, isExecutable); - - // Convert signer's public key to address form - String signerAddress = Crypto.toAddress(this.transactionData.getCreatorPublicKey()); - - for (ATData atData : atsData) { - ByteArray atCodeHash = new ByteArray(atData.getCodeHash()); - Supplier acctSupplier = acctSuppliersByCodeHash.get(atCodeHash); - if (acctSupplier == null) - continue; - - CrossChainTradeData crossChainTradeData = acctSupplier.get().populateTradeData(repository, atData); - - // OK if signer's public key (in address form) matches Bob's trade public key (in address form) - if (signerAddress.equals(crossChainTradeData.qortalCreatorTradeAddress)) - return ValidationResult.OK; - - // OK if signer's public key (in address form) matches Alice's trade public key (in address form) - if (signerAddress.equals(crossChainTradeData.qortalPartnerAddress)) - return ValidationResult.OK; - } - - return ValidationResult.AT_UNKNOWN; - } - - @Override - public boolean isSignatureValid() { - byte[] signature = this.transactionData.getSignature(); - if (signature == null) - return false; - - byte[] transactionBytes; - - try { - transactionBytes = PresenceTransactionTransformer.toBytesForSigning(this.transactionData); - } catch (TransformationException e) { - throw new RuntimeException("Unable to transform transaction to byte array for verification", e); - } - - if (!Crypto.verify(this.transactionData.getCreatorPublicKey(), signature, transactionBytes)) - return false; - - int nonce = this.presenceTransactionData.getNonce(); - - // Clear nonce from transactionBytes - PresenceTransactionTransformer.clearNonce(transactionBytes); - - // Check nonce - return MemoryPoW.verify2(transactionBytes, POW_BUFFER_SIZE, POW_DIFFICULTY, nonce); - } - - /** - * Remove any PRESENCE transactions by the same signer that have older timestamps. - */ - @Override - protected void onImportAsUnconfirmed() throws DataException { - byte[] creatorPublicKey = this.transactionData.getCreatorPublicKey(); - List creatorsPresenceTransactions = this.repository.getTransactionRepository().getUnconfirmedTransactions(TransactionType.PRESENCE, creatorPublicKey); - - if (creatorsPresenceTransactions.isEmpty()) - return; - - for (TransactionData transactionData : creatorsPresenceTransactions) { - if (transactionData.getTimestamp() >= this.transactionData.getTimestamp()) - continue; - - LOGGER.debug(() -> String.format("Deleting older PRESENCE transaction %s", Base58.encode(transactionData.getSignature()))); - this.repository.getTransactionRepository().delete(transactionData); - } - } - - @Override - public void process() throws DataException { - throw new DataException("PRESENCE transactions should never be processed"); - } - - @Override - public void orphan() throws DataException { - throw new DataException("PRESENCE transactions should never be orphaned"); - } - -} diff --git a/src/main/java/org/qortal/transform/transaction/PresenceTransactionTransformer.java b/src/main/java/org/qortal/transform/transaction/PresenceTransactionTransformer.java deleted file mode 100644 index bf69d102..00000000 --- a/src/main/java/org/qortal/transform/transaction/PresenceTransactionTransformer.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.qortal.transform.transaction; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.nio.ByteBuffer; - -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.PresenceTransactionData; -import org.qortal.data.transaction.TransactionData; -import org.qortal.transaction.PresenceTransaction.PresenceType; -import org.qortal.transaction.Transaction.TransactionType; -import org.qortal.transform.TransformationException; -import org.qortal.utils.Serialization; - -import com.google.common.primitives.Ints; -import com.google.common.primitives.Longs; - -public class PresenceTransactionTransformer extends TransactionTransformer { - - // Property lengths - private static final int NONCE_LENGTH = INT_LENGTH; - private static final int PRESENCE_TYPE_LENGTH = BYTE_LENGTH; - private static final int TIMESTAMP_SIGNATURE_LENGTH = SIGNATURE_LENGTH; - - private static final int EXTRAS_LENGTH = NONCE_LENGTH + PRESENCE_TYPE_LENGTH + TIMESTAMP_SIGNATURE_LENGTH; - - protected static final TransactionLayout layout; - - static { - layout = new TransactionLayout(); - layout.add("txType: " + TransactionType.PRESENCE.valueString, TransformationType.INT); - layout.add("timestamp", TransformationType.TIMESTAMP); - layout.add("transaction's groupID", TransformationType.INT); - layout.add("reference", TransformationType.SIGNATURE); - layout.add("sender's public key", TransformationType.PUBLIC_KEY); - layout.add("proof-of-work nonce", TransformationType.INT); - layout.add("presence type (reward-share=0, trade-bot=1)", TransformationType.BYTE); - layout.add("timestamp-signature", TransformationType.SIGNATURE); - layout.add("fee", TransformationType.AMOUNT); - layout.add("signature", TransformationType.SIGNATURE); - } - - public static TransactionData fromByteBuffer(ByteBuffer byteBuffer) throws TransformationException { - long timestamp = byteBuffer.getLong(); - - int txGroupId = byteBuffer.getInt(); - - byte[] reference = new byte[REFERENCE_LENGTH]; - byteBuffer.get(reference); - - byte[] senderPublicKey = Serialization.deserializePublicKey(byteBuffer); - - int nonce = byteBuffer.getInt(); - - PresenceType presenceType = PresenceType.valueOf(byteBuffer.get()); - - byte[] timestampSignature = new byte[SIGNATURE_LENGTH]; - byteBuffer.get(timestampSignature); - - long fee = byteBuffer.getLong(); - - byte[] signature = new byte[SIGNATURE_LENGTH]; - byteBuffer.get(signature); - - BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, senderPublicKey, fee, signature); - - return new PresenceTransactionData(baseTransactionData, nonce, presenceType, timestampSignature); - } - - public static int getDataLength(TransactionData transactionData) { - return getBaseLength(transactionData) + EXTRAS_LENGTH; - } - - public static byte[] toBytes(TransactionData transactionData) throws TransformationException { - try { - PresenceTransactionData presenceTransactionData = (PresenceTransactionData) transactionData; - - ByteArrayOutputStream bytes = new ByteArrayOutputStream(); - - transformCommonBytes(transactionData, bytes); - - bytes.write(Ints.toByteArray(presenceTransactionData.getNonce())); - - bytes.write(presenceTransactionData.getPresenceType().value); - - bytes.write(presenceTransactionData.getTimestampSignature()); - - bytes.write(Longs.toByteArray(presenceTransactionData.getFee())); - - if (presenceTransactionData.getSignature() != null) - bytes.write(presenceTransactionData.getSignature()); - - return bytes.toByteArray(); - } catch (IOException | ClassCastException e) { - throw new TransformationException(e); - } - } - - public static void clearNonce(byte[] transactionBytes) { - int nonceIndex = TYPE_LENGTH + TIMESTAMP_LENGTH + GROUPID_LENGTH + REFERENCE_LENGTH + PUBLIC_KEY_LENGTH; - - transactionBytes[nonceIndex++] = (byte) 0; - transactionBytes[nonceIndex++] = (byte) 0; - transactionBytes[nonceIndex++] = (byte) 0; - transactionBytes[nonceIndex++] = (byte) 0; - } - -} diff --git a/src/test/java/org/qortal/test/PresenceTests.java b/src/test/java/org/qortal/test/PresenceTests.java deleted file mode 100644 index b53b72cb..00000000 --- a/src/test/java/org/qortal/test/PresenceTests.java +++ /dev/null @@ -1,133 +0,0 @@ -package org.qortal.test; - -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.asset.Asset; -import org.qortal.crosschain.BitcoinACCTv1; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.DeployAtTransactionData; -import org.qortal.data.transaction.PresenceTransactionData; -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.test.common.BlockUtils; -import org.qortal.test.common.Common; -import org.qortal.test.common.TransactionUtils; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transaction.PresenceTransaction; -import org.qortal.transaction.PresenceTransaction.PresenceType; -import org.qortal.transaction.Transaction; -import org.qortal.transaction.Transaction.ValidationResult; -import org.qortal.utils.NTP; - -import com.google.common.primitives.Longs; - -import static org.junit.Assert.*; - -public class PresenceTests extends Common { - - private static final byte[] BITCOIN_PKH = new byte[20]; - private static final byte[] HASH_OF_SECRET_B = new byte[32]; - - private PrivateKeyAccount signer; - private Repository repository; - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); - - this.repository = RepositoryManager.getRepository(); - this.signer = Common.getTestAccount(this.repository, "bob"); - - // We need to create corresponding test trade offer - byte[] creationBytes = BitcoinACCTv1.buildQortalAT(this.signer.getAddress(), BITCOIN_PKH, HASH_OF_SECRET_B, - 0L, 0L, - 7 * 24 * 60 * 60); - - long txTimestamp = NTP.getTime(); - byte[] lastReference = this.signer.getLastReference(); - - 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, this.signer.getPublicKey(), fee, null); - TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, 1L, Asset.QORT); - - Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); - - fee = deployAtTransaction.calcRecommendedFee(); - deployAtTransactionData.setFee(fee); - - TransactionUtils.signAndImportValid(this.repository, deployAtTransactionData, this.signer); - BlockUtils.mintBlock(this.repository); - } - - @After - public void afterTest() throws DataException { - if (this.repository != null) - this.repository.close(); - - this.repository = null; - } - - @Test - public void validityTests() throws DataException { - long timestamp = System.currentTimeMillis(); - byte[] timestampBytes = Longs.toByteArray(timestamp); - - byte[] timestampSignature = this.signer.sign(timestampBytes); - - assertTrue(isValid(Group.NO_GROUP, this.signer, timestamp, timestampSignature)); - - PrivateKeyAccount nonTrader = Common.getTestAccount(repository, "alice"); - assertFalse(isValid(Group.NO_GROUP, nonTrader, timestamp, timestampSignature)); - } - - @Test - public void newestOnlyTests() throws DataException { - long OLDER_TIMESTAMP = System.currentTimeMillis() - 2000L; - long NEWER_TIMESTAMP = OLDER_TIMESTAMP + 1000L; - - PresenceTransaction older = buildPresenceTransaction(Group.NO_GROUP, this.signer, OLDER_TIMESTAMP, null); - older.computeNonce(); - TransactionUtils.signAndImportValid(repository, older.getTransactionData(), this.signer); - - assertTrue(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature())); - - PresenceTransaction newer = buildPresenceTransaction(Group.NO_GROUP, this.signer, NEWER_TIMESTAMP, null); - newer.computeNonce(); - TransactionUtils.signAndImportValid(repository, newer.getTransactionData(), this.signer); - - assertTrue(this.repository.getTransactionRepository().exists(newer.getTransactionData().getSignature())); - assertFalse(this.repository.getTransactionRepository().exists(older.getTransactionData().getSignature())); - } - - private boolean isValid(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException { - Transaction transaction = buildPresenceTransaction(txGroupId, signer, timestamp, timestampSignature); - return transaction.isValidUnconfirmed() == ValidationResult.OK; - } - - private PresenceTransaction buildPresenceTransaction(int txGroupId, PrivateKeyAccount signer, long timestamp, byte[] timestampSignature) throws DataException { - int nonce = 0; - - byte[] reference = signer.getLastReference(); - byte[] creatorPublicKey = signer.getPublicKey(); - long fee = 0L; - - if (timestampSignature == null) - timestampSignature = this.signer.sign(Longs.toByteArray(timestamp)); - - BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, txGroupId, reference, creatorPublicKey, fee, null); - PresenceTransactionData transactionData = new PresenceTransactionData(baseTransactionData, nonce, PresenceType.TRADE_BOT, timestampSignature); - - return new PresenceTransaction(this.repository, transactionData); - } - -} diff --git a/src/test/java/org/qortal/test/RepositoryTests.java b/src/test/java/org/qortal/test/RepositoryTests.java index 434e03f0..269e2aa3 100644 --- a/src/test/java/org/qortal/test/RepositoryTests.java +++ b/src/test/java/org/qortal/test/RepositoryTests.java @@ -4,7 +4,6 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.Account; import org.qortal.asset.Asset; -import org.qortal.crosschain.BitcoinACCTv1; import org.qortal.crypto.Crypto; import org.qortal.repository.DataException; import org.qortal.repository.Repository; @@ -394,25 +393,6 @@ public class RepositoryTests extends Common { } } - /** Specifically test LATERAL() usage in AT repository */ - @Test - public void testAtLateral() { - try (final HSQLDBRepository hsqldb = (HSQLDBRepository) RepositoryManager.getRepository()) { - byte[] codeHash = BitcoinACCTv1.CODE_BYTES_HASH; - Boolean isFinished = null; - Integer dataByteOffset = null; - Long expectedValue = null; - Integer minimumFinalHeight = 2; - Integer limit = null; - Integer offset = null; - Boolean reverse = null; - - hsqldb.getATRepository().getMatchingFinalATStates(codeHash, isFinished, dataByteOffset, expectedValue, minimumFinalHeight, limit, offset, reverse); - } catch (DataException e) { - fail("HSQLDB bug #1580"); - } - } - /** Specifically test LATERAL() usage in Chat repository */ @Test public void testChatLateral() { diff --git a/src/test/java/org/qortal/test/api/CrossChainApiTests.java b/src/test/java/org/qortal/test/api/CrossChainApiTests.java deleted file mode 100644 index d4f25bce..00000000 --- a/src/test/java/org/qortal/test/api/CrossChainApiTests.java +++ /dev/null @@ -1,42 +0,0 @@ -package org.qortal.test.api; - -import org.junit.Before; -import org.junit.Test; -import org.qortal.api.ApiError; -import org.qortal.api.resource.CrossChainResource; -import org.qortal.crosschain.SupportedBlockchain; -import org.qortal.test.common.ApiCommon; - -public class CrossChainApiTests extends ApiCommon { - - private static final SupportedBlockchain SPECIFIC_BLOCKCHAIN = null; - - private CrossChainResource crossChainResource; - - @Before - public void buildResource() { - this.crossChainResource = (CrossChainResource) ApiCommon.buildResource(CrossChainResource.class); - } - - @Test - public void testGetTradeOffers() { - assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getTradeOffers(SPECIFIC_BLOCKCHAIN, limit, offset, reverse)); - } - - @Test - public void testGetCompletedTrades() { - long minimumTimestamp = System.currentTimeMillis(); - assertNoApiError((limit, offset, reverse) -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, minimumTimestamp, limit, offset, reverse)); - } - - @Test - public void testInvalidGetCompletedTrades() { - Integer limit = null; - Integer offset = null; - Boolean reverse = null; - - assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, -1L /*minimumTimestamp*/, limit, offset, reverse)); - assertApiError(ApiError.INVALID_CRITERIA, () -> this.crossChainResource.getCompletedTrades(SPECIFIC_BLOCKCHAIN, 0L /*minimumTimestamp*/, limit, offset, reverse)); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/BitcoinTests.java b/src/test/java/org/qortal/test/crosschain/BitcoinTests.java deleted file mode 100644 index af879e08..00000000 --- a/src/test/java/org/qortal/test/crosschain/BitcoinTests.java +++ /dev/null @@ -1,115 +0,0 @@ -package org.qortal.test.crosschain; - -import static org.junit.Assert.*; - -import java.util.Arrays; - -import org.bitcoinj.core.Transaction; -import org.bitcoinj.store.BlockStoreException; -import org.junit.After; -import org.junit.Before; -import org.junit.Test; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.repository.DataException; -import org.qortal.test.common.Common; - -public class BitcoinTests extends Common { - - private Bitcoin bitcoin; - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); // TestNet3 - bitcoin = Bitcoin.getInstance(); - } - - @After - public void afterTest() { - Bitcoin.resetForTesting(); - bitcoin = null; - } - - @Test - public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { - System.out.println(String.format("Starting BTC instance...")); - System.out.println(String.format("BTC instance started")); - - long before = System.currentTimeMillis(); - System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime())); - long afterFirst = System.currentTimeMillis(); - - System.out.println(String.format("Bitcoin median blocktime: %d", bitcoin.getMedianBlockTime())); - long afterSecond = System.currentTimeMillis(); - - long firstPeriod = afterFirst - before; - long secondPeriod = afterSecond - afterFirst; - - System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod)); - - assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod); - assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L); - } - - @Test - public void testFindHtlcSecret() throws ForeignBlockchainException { - // This actually exists on TEST3 but can take a while to fetch - String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - - byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); - byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress); - - assertNotNull(secret); - assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); - } - - @Test - public void testBuildSpend() { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - long amount = 1000L; - - Transaction transaction = bitcoin.buildSpend(xprv58, recipient, amount); - assertNotNull(transaction); - - // Check spent key caching doesn't affect outcome - - transaction = bitcoin.buildSpend(xprv58, recipient, amount); - assertNotNull(transaction); - } - - @Test - public void testGetWalletBalance() { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - Long balance = bitcoin.getWalletBalance(xprv58); - - assertNotNull(balance); - - System.out.println(bitcoin.format(balance)); - - // Check spent key caching doesn't affect outcome - - Long repeatBalance = bitcoin.getWalletBalance(xprv58); - - assertNotNull(repeatBalance); - - System.out.println(bitcoin.format(repeatBalance)); - - assertEquals(balance, repeatBalance); - } - - @Test - public void testGetUnusedReceiveAddress() throws ForeignBlockchainException { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - String address = bitcoin.getUnusedReceiveAddress(xprv58); - - assertNotNull(address); - - System.out.println(address); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java b/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java deleted file mode 100644 index b7e57cf3..00000000 --- a/src/test/java/org/qortal/test/crosschain/ElectrumXTests.java +++ /dev/null @@ -1,201 +0,0 @@ -package org.qortal.test.crosschain; - -import static org.junit.Assert.*; - -import java.security.Security; -import java.util.EnumMap; -import java.util.List; -import java.util.Map; - -import org.bitcoinj.core.Address; -import org.bitcoinj.params.TestNet3Params; -import org.bitcoinj.script.ScriptBuilder; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.junit.Test; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.BitcoinyTransaction; -import org.qortal.crosschain.ElectrumX; -import org.qortal.crosschain.TransactionHash; -import org.qortal.crosschain.UnspentOutput; -import org.qortal.crosschain.Bitcoin.BitcoinNet; -import org.qortal.crosschain.ElectrumX.Server.ConnectionType; -import org.qortal.utils.BitTwiddling; - -import com.google.common.hash.HashCode; - -public class ElectrumXTests { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - } - - 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); - } - - private ElectrumX getInstance() { - return new ElectrumX("Bitcoin-" + BitcoinNet.TEST3.name(), BitcoinNet.TEST3.getGenesisHash(), BitcoinNet.TEST3.getServers(), DEFAULT_ELECTRUMX_PORTS); - } - - @Test - public void testInstance() { - ElectrumX electrumX = getInstance(); - assertNotNull(electrumX); - } - - @Test - public void testGetCurrentHeight() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - int height = electrumX.getCurrentHeight(); - - assertTrue(height > 10000); - System.out.println("Current TEST3 height: " + height); - } - - @Test - public void testInvalidRequest() { - ElectrumX electrumX = getInstance(); - try { - electrumX.getRawBlockHeaders(-1, -1); - } catch (ForeignBlockchainException e) { - // Should throw due to negative start block height - return; - } - - fail("Negative start block height should cause error"); - } - - @Test - public void testGetRecentBlocks() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - int height = electrumX.getCurrentHeight(); - assertTrue(height > 10000); - - List recentBlockHeaders = electrumX.getRawBlockHeaders(height - 11, 11); - - System.out.println(String.format("Returned %d recent blocks", recentBlockHeaders.size())); - for (int i = 0; i < recentBlockHeaders.size(); ++i) { - byte[] blockHeader = recentBlockHeaders.get(i); - - // Timestamp(int) is at 4 + 32 + 32 = 68 bytes offset - int offset = 4 + 32 + 32; - int timestamp = BitTwiddling.intFromLEBytes(blockHeader, offset); - System.out.println(String.format("Block %d timestamp: %d", height + i, timestamp)); - } - } - - @Test - public void testGetP2PKHBalance() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - Address address = Address.fromString(TestNet3Params.get(), "n3GNqMveyvaPvUbH469vDRadqpJMPc84JA"); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - long balance = electrumX.getConfirmedBalance(script); - - assertTrue(balance > 0L); - - System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L))); - } - - @Test - public void testGetP2SHBalance() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - long balance = electrumX.getConfirmedBalance(script); - - assertTrue(balance > 0L); - - System.out.println(String.format("TestNet address %s has balance: %d sats / %d.%08d BTC", address, balance, (balance / 100000000L), (balance % 100000000L))); - } - - @Test - public void testGetUnspentOutputs() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - Address address = Address.fromString(TestNet3Params.get(), "2N4szZUfigj7fSBCEX4PaC8TVbC5EvidaVF"); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - List unspentOutputs = electrumX.getUnspentOutputs(script, false); - - assertFalse(unspentOutputs.isEmpty()); - - for (UnspentOutput unspentOutput : unspentOutputs) - System.out.println(String.format("TestNet address %s has unspent output at tx %s, output index %d", address, HashCode.fromBytes(unspentOutput.hash), unspentOutput.index)); - } - - @Test - public void testGetRawTransaction() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - byte[] txHash = HashCode.fromString("7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af").asBytes(); - - byte[] rawTransactionBytes = electrumX.getRawTransaction(txHash); - - assertFalse(rawTransactionBytes.length == 0); - } - - @Test - public void testGetUnknownRawTransaction() { - ElectrumX electrumX = getInstance(); - - byte[] txHash = HashCode.fromString("f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0").asBytes(); - - try { - electrumX.getRawTransaction(txHash); - fail("Bitcoin transaction should be unknown and hence throw exception"); - } catch (ForeignBlockchainException e) { - if (!(e instanceof ForeignBlockchainException.NotFoundException)) - fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); - } - } - - @Test - public void testGetTransaction() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - String txHash = "7653fea9ffcd829d45ed2672938419a94951b08175982021e77d619b553f29af"; - - BitcoinyTransaction transaction = electrumX.getTransaction(txHash); - - assertNotNull(transaction); - assertTrue(transaction.txHash.equals(txHash)); - } - - @Test - public void testGetUnknownTransaction() { - ElectrumX electrumX = getInstance(); - - String txHash = "f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0f0"; - - try { - electrumX.getTransaction(txHash); - fail("Bitcoin transaction should be unknown and hence throw exception"); - } catch (ForeignBlockchainException e) { - if (!(e instanceof ForeignBlockchainException.NotFoundException)) - fail("Bitcoin transaction should be unknown and hence throw NotFoundException"); - } - } - - @Test - public void testGetAddressTransactions() throws ForeignBlockchainException { - ElectrumX electrumX = getInstance(); - - Address address = Address.fromString(TestNet3Params.get(), "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - - List transactionHashes = electrumX.getAddressTransactions(script, false); - - assertFalse(transactionHashes.isEmpty()); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/HtlcTests.java b/src/test/java/org/qortal/test/crosschain/HtlcTests.java deleted file mode 100644 index 75b290bf..00000000 --- a/src/test/java/org/qortal/test/crosschain/HtlcTests.java +++ /dev/null @@ -1,128 +0,0 @@ -package org.qortal.test.crosschain; - -import static org.junit.Assert.*; - -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crypto.Crypto; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.repository.DataException; -import org.qortal.test.common.Common; - -import com.google.common.primitives.Longs; - -public class HtlcTests extends Common { - - private Bitcoin bitcoin; - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); // TestNet3 - bitcoin = Bitcoin.getInstance(); - } - - @After - public void afterTest() { - Bitcoin.resetForTesting(); - bitcoin = null; - } - - @Test - public void testFindHtlcSecret() throws ForeignBlockchainException { - // This actually exists on TEST3 but can take a while to fetch - String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - - byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); - byte[] secret = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress); - - assertNotNull(secret); - assertArrayEquals("secret incorrect", expectedSecret, secret); - } - - @Test - @Ignore(value = "Doesn't work, to be fixed later") - public void testHtlcSecretCaching() throws ForeignBlockchainException { - String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); - - do { - // We need to perform fresh setup for 1st test - Bitcoin.resetForTesting(); - bitcoin = Bitcoin.getInstance(); - - long now = System.currentTimeMillis(); - long timestampBoundary = now / 30_000L; - - byte[] secret1 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress); - long executionPeriod1 = System.currentTimeMillis() - now; - - assertNotNull(secret1); - assertArrayEquals("secret1 incorrect", expectedSecret, secret1); - - assertTrue("1st execution period should not be instant!", executionPeriod1 > 10); - - byte[] secret2 = BitcoinyHTLC.findHtlcSecret(bitcoin, p2shAddress); - long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1; - - assertNotNull(secret2); - assertArrayEquals("secret2 incorrect", expectedSecret, secret2); - - // Test is only valid if we've called within same timestampBoundary - if (System.currentTimeMillis() / 30_000L != timestampBoundary) - continue; - - assertArrayEquals(secret1, secret2); - - assertTrue("2st execution period should be effectively instant!", executionPeriod2 < 10); - } while (false); - } - - @Test - public void testDetermineHtlcStatus() throws ForeignBlockchainException { - // This actually exists on TEST3 but can take a while to fetch - String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - - BitcoinyHTLC.Status htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L); - assertNotNull(htlcStatus); - - System.out.println(String.format("HTLC %s status: %s", p2shAddress, htlcStatus.name())); - } - - @Test - public void testHtlcStatusCaching() throws ForeignBlockchainException { - do { - // We need to perform fresh setup for 1st test - Bitcoin.resetForTesting(); - bitcoin = Bitcoin.getInstance(); - - long now = System.currentTimeMillis(); - long timestampBoundary = now / 30_000L; - - // Won't ever exist - String p2shAddress = bitcoin.deriveP2shAddress(Crypto.hash160(Longs.toByteArray(now))); - - BitcoinyHTLC.Status htlcStatus1 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L); - long executionPeriod1 = System.currentTimeMillis() - now; - - assertNotNull(htlcStatus1); - assertTrue("1st execution period should not be instant!", executionPeriod1 > 10); - - BitcoinyHTLC.Status htlcStatus2 = BitcoinyHTLC.determineHtlcStatus(bitcoin.getBlockchainProvider(), p2shAddress, 1L); - long executionPeriod2 = System.currentTimeMillis() - now - executionPeriod1; - - assertNotNull(htlcStatus2); - assertEquals(htlcStatus1, htlcStatus2); - - // Test is only valid if we've called within same timestampBoundary - if (System.currentTimeMillis() / 30_000L != timestampBoundary) - continue; - - assertTrue("2st execution period should be effectively instant!", executionPeriod2 < 10); - } while (false); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/LitecoinTests.java b/src/test/java/org/qortal/test/crosschain/LitecoinTests.java deleted file mode 100644 index 64837347..00000000 --- a/src/test/java/org/qortal/test/crosschain/LitecoinTests.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.qortal.test.crosschain; - -import static org.junit.Assert.*; - -import java.util.Arrays; - -import org.bitcoinj.core.Transaction; -import org.bitcoinj.store.BlockStoreException; -import org.junit.After; -import org.junit.Before; -import org.junit.Ignore; -import org.junit.Test; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.repository.DataException; -import org.qortal.test.common.Common; - -public class LitecoinTests extends Common { - - private Litecoin litecoin; - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); // TestNet3 - litecoin = Litecoin.getInstance(); - } - - @After - public void afterTest() { - Litecoin.resetForTesting(); - litecoin = null; - } - - @Test - public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException { - long before = System.currentTimeMillis(); - System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime())); - long afterFirst = System.currentTimeMillis(); - - System.out.println(String.format("Bitcoin median blocktime: %d", litecoin.getMedianBlockTime())); - long afterSecond = System.currentTimeMillis(); - - long firstPeriod = afterFirst - before; - long secondPeriod = afterSecond - afterFirst; - - System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod)); - - assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod); - assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L); - } - - @Test - @Ignore(value = "Doesn't work, to be fixed later") - public void testFindHtlcSecret() throws ForeignBlockchainException { - // This actually exists on TEST3 but can take a while to fetch - String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - - byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes(); - byte[] secret = BitcoinyHTLC.findHtlcSecret(litecoin, p2shAddress); - - assertNotNull("secret not found", secret); - assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret)); - } - - @Test - public void testBuildSpend() { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE"; - long amount = 1000L; - - Transaction transaction = litecoin.buildSpend(xprv58, recipient, amount); - assertNotNull("insufficient funds", transaction); - - // Check spent key caching doesn't affect outcome - - transaction = litecoin.buildSpend(xprv58, recipient, amount); - assertNotNull("insufficient funds", transaction); - } - - @Test - public void testGetWalletBalance() { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - Long balance = litecoin.getWalletBalance(xprv58); - - assertNotNull(balance); - - System.out.println(litecoin.format(balance)); - - // Check spent key caching doesn't affect outcome - - Long repeatBalance = litecoin.getWalletBalance(xprv58); - - assertNotNull(repeatBalance); - - System.out.println(litecoin.format(repeatBalance)); - - assertEquals(balance, repeatBalance); - } - - @Test - public void testGetUnusedReceiveAddress() throws ForeignBlockchainException { - String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ"; - - String address = litecoin.getUnusedReceiveAddress(xprv58); - - assertNotNull(address); - - System.out.println(address); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java deleted file mode 100644 index fa92fde7..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/BuildHTLC.java +++ /dev/null @@ -1,114 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.script.Script.ScriptType; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.BitcoinyHTLC; - -import com.google.common.hash.HashCode; - -public class BuildHTLC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: BuildHTLC (-b | -l) ")); - System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); - System.err.println(String.format("example: BuildHTLC -l " - + "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\t0.00008642 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600000000")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 6 || args.length > 6) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = null; - NetworkParameters params = null; - - Address refundAddress = null; - Coin amount = null; - Address redeemAddress = null; - byte[] hashOfSecret = null; - int lockTime = 0; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - params = bitcoiny.getNetworkParameters(); - - refundAddress = Address.fromString(params, args[argIndex++]); - if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund address must be in P2PKH form"); - - amount = Coin.parseCoin(args[argIndex++]); - - redeemAddress = Address.fromString(params, args[argIndex++]); - if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem address must be in P2PKH form"); - - hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes(); - if (hashOfSecret.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - int refundTimeoutDelay = lockTime - (int) (System.currentTimeMillis() / 1000L); - if (refundTimeoutDelay < 600 || refundTimeoutDelay > 30 * 24 * 60 * 60) - usage("Locktime (seconds) should be at between 10 minutes and 1 month from now"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); - if (p2shFee.isZero()) - return; - - System.out.println(String.format("Refund address: %s", refundAddress)); - System.out.println(String.format("Amount: %s", amount.toPlainString())); - System.out.println(String.format("Redeem address: %s", redeemAddress)); - System.out.println(String.format("Refund/redeem miner's fee: %s", bitcoiny.format(p2shFee))); - System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret))); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret); - System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); - - String p2shAddress = bitcoiny.deriveP2shAddress(redeemScriptBytes); - System.out.println(String.format("P2SH address: %s", p2shAddress)); - - amount = amount.add(p2shFee); - - // Fund P2SH - System.out.println(String.format("\nYou need to fund %s with %s (includes redeem/refund fee of %s)", - p2shAddress, bitcoiny.format(amount), bitcoiny.format(p2shFee))); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java deleted file mode 100644 index 8b1cc423..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/CheckHTLC.java +++ /dev/null @@ -1,135 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.script.Script.ScriptType; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; - -import com.google.common.hash.HashCode; - -public class CheckHTLC { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: CheckHTLC (-b | -l) ")); - System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); - System.err.println(String.format("example: CheckP2SH -l " - + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" - + "msAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\t0.00008642 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600184800")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 7 || args.length > 7) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = null; - NetworkParameters params = null; - - Address p2shAddress = null; - Address refundAddress = null; - Coin amount = null; - Address redeemAddress = null; - byte[] hashOfSecret = null; - int lockTime = 0; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - params = bitcoiny.getNetworkParameters(); - - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundAddress = Address.fromString(params, args[argIndex++]); - if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund address must be in P2PKH form"); - - amount = Coin.parseCoin(args[argIndex++]); - - redeemAddress = Address.fromString(params, args[argIndex++]); - if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem address must be in P2PKH form"); - - hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes(); - if (hashOfSecret.length != 20) - usage("Hash of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); - if (p2shFee.isZero()) - return; - - System.out.println(String.format("P2SH address: %s", p2shAddress)); - System.out.println(String.format("Refund PKH: %s", refundAddress)); - System.out.println(String.format("Redeem/refund amount: %s", amount.toPlainString())); - System.out.println(String.format("Redeem PKH: %s", redeemAddress)); - System.out.println(String.format("Hash of secret: %s", HashCode.fromBytes(hashOfSecret))); - System.out.println(String.format("Script lockTime: %s (%d)", LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC), lockTime)); - - System.out.println(String.format("Redeem/refund miner's fee: %s", bitcoiny.format(p2shFee))); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret); - System.out.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - amount = amount.add(p2shFee); - - // Check network's median block time - int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null); - if (medianBlockTime == 0) - return; - - // Check P2SH is funded - Common.getBalance(bitcoiny, p2shAddress.toString()); - - // Grab all unspent outputs - Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); - - Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), amount.value); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/Common.java b/src/test/java/org/qortal/test/crosschain/apps/Common.java deleted file mode 100644 index 78066fe7..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/Common.java +++ /dev/null @@ -1,158 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.security.Security; -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.util.Collections; -import java.util.List; - -import org.bitcoinj.core.Transaction; -import org.bitcoinj.core.TransactionOutput; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.settings.Settings; -import org.qortal.utils.NTP; - -import com.google.common.hash.HashCode; - -public abstract class Common { - - public static void init() { - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - NTP.setFixedOffset(0L); - } - - public static long getP2shFee(Bitcoiny bitcoiny) { - long p2shFee; - - try { - p2shFee = bitcoiny.getP2shFee(null); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to determine P2SH fee: %s", e.getMessage())); - return 0; - } - - return p2shFee; - } - - public static int checkMedianBlockTime(Bitcoiny bitcoiny, Integer lockTime) { - int medianBlockTime; - - try { - medianBlockTime = bitcoiny.getMedianBlockTime(); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to determine median block time: %s", e.getMessage())); - return 0; - } - - System.out.println(String.format("Median block time: %s", LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - - long now = System.currentTimeMillis(); - - if (now < medianBlockTime * 1000L) { - System.out.println(String.format("Too soon (%s) based on median block time %s", - LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), - LocalDateTime.ofInstant(Instant.ofEpochSecond(medianBlockTime), ZoneOffset.UTC))); - return 0; - } - - if (lockTime != null && now < lockTime * 1000L) { - System.err.println(String.format("Too soon (%s) based on lockTime %s", - LocalDateTime.ofInstant(Instant.ofEpochMilli(now), ZoneOffset.UTC), - LocalDateTime.ofInstant(Instant.ofEpochSecond(lockTime), ZoneOffset.UTC))); - return 0; - } - - return medianBlockTime; - } - - public static long getBalance(Bitcoiny bitcoiny, String address58) { - long balance; - - try { - balance = bitcoiny.getConfirmedBalance(address58); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to check address %s balance: %s", address58, e.getMessage())); - return 0; - } - - System.out.println(String.format("Address %s balance: %s", address58, bitcoiny.format(balance))); - - return balance; - } - - public static List getUnspentOutputs(Bitcoiny bitcoiny, String address58) { - List unspentOutputs = Collections.emptyList(); - - try { - unspentOutputs = bitcoiny.getUnspentOutputs(address58); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Can't find unspent outputs for %s: %s", address58, e.getMessage())); - return unspentOutputs; - } - - System.out.println(String.format("Found %d output%s for %s", - unspentOutputs.size(), - (unspentOutputs.size() != 1 ? "s" : ""), - address58)); - - for (TransactionOutput fundingOutput : unspentOutputs) - System.out.println(String.format("Output %s:%d amount %s", - HashCode.fromBytes(fundingOutput.getParentTransactionHash().getBytes()), fundingOutput.getIndex(), - bitcoiny.format(fundingOutput.getValue()))); - - if (unspentOutputs.isEmpty()) - System.err.println(String.format("Can't use spent/unfunded %s", address58)); - - if (unspentOutputs.size() != 1) - System.err.println(String.format("Expecting only one unspent output?")); - - return unspentOutputs; - } - - public static BitcoinyHTLC.Status determineHtlcStatus(Bitcoiny bitcoiny, String address58, long minimumAmount) { - BitcoinyHTLC.Status htlcStatus = null; - - try { - htlcStatus = BitcoinyHTLC.determineHtlcStatus(bitcoiny.getBlockchainProvider(), address58, minimumAmount); - - System.out.println(String.format("HTLC status: %s", htlcStatus.name())); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Unable to determine HTLC status: %s", e.getMessage())); - } - - return htlcStatus; - } - - public static void broadcastTransaction(Bitcoiny bitcoiny, Transaction transaction) { - byte[] rawTransactionBytes = transaction.bitcoinSerialize(); - - System.out.println(String.format("%nRaw transaction bytes:%n%s%n", HashCode.fromBytes(rawTransactionBytes).toString())); - - for (int countDown = 5; countDown >= 1; --countDown) { - System.out.print(String.format("\rBroadcasting transaction in %d second%s... use CTRL-C to abort ", countDown, (countDown != 1 ? "s" : ""))); - try { - Thread.sleep(1000L); - } catch (InterruptedException e) { - System.exit(0); - } - } - System.out.println("Broadcasting transaction... "); - - try { - bitcoiny.broadcastTransaction(transaction); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Failed to broadcast transaction: %s", e.getMessage())); - System.exit(1); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/GetNextReceiveAddress.java b/src/test/java/org/qortal/test/crosschain/apps/GetNextReceiveAddress.java deleted file mode 100644 index ef22355b..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/GetNextReceiveAddress.java +++ /dev/null @@ -1,78 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.security.Security; - -import org.bitcoinj.core.AddressFormatException; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.settings.Settings; - -public class GetNextReceiveAddress { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: GetNextReceiveAddress (-b | -l) ")); - System.err.println(String.format("example (testnet): GetNextReceiveAddress -l tpubD6NzVbkrYhZ4X3jV96Wo3Kr8Au2v9cUUEmPRk1smwduFrRVfBjkkw49rRYjgff1fGSktFMfabbvv8b1dmfyLjjbDax6QGyxpsNsx5PXukCB")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 2) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoiny bitcoiny = null; - String key58 = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - - key58 = args[argIndex++]; - - if (!bitcoiny.isValidDeterministicKey(key58)) - usage("Not valid xprv/xpub/tprv/tpub"); - } catch (NumberFormatException | AddressFormatException e) { - usage(String.format("Argument format exception: %s", e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - String receiveAddress = null; - try { - receiveAddress = bitcoiny.getUnusedReceiveAddress(key58); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Failed to determine next receive address: %s", e.getMessage())); - System.exit(1); - } - - System.out.println(String.format("Next receive address: %s", receiveAddress)); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/GetTransaction.java b/src/test/java/org/qortal/test/crosschain/apps/GetTransaction.java deleted file mode 100644 index 9d903a56..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/GetTransaction.java +++ /dev/null @@ -1,84 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.security.Security; -import java.util.List; - -import org.bitcoinj.core.AddressFormatException; -import org.bitcoinj.core.TransactionOutput; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.ForeignBlockchainException; -import org.qortal.crosschain.Litecoin; -import org.qortal.settings.Settings; - -import com.google.common.hash.HashCode; - -public class GetTransaction { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: GetTransaction (-b | -l) ")); - System.err.println(String.format("example (mainnet): GetTransaction -b 816407e79568f165f13e09e9912c5f2243e0a23a007cec425acedc2e89284660")); - System.err.println(String.format("example (testnet): GetTransaction -b 3bfd17a492a4e3d6cb7204e17e20aca6c1ab82e1828bd1106eefbaf086fb8a4e")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 2) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoiny bitcoiny = null; - byte[] transactionId = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - - transactionId = HashCode.fromString(args[argIndex++]).asBytes(); - } catch (NumberFormatException | AddressFormatException e) { - usage(String.format("Argument format exception: %s", e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - // Grab all outputs from transaction - List fundingOutputs; - try { - fundingOutputs = bitcoiny.getOutputs(transactionId); - } catch (ForeignBlockchainException e) { - System.out.println(String.format("Transaction not found (or error occurred)")); - return; - } - - System.out.println(String.format("Found %d output%s", fundingOutputs.size(), (fundingOutputs.size() != 1 ? "s" : ""))); - - for (TransactionOutput fundingOutput : fundingOutputs) - System.out.println(String.format("Output %d: %s", fundingOutput.getIndex(), fundingOutput.getValue().toPlainString())); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/GetWalletTransactions.java b/src/test/java/org/qortal/test/crosschain/apps/GetWalletTransactions.java deleted file mode 100644 index 7a880b1a..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/GetWalletTransactions.java +++ /dev/null @@ -1,82 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.security.Security; -import java.util.Comparator; -import java.util.List; -import java.util.stream.Collectors; - -import org.bitcoinj.core.AddressFormatException; -import org.bouncycastle.jce.provider.BouncyCastleProvider; -import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import org.qortal.crosschain.*; -import org.qortal.settings.Settings; - -public class GetWalletTransactions { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: GetWalletTransactions (-b | -l) ")); - System.err.println(String.format("example (testnet): GetWalletTransactions -l tpubD6NzVbkrYhZ4X3jV96Wo3Kr8Au2v9cUUEmPRk1smwduFrRVfBjkkw49rRYjgff1fGSktFMfabbvv8b1dmfyLjjbDax6QGyxpsNsx5PXukCB")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 2) - usage(null); - - Security.insertProviderAt(new BouncyCastleProvider(), 0); - Security.insertProviderAt(new BouncyCastleJsseProvider(), 1); - - Settings.fileInstance("settings-test.json"); - - Bitcoiny bitcoiny = null; - String key58 = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - - key58 = args[argIndex++]; - - if (!bitcoiny.isValidDeterministicKey(key58)) - usage("Not valid xprv/xpub/tprv/tpub"); - } catch (NumberFormatException | AddressFormatException e) { - usage(String.format("Argument format exception: %s", e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - // Grab all outputs from transaction - List transactions = null; - try { - transactions = bitcoiny.getWalletTransactions(key58); - } catch (ForeignBlockchainException e) { - System.err.println(String.format("Failed to obtain wallet transactions: %s", e.getMessage())); - System.exit(1); - } - - System.out.println(String.format("Found %d transaction%s", transactions.size(), (transactions.size() != 1 ? "s" : ""))); - - for (SimpleTransaction transaction : transactions.stream().sorted(Comparator.comparingInt(SimpleTransaction::getTimestamp)).collect(Collectors.toList())) - System.out.println(String.format("%s", transaction)); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/Pay.java b/src/test/java/org/qortal/test/crosschain/apps/Pay.java deleted file mode 100644 index 93c7aede..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/Pay.java +++ /dev/null @@ -1,80 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.Coin; -import org.bitcoinj.core.NetworkParameters; -import org.bitcoinj.core.Transaction; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.Litecoin; - -public class Pay { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: Pay (-b | -l) ")); - System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); - System.err.println(String.format("example: Pay -l " - + "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ \\\n" - + "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\t0.00008642")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 4 || args.length > 4) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = null; - NetworkParameters params = null; - - String xprv58 = null; - Address address = null; - Coin amount = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - params = bitcoiny.getNetworkParameters(); - - xprv58 = args[argIndex++]; - if (!bitcoiny.isValidDeterministicKey(xprv58)) - usage("xprv invalid"); - - address = Address.fromString(params, args[argIndex++]); - - amount = Coin.parseCoin(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - System.out.println(String.format("Address: %s", address)); - System.out.println(String.format("Amount: %s", amount.toPlainString())); - - Transaction transaction = bitcoiny.buildSpend(xprv58, address.toString(), amount.value); - if (transaction == null) { - System.err.println("Insufficent funds"); - System.exit(1); - } - - Common.broadcastTransaction(bitcoiny, transaction); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java deleted file mode 100644 index d4f1bcf1..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/RedeemHTLC.java +++ /dev/null @@ -1,166 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.util.Arrays; -import java.util.List; - -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.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; - -import com.google.common.hash.HashCode; - -public class RedeemHTLC { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: Redeem (-b | -l) ")); - System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); - System.err.println(String.format("example: Redeem -l " - + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" - + "\tmsAfaDaJ8JiprxxFaAXEEPxKK3JaZCYpLv \\\n" - + "\tefdaed23c4bc85c8ccae40d774af3c2a10391c648b6420cdd83cd44c27fcb5955201c64e372d \\\n" - + "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" - + "\t1600184800 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 7 || args.length > 7) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = null; - NetworkParameters params = null; - - Address p2shAddress = null; - Address refundAddress = null; - byte[] redeemPrivateKey = null; - byte[] secret = null; - int lockTime = 0; - Address outputAddress = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - params = bitcoiny.getNetworkParameters(); - - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundAddress = Address.fromString(params, args[argIndex++]); - if (refundAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Refund address must be in P2PKH form"); - - redeemPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - // Auto-trim - if (redeemPrivateKey.length >= 37 && redeemPrivateKey.length <= 38) - redeemPrivateKey = Arrays.copyOfRange(redeemPrivateKey, 1, 33); - if (redeemPrivateKey.length != 32) - usage("Redeem private key must be 32 bytes"); - - secret = HashCode.fromString(args[argIndex++]).asBytes(); - if (secret.length == 0) - usage("Invalid secret bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - - outputAddress = Address.fromString(params, args[argIndex++]); - if (outputAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Output address invalid"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); - if (p2shFee.isZero()) - return; - - System.out.println(String.format("Attempting to redeem HTLC %s to %s", p2shAddress, outputAddress)); - - byte[] hashOfSecret = Crypto.hash160(secret); - - ECKey redeemKey = ECKey.fromPrivate(redeemPrivateKey); - Address redeemAddress = Address.fromKey(params, redeemKey, ScriptType.P2PKH); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - return; - } - - // Actual live processing... - - int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, null); - if (medianBlockTime == 0) - return; - - // Check P2SH is funded - long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString()); - if (p2shBalance == 0) - return; - - // Grab all unspent outputs - List unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); - if (unspentOutputs.isEmpty()) - return; - - Coin redeemAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); - - BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), redeemAmount.value); - if (htlcStatus == null) - return; - - if (htlcStatus != BitcoinyHTLC.Status.FUNDED) { - System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name())); - System.exit(2); - return; - } - - System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(redeemAmount), bitcoiny.format(p2shFee))); - - Transaction redeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey, - unspentOutputs, redeemScriptBytes, secret, outputAddress.getHash()); - - Common.broadcastTransaction(bitcoiny, redeemTransaction); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java b/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java deleted file mode 100644 index 723185f0..00000000 --- a/src/test/java/org/qortal/test/crosschain/apps/RefundHTLC.java +++ /dev/null @@ -1,163 +0,0 @@ -package org.qortal.test.crosschain.apps; - -import java.util.Arrays; -import java.util.List; - -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.TransactionOutput; -import org.bitcoinj.script.Script.ScriptType; -import org.qortal.crosschain.Litecoin; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.crosschain.BitcoinyHTLC; -import org.qortal.crypto.Crypto; - -import com.google.common.hash.HashCode; - -public class RefundHTLC { - - static { - // This must go before any calls to LogManager/Logger - System.setProperty("java.util.logging.manager", "org.apache.logging.log4j.jul.LogManager"); - } - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: RefundHTLC (-b | -l) ")); - System.err.println("where: -b means use Bitcoin, -l means use Litecoin"); - System.err.println(String.format("example: RefundHTLC -l " - + "2N4378NbEVGjmiUmoUD9g1vCY6kyx9tDUJ6 \\\n" - + "\tef8f31b49c31b4a140aebcd9605fded88cc2dad0844c4b984f9191a5a416f72d3801e16447b0 \\\n" - + "\tmrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600184800 \\\n" - + "\tmoJtbbhs7T4Z5hmBH2iyKhGrCWBzQWS2CL")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length < 7 || args.length > 7) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = null; - NetworkParameters params = null; - - Address p2shAddress = null; - byte[] refundPrivateKey = null; - Address redeemAddress = null; - byte[] hashOfSecret = null; - int lockTime = 0; - Address outputAddress = null; - - int argIndex = 0; - try { - switch (args[argIndex++]) { - case "-b": - bitcoiny = Bitcoin.getInstance(); - break; - - case "-l": - bitcoiny = Litecoin.getInstance(); - break; - - default: - usage("Only Bitcoin (-b) or Litecoin (-l) supported"); - } - params = bitcoiny.getNetworkParameters(); - - p2shAddress = Address.fromString(params, args[argIndex++]); - if (p2shAddress.getOutputScriptType() != ScriptType.P2SH) - usage("P2SH address invalid"); - - refundPrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - // Auto-trim - if (refundPrivateKey.length >= 37 && refundPrivateKey.length <= 38) - refundPrivateKey = Arrays.copyOfRange(refundPrivateKey, 1, 33); - if (refundPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - redeemAddress = Address.fromString(params, args[argIndex++]); - if (redeemAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Redeem address must be in P2PKH form"); - - hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes(); - if (hashOfSecret.length != 20) - usage("HASH160 of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - - outputAddress = Address.fromString(params, args[argIndex++]); - if (outputAddress.getOutputScriptType() != ScriptType.P2PKH) - usage("Output address invalid"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - System.out.println(String.format("Using %s", bitcoiny.getBlockchainProvider().getNetId())); - - Coin p2shFee = Coin.valueOf(Common.getP2shFee(bitcoiny)); - if (p2shFee.isZero()) - return; - - System.out.println(String.format("Attempting to refund HTLC %s to %s", p2shAddress, outputAddress)); - - ECKey refundKey = ECKey.fromPrivate(refundPrivateKey); - Address refundAddress = Address.fromKey(params, refundKey, ScriptType.P2PKH); - - byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(refundAddress.getHash(), lockTime, redeemAddress.getHash(), hashOfSecret); - - byte[] redeemScriptHash = Crypto.hash160(redeemScriptBytes); - Address derivedP2shAddress = LegacyAddress.fromScriptHash(params, redeemScriptHash); - - if (!derivedP2shAddress.equals(p2shAddress)) { - System.err.println(String.format("Raw script bytes: %s", HashCode.fromBytes(redeemScriptBytes))); - System.err.println(String.format("Derived P2SH address %s does not match given address %s", derivedP2shAddress, p2shAddress)); - System.exit(2); - } - - // Actual live processing... - - int medianBlockTime = Common.checkMedianBlockTime(bitcoiny, lockTime); - if (medianBlockTime == 0) - return; - - // Check P2SH is funded - long p2shBalance = Common.getBalance(bitcoiny, p2shAddress.toString()); - if (p2shBalance == 0) - return; - - // Grab all unspent outputs - List unspentOutputs = Common.getUnspentOutputs(bitcoiny, p2shAddress.toString()); - if (unspentOutputs.isEmpty()) - return; - - Coin refundAmount = Coin.valueOf(p2shBalance).subtract(p2shFee); - - BitcoinyHTLC.Status htlcStatus = Common.determineHtlcStatus(bitcoiny, p2shAddress.toString(), refundAmount.value); - if (htlcStatus == null) - return; - - if (htlcStatus != BitcoinyHTLC.Status.FUNDED) { - System.err.println(String.format("Expecting %s HTLC status, but actual status is %s", "FUNDED", htlcStatus.name())); - System.exit(2); - return; - } - - System.out.println(String.format("Spending %s of outputs, with %s as mining fee", bitcoiny.format(refundAmount), bitcoiny.format(p2shFee))); - - Transaction refundTransaction = BitcoinyHTLC.buildRefundTransaction(bitcoiny.getNetworkParameters(), refundAmount, refundKey, - unspentOutputs, redeemScriptBytes, lockTime, outputAddress.getHash()); - - Common.broadcastTransaction(bitcoiny, refundTransaction); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java deleted file mode 100644 index 4487e874..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/BitcoinACCTv1Tests.java +++ /dev/null @@ -1,795 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import static org.junit.Assert.*; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.function.Function; - -import org.junit.Before; -import org.junit.Test; -import org.qortal.account.Account; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.asset.Asset; -import org.qortal.block.Block; -import org.qortal.crosschain.BitcoinACCTv1; -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.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.test.common.BlockUtils; -import org.qortal.test.common.Common; -import org.qortal.test.common.TransactionUtils; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transaction.MessageTransaction; -import org.qortal.utils.Amounts; - -import com.google.common.hash.HashCode; -import com.google.common.primitives.Bytes; - -public class BitcoinACCTv1Tests extends Common { - - public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes(); - public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a - public static final byte[] secretB = "This string is roughly 32 bytes?".getBytes(); - public static final byte[] hashOfSecretB = Crypto.hash160(secretB); // 31f0dd71decf59bbc8ef0661f4030479255cfa58 - public static final byte[] bitcoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); - public static final int tradeTimeout = 20; // blocks - public static final long redeemAmount = 80_40200000L; - public static final long fundingAmount = 123_45600000L; - public static final long bitcoinAmount = 864200L; // 0.00864200 BTC - - private static final Random RANDOM = new Random(); - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); - } - - @Test - public void testCompile() { - PrivateKeyAccount tradeAccount = createTradeAccount(null); - - byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); - assertNotNull(creationBytes); - - System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); - } - - @Test - public void testDeploy() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - - long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee(); - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = fundingAmount; - actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); - - assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = partnersInitialBalance; - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance); - - // Test orphaning - BlockUtils.orphanLastBlock(repository); - - expectedBalance = deployersInitialBalance; - actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = 0; - actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); - - assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = partnersInitialBalance; - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testOfferCancel() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - // Send creator's address to AT, instead of typical partner's address - byte[] messageData = BitcoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress()); - MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); - long messageFee = messageTransaction.getTransactionData().getFee(); - - // AT should process 'cancel' message in next block - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in CANCELLED mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.CANCELLED, tradeData.mode); - - // Check balances - long expectedMinimumBalance = deployersPostDeploymentBalance; - long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee; - - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance); - assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance); - - // Test orphaning - BlockUtils.orphanLastBlock(repository); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance - messageFee; - actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testOfferCancelInvalidLength() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - // Instead of sending creator's address to AT, send too-short/invalid message - byte[] messageData = new byte[7]; - RANDOM.nextBytes(messageData); - MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); - long messageFee = messageTransaction.getTransactionData().getFee(); - - // AT should process 'cancel' message in next block - // As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in CANCELLED mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.CANCELLED, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testTradingInfoProcessing() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - Block postDeploymentBlock = BlockUtils.mintBlock(repository); - int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - describeAt(repository, atAddress); - - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - - // AT should be in TRADE mode - assertEquals(AcctMode.TRADING, tradeData.mode); - - // Check hashOfSecretA was extracted correctly - assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA)); - - // Check trade partner Qortal address was extracted correctly - assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress); - - // Check trade partner's Bitcoin PKH was extracted correctly - assertTrue(Arrays.equals(bitcoinPublicKeyHash, tradeData.partnerForeignPKH)); - - // Test orphaning - BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance; - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - // TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) - @SuppressWarnings("unused") - @Test - public void testIncorrectTradeSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT BUT NOT FROM AT CREATOR - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); - - BlockUtils.mintBlock(repository); - - long expectedBalance = partnersInitialBalance; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance); - - describeAt(repository, atAddress); - - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - - // AT should still be in OFFER mode - assertEquals(AcctMode.OFFERING, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testAutomaticTradeRefund() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - Block postDeploymentBlock = BlockUtils.mintBlock(repository); - int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight(); - - // Check refund - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in REFUNDED mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.REFUNDED, tradeData.mode); - - // Test orphaning - BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance; - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretsCorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secrets to AT, from correct account - messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress()); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in REDEEMED mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.REDEEMED, tradeData.mode); - - // Check balances - long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance); - - // Orphan redeem - BlockUtils.orphanLastBlock(repository); - - // Check balances - expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance); - - // Check AT state - ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress); - - assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData())); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretsIncorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secrets to AT, but from wrong account - messageData = BitcoinACCTv1.buildRedeemMessage(secretA, secretB, partner.getAddress()); - messageTransaction = sendMessage(repository, bystander, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should still be in TRADE mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - - // Check balances - long expectedBalance = partnersInitialBalance; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); - - // Check eventual refund - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - } - } - - @SuppressWarnings("unused") - @Test - public void testIncorrectSecretsCorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send incorrect secrets to AT, from correct account - byte[] wrongSecret = new byte[32]; - RANDOM.nextBytes(wrongSecret); - messageData = BitcoinACCTv1.buildRedeemMessage(wrongSecret, secretB, partner.getAddress()); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should still be in TRADE mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - - long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); - - // Send incorrect secrets to AT, from correct account - messageData = BitcoinACCTv1.buildRedeemMessage(secretA, wrongSecret, partner.getAddress()); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should NOT send funds in the next block - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should still be in TRADE mode - tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - - // Check balances - expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() * 2; - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); - - // Check eventual refund - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretsCorrectSenderInvalidMessageLength() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int lockTimeB = BitcoinACCTv1.calcLockTimeB(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = BitcoinACCTv1.buildTradeMessage(partner.getAddress(), bitcoinPublicKeyHash, hashOfSecretA, lockTimeA, lockTimeB); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secrets to AT, from correct account, but missing receive address, hence incorrect length - messageData = Bytes.concat(secretA, secretB); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should be in TRADING mode - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testDescribeDeployed() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - - List executableAts = repository.getATRepository().getAllExecutableATs(); - - for (ATData atData : executableAts) { - String atAddress = atData.getATAddress(); - byte[] codeBytes = atData.getCodeBytes(); - byte[] codeHash = Crypto.digest(codeBytes); - - System.out.println(String.format("%s: code length: %d byte%s, code hash: %s", - atAddress, - codeBytes.length, - (codeBytes.length != 1 ? "s": ""), - HashCode.fromBytes(codeHash))); - - // Not one of ours? - if (!Arrays.equals(codeHash, BitcoinACCTv1.CODE_BYTES_HASH)) - continue; - - describeAt(repository, atAddress); - } - } - } - - private int calcTestLockTimeA(long messageTimestamp) { - return (int) (messageTimestamp / 1000L + tradeTimeout * 60); - } - - private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException { - byte[] creationBytes = BitcoinACCTv1.buildQortalAT(tradeAddress, bitcoinPublicKeyHash, hashOfSecretB, redeemAmount, bitcoinAmount, tradeTimeout); - - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = deployer.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress())); - System.exit(2); - } - - Long fee = null; - String name = "QORT-BTC cross-chain trade"; - String description = String.format("Qortal-Bitcoin cross-chain trade"); - String atType = "ACCT"; - String tags = "QORT-BTC ACCT"; - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null); - TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); - - DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); - - fee = deployAtTransaction.calcRecommendedFee(); - deployAtTransactionData.setFee(fee); - - TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer); - - return deployAtTransaction; - } - - private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = sender.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress())); - System.exit(2); - } - - Long fee = null; - int version = 4; - int nonce = 0; - long amount = 0; - Long assetId = null; // because amount is zero - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null); - TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false); - - MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData); - - fee = messageTransaction.calcRecommendedFee(); - messageTransactionData.setFee(fee); - - TransactionUtils.signAndMint(repository, messageTransactionData, sender); - - return messageTransaction; - } - - private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException { - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - int refundTimeout = tradeTimeout * 3 / 4 + 1; // close enough - - // AT should automatically refund deployer after 'refundTimeout' blocks - for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount) - BlockUtils.mintBlock(repository); - - // We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range - long expectedMinimumBalance = deployersPostDeploymentBalance; - long expectedMaximumBalance = deployersInitialBalance - deployAtFee; - - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance); - assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance); - } - - private void describeAt(Repository repository, String atAddress) throws DataException { - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = BitcoinACCTv1.getInstance().populateTradeData(repository, atData); - - Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); - int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); - - System.out.print(String.format("%s:\n" - + "\tmode: %s\n" - + "\tcreator: %s,\n" - + "\tcreation timestamp: %s,\n" - + "\tcurrent balance: %s QORT,\n" - + "\tis finished: %b,\n" - + "\tHASH160 of secret-B: %s,\n" - + "\tredeem payout: %s QORT,\n" - + "\texpected bitcoin: %s BTC,\n" - + "\tcurrent block height: %d,\n", - tradeData.qortalAtAddress, - tradeData.mode, - tradeData.qortalCreator, - epochMilliFormatter.apply(tradeData.creationTimestamp), - Amounts.prettyAmount(tradeData.qortBalance), - atData.getIsFinished(), - HashCode.fromBytes(tradeData.hashOfSecretB).toString().substring(0, 40), - Amounts.prettyAmount(tradeData.qortAmount), - Amounts.prettyAmount(tradeData.expectedForeignAmount), - currentBlockHeight)); - - if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) { - System.out.println(String.format("\trefund height: block %d,\n" - + "\tHASH160 of secret-A: %s,\n" - + "\tBitcoin P2SH-A nLockTime: %d (%s),\n" - + "\tBitcoin P2SH-B nLockTime: %d (%s),\n" - + "\ttrade partner: %s", - tradeData.tradeRefundHeight, - HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40), - tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L), - tradeData.lockTimeB, epochMilliFormatter.apply(tradeData.lockTimeB * 1000L), - tradeData.qortalPartnerAddress)); - } - } - - private PrivateKeyAccount createTradeAccount(Repository repository) { - // We actually use a known test account with funds to avoid PoW compute - return Common.getTestAccount(repository, "alice"); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java b/src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java deleted file mode 100644 index f27f7a7b..00000000 --- a/src/test/java/org/qortal/test/crosschain/bitcoinv1/DeployAT.java +++ /dev/null @@ -1,169 +0,0 @@ -package org.qortal.test.crosschain.bitcoinv1; - -import org.bitcoinj.core.Address; -import org.bitcoinj.core.AddressFormatException; -import org.bitcoinj.core.LegacyAddress; -import org.bitcoinj.core.NetworkParameters; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.asset.Asset; -import org.qortal.controller.Controller; -import org.qortal.crosschain.Bitcoin; -import org.qortal.crosschain.BitcoinACCTv1; -import org.qortal.crosschain.Bitcoiny; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.DeployAtTransactionData; -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.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.test.crosschain.apps.Common; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transaction.Transaction; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Amounts; -import org.qortal.utils.Base58; - -import com.google.common.hash.HashCode; - -public class DeployAT { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: DeployAT ")); - System.err.println(String.format("example: DeployAT " - + "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n" - + "\t10 \\\n" - + "\t10.1 \\\n" - + "\t0.00864200 \\\n" - + "\t750b06757a2448b8a4abebaa6e4662833fd5ddbb (or mrBpZYYGYMwUa8tRjTiXfP1ySqNXszWN5h) \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t10080")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 7) - usage(null); - - Common.init(); - - Bitcoiny bitcoiny = Bitcoin.getInstance(); - NetworkParameters params = bitcoiny.getNetworkParameters(); - - byte[] refundPrivateKey = null; - long redeemAmount = 0; - long fundingAmount = 0; - long expectedBitcoin = 0; - byte[] bitcoinPublicKeyHash = null; - byte[] hashOfSecret = null; - int tradeTimeout = 0; - - int argIndex = 0; - try { - refundPrivateKey = Base58.decode(args[argIndex++]); - if (refundPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - redeemAmount = Long.parseLong(args[argIndex++]); - if (redeemAmount <= 0) - usage("QORT amount must be positive"); - - fundingAmount = Long.parseLong(args[argIndex++]); - if (fundingAmount <= redeemAmount) - usage("AT funding amount must be greater than QORT redeem amount"); - - expectedBitcoin = Long.parseLong(args[argIndex++]); - if (expectedBitcoin <= 0) - usage("Expected BTC amount must be positive"); - - String bitcoinPKHish = args[argIndex++]; - // Try P2PKH first - try { - Address bitcoinAddress = LegacyAddress.fromBase58(params, bitcoinPKHish); - bitcoinPublicKeyHash = bitcoinAddress.getHash(); - } catch (AddressFormatException e) { - // Try parsing as PKH hex string instead - bitcoinPublicKeyHash = HashCode.fromString(bitcoinPKHish).asBytes(); - } - if (bitcoinPublicKeyHash.length != 20) - usage("Bitcoin PKH must be 20 bytes"); - - hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes(); - if (hashOfSecret.length != 20) - usage("Hash of secret must be 20 bytes"); - - tradeTimeout = Integer.parseInt(args[argIndex++]); - if (tradeTimeout < 60 || tradeTimeout > 50000) - usage("Trade timeout (minutes) must be between 60 and 50000"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); - System.exit(2); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount refundAccount = new PrivateKeyAccount(repository, refundPrivateKey); - System.out.println(String.format("Refund Qortal address: %s", refundAccount.getAddress())); - - System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount))); - - System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount))); - - System.out.println(String.format("HASH160 of secret: %s", HashCode.fromBytes(hashOfSecret))); - - // Deploy AT - byte[] creationBytes = BitcoinACCTv1.buildQortalAT(refundAccount.getAddress(), bitcoinPublicKeyHash, hashOfSecret, redeemAmount, expectedBitcoin, tradeTimeout); - System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); - - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = refundAccount.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", refundAccount.getAddress())); - System.exit(2); - } - - Long fee = null; - String name = "QORT-BTC cross-chain trade"; - String description = String.format("Qortal-Bitcoin cross-chain trade"); - String atType = "ACCT"; - String tags = "QORT-BTC ACCT"; - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, refundAccount.getPublicKey(), fee, null); - TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); - - Transaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); - - fee = deployAtTransaction.calcRecommendedFee(); - deployAtTransactionData.setFee(fee); - - deployAtTransaction.sign(refundAccount); - - byte[] signedBytes = null; - try { - signedBytes = TransactionTransformer.toBytes(deployAtTransactionData); - } catch (TransformationException e) { - System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage())); - System.exit(2); - } - - System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); - } catch (DataException e) { - System.err.println(String.format("Repository issue: %s", e.getMessage())); - System.exit(2); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/DeployAT.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/DeployAT.java deleted file mode 100644 index 3a1f9208..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/DeployAT.java +++ /dev/null @@ -1,150 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import java.math.BigDecimal; - -import org.bitcoinj.core.ECKey; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.asset.Asset; -import org.qortal.controller.Controller; -import org.qortal.crosschain.LitecoinACCTv1; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.DeployAtTransactionData; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.test.crosschain.apps.Common; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Amounts; -import org.qortal.utils.Base58; - -import com.google.common.hash.HashCode; - -public class DeployAT { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: DeployAT ")); - System.err.println("A trading key-pair will be generated for you!"); - System.err.println(String.format("example: DeployAT " - + "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n" - + "\t10 \\\n" - + "\t10.1 \\\n" - + "\t0.00864200 \\\n" - + "\t120")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 5) - usage(null); - - Common.init(); - - byte[] creatorPrivateKey = null; - long redeemAmount = 0; - long fundingAmount = 0; - long expectedLitecoin = 0; - int tradeTimeout = 0; - - int argIndex = 0; - try { - creatorPrivateKey = Base58.decode(args[argIndex++]); - if (creatorPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - redeemAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); - if (redeemAmount <= 0) - usage("QORT amount must be positive"); - - fundingAmount = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); - if (fundingAmount <= redeemAmount) - usage("AT funding amount must be greater than QORT redeem amount"); - - expectedLitecoin = new BigDecimal(args[argIndex++]).setScale(8).unscaledValue().longValue(); - if (expectedLitecoin <= 0) - usage("Expected LTC amount must be positive"); - - tradeTimeout = Integer.parseInt(args[argIndex++]); - if (tradeTimeout < 60 || tradeTimeout > 50000) - usage("Trade timeout (minutes) must be between 60 and 50000"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); - System.exit(2); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount creatorAccount = new PrivateKeyAccount(repository, creatorPrivateKey); - System.out.println(String.format("Creator Qortal address: %s", creatorAccount.getAddress())); - System.out.println(String.format("QORT redeem amount: %s", Amounts.prettyAmount(redeemAmount))); - System.out.println(String.format("AT funding amount: %s", Amounts.prettyAmount(fundingAmount))); - - // Generate trading key-pair - byte[] tradePrivateKey = new ECKey().getPrivKeyBytes(); - PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); - byte[] litecoinPublicKeyHash = ECKey.fromPrivate(tradePrivateKey).getPubKeyHash(); - - System.out.println(String.format("Trade private key: %s", HashCode.fromBytes(tradePrivateKey))); - - // Deploy AT - byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, expectedLitecoin, tradeTimeout); - System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); - - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = creatorAccount.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", creatorAccount.getAddress())); - System.exit(2); - } - - Long fee = null; - String name = "QORT-LTC cross-chain trade"; - String description = String.format("Qortal-Litecoin cross-chain trade"); - String atType = "ACCT"; - String tags = "QORT-LTC ACCT"; - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, creatorAccount.getPublicKey(), fee, null); - DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); - - DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); - - fee = deployAtTransaction.calcRecommendedFee(); - deployAtTransactionData.setFee(fee); - - deployAtTransaction.sign(creatorAccount); - - byte[] signedBytes = null; - try { - signedBytes = TransactionTransformer.toBytes(deployAtTransactionData); - } catch (TransformationException e) { - System.err.println(String.format("Unable to convert transaction to base58: %s", e.getMessage())); - System.exit(2); - } - - DeployAtTransaction.ensureATAddress(deployAtTransactionData); - String atAddress = deployAtTransactionData.getAtAddress(); - - System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); - - System.out.println(String.format("AT address: %s", atAddress)); - } catch (DataException e) { - System.err.println(String.format("Repository issue: %s", e.getMessage())); - System.exit(2); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java deleted file mode 100644 index 609ff5f3..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/LitecoinACCTv1Tests.java +++ /dev/null @@ -1,770 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import static org.junit.Assert.*; - -import java.time.Instant; -import java.time.LocalDateTime; -import java.time.ZoneOffset; -import java.time.format.DateTimeFormatter; -import java.time.format.FormatStyle; -import java.util.Arrays; -import java.util.List; -import java.util.Random; -import java.util.function.Function; - -import org.junit.Before; -import org.junit.Test; -import org.qortal.account.Account; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.asset.Asset; -import org.qortal.block.Block; -import org.qortal.crosschain.LitecoinACCTv1; -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.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.test.common.BlockUtils; -import org.qortal.test.common.Common; -import org.qortal.test.common.TransactionUtils; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transaction.MessageTransaction; -import org.qortal.utils.Amounts; - -import com.google.common.hash.HashCode; -import com.google.common.primitives.Bytes; - -public class LitecoinACCTv1Tests extends Common { - - public static final byte[] secretA = "This string is exactly 32 bytes!".getBytes(); - public static final byte[] hashOfSecretA = Crypto.hash160(secretA); // daf59884b4d1aec8c1b17102530909ee43c0151a - public static final byte[] litecoinPublicKeyHash = HashCode.fromString("bb00bb11bb22bb33bb44bb55bb66bb77bb88bb99").asBytes(); - public static final int tradeTimeout = 20; // blocks - public static final long redeemAmount = 80_40200000L; - public static final long fundingAmount = 123_45600000L; - public static final long litecoinAmount = 864200L; // 0.00864200 LTC - - private static final Random RANDOM = new Random(); - - @Before - public void beforeTest() throws DataException { - Common.useDefaultSettings(); - } - - @Test - public void testCompile() { - PrivateKeyAccount tradeAccount = createTradeAccount(null); - - byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAccount.getAddress(), litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout); - assertNotNull(creationBytes); - - System.out.println("AT creation bytes: " + HashCode.fromBytes(creationBytes).toString()); - } - - @Test - public void testDeploy() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - - long expectedBalance = deployersInitialBalance - fundingAmount - deployAtTransaction.getTransactionData().getFee(); - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = fundingAmount; - actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); - - assertEquals("AT's post-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = partnersInitialBalance; - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-deployment balance incorrect", expectedBalance, actualBalance); - - // Test orphaning - BlockUtils.orphanLastBlock(repository); - - expectedBalance = deployersInitialBalance; - actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = 0; - actualBalance = deployAtTransaction.getATAccount().getConfirmedBalance(Asset.QORT); - - assertEquals("AT's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - - expectedBalance = partnersInitialBalance; - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-orphan/pre-deployment balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testOfferCancel() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - // Send creator's address to AT, instead of typical partner's address - byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(deployer.getAddress()); - MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); - long messageFee = messageTransaction.getTransactionData().getFee(); - - // AT should process 'cancel' message in next block - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in CANCELLED mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.CANCELLED, tradeData.mode); - - // Check balances - long expectedMinimumBalance = deployersPostDeploymentBalance; - long expectedMaximumBalance = deployersInitialBalance - deployAtFee - messageFee; - - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance); - assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance); - - // Test orphaning - BlockUtils.orphanLastBlock(repository); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance - messageFee; - actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testOfferCancelInvalidLength() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - // Instead of sending creator's address to AT, send too-short/invalid message - byte[] messageData = new byte[7]; - RANDOM.nextBytes(messageData); - MessageTransaction messageTransaction = sendMessage(repository, deployer, messageData, atAddress); - long messageFee = messageTransaction.getTransactionData().getFee(); - - // AT should process 'cancel' message in next block - // As message is too short, it will be padded to 32bytes but cancel code doesn't care about message content, so should be ok - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in CANCELLED mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.CANCELLED, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testTradingInfoProcessing() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - Block postDeploymentBlock = BlockUtils.mintBlock(repository); - int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight(); - - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - describeAt(repository, atAddress); - - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - - // AT should be in TRADE mode - assertEquals(AcctMode.TRADING, tradeData.mode); - - // Check hashOfSecretA was extracted correctly - assertTrue(Arrays.equals(hashOfSecretA, tradeData.hashOfSecretA)); - - // Check trade partner Qortal address was extracted correctly - assertEquals(partner.getAddress(), tradeData.qortalPartnerAddress); - - // Check trade partner's Litecoin PKH was extracted correctly - assertTrue(Arrays.equals(litecoinPublicKeyHash, tradeData.partnerForeignPKH)); - - // Test orphaning - BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance; - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - // TEST SENDING TRADING INFO BUT NOT FROM AT CREATOR (SHOULD BE IGNORED) - @SuppressWarnings("unused") - @Test - public void testIncorrectTradeSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT BUT NOT FROM AT CREATOR - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, bystander, messageData, atAddress); - - BlockUtils.mintBlock(repository); - - long expectedBalance = partnersInitialBalance; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-initial-payout balance incorrect", expectedBalance, actualBalance); - - describeAt(repository, atAddress); - - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - - // AT should still be in OFFER mode - assertEquals(AcctMode.OFFERING, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testAutomaticTradeRefund() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - Block postDeploymentBlock = BlockUtils.mintBlock(repository); - int postDeploymentBlockHeight = postDeploymentBlock.getBlockData().getHeight(); - - // Check refund - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in REFUNDED mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.REFUNDED, tradeData.mode); - - // Test orphaning - BlockUtils.orphanToBlock(repository, postDeploymentBlockHeight); - - // Check balances - long expectedBalance = deployersPostDeploymentBalance; - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertEquals("Deployer's post-orphan/pre-refund balance incorrect", expectedBalance, actualBalance); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretCorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secret to AT, from correct account - messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress()); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertTrue(atData.getIsFinished()); - - // AT should be in REDEEMED mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.REDEEMED, tradeData.mode); - - // Check balances - long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee() + redeemAmount; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-redeem balance incorrect", expectedBalance, actualBalance); - - // Orphan redeem - BlockUtils.orphanLastBlock(repository); - - // Check balances - expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); - actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's post-orphan/pre-redeem balance incorrect", expectedBalance, actualBalance); - - // Check AT state - ATStateData postOrphanAtStateData = repository.getATRepository().getLatestATState(atAddress); - - assertTrue("AT states mismatch", Arrays.equals(preRedeemAtStateData.getStateData(), postOrphanAtStateData.getStateData())); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretIncorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - PrivateKeyAccount bystander = Common.getTestAccount(repository, "bob"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secret to AT, but from wrong account - messageData = LitecoinACCTv1.buildRedeemMessage(secretA, partner.getAddress()); - messageTransaction = sendMessage(repository, bystander, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should still be in TRADE mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - - // Check balances - long expectedBalance = partnersInitialBalance; - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); - - // Check eventual refund - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - } - } - - @SuppressWarnings("unused") - @Test - public void testIncorrectSecretCorrectSender() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - long deployAtFee = deployAtTransaction.getTransactionData().getFee(); - - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send incorrect secret to AT, from correct account - byte[] wrongSecret = new byte[32]; - RANDOM.nextBytes(wrongSecret); - messageData = LitecoinACCTv1.buildRedeemMessage(wrongSecret, partner.getAddress()); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should still be in TRADE mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - - long expectedBalance = partnersInitialBalance - messageTransaction.getTransactionData().getFee(); - long actualBalance = partner.getConfirmedBalance(Asset.QORT); - - assertEquals("Partner's balance incorrect", expectedBalance, actualBalance); - - // Check eventual refund - checkTradeRefund(repository, deployer, deployersInitialBalance, deployAtFee); - } - } - - @SuppressWarnings("unused") - @Test - public void testCorrectSecretCorrectSenderInvalidMessageLength() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - Account at = deployAtTransaction.getATAccount(); - String atAddress = at.getAddress(); - - long partnersOfferMessageTransactionTimestamp = System.currentTimeMillis(); - int lockTimeA = calcTestLockTimeA(partnersOfferMessageTransactionTimestamp); - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(partnersOfferMessageTransactionTimestamp, lockTimeA); - - // Send trade info to AT - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partner.getAddress(), litecoinPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout); - MessageTransaction messageTransaction = sendMessage(repository, tradeAccount, messageData, atAddress); - - // Give AT time to process message - BlockUtils.mintBlock(repository); - - // Send correct secret to AT, from correct account, but missing receive address, hence incorrect length - messageData = Bytes.concat(secretA); - messageTransaction = sendMessage(repository, partner, messageData, atAddress); - - // AT should NOT send funds in the next block - ATStateData preRedeemAtStateData = repository.getATRepository().getLatestATState(atAddress); - BlockUtils.mintBlock(repository); - - describeAt(repository, atAddress); - - // Check AT is NOT finished - ATData atData = repository.getATRepository().fromATAddress(atAddress); - assertFalse(atData.getIsFinished()); - - // AT should be in TRADING mode - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - assertEquals(AcctMode.TRADING, tradeData.mode); - } - } - - @SuppressWarnings("unused") - @Test - public void testDescribeDeployed() throws DataException { - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount deployer = Common.getTestAccount(repository, "chloe"); - PrivateKeyAccount tradeAccount = createTradeAccount(repository); - - PrivateKeyAccount partner = Common.getTestAccount(repository, "dilbert"); - - long deployersInitialBalance = deployer.getConfirmedBalance(Asset.QORT); - long partnersInitialBalance = partner.getConfirmedBalance(Asset.QORT); - - DeployAtTransaction deployAtTransaction = doDeploy(repository, deployer, tradeAccount.getAddress()); - - List executableAts = repository.getATRepository().getAllExecutableATs(); - - for (ATData atData : executableAts) { - String atAddress = atData.getATAddress(); - byte[] codeBytes = atData.getCodeBytes(); - byte[] codeHash = Crypto.digest(codeBytes); - - System.out.println(String.format("%s: code length: %d byte%s, code hash: %s", - atAddress, - codeBytes.length, - (codeBytes.length != 1 ? "s": ""), - HashCode.fromBytes(codeHash))); - - // Not one of ours? - if (!Arrays.equals(codeHash, LitecoinACCTv1.CODE_BYTES_HASH)) - continue; - - describeAt(repository, atAddress); - } - } - } - - private int calcTestLockTimeA(long messageTimestamp) { - return (int) (messageTimestamp / 1000L + tradeTimeout * 60); - } - - private DeployAtTransaction doDeploy(Repository repository, PrivateKeyAccount deployer, String tradeAddress) throws DataException { - byte[] creationBytes = LitecoinACCTv1.buildQortalAT(tradeAddress, litecoinPublicKeyHash, redeemAmount, litecoinAmount, tradeTimeout); - - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = deployer.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", deployer.getAddress())); - System.exit(2); - } - - Long fee = null; - String name = "QORT-LTC cross-chain trade"; - String description = String.format("Qortal-Litecoin cross-chain trade"); - String atType = "ACCT"; - String tags = "QORT-LTC ACCT"; - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, deployer.getPublicKey(), fee, null); - TransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, atType, tags, creationBytes, fundingAmount, Asset.QORT); - - DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData); - - fee = deployAtTransaction.calcRecommendedFee(); - deployAtTransactionData.setFee(fee); - - TransactionUtils.signAndMint(repository, deployAtTransactionData, deployer); - - return deployAtTransaction; - } - - private MessageTransaction sendMessage(Repository repository, PrivateKeyAccount sender, byte[] data, String recipient) throws DataException { - long txTimestamp = System.currentTimeMillis(); - byte[] lastReference = sender.getLastReference(); - - if (lastReference == null) { - System.err.println(String.format("Qortal account %s has no last reference", sender.getAddress())); - System.exit(2); - } - - Long fee = null; - int version = 4; - int nonce = 0; - long amount = 0; - Long assetId = null; // because amount is zero - - BaseTransactionData baseTransactionData = new BaseTransactionData(txTimestamp, Group.NO_GROUP, lastReference, sender.getPublicKey(), fee, null); - TransactionData messageTransactionData = new MessageTransactionData(baseTransactionData, version, nonce, recipient, amount, assetId, data, false, false); - - MessageTransaction messageTransaction = new MessageTransaction(repository, messageTransactionData); - - fee = messageTransaction.calcRecommendedFee(); - messageTransactionData.setFee(fee); - - TransactionUtils.signAndMint(repository, messageTransactionData, sender); - - return messageTransaction; - } - - private void checkTradeRefund(Repository repository, Account deployer, long deployersInitialBalance, long deployAtFee) throws DataException { - long deployersPostDeploymentBalance = deployersInitialBalance - fundingAmount - deployAtFee; - int refundTimeout = tradeTimeout / 2 + 1; // close enough - - // AT should automatically refund deployer after 'refundTimeout' blocks - for (int blockCount = 0; blockCount <= refundTimeout; ++blockCount) - BlockUtils.mintBlock(repository); - - // We don't bother to exactly calculate QORT spent running AT for several blocks, but we do know the expected range - long expectedMinimumBalance = deployersPostDeploymentBalance; - long expectedMaximumBalance = deployersInitialBalance - deployAtFee; - - long actualBalance = deployer.getConfirmedBalance(Asset.QORT); - - assertTrue(String.format("Deployer's balance %s should be above minimum %s", actualBalance, expectedMinimumBalance), actualBalance > expectedMinimumBalance); - assertTrue(String.format("Deployer's balance %s should be below maximum %s", actualBalance, expectedMaximumBalance), actualBalance < expectedMaximumBalance); - } - - private void describeAt(Repository repository, String atAddress) throws DataException { - ATData atData = repository.getATRepository().fromATAddress(atAddress); - CrossChainTradeData tradeData = LitecoinACCTv1.getInstance().populateTradeData(repository, atData); - - Function epochMilliFormatter = (timestamp) -> LocalDateTime.ofInstant(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC).format(DateTimeFormatter.ofLocalizedDateTime(FormatStyle.MEDIUM)); - int currentBlockHeight = repository.getBlockRepository().getBlockchainHeight(); - - System.out.print(String.format("%s:\n" - + "\tmode: %s\n" - + "\tcreator: %s,\n" - + "\tcreation timestamp: %s,\n" - + "\tcurrent balance: %s QORT,\n" - + "\tis finished: %b,\n" - + "\tredeem payout: %s QORT,\n" - + "\texpected Litecoin: %s LTC,\n" - + "\tcurrent block height: %d,\n", - tradeData.qortalAtAddress, - tradeData.mode, - tradeData.qortalCreator, - epochMilliFormatter.apply(tradeData.creationTimestamp), - Amounts.prettyAmount(tradeData.qortBalance), - atData.getIsFinished(), - Amounts.prettyAmount(tradeData.qortAmount), - Amounts.prettyAmount(tradeData.expectedForeignAmount), - currentBlockHeight)); - - if (tradeData.mode != AcctMode.OFFERING && tradeData.mode != AcctMode.CANCELLED) { - System.out.println(String.format("\trefund timeout: %d minutes,\n" - + "\trefund height: block %d,\n" - + "\tHASH160 of secret-A: %s,\n" - + "\tLitecoin P2SH-A nLockTime: %d (%s),\n" - + "\ttrade partner: %s\n" - + "\tpartner's receiving address: %s", - tradeData.refundTimeout, - tradeData.tradeRefundHeight, - HashCode.fromBytes(tradeData.hashOfSecretA).toString().substring(0, 40), - tradeData.lockTimeA, epochMilliFormatter.apply(tradeData.lockTimeA * 1000L), - tradeData.qortalPartnerAddress, - tradeData.qortalPartnerReceivingAddress)); - } - } - - private PrivateKeyAccount createTradeAccount(Repository repository) { - // We actually use a known test account with funds to avoid PoW compute - return Common.getTestAccount(repository, "alice"); - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendCancelMessage.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/SendCancelMessage.java deleted file mode 100644 index 2d04098c..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendCancelMessage.java +++ /dev/null @@ -1,90 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import org.qortal.account.PrivateKeyAccount; -import org.qortal.controller.Controller; -import org.qortal.crosschain.LitecoinACCTv1; -import org.qortal.crypto.Crypto; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.test.crosschain.apps.Common; -import org.qortal.transaction.MessageTransaction; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Base58; - -public class SendCancelMessage { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: SendCancelMessage ")); - System.err.println(String.format("example: SendCancelMessage " - + "7Eztjz2TsxwbrWUYEaSdLbASKQGTfK2rR7ViFc5gaiZw \\\n" - + "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 2) - usage(null); - - Common.init(); - - byte[] qortalPrivateKey = null; - String atAddress = null; - - int argIndex = 0; - try { - qortalPrivateKey = Base58.decode(args[argIndex++]); - if (qortalPrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - atAddress = args[argIndex++]; - if (!Crypto.isValidAtAddress(atAddress)) - usage("Invalid AT address"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); - System.exit(2); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount qortalAccount = new PrivateKeyAccount(repository, qortalPrivateKey); - - String creatorQortalAddress = qortalAccount.getAddress(); - System.out.println(String.format("Qortal address: %s", creatorQortalAddress)); - - byte[] messageData = LitecoinACCTv1.getInstance().buildCancelMessage(creatorQortalAddress); - MessageTransaction messageTransaction = MessageTransaction.build(repository, qortalAccount, Group.NO_GROUP, atAddress, messageData, false, false); - - System.out.println("Computing nonce..."); - messageTransaction.computeNonce(); - messageTransaction.sign(qortalAccount); - - byte[] signedBytes = null; - try { - signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); - } catch (TransformationException e) { - System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); - System.exit(2); - } - - System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); - } catch (DataException e) { - System.err.println(String.format("Repository issue: %s", e.getMessage())); - System.exit(2); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendRedeemMessage.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/SendRedeemMessage.java deleted file mode 100644 index 20386d2a..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendRedeemMessage.java +++ /dev/null @@ -1,101 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import org.qortal.account.PrivateKeyAccount; -import org.qortal.controller.Controller; -import org.qortal.crosschain.LitecoinACCTv1; -import org.qortal.crypto.Crypto; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.test.crosschain.apps.Common; -import org.qortal.transaction.MessageTransaction; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Base58; - -import com.google.common.hash.HashCode; - -public class SendRedeemMessage { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: SendRedeemMessage ")); - System.err.println(String.format("example: SendRedeemMessage " - + "dbfe739f5a3ecf7b0a22cea71f73d86ec71355b740e5972bcdf9e8bb4721ab9d \\\n" - + "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n" - + "\t5468697320737472696e672069732065786163746c7920333220627974657321 \\\n" - + "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 4) - usage(null); - - Common.init(); - - byte[] tradePrivateKey = null; - String atAddress = null; - byte[] secret = null; - String receiveAddress = null; - - int argIndex = 0; - try { - tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - if (tradePrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - atAddress = args[argIndex++]; - if (!Crypto.isValidAtAddress(atAddress)) - usage("Invalid AT address"); - - secret = HashCode.fromString(args[argIndex++]).asBytes(); - if (secret.length != 32) - usage("Secret must be 32 bytes"); - - receiveAddress = args[argIndex++]; - if (!Crypto.isValidAddress(receiveAddress)) - usage("Invalid Qortal receive address"); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); - System.exit(2); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); - - byte[] messageData = LitecoinACCTv1.buildRedeemMessage(secret, receiveAddress); - MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false); - - System.out.println("Computing nonce..."); - messageTransaction.computeNonce(); - messageTransaction.sign(tradeAccount); - - byte[] signedBytes = null; - try { - signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); - } catch (TransformationException e) { - System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); - System.exit(2); - } - - System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); - } catch (DataException e) { - System.err.println(String.format("Repository issue: %s", e.getMessage())); - System.exit(2); - } - } - -} diff --git a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendTradeMessage.java b/src/test/java/org/qortal/test/crosschain/litecoinv1/SendTradeMessage.java deleted file mode 100644 index 83e9a20e..00000000 --- a/src/test/java/org/qortal/test/crosschain/litecoinv1/SendTradeMessage.java +++ /dev/null @@ -1,118 +0,0 @@ -package org.qortal.test.crosschain.litecoinv1; - -import org.qortal.account.PrivateKeyAccount; -import org.qortal.controller.Controller; -import org.qortal.crosschain.LitecoinACCTv1; -import org.qortal.crypto.Crypto; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.repository.RepositoryFactory; -import org.qortal.repository.RepositoryManager; -import org.qortal.repository.hsqldb.HSQLDBRepositoryFactory; -import org.qortal.test.crosschain.apps.Common; -import org.qortal.transaction.MessageTransaction; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.TransactionTransformer; -import org.qortal.utils.Base58; -import org.qortal.utils.NTP; - -import com.google.common.hash.HashCode; - -public class SendTradeMessage { - - private static void usage(String error) { - if (error != null) - System.err.println(error); - - System.err.println(String.format("usage: SendTradeMessage ")); - System.err.println(String.format("example: SendTradeMessage " - + "ed77aa2c62d785a9428725fc7f95b907be8a1cc43213239876a62cf70fdb6ecb \\\n" - + "\tAaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa \\\n" - + "\tQqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq \\\n" - + "\tffffffffffffffffffffffffffffffffffffffff \\\n" - + "\tdaf59884b4d1aec8c1b17102530909ee43c0151a \\\n" - + "\t1600184800")); - System.exit(1); - } - - public static void main(String[] args) { - if (args.length != 6) - usage(null); - - Common.init(); - - byte[] tradePrivateKey = null; - String atAddress = null; - String partnerTradeAddress = null; - byte[] partnerTradePublicKeyHash = null; - byte[] hashOfSecret = null; - int lockTime = 0; - - int argIndex = 0; - try { - tradePrivateKey = HashCode.fromString(args[argIndex++]).asBytes(); - if (tradePrivateKey.length != 32) - usage("Refund private key must be 32 bytes"); - - atAddress = args[argIndex++]; - if (!Crypto.isValidAtAddress(atAddress)) - usage("Invalid AT address"); - - partnerTradeAddress = args[argIndex++]; - if (!Crypto.isValidAddress(partnerTradeAddress)) - usage("Invalid partner trade Qortal address"); - - partnerTradePublicKeyHash = HashCode.fromString(args[argIndex++]).asBytes(); - if (partnerTradePublicKeyHash.length != 20) - usage("Partner trade PKH must be 20 bytes"); - - hashOfSecret = HashCode.fromString(args[argIndex++]).asBytes(); - if (hashOfSecret.length != 20) - usage("HASH160 of secret must be 20 bytes"); - - lockTime = Integer.parseInt(args[argIndex++]); - } catch (IllegalArgumentException e) { - usage(String.format("Invalid argument %d: %s", argIndex, e.getMessage())); - } - - try { - RepositoryFactory repositoryFactory = new HSQLDBRepositoryFactory(Controller.getRepositoryUrl()); - RepositoryManager.setRepositoryFactory(repositoryFactory); - } catch (DataException e) { - System.err.println(String.format("Repository start-up issue: %s", e.getMessage())); - System.exit(2); - } - - try (final Repository repository = RepositoryManager.getRepository()) { - PrivateKeyAccount tradeAccount = new PrivateKeyAccount(repository, tradePrivateKey); - - int refundTimeout = LitecoinACCTv1.calcRefundTimeout(NTP.getTime(), lockTime); - if (refundTimeout < 1) { - System.err.println("Refund timeout too small. Is locktime in the past?"); - System.exit(2); - } - - byte[] messageData = LitecoinACCTv1.buildTradeMessage(partnerTradeAddress, partnerTradePublicKeyHash, hashOfSecret, lockTime, refundTimeout); - MessageTransaction messageTransaction = MessageTransaction.build(repository, tradeAccount, Group.NO_GROUP, atAddress, messageData, false, false); - - System.out.println("Computing nonce..."); - messageTransaction.computeNonce(); - messageTransaction.sign(tradeAccount); - - byte[] signedBytes = null; - try { - signedBytes = TransactionTransformer.toBytes(messageTransaction.getTransactionData()); - } catch (TransformationException e) { - System.err.println(String.format("Unable to convert transaction to bytes: %s", e.getMessage())); - System.exit(2); - } - - System.out.println(String.format("%nSigned transaction in base58, ready for POST /transactions/process:%n%s", Base58.encode(signedBytes))); - } catch (DataException e) { - System.err.println(String.format("Repository issue: %s", e.getMessage())); - System.exit(2); - } - } - -}