diff --git a/src/main/java/org/qortal/api/model/crosschain/BitcoinyTBDRequest.java b/src/main/java/org/qortal/api/model/crosschain/BitcoinyTBDRequest.java new file mode 100644 index 00000000..3a531413 --- /dev/null +++ b/src/main/java/org/qortal/api/model/crosschain/BitcoinyTBDRequest.java @@ -0,0 +1,692 @@ +package org.qortal.api.model.crosschain; + +import org.qortal.crosschain.ServerInfo; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import java.util.Arrays; + +@XmlAccessorType(XmlAccessType.FIELD) +public class BitcoinyTBDRequest { + + /** + * Target Timespan + * + * extracted from /src/chainparams.cpp class + * consensus.nPowTargetTimespan + */ + private int targetTimespan; + + /** + * Target Spacing + * + * extracted from /src/chainparams.cpp class + * consensus.nPowTargetSpacing + */ + private int targetSpacing; + + /** + * Packet Magic + * + * extracted from /src/chainparams.cpp class + * Concatenate the 4 values in pchMessageStart, then convert the hex to decimal. + * + * Ex. litecoin + * pchMessageStart[0] = 0xfb; + * pchMessageStart[1] = 0xc0; + * pchMessageStart[2] = 0xb6; + * pchMessageStart[3] = 0xdb; + * packetMagic = 0xfbc0b6db = 4223710939 + */ + private long packetMagic; + + /** + * Port + * + * extracted from /src/chainparams.cpp class + * nDefaultPort + */ + private int port; + + /** + * Address Header + * + * extracted from /src/chainparams.cpp class + * base58Prefixes[PUBKEY_ADDRESS] from Main Network + */ + private int addressHeader; + + /** + * P2sh Header + * + * extracted from /src/chainparams.cpp class + * base58Prefixes[SCRIPT_ADDRESS] from Main Network + */ + private int p2shHeader; + + /** + * Segwit Address Hrp + * + * HRP -> Human Readable Parts + * + * extracted from /src/chainparams.cpp class + * bech32_hrp + */ + private String segwitAddressHrp; + + /** + * Dumped Private Key Header + * + * extracted from /src/chainparams.cpp class + * base58Prefixes[SECRET_KEY] from Main Network + * This is usually, but not always ... addressHeader + 128 + */ + private int dumpedPrivateKeyHeader; + + /** + * Subsidy Decreased Block Count + * + * extracted from /src/chainparams.cpp class + * consensus.nSubsidyHalvingInterval + * + * Digibyte does not support this, because they do halving differently. + */ + private int subsidyDecreaseBlockCount; + + /** + * Expected Genesis Hash + * + * extracted from /src/chainparams.cpp class + * consensus.hashGenesisBlock + * Remove '0x' prefix + */ + private String expectedGenesisHash; + + /** + * Common Script Pub Key + * + * extracted from /src/chainparams.cpp class + * This is the key commonly used to sign alerts for altcoins. Bitcoin and Digibyte are know exceptions. + */ + public static final String SCRIPT_PUB_KEY = "040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9"; + + /** + * The Script Pub Key + * + * extracted from /src/chainparams.cpp class + * The key to sign alerts. + * + * const CScript genesisOutputScript = CScript() << ParseHex("040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9") << OP_CHECKSIG; + * + * ie LTC = 040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9 + * + * this may be the same value as scripHex + */ + private String pubKey; + + /** + * DNS Seeds + * + * extracted from /src/chainparams.cpp class + * vSeeds + */ + private String[] dnsSeeds; + + /** + * BIP32 Header P2PKH Pub + * + * extracted from /src/chainparams.cpp class + * Concatenate the 4 values in base58Prefixes[EXT_PUBLIC_KEY] + * base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E} = 0x0488B21E + */ + private int bip32HeaderP2PKHpub; + + /** + * BIP32 Header P2PKH Priv + * + * extracted from /src/chainparams.cpp class + * Concatenate the 4 values in base58Prefixes[EXT_SECRET_KEY] + * base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4} = 0x0488ADE4 + */ + private int bip32HeaderP2PKHpriv; + + /** + * Address Header (Testnet) + * + * extracted from /src/chainparams.cpp class + * base58Prefixes[PUBKEY_ADDRESS] from Testnet + */ + private int addressHeaderTestnet; + + /** + * BIP32 Header P2PKH Pub (Testnet) + * + * extracted from /src/chainparams.cpp class + * Concatenate the 4 values in base58Prefixes[EXT_PUBLIC_KEY] + * base58Prefixes[EXT_PUBLIC_KEY] = {0x04, 0x88, 0xB2, 0x1E} = 0x0488B21E + */ + private int bip32HeaderP2PKHpubTestnet; + + /** + * BIP32 Header P2PKH Priv (Testnet) + * + * extracted from /src/chainparams.cpp class + * Concatenate the 4 values in base58Prefixes[EXT_SECRET_KEY] + * base58Prefixes[EXT_SECRET_KEY] = {0x04, 0x88, 0xAD, 0xE4} = 0x0488ADE4 + */ + private int bip32HeaderP2PKHprivTestnet; + + /** + * Id + * + * "org.litecoin.production" for LTC + * I'm guessing this just has to match others for trading purposes. + */ + private String id; + + /** + * Majority Enforce Block Upgrade + * + * All coins are setting this to 750, except DOGE is setting this to 1500. + */ + private int majorityEnforceBlockUpgrade; + + /** + * Majority Reject Block Outdated + * + * All coins are setting this to 950, except DOGE is setting this to 1900. + */ + private int majorityRejectBlockOutdated; + + /** + * Majority Window + * + * All coins are setting this to 1000, except DOGE is setting this to 2000. + */ + private int majorityWindow; + + /** + * Code + * + * "LITE" for LTC + * Currency code for full unit. + */ + private String code; + + /** + * mCode + * + * "mLITE" for LTC + * Currency code for milli unit. + */ + private String mCode; + + /** + * Base Code + * + * "Liteoshi" for LTC + * Currency code for base unit. + */ + private String baseCode; + + /** + * Min Non Dust Output + * + * 100000 for LTC, web search for minimum transaction fee per kB + */ + private int minNonDustOutput; + + /** + * URI Scheme + * + * uriScheme = "litecoin" for LTC + * Do a web search to find this value. + */ + private String uriScheme; + + /** + * Protocol Version Minimum + * + * 70002 for LTC + * extracted from /src/protocol.h class + */ + private int protocolVersionMinimum; + + /** + * Protocol Version Current + * + * 70003 for LTC + * extracted from /src/protocol.h class + */ + private int protocolVersionCurrent; + + /** + * Has Max Money + * + * false for DOGE, true for BTC and LTC + */ + private boolean hasMaxMoney; + + /** + * Max Money + * + * 84000000 for LTC, 21000000 for BTC + * extracted from src/amount.h class + */ + private long maxMoney; + + /** + * Currency Code + * + * The trading symbol, ie LTC, BTC, DOGE + */ + private String currencyCode; + + /** + * Minimum Order Amount + * + * web search, LTC minimumOrderAmount = 1000000, 0.01 LTC minimum order to avoid dust errors + */ + private long minimumOrderAmount; + + /** + * Fee Per Kb + * + * web search, LTC feePerKb = 10000, 0.0001 LTC per 1000 bytes + */ + private long feePerKb; + + /** + * Network Name + * + * ie Litecoin-MAIN + */ + private String networkName; + + /** + * Fee Ceiling + * + * web search, LTC fee ceiling = 1000L + */ + private long feeCeiling; + + /** + * Extended Public Key + * + * xpub for operations that require wallet watching + */ + private String extendedPublicKey; + + /** + * Send Amount + * + * The amount to send in base units. Also, requires sending fee per byte, receiving address and sender's extended private key. + */ + private long sendAmount; + + /** + * Sending Fee Per Byte + * + * The fee to include on a send request in base units. Also, requires receiving address, sender's extended private key and send amount. + */ + private long sendingFeePerByte; + + /** + * Receiving Address + * + * The receiving address for a send request. Also, requires send amount, sender's extended private key and sending fee per byte. + */ + private String receivingAddress; + + /** + * Extended Private Key + * + * xpriv address for a send request. Also, requires receiving address, send amount and sending fee per byte. + */ + private String extendedPrivateKey; + + /** + * Server Info + * + * For adding, removing, setting current server requests. + */ + private ServerInfo serverInfo; + + /** + * Script Sig + * + * extracted from /src/chainparams.cpp class + * pszTimestamp + * + * transform this value - https://bitcoin.stackexchange.com/questions/13122/scriptsig-coinbase-structure-of-the-genesis-block + * ie LTC = 04ffff001d0104404e592054696d65732030352f4f63742f32303131205374657665204a6f62732c204170706c65e280997320566973696f6e6172792c2044696573206174203536 + * ie DOGE = 04ffff001d0104084e696e746f6e646f + */ + private String scriptSig; + + /** + * Script Hex + * + * extracted from /src/chainparams.cpp class + * genesisOutputScript + * + * ie LTC = 040184710fa689ad5023690c80f3a49c8f13f8d45b8c857fbcbc8bc4a8e4d3eb4b10f4d4604fa08dce601aaf0f470216fe1b51850b4acf21b179c45070ac7b03a9 + * + * this may be the same value as pubKey + */ + private String scriptHex; + + /** + * Reward + * + * extracted from /src/chainparams.cpp class + * CreateGenesisBlock(..., [reward] * COIN) + * + * ie LTC = 50, BTC = 50, DOGE = 88 + */ + private int reward; + + /** + * Genesis Creation Version + */ + private int genesisCreationVersion; + + /** + * Genesis Block Version + */ + private long genesisBlockVersion; + + /** + * Genesis Time + * + * extracted from /src/chainparams.cpp class + * CreateGenesisBlock(nTime, ...) + * + * ie LTC = 1317972665 + */ + private long genesisTime; + + /** + * Difficulty Target + * + * extracted from /src/chainparams.cpp class + * CreateGenesisBlock(genesisTime, nonce, difficultyTarget, 1, reward * COIN); + * + * convert from hex to decimal + * + * ie LTC = 0x1e0ffff0 = 504365040 + */ + private long difficultyTarget; + + /** + * Merkle Hex + */ + private String merkleHex; + + /** + * Nonce + * + * extracted from /src/chainparams.cpp class + * CreateGenesisBlock(genesisTime, nonce, difficultyTarget, 1, reward * COIN); + * + * ie LTC = 2084524493 + */ + private long nonce; + + + public int getTargetTimespan() { + return targetTimespan; + } + + public int getTargetSpacing() { + return targetSpacing; + } + + public long getPacketMagic() { + return packetMagic; + } + + public int getPort() { + return port; + } + + public int getAddressHeader() { + return addressHeader; + } + + public int getP2shHeader() { + return p2shHeader; + } + + public String getSegwitAddressHrp() { + return segwitAddressHrp; + } + + public int getDumpedPrivateKeyHeader() { + return dumpedPrivateKeyHeader; + } + + public int getSubsidyDecreaseBlockCount() { + return subsidyDecreaseBlockCount; + } + + public String getExpectedGenesisHash() { + return expectedGenesisHash; + } + + public String getPubKey() { + return pubKey; + } + + public String[] getDnsSeeds() { + return dnsSeeds; + } + + public int getBip32HeaderP2PKHpub() { + return bip32HeaderP2PKHpub; + } + + public int getBip32HeaderP2PKHpriv() { + return bip32HeaderP2PKHpriv; + } + + public int getAddressHeaderTestnet() { + return addressHeaderTestnet; + } + + public int getBip32HeaderP2PKHpubTestnet() { + return bip32HeaderP2PKHpubTestnet; + } + + public int getBip32HeaderP2PKHprivTestnet() { + return bip32HeaderP2PKHprivTestnet; + } + + public String getId() { + return this.id; + } + + public int getMajorityEnforceBlockUpgrade() { + return this.majorityEnforceBlockUpgrade; + } + + public int getMajorityRejectBlockOutdated() { + return this.majorityRejectBlockOutdated; + } + + public int getMajorityWindow() { + return this.majorityWindow; + } + + public String getCode() { + return this.code; + } + + public String getmCode() { + return this.mCode; + } + + public String getBaseCode() { + return this.baseCode; + } + + public int getMinNonDustOutput() { + return this.minNonDustOutput; + } + + public String getUriScheme() { + return this.uriScheme; + } + + public int getProtocolVersionMinimum() { + return this.protocolVersionMinimum; + } + + public int getProtocolVersionCurrent() { + return this.protocolVersionCurrent; + } + + public boolean isHasMaxMoney() { + return this.hasMaxMoney; + } + + public long getMaxMoney() { + return this.maxMoney; + } + + public String getCurrencyCode() { + return this.currencyCode; + } + + public long getMinimumOrderAmount() { + return this.minimumOrderAmount; + } + + public long getFeePerKb() { + return this.feePerKb; + } + + public String getNetworkName() { + return this.networkName; + } + + public long getFeeCeiling() { + return this.feeCeiling; + } + + public String getExtendedPublicKey() { + return this.extendedPublicKey; + } + + public long getSendAmount() { + return this.sendAmount; + } + + public long getSendingFeePerByte() { + return this.sendingFeePerByte; + } + + public String getReceivingAddress() { + return this.receivingAddress; + } + + public String getExtendedPrivateKey() { + return this.extendedPrivateKey; + } + + public ServerInfo getServerInfo() { + return this.serverInfo; + } + + public String getScriptSig() { + return this.scriptSig; + } + + public String getScriptHex() { + return this.scriptHex; + } + + public int getReward() { + return this.reward; + } + + public int getGenesisCreationVersion() { + return this.genesisCreationVersion; + } + + public long getGenesisBlockVersion() { + return this.genesisBlockVersion; + } + + public long getGenesisTime() { + return this.genesisTime; + } + + public long getDifficultyTarget() { + return this.difficultyTarget; + } + + public String getMerkleHex() { + return this.merkleHex; + } + + public long getNonce() { + return this.nonce; + } + + @Override + public String toString() { + return "BitcoinyTBDRequest{" + + "targetTimespan=" + targetTimespan + + ", targetSpacing=" + targetSpacing + + ", packetMagic=" + packetMagic + + ", port=" + port + + ", addressHeader=" + addressHeader + + ", p2shHeader=" + p2shHeader + + ", segwitAddressHrp='" + segwitAddressHrp + '\'' + + ", dumpedPrivateKeyHeader=" + dumpedPrivateKeyHeader + + ", subsidyDecreaseBlockCount=" + subsidyDecreaseBlockCount + + ", expectedGenesisHash='" + expectedGenesisHash + '\'' + + ", pubKey='" + pubKey + '\'' + + ", dnsSeeds=" + Arrays.toString(dnsSeeds) + + ", bip32HeaderP2PKHpub=" + bip32HeaderP2PKHpub + + ", bip32HeaderP2PKHpriv=" + bip32HeaderP2PKHpriv + + ", addressHeaderTestnet=" + addressHeaderTestnet + + ", bip32HeaderP2PKHpubTestnet=" + bip32HeaderP2PKHpubTestnet + + ", bip32HeaderP2PKHprivTestnet=" + bip32HeaderP2PKHprivTestnet + + ", id='" + id + '\'' + + ", majorityEnforceBlockUpgrade=" + majorityEnforceBlockUpgrade + + ", majorityRejectBlockOutdated=" + majorityRejectBlockOutdated + + ", majorityWindow=" + majorityWindow + + ", code='" + code + '\'' + + ", mCode='" + mCode + '\'' + + ", baseCode='" + baseCode + '\'' + + ", minNonDustOutput=" + minNonDustOutput + + ", uriScheme='" + uriScheme + '\'' + + ", protocolVersionMinimum=" + protocolVersionMinimum + + ", protocolVersionCurrent=" + protocolVersionCurrent + + ", hasMaxMoney=" + hasMaxMoney + + ", maxMoney=" + maxMoney + + ", currencyCode='" + currencyCode + '\'' + + ", minimumOrderAmount=" + minimumOrderAmount + + ", feePerKb=" + feePerKb + + ", networkName='" + networkName + '\'' + + ", feeCeiling=" + feeCeiling + + ", extendedPublicKey='" + extendedPublicKey + '\'' + + ", sendAmount=" + sendAmount + + ", sendingFeePerByte=" + sendingFeePerByte + + ", receivingAddress='" + receivingAddress + '\'' + + ", extendedPrivateKey='" + extendedPrivateKey + '\'' + + ", serverInfo=" + serverInfo + + ", scriptSig='" + scriptSig + '\'' + + ", scriptHex='" + scriptHex + '\'' + + ", reward=" + reward + + ", genesisCreationVersion=" + genesisCreationVersion + + ", genesisBlockVersion=" + genesisBlockVersion + + ", genesisTime=" + genesisTime + + ", difficultyTarget=" + difficultyTarget + + ", merkleHex='" + merkleHex + '\'' + + ", nonce=" + nonce + + '}'; + } +} diff --git a/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequests.java b/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequests.java new file mode 100644 index 00000000..e78f951d --- /dev/null +++ b/src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequests.java @@ -0,0 +1,68 @@ +package org.qortal.api.model.crosschain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlElement; +import java.util.List; + +@XmlAccessorType(XmlAccessType.FIELD) +public class TradeBotRespondRequests { + + @Schema(description = "Foreign blockchain private key, e.g. BIP32 'm' key for Bitcoin/Litecoin starting with 'xprv'", + example = "xprv___________________________________________________________________________________________________________") + public String foreignKey; + + @Schema(description = "List of address matches") + @XmlElement(name = "addresses") + public List addresses; + + @Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq") + public String receivingAddress; + + public TradeBotRespondRequests() { + } + + public TradeBotRespondRequests(String foreignKey, List addresses, String receivingAddress) { + this.foreignKey = foreignKey; + this.addresses = addresses; + this.receivingAddress = receivingAddress; + } + + @Schema(description = "Address Match") + // All properties to be converted to JSON via JAX-RS + @XmlAccessorType(XmlAccessType.FIELD) + public static class AddressMatch { + @Schema(description = "AT Address") + public String atAddress; + + @Schema(description = "Receiving Address") + public String receivingAddress; + + // For JAX-RS + protected AddressMatch() { + } + + public AddressMatch(String atAddress, String receivingAddress) { + this.atAddress = atAddress; + this.receivingAddress = receivingAddress; + } + + @Override + public String toString() { + return "AddressMatch{" + + "atAddress='" + atAddress + '\'' + + ", receivingAddress='" + receivingAddress + '\'' + + '}'; + } + } + + @Override + public String toString() { + return "TradeBotRespondRequests{" + + "foreignKey='" + foreignKey + '\'' + + ", addresses=" + addresses + + '}'; + } +} \ 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 index 3f05643d..c8f9ea6b 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java @@ -157,7 +157,7 @@ public class CrossChainHtlcResource { htlcStatus.bitcoinP2shAddress = p2shAddress; htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8); - List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString()); + List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString(), false); if (p2shBalance > 0L && !fundingOutputs.isEmpty()) { htlcStatus.canRedeem = now >= medianBlockTime * 1000L; @@ -401,7 +401,7 @@ public class CrossChainHtlcResource { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(decodedTradePrivateKey); - List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoiny.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, decodedSecret, foreignBlockchainReceivingAccountInfo); @@ -664,7 +664,7 @@ public class CrossChainHtlcResource { // ElectrumX coins ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA, false); // Validate the destination foreign blockchain address Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress); diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java index 5a50222a..de646a9f 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java @@ -17,13 +17,16 @@ 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.api.model.crosschain.TradeBotRespondRequests; import org.qortal.asset.Asset; import org.qortal.controller.Controller; import org.qortal.controller.tradebot.AcctTradeBot; import org.qortal.controller.tradebot.TradeBot; import org.qortal.crosschain.ACCT; import org.qortal.crosschain.AcctMode; +import org.qortal.crosschain.Bitcoiny; import org.qortal.crosschain.ForeignBlockchain; +import org.qortal.crosschain.PirateChain; import org.qortal.crosschain.SupportedBlockchain; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; @@ -42,8 +45,10 @@ import javax.servlet.http.HttpServletRequest; import javax.ws.rs.*; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; +import java.util.ArrayList; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.stream.Collectors; @Path("/crosschain/tradebot") @@ -187,6 +192,39 @@ public class CrossChainTradeBotResource { public String tradeBotResponder(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequest tradeBotRespondRequest) { Security.checkApiCallAllowed(request); + return createTradeBotResponse(tradeBotRespondRequest); + } + + @POST + @Path("/respondmultiple") + @Operation( + summary = "Respond to multiple trade offers. NOTE: WILL SPEND FUNDS!)", + description = "Start a new trade-bot entry to respond to chosen trade offers. Pirate Chain is not supported and will throw an invalid criteria error.", + requestBody = @RequestBody( + required = true, + content = @Content( + mediaType = MediaType.APPLICATION_JSON, + schema = @Schema( + implementation = TradeBotRespondRequests.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") + @SecurityRequirement(name = "apiKey") + public String tradeBotResponderMultiple(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequests tradeBotRespondRequest) { + Security.checkApiCallAllowed(request); + + return createTradeBotResponseMultiple(tradeBotRespondRequest); + } + + private String createTradeBotResponse(TradeBotRespondRequest tradeBotRespondRequest) { final String atAddress = tradeBotRespondRequest.atAddress; // We prefer foreignKey to deprecated xprv58 @@ -257,6 +295,99 @@ public class CrossChainTradeBotResource { } } + private String createTradeBotResponseMultiple(TradeBotRespondRequests respondRequests) { + try (final Repository repository = RepositoryManager.getRepository()) { + + if (respondRequests.foreignKey == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + List crossChainTradeDataList = new ArrayList<>(respondRequests.addresses.size()); + Optional acct = Optional.empty(); + + for(String atAddress : respondRequests.addresses ) { + + if (atAddress == null || !Crypto.isValidAtAddress(atAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + if (respondRequests.receivingAddress == null || !Crypto.isValidAddress(respondRequests.receivingAddress)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + + final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L); + if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC); + + // Extract data from cross-chain trading AT + ATData atData = fetchAtDataWithChecking(repository, atAddress); + + // TradeBot uses AT's code hash to map to ACCT + ACCT acctUsingAtData = TradeBot.getInstance().getAcctUsingAtData(atData); + if (acctUsingAtData == null) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS); + // if the optional is empty, + // then ensure the ACCT blockchain is a Bitcoiny blockchain, but not Pirate Chain and fill the optional + // Even though the Pirate Chain protocol does support multi send, + // the Pirate Chain API we are using does not support multi send + else if( acct.isEmpty() ) { + if( !(acctUsingAtData.getBlockchain() instanceof Bitcoiny) || + acctUsingAtData.getBlockchain() instanceof PirateChain ) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + acct = Optional.of(acctUsingAtData); + } + // if the optional is filled, then ensure it is equal to the AT in this iteration + else if( !acctUsingAtData.getCodeBytesHash().equals(acct.get().getCodeBytesHash()) ) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); + + if (!acctUsingAtData.getBlockchain().isValidWalletKey(respondRequests.foreignKey)) + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY); + + CrossChainTradeData crossChainTradeData = acctUsingAtData.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); + + // Check if there is a buy or a cancel request in progress for this trade + List txTypes = List.of(Transaction.TransactionType.MESSAGE); + List unconfirmed = repository.getTransactionRepository().getUnconfirmedTransactions(txTypes, null, 0, 0, false); + for (TransactionData transactionData : unconfirmed) { + MessageTransactionData messageTransactionData = (MessageTransactionData) transactionData; + if (Objects.equals(messageTransactionData.getRecipient(), atAddress)) { + // There is a pending request for this trade, so block this buy attempt to reduce the risk of refunds + throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "Trade has an existing buy request or is pending cancellation."); + } + } + + crossChainTradeDataList.add(crossChainTradeData); + } + + AcctTradeBot.ResponseResult result + = TradeBot.getInstance().startResponseMultiple( + repository, + acct.get(), + crossChainTradeDataList, + respondRequests.receivingAddress, + respondRequests.foreignKey, + (Bitcoiny) acct.get().getBlockchain()); + + 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.createCustomException(request, ApiError.REPOSITORY_ISSUE, e.getMessage()); + } + } + @DELETE @Operation( summary = "Delete completed trade", diff --git a/src/main/java/org/qortal/api/resource/CrossChainUtils.java b/src/main/java/org/qortal/api/resource/CrossChainUtils.java index d1453bda..802faca1 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainUtils.java +++ b/src/main/java/org/qortal/api/resource/CrossChainUtils.java @@ -1,5 +1,6 @@ package org.qortal.api.resource; +import com.google.common.primitives.Bytes; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.bitcoinj.core.Address; @@ -7,11 +8,15 @@ import org.bitcoinj.core.Coin; import org.bitcoinj.script.Script; import org.bitcoinj.script.ScriptBuilder; +import org.bouncycastle.util.Strings; +import org.json.simple.JSONObject; +import org.qortal.api.model.crosschain.BitcoinyTBDRequest; import org.qortal.crosschain.*; import org.qortal.data.at.ATData; import org.qortal.data.crosschain.*; import org.qortal.repository.DataException; import org.qortal.repository.Repository; +import org.qortal.utils.BitTwiddling; import java.util.*; import java.util.stream.Collectors; @@ -545,4 +550,86 @@ public class CrossChainUtils { server.getConnectionType().toString(), false); } + + /** + * Get Bitcoiny TBD (To Be Determined) + * + * @param bitcoinyTBDRequest the parameters for the Bitcoiny TBD + * @return the Bitcoiny TBD + * @throws DataException + */ + public static BitcoinyTBD getBitcoinyTBD(BitcoinyTBDRequest bitcoinyTBDRequest) throws DataException { + + try { + DeterminedNetworkParams networkParams = new DeterminedNetworkParams(bitcoinyTBDRequest); + + BitcoinyTBD bitcoinyTBD + = BitcoinyTBD.getInstance(bitcoinyTBDRequest.getCode()) + .orElse(BitcoinyTBD.buildInstance( + bitcoinyTBDRequest, + networkParams) + ); + + return bitcoinyTBD; + } catch (Exception e) { + LOGGER.error(e.getMessage(), e); + } + + return null; + } + + /** + * Get Version Decimal + * + * @param jsonObject the JSON object with the version attribute + * @param attribute the attribute that hold the version value + * @return the version as a decimal number, discarding + * @throws NumberFormatException + */ + public static double getVersionDecimal(JSONObject jsonObject, String attribute) throws NumberFormatException { + String versionString = (String) jsonObject.get(attribute); + return Double.parseDouble(reduceDelimeters(versionString, 1, '.')); + } + + /** + * Reduce Delimeters + * + * @param value the raw string + * @param max the max number of the delimeter + * @param delimeter the delimeter + * @return the processed value with the max number of delimeters + */ + public static String reduceDelimeters(String value, int max, char delimeter) { + + if( max < 1 ) return value; + + String[] splits = Strings.split(value, delimeter); + + StringBuffer buffer = new StringBuffer(splits[0]); + + int limit = Math.min(max + 1, splits.length); + + for( int index = 1; index < limit; index++) { + buffer.append(delimeter); + buffer.append(splits[index]); + } + + return buffer.toString(); + } + + /** Returns + + + /** + * Build Offer Message + * + * @param partnerBitcoinPKH + * @param hashOfSecretA + * @param lockTimeA + * @return '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); + } } \ No newline at end of file diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java index 259a16b8..e7cb0fb8 100644 --- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -527,7 +528,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { // P2SH-A funding confirmed // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = BitcoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -893,7 +894,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { // 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); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB, false); byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, @@ -1063,7 +1064,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -1135,7 +1136,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { 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); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB, false); // Determine receive address for refund String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); @@ -1201,7 +1202,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java index 9ab97be9..18f79b81 100644 --- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java @@ -7,7 +7,9 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; +import org.qortal.controller.tradebot.TradeStates.State; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; import org.qortal.data.at.ATData; @@ -30,12 +32,8 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - /** * Performing cross-chain trading steps on behalf of user. *

@@ -50,45 +48,6 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(BitcoinACCTv3TradeBot.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 @@ -313,7 +272,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = BitcoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +752,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +816,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java index 4b1ba7bb..5b65c9a1 100644 --- a/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -30,11 +31,9 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; +import org.qortal.controller.tradebot.TradeStates.State; /** * Performing cross-chain trading steps on behalf of user. @@ -50,45 +49,6 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(DigibyteACCTv3TradeBot.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 @@ -313,7 +273,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +753,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA); + List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +817,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA); + List fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java index 52e7bb24..6c9f5a29 100644 --- a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -313,7 +314,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = DogecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +794,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +858,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java index b57b9354..6a2ef700 100644 --- a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -30,11 +31,9 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; +import org.qortal.controller.tradebot.TradeStates.State; /** * Performing cross-chain trading steps on behalf of user. @@ -50,45 +49,6 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv3TradeBot.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 @@ -313,7 +273,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = DogecoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +753,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +817,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java index 0b612d11..cef93d12 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -312,7 +313,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = LitecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -756,7 +757,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -820,7 +821,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java index b5631f0b..aa791e96 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -30,12 +31,9 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - +import org.qortal.controller.tradebot.TradeStates.State; /** * Performing cross-chain trading steps on behalf of user. *

@@ -50,45 +48,6 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv3TradeBot.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 @@ -313,7 +272,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = LitecoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +752,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +816,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java index c48f23e2..70ee8705 100644 --- a/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/PirateChainACCTv3TradeBot.java @@ -9,6 +9,7 @@ import org.bitcoinj.core.Coin; import org.qortal.account.PrivateKeyAccount; import org.qortal.account.PublicKeyAccount; import org.qortal.api.model.crosschain.TradeBotCreateRequest; +import org.qortal.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -32,11 +33,9 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; +import org.qortal.controller.tradebot.TradeStates.State; /** * Performing cross-chain trading steps on behalf of user. @@ -52,45 +51,6 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(PirateChainACCTv3TradeBot.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 @@ -317,7 +277,7 @@ public class PirateChainACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = PirateChainACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKey(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKey(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); diff --git a/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java index ed71d0e3..a383dfd8 100644 --- a/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java @@ -7,6 +7,7 @@ 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.api.resource.CrossChainUtils; import org.qortal.asset.Asset; import org.qortal.crosschain.*; import org.qortal.crypto.Crypto; @@ -30,11 +31,9 @@ import org.qortal.utils.NTP; import java.util.Arrays; import java.util.List; -import java.util.Map; import java.util.stream.Collectors; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; +import org.qortal.controller.tradebot.TradeStates.State; /** * Performing cross-chain trading steps on behalf of user. @@ -50,45 +49,6 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot { private static final Logger LOGGER = LogManager.getLogger(RavencoinACCTv3TradeBot.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 @@ -313,7 +273,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot { } // Attempt to send MESSAGE to Bob's Qortal trade address - byte[] messageData = RavencoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); @@ -793,7 +753,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot { case FUNDED: { Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA, false); Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(ravencoin.getNetworkParameters(), redeemAmount, redeemKey, fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); @@ -857,7 +817,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot { case FUNDED:{ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount); ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey()); - List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA); + List fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA, false); // Determine receive address for refund String receiveAddress = ravencoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index 3699bd2a..654513f2 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -215,6 +215,41 @@ public class TradeBot implements Listener { return acctTradeBot.startResponse(repository, atData, acct, crossChainTradeData, foreignKey, receivingAddress); } + /** + * Creates a trade-bot entries from the 'Alice' viewpoint, + * i.e. matching foreign blockchain currency to existing QORT offers. + *

+ * Requires chosen trade offers from Bob, passed by crossChainTradeData + * and access to a foreign blockchain wallet via foreignKey. + *

+ * @param repository + * @param crossChainTradeDataList chosen trade OFFERs that Alice wants to match + * @param receiveAddress Alice's Qortal address to receive her QORT + * @param foreignKey foreign blockchain wallet key + * @param bitcoiny + * @throws DataException + */ + public ResponseResult startResponseMultiple( + Repository repository, + ACCT acct, + List crossChainTradeDataList, + String receiveAddress, + String foreignKey, + Bitcoiny bitcoiny) throws DataException { + AcctTradeBot acctTradeBot = findTradeBotForAcct(acct); + if (acctTradeBot == null) { + LOGGER.debug(() -> String.format("Couldn't find ACCT trade-bot for %s", acct.getBlockchain())); + return ResponseResult.NETWORK_ISSUE; + } + + for( CrossChainTradeData tradeData : crossChainTradeDataList) { + // Check Alice doesn't already have an existing, on-going trade-bot entry for this AT. + if (repository.getCrossChainRepository().existsTradeWithAtExcludingStates(tradeData.qortalAtAddress, acctTradeBot.getEndStates())) + return ResponseResult.TRADE_ALREADY_EXISTS; + } + return TradeBotUtils.startResponseMultiple(repository, acct, crossChainTradeDataList, receiveAddress, foreignKey, bitcoiny); + } + public boolean deleteEntry(Repository repository, byte[] tradePrivateKey) throws DataException { TradeBotData tradeBotData = repository.getCrossChainRepository().getTradeBotData(tradePrivateKey); if (tradeBotData == null) diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java b/src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java new file mode 100644 index 00000000..67a262fc --- /dev/null +++ b/src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java @@ -0,0 +1,217 @@ +package org.qortal.controller.tradebot; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.bitcoinj.core.Transaction; +import org.qortal.account.PrivateKeyAccount; +import org.qortal.api.resource.CrossChainUtils; +import org.qortal.crosschain.ACCT; +import org.qortal.crosschain.Bitcoiny; +import org.qortal.crosschain.BitcoinyHTLC; +import org.qortal.crosschain.ForeignBlockchainException; +import org.qortal.crypto.Crypto; +import org.qortal.data.crosschain.CrossChainTradeData; +import org.qortal.data.crosschain.TradeBotData; +import org.qortal.data.transaction.MessageTransactionData; +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.utils.Base58; +import org.qortal.utils.NTP; +import org.qortal.transaction.Transaction.ValidationResult; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.qortal.controller.tradebot.TradeStates.State; + +public class TradeBotUtils { + + private static final Logger LOGGER = LogManager.getLogger(TradeBotUtils.class); + /** + * Creates trade-bot entries from the 'Alice' viewpoint, i.e. matching Bitcoiny coin to existing offers. + *

+ * Requires chosen trade offers from Bob, passed by crossChainTradeData + * and access to a Blockchain wallet via foreignKey. + *

+ * The crossChainTradeData contains the current trade offers state + * as extracted from the AT's data segment. + *

+ * Access to a funded wallet is via a Blockchain BIP32 hierarchical deterministic key, + * passed via foreignKey. + * 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 foreignKey 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 Blockchain main-net) + * or 'tprv' for (Blockchain test-net). + *

+ * It is envisaged that the value in foreignKey will actually come from a Qortal-UI-managed wallet. + *

+ * If sufficient funds are available, this method will actually fund the P2SH-A + * with the Blockchain amount expected by 'Bob'. + *

+ * If the Blockchain transaction is successfully broadcast to the network then + * we also send a MESSAGE to Bob's trade-bot to let them know; one message for each trade. + *

+ * The trade-bot entries are saved to the repository and the cross-chain trading process commences. + *

+ * + * @param repository for backing up the trade bot data + * @param crossChainTradeDataList chosen trade OFFERs that Alice wants to match + * @param receiveAddress Alice's Qortal address + * @param foreignKey funded wallet xprv in base58 + * @param bitcoiny the bitcoiny chain to match the sell offer with + * @return true if P2SH-A funding transaction successfully broadcast to Blockchain network, false otherwise + * @throws DataException + */ + public static AcctTradeBot.ResponseResult startResponseMultiple( + Repository repository, + ACCT acct, + List crossChainTradeDataList, + String receiveAddress, + String foreignKey, + Bitcoiny bitcoiny) throws DataException { + + // Check we have enough funds via foreignKey to fund P2SH to cover expectedForeignAmount + long now = NTP.getTime(); + long p2shFee; + try { + p2shFee = bitcoiny.getP2shFee(now); + } catch (ForeignBlockchainException e) { + LOGGER.debug("Couldn't estimate blockchain transaction fees?"); + return AcctTradeBot.ResponseResult.NETWORK_ISSUE; + } + + Map valueByP2shAddress = new HashMap<>(crossChainTradeDataList.size()); + + class DataCombiner{ + CrossChainTradeData crossChainTradeData; + TradeBotData tradeBotData; + String p2shAddress; + + public DataCombiner(CrossChainTradeData crossChainTradeData, TradeBotData tradeBotData, String p2shAddress) { + this.crossChainTradeData = crossChainTradeData; + this.tradeBotData = tradeBotData; + this.p2shAddress = p2shAddress; + } + } + + List dataToProcess = new ArrayList<>(); + + for(CrossChainTradeData crossChainTradeData : crossChainTradeDataList) { + 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); + // We need to generate lockTime-A: add tradeTimeout to now + int lockTimeA = (crossChainTradeData.tradeTimeout * 60) + (int) (now / 1000L); + byte[] receivingPublicKeyHash = Base58.decode(receiveAddress); // Actually the whole address, not just PKH + + TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, acct.getClass().getSimpleName(), + State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value, + receiveAddress, + crossChainTradeData.qortalAtAddress, + now, + crossChainTradeData.qortAmount, + tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress, + secretA, hashOfSecretA, + crossChainTradeData.foreignBlockchain, + tradeForeignPublicKey, tradeForeignPublicKeyHash, + crossChainTradeData.expectedForeignAmount, + foreignKey, null, lockTimeA, receivingPublicKeyHash); + + // Attempt to backup the trade bot data + // Include tradeBotData as an additional parameter, since it's not in the repository yet + TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData)); + + // 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 = bitcoiny.deriveP2shAddress(redeemScriptBytes); + + valueByP2shAddress.put(p2shAddress, amountA); + + dataToProcess.add(new DataCombiner(crossChainTradeData, tradeBotData, p2shAddress)); + } + + // Build transaction for funding P2SH-A + Transaction p2shFundingTransaction = bitcoiny.buildSpendMultiple(foreignKey, valueByP2shAddress, null); + if (p2shFundingTransaction == null) { + LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?"); + return AcctTradeBot.ResponseResult.BALANCE_ISSUE; + } + + try { + bitcoiny.broadcastTransaction(p2shFundingTransaction); + } catch (ForeignBlockchainException e) { + // We couldn't fund P2SH-A at this time + LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?"); + return AcctTradeBot.ResponseResult.NETWORK_ISSUE; + } + + for(DataCombiner datumToProcess : dataToProcess ) { + // Attempt to send MESSAGE to Bob's Qortal trade address + TradeBotData tradeBotData = datumToProcess.tradeBotData; + + byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA()); + CrossChainTradeData crossChainTradeData = datumToProcess.crossChainTradeData; + String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress; + + boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData); + if (!isMessageAlreadySent) { + // Do this in a new thread so caller doesn't have to wait for computeNonce() + // In the unlikely event that the transaction doesn't validate then the buy won't happen and eventually Alice's AT will be refunded + new Thread(() -> { + try (final Repository threadsRepository = RepositoryManager.getRepository()) { + PrivateKeyAccount sender = new PrivateKeyAccount(threadsRepository, tradeBotData.getTradePrivateKey()); + MessageTransaction messageTransaction = MessageTransaction.build(threadsRepository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false); + + LOGGER.info("Computing nonce at difficulty {} for AT {} and recipient {}", messageTransaction.getPoWDifficulty(), tradeBotData.getAtAddress(), messageRecipient); + messageTransaction.computeNonce(); + MessageTransactionData newMessageTransactionData = (MessageTransactionData) messageTransaction.getTransactionData(); + LOGGER.info("Computed nonce {} at difficulty {}", newMessageTransactionData.getNonce(), messageTransaction.getPoWDifficulty()); + messageTransaction.sign(sender); + + // reset repository state to prevent deadlock + threadsRepository.discardChanges(); + + if (messageTransaction.isSignatureValid()) { + 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())); + } + } else { + LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: signature invalid", messageRecipient)); + } + } catch (DataException e) { + LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, e.getMessage())); + } + }, "TradeBot response").start(); + } + + TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", datumToProcess.p2shAddress)); + } + + return AcctTradeBot.ResponseResult.OK; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/controller/tradebot/TradeStates.java b/src/main/java/org/qortal/controller/tradebot/TradeStates.java new file mode 100644 index 00000000..a1dbb081 --- /dev/null +++ b/src/main/java/org/qortal/controller/tradebot/TradeStates.java @@ -0,0 +1,47 @@ +package org.qortal.controller.tradebot; + +import java.util.Map; + +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toMap; + +public class TradeStates { + 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; + } + } +} diff --git a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java index 9de95c17..cb855466 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java +++ b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java @@ -802,12 +802,6 @@ public class BitcoinACCTv1 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/BitcoinACCTv3.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv3.java index ad5984c1..ecf768ed 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinACCTv3.java +++ b/src/main/java/org/qortal/crosschain/BitcoinACCTv3.java @@ -751,12 +751,6 @@ public class BitcoinACCTv3 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 7f624e20..4a819209 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -208,8 +208,8 @@ public abstract class Bitcoiny implements ForeignBlockchain { * @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.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false); + public List getUnspentOutputs(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException { + List unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), includeUnconfirmed); List unspentTransactionOutputs = new ArrayList<>(); for (UnspentOutput unspentOutput : unspentOutputs) { @@ -343,6 +343,45 @@ public abstract class Bitcoiny implements ForeignBlockchain { } } + /** + * Returns bitcoinj transaction sending the recipient's amount to each recipient given. + * + * + * @param xprv58 the private master key + * @param amountByRecipient each amount to send indexed by the recipient to send to + * @param feePerByte the satoshis per byte + * + * @return the completed transaction, ready to broadcast + */ + public Transaction buildSpendMultiple(String xprv58, Map amountByRecipient, Long feePerByte) { + Context.propagate(bitcoinjContext); + + Wallet wallet = Wallet.fromSpendingKeyB58(this.params, xprv58, DeterministicHierarchy.BIP32_STANDARDISATION_TIME_SECS); + wallet.setUTXOProvider(new WalletAwareUTXOProvider(this, wallet)); + + Transaction transaction = new Transaction(this.params); + + for(Map.Entry amountForRecipient : amountByRecipient.entrySet()) { + Address destination = Address.fromString(this.params, amountForRecipient.getKey()); + transaction.addOutput(Coin.valueOf(amountForRecipient.getValue()), destination); + } + + SendRequest sendRequest = SendRequest.forTx(transaction); + + 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; + } + } + /** * Get Spending Candidate Addresses * @@ -391,7 +430,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { List allUnspentOutputs = new ArrayList<>(); Set walletAddresses = this.getWalletAddresses(key58); for (String address : walletAddresses) { - allUnspentOutputs.addAll(this.getUnspentOutputs(address)); + allUnspentOutputs.addAll(this.getUnspentOutputs(address, true)); } for (TransactionOutput output : allUnspentOutputs) { if (!output.isAvailableForSpending()) { @@ -465,7 +504,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.getAddressTransactions(script, false); + List historicTransactionHashes = this.getAddressTransactions(script, true); if (!historicTransactionHashes.isEmpty()) { areAllKeysUnused = false; @@ -569,7 +608,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.getAddressTransactions(script, false); + List historicTransactionHashes = this.getAddressTransactions(script, true); if (!historicTransactionHashes.isEmpty()) { areAllKeysUnused = false; @@ -803,7 +842,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { List unspentOutputs; try { - unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false); + unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, true); } catch (ForeignBlockchainException e) { throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address)); } @@ -893,7 +932,7 @@ public abstract class Bitcoiny implements ForeignBlockchain { } private Long summingUnspentOutputs(String walletAddress) throws ForeignBlockchainException { - return this.getUnspentOutputs(walletAddress).stream() + return this.getUnspentOutputs(walletAddress, true).stream() .map(TransactionOutput::getValue) .mapToLong(Coin::longValue) .sum(); diff --git a/src/main/java/org/qortal/crosschain/BitcoinyTBD.java b/src/main/java/org/qortal/crosschain/BitcoinyTBD.java new file mode 100644 index 00000000..c25d2094 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BitcoinyTBD.java @@ -0,0 +1,151 @@ +package org.qortal.crosschain; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.Context; +import org.bitcoinj.core.NetworkParameters; +import org.qortal.api.model.crosschain.BitcoinyTBDRequest; +import org.qortal.crosschain.ChainableServer.ConnectionType; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class BitcoinyTBD extends Bitcoiny { + + private static HashMap requestsById = new HashMap<>(); + + private long minimumOrderAmount; + + private static Map instanceByCode = new HashMap<>(); + + private final NetTBD netTBD; + + /** + * Default ElectrumX Ports + * + * These are the defualts for all Bitcoin forks. + */ + private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class); + static { + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001); + DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002); + } + + /** + * Constructor + * + * @param netTBD network access to the blockchain provider + * @param blockchain blockchain provider + * @param bitcoinjContext + * @param currencyCode the trading symbol, ie LTC + * @param minimumOrderAmount web search, LTC minimumOrderAmount = 1000000, 0.01 LTC minimum order to avoid dust errors + * @param feePerKb web search, LTC feePerKb = 10000, 0.0001 LTC per 1000 bytes + */ + private BitcoinyTBD( + NetTBD netTBD, + BitcoinyBlockchainProvider blockchain, + Context bitcoinjContext, + String currencyCode, + long minimumOrderAmount, + long feePerKb) { + + super(blockchain, bitcoinjContext, currencyCode, Coin.valueOf( feePerKb)); + + this.netTBD = netTBD; + this.minimumOrderAmount = minimumOrderAmount; + + LOGGER.info(() -> String.format("Starting BitcoinyTBD support using %s", this.netTBD.getName())); + } + + /** + * Get Instance + * + * @param currencyCode the trading symbol, ie LTC + * + * @return the instance + */ + public static synchronized Optional getInstance(String currencyCode) { + + return Optional.ofNullable(instanceByCode.get(currencyCode)); + } + + /** + * Build Instance + * + * @param bitcoinyTBDRequest + * @param networkParams + * @return the instance + */ + public static synchronized BitcoinyTBD buildInstance( + BitcoinyTBDRequest bitcoinyTBDRequest, + NetworkParameters networkParams + ) { + + NetTBD netTBD + = new NetTBD( + bitcoinyTBDRequest.getNetworkName(), + bitcoinyTBDRequest.getFeeCeiling(), + networkParams, + Collections.emptyList(), + bitcoinyTBDRequest.getExpectedGenesisHash() + ); + + BitcoinyBlockchainProvider electrumX = new ElectrumX(netTBD.getName(), netTBD.getGenesisHash(), netTBD.getServers(), DEFAULT_ELECTRUMX_PORTS); + Context bitcoinjContext = new Context(netTBD.getParams()); + + BitcoinyTBD instance + = new BitcoinyTBD( + netTBD, + electrumX, + bitcoinjContext, + bitcoinyTBDRequest.getCurrencyCode(), + bitcoinyTBDRequest.getMinimumOrderAmount(), + bitcoinyTBDRequest.getFeePerKb()); + electrumX.setBlockchain(instance); + + instanceByCode.put(bitcoinyTBDRequest.getCurrencyCode(), instance); + requestsById.put(bitcoinyTBDRequest.getId(), bitcoinyTBDRequest); + + return instance; + } + + public static List getRequests() { + + Collection requests = requestsById.values(); + + List list = new ArrayList<>( requests.size() ); + + list.addAll( requests ); + + return list; + } + + @Override + public long getMinimumOrderAmount() { + + return minimumOrderAmount; + } + + @Override + public long getP2shFee(Long timestamp) throws ForeignBlockchainException { + + return this.netTBD.getFeeCeiling(); + } + + @Override + public long getFeeCeiling() { + + return this.netTBD.getFeeCeiling(); + } + + @Override + public void setFeeCeiling(long fee) { + + this.netTBD.setFeeCeiling( fee ); + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java index df596de4..2fcd7cee 100644 --- a/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java +++ b/src/main/java/org/qortal/crosschain/BitcoinyUTXOProvider.java @@ -30,7 +30,7 @@ public class BitcoinyUTXOProvider implements UTXOProvider { byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); // collection UTXO's for all confirmed unspent outputs - for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false)) { + for (UnspentOutput output : this.bitcoiny.blockchainProvider.getUnspentOutputs(script, true)) { utxos.add(toUTXO(output)); } } diff --git a/src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java b/src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java new file mode 100644 index 00000000..af7d19ac --- /dev/null +++ b/src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java @@ -0,0 +1,387 @@ +package org.qortal.crosschain; + +import org.apache.logging.log4j.LogManager; +import org.bitcoinj.core.*; +import org.bitcoinj.store.BlockStore; +import org.bitcoinj.store.BlockStoreException; +import org.bitcoinj.utils.MonetaryFormat; +import org.bouncycastle.util.encoders.Hex; +import org.libdohj.core.AltcoinNetworkParameters; +import org.libdohj.core.AltcoinSerializer; +import org.qortal.api.model.crosschain.BitcoinyTBDRequest; +import org.qortal.repository.DataException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import java.math.BigInteger; + +import static org.bitcoinj.core.Coin.COIN; + +/** + * Common parameters Bitcoin fork networks. + */ +public class DeterminedNetworkParams extends NetworkParameters implements AltcoinNetworkParameters { + + private static final org.apache.logging.log4j.Logger LOGGER = LogManager.getLogger(DeterminedNetworkParams.class); + + public static final long MAX_TARGET_COMPACT_BITS = 0x1e0fffffL; + /** + * Standard format for the LITE denomination. + */ + private MonetaryFormat fullUnit; + + /** + * Standard format for the mLITE denomination. + * */ + private MonetaryFormat mUnit; + + /** + * Base Unit + * + * The equivalent for Satoshi for Bitcoin + */ + private MonetaryFormat baseUnit; + + /** + * The maximum money to be generated + */ + public final Coin maxMoney; + + /** + * Currency code for full unit. + * */ + private String code = "LITE"; + + /** + * Currency code for milli Unit. + * */ + private String mCode = "mLITE"; + + /** + * Currency code for base unit. + * */ + private String baseCode = "Liteoshi"; + + + private int protocolVersionMinimum; + private int protocolVersionCurrent; + + private static final Coin BASE_SUBSIDY = COIN.multiply(50); + + protected Logger log = LoggerFactory.getLogger(DeterminedNetworkParams.class); + + private int minNonDustOutput; + + private String uriScheme; + + private boolean hasMaxMoney; + + public DeterminedNetworkParams( BitcoinyTBDRequest request ) throws DataException { + super(); + + if( request.getTargetTimespan() > 0 && request.getTargetSpacing() > 0 ) + this.interval = request.getTargetTimespan() / request.getTargetSpacing(); + + this.targetTimespan = request.getTargetTimespan(); + + // this compact value is used for every Bitcoin fork for no documented reason + this.maxTarget = Utils.decodeCompactBits(MAX_TARGET_COMPACT_BITS); + + this.packetMagic = request.getPacketMagic(); + + this.id = request.getId(); + this.port = request.getPort(); + this.addressHeader = request.getAddressHeader(); + this.p2shHeader = request.getP2shHeader(); + this.segwitAddressHrp = request.getSegwitAddressHrp(); + + this.dumpedPrivateKeyHeader = request.getDumpedPrivateKeyHeader(); + + LOGGER.info( "Creating Genesis Block ..."); + + //this.genesisBlock = CoinParamsUtil.createGenesisBlockFromRequest(this, request); + + LOGGER.info("Created Genesis Block: genesisBlock = " + genesisBlock ); + + // this is 100 for each coin from what I can tell + this.spendableCoinbaseDepth = 100; + + this.subsidyDecreaseBlockCount = request.getSubsidyDecreaseBlockCount(); + +// String genesisHash = genesisBlock.getHashAsString(); +// +// LOGGER.info("genesisHash = " + genesisHash); +// +// LOGGER.info("request = " + request); +// +// checkState(genesisHash.equals(request.getExpectedGenesisHash())); + this.alertSigningKey = Hex.decode(request.getPubKey()); + + this.majorityEnforceBlockUpgrade = request.getMajorityEnforceBlockUpgrade(); + this.majorityRejectBlockOutdated = request.getMajorityRejectBlockOutdated(); + this.majorityWindow = request.getMajorityWindow(); + + this.dnsSeeds = request.getDnsSeeds(); + + this.bip32HeaderP2PKHpub = request.getBip32HeaderP2PKHpub(); + this.bip32HeaderP2PKHpriv = request.getBip32HeaderP2PKHpriv(); + + this.code = request.getCode(); + this.mCode = request.getmCode(); + this.baseCode = request.getBaseCode(); + + this.fullUnit = MonetaryFormat.BTC.noCode() + .code(0, this.code) + .code(3, this.mCode) + .code(7, this.baseCode); + this.mUnit = fullUnit.shift(3).minDecimals(2).optionalDecimals(2); + this.baseUnit = fullUnit.shift(7).minDecimals(0).optionalDecimals(2); + + this.protocolVersionMinimum = request.getProtocolVersionMinimum(); + this.protocolVersionCurrent = request.getProtocolVersionCurrent(); + + this.minNonDustOutput = request.getMinNonDustOutput(); + + this.uriScheme = request.getUriScheme(); + + this.hasMaxMoney = request.isHasMaxMoney(); + + this.maxMoney = COIN.multiply(request.getMaxMoney()); + } + + @Override + public Coin getBlockSubsidy(final int height) { + // return BASE_SUBSIDY.shiftRight(height / getSubsidyDecreaseBlockCount()); + // return something concerning Digishield for Dogecoin + // return something different for Digibyte validation.cpp::GetBlockSubsidy + // we may not need to support this + throw new UnsupportedOperationException(); + } + + /** + * Get the hash to use for a block. + */ + @Override + public Sha256Hash getBlockDifficultyHash(Block block) { + + return ((AltcoinBlock) block).getScryptHash(); + } + + @Override + public boolean isTestNet() { + return false; + } + + public MonetaryFormat getMonetaryFormat() { + + return this.fullUnit; + } + + @Override + public Coin getMaxMoney() { + + return this.maxMoney; + } + + @Override + public Coin getMinNonDustOutput() { + + return Coin.valueOf(this.minNonDustOutput); + } + + @Override + public String getUriScheme() { + + return this.uriScheme; + } + + @Override + public boolean hasMaxMoney() { + + return this.hasMaxMoney; + } + + + @Override + public String getPaymentProtocolId() { + return this.id; + } + + @Override + public void checkDifficultyTransitions(StoredBlock storedPrev, Block nextBlock, BlockStore blockStore) + throws VerificationException, BlockStoreException { + try { + final long newTargetCompact = calculateNewDifficultyTarget(storedPrev, nextBlock, blockStore); + final long receivedTargetCompact = nextBlock.getDifficultyTarget(); + + if (newTargetCompact != receivedTargetCompact) + throw new VerificationException("Network provided difficulty bits do not match what was calculated: " + + newTargetCompact + " vs " + receivedTargetCompact); + } catch (CheckpointEncounteredException ex) { + // Just have to take it on trust then + } + } + + /** + * Get the difficulty target expected for the next block. This includes all + * the weird cases for Litecoin such as testnet blocks which can be maximum + * difficulty if the block interval is high enough. + * + * @throws CheckpointEncounteredException if a checkpoint is encountered while + * calculating difficulty target, and therefore no conclusive answer can + * be provided. + */ + public long calculateNewDifficultyTarget(StoredBlock storedPrev, Block nextBlock, BlockStore blockStore) + throws VerificationException, BlockStoreException, CheckpointEncounteredException { + final Block prev = storedPrev.getHeader(); + final int previousHeight = storedPrev.getHeight(); + final int retargetInterval = this.getInterval(); + + // Is this supposed to be a difficulty transition point? + if ((storedPrev.getHeight() + 1) % retargetInterval != 0) { + if (this.allowMinDifficultyBlocks()) { + // Special difficulty rule for testnet: + // If the new block's timestamp is more than 5 minutes + // then allow mining of a min-difficulty block. + if (nextBlock.getTimeSeconds() > prev.getTimeSeconds() + getTargetSpacing() * 2) { + return Utils.encodeCompactBits(maxTarget); + } else { + // Return the last non-special-min-difficulty-rules-block + StoredBlock cursor = storedPrev; + + while (cursor.getHeight() % retargetInterval != 0 + && cursor.getHeader().getDifficultyTarget() == Utils.encodeCompactBits(this.getMaxTarget())) { + StoredBlock prevCursor = cursor.getPrev(blockStore); + if (prevCursor == null) { + break; + } + cursor = prevCursor; + } + + return cursor.getHeader().getDifficultyTarget(); + } + } + + // No ... so check the difficulty didn't actually change. + return prev.getDifficultyTarget(); + } + + // We need to find a block far back in the chain. It's OK that this is expensive because it only occurs every + // two weeks after the initial block chain download. + StoredBlock cursor = storedPrev; + int goBack = retargetInterval - 1; + + // Litecoin: This fixes an issue where a 51% attack can change difficulty at will. + // Go back the full period unless it's the first retarget after genesis. + // Code based on original by Art Forz + if (cursor.getHeight()+1 != retargetInterval) + goBack = retargetInterval; + + for (int i = 0; i < goBack; i++) { + if (cursor == null) { + // This should never happen. If it does, it means we are following an incorrect or busted chain. + throw new VerificationException( + "Difficulty transition point but we did not find a way back to the genesis block."); + } + cursor = blockStore.get(cursor.getHeader().getPrevBlockHash()); + } + + //We used checkpoints... + if (cursor == null) { + log.debug("Difficulty transition: Hit checkpoint!"); + throw new CheckpointEncounteredException(); + } + + Block blockIntervalAgo = cursor.getHeader(); + return this.calculateNewDifficultyTargetInner(previousHeight, prev.getTimeSeconds(), + prev.getDifficultyTarget(), blockIntervalAgo.getTimeSeconds(), + nextBlock.getDifficultyTarget()); + } + + /** + * Calculate the difficulty target expected for the next block after a normal + * recalculation interval. Does not handle special cases such as testnet blocks + * being setting the target to maximum for blocks after a long interval. + * + * @param previousHeight height of the block immediately before the retarget. + * @param prev the block immediately before the retarget block. + * @param nextBlock the block the retarget happens at. + * @param blockIntervalAgo The last retarget block. + * @return New difficulty target as compact bytes. + */ + protected long calculateNewDifficultyTargetInner(int previousHeight, final Block prev, + final Block nextBlock, final Block blockIntervalAgo) { + return this.calculateNewDifficultyTargetInner(previousHeight, prev.getTimeSeconds(), + prev.getDifficultyTarget(), blockIntervalAgo.getTimeSeconds(), + nextBlock.getDifficultyTarget()); + } + + /** + * + * @param previousHeight Height of the block immediately previous to the one we're calculating difficulty of. + * @param previousBlockTime Time of the block immediately previous to the one we're calculating difficulty of. + * @param lastDifficultyTarget Compact difficulty target of the last retarget block. + * @param lastRetargetTime Time of the last difficulty retarget. + * @param nextDifficultyTarget The expected difficulty target of the next + * block, used for determining precision of the result. + * @return New difficulty target as compact bytes. + */ + protected long calculateNewDifficultyTargetInner(int previousHeight, long previousBlockTime, + final long lastDifficultyTarget, final long lastRetargetTime, + final long nextDifficultyTarget) { + final int retargetTimespan = this.getTargetTimespan(); + int actualTime = (int) (previousBlockTime - lastRetargetTime); + final int minTimespan = retargetTimespan / 4; + final int maxTimespan = retargetTimespan * 4; + + actualTime = Math.min(maxTimespan, Math.max(minTimespan, actualTime)); + + BigInteger newTarget = Utils.decodeCompactBits(lastDifficultyTarget); + newTarget = newTarget.multiply(BigInteger.valueOf(actualTime)); + newTarget = newTarget.divide(BigInteger.valueOf(retargetTimespan)); + + if (newTarget.compareTo(this.getMaxTarget()) > 0) { + log.info("Difficulty hit proof of work limit: {}", newTarget.toString(16)); + newTarget = this.getMaxTarget(); + } + + int accuracyBytes = (int) (nextDifficultyTarget >>> 24) - 3; + + // The calculated difficulty is to a higher precision than received, so reduce here. + BigInteger mask = BigInteger.valueOf(0xFFFFFFL).shiftLeft(accuracyBytes * 8); + newTarget = newTarget.and(mask); + return Utils.encodeCompactBits(newTarget); + } + + @Override + public AltcoinSerializer getSerializer(boolean parseRetain) { + return new AltcoinSerializer(this, parseRetain); + } + + @Override + public int getProtocolVersionNum(final ProtocolVersion version) { + switch (version) { + case PONG: + case BLOOM_FILTER: + return version.getBitcoinProtocolVersion(); + case CURRENT: + return protocolVersionCurrent; + case MINIMUM: + default: + return protocolVersionMinimum; + } + } + + /** + * Whether this network has special rules to enable minimum difficulty blocks + * after a long interval between two blocks (i.e. testnet). + */ + public boolean allowMinDifficultyBlocks() { + return this.isTestNet(); + } + + public int getTargetSpacing() { + return this.getTargetTimespan() / this.getInterval(); + } + + private static class CheckpointEncounteredException extends Exception { } +} diff --git a/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java b/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java index e1e33862..9fa67592 100644 --- a/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java +++ b/src/main/java/org/qortal/crosschain/DigibyteACCTv3.java @@ -751,12 +751,6 @@ public class DigibyteACCTv3 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java b/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java index 36ff7c5c..a5ec6f1f 100644 --- a/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java +++ b/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java @@ -748,12 +748,6 @@ public class DogecoinACCTv1 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/DogecoinACCTv3.java b/src/main/java/org/qortal/crosschain/DogecoinACCTv3.java index 002a4448..06b04705 100644 --- a/src/main/java/org/qortal/crosschain/DogecoinACCTv3.java +++ b/src/main/java/org/qortal/crosschain/DogecoinACCTv3.java @@ -751,12 +751,6 @@ public class DogecoinACCTv3 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java index 0e70f787..6c917659 100644 --- a/src/main/java/org/qortal/crosschain/ElectrumX.java +++ b/src/main/java/org/qortal/crosschain/ElectrumX.java @@ -46,7 +46,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider { private static final int RESPONSE_TIME_READINGS = 5; private static final long MAX_AVG_RESPONSE_TIME = 2000L; // ms - public static final String MINIMUM_VERSION_ERROR = "MINIMUM VERSION ERROR"; + public static final String MISSING_FEATURES_ERROR = "MISSING FEATURES ERROR"; public static final String EXPECTED_GENESIS_ERROR = "EXPECTED GENESIS ERROR"; private ChainableServerConnectionRecorder recorder = new ChainableServerConnectionRecorder(100); @@ -721,8 +721,19 @@ public class ElectrumX extends BitcoinyBlockchainProvider { // Check connection is suitable by asking for server features, including genesis block hash JSONObject featuresJson = (JSONObject) this.connectedRpc("server.features"); - if (featuresJson == null || Double.parseDouble((String) featuresJson.get("protocol_min")) < MIN_PROTOCOL_VERSION) - return Optional.of( recorder.recordConnection(server, requestedBy, true, false, MINIMUM_VERSION_ERROR) ); + if (featuresJson == null ) + return Optional.of( recorder.recordConnection(server, requestedBy, true, false, MISSING_FEATURES_ERROR) ); + + try { + double protocol_min = CrossChainUtils.getVersionDecimal(featuresJson, "protocol_min"); + + if (protocol_min < MIN_PROTOCOL_VERSION) + return Optional.of( recorder.recordConnection(server, requestedBy, true, false, "old version: protocol_min = " + protocol_min + " < MIN_PROTOCOL_VERSION = " + MIN_PROTOCOL_VERSION) ); + } catch (NumberFormatException e) { + return Optional.of( recorder.recordConnection(server, requestedBy,true, false,featuresJson.get("protocol_min").toString() + " is not a valid version")); + } catch (NullPointerException e) { + return Optional.of( recorder.recordConnection(server, requestedBy,true, false,"server version not available: protocol_min")); + } if (this.expectedGenesisHash != null && !((String) featuresJson.get("genesis_hash")).equals(this.expectedGenesisHash)) return Optional.of( recorder.recordConnection(server, requestedBy, true, false, EXPECTED_GENESIS_ERROR) ); diff --git a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java index ea91501e..6a828981 100644 --- a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java +++ b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java @@ -741,12 +741,6 @@ public class LitecoinACCTv1 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/LitecoinACCTv3.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv3.java index a321a7dc..4a533b4b 100644 --- a/src/main/java/org/qortal/crosschain/LitecoinACCTv3.java +++ b/src/main/java/org/qortal/crosschain/LitecoinACCTv3.java @@ -744,12 +744,6 @@ public class LitecoinACCTv3 implements ACCT { 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) diff --git a/src/main/java/org/qortal/crosschain/NetTBD.java b/src/main/java/org/qortal/crosschain/NetTBD.java new file mode 100644 index 00000000..c52449b4 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/NetTBD.java @@ -0,0 +1,52 @@ +package org.qortal.crosschain; + +import org.bitcoinj.core.NetworkParameters; + +import java.util.Collection; + +public class NetTBD { + + private String name; + private long feeCeiling; + private NetworkParameters params; + private Collection servers; + private String genesisHash; + + public NetTBD(String name, long feeCeiling, NetworkParameters params, Collection servers, String genesisHash) { + this.name = name; + this.feeCeiling = feeCeiling; + this.params = params; + this.servers = servers; + this.genesisHash = genesisHash; + } + + public String getName() { + + return this.name; + } + + public long getFeeCeiling() { + + return feeCeiling; + } + + public void setFeeCeiling(long feeCeiling) { + + this.feeCeiling = feeCeiling; + } + + public NetworkParameters getParams() { + + return this.params; + } + + public Collection getServers() { + + return this.servers; + } + + public String getGenesisHash() { + + return this.genesisHash; + } +} \ No newline at end of file diff --git a/src/main/java/org/qortal/crosschain/PirateChainACCTv3.java b/src/main/java/org/qortal/crosschain/PirateChainACCTv3.java index f5addafe..8873eeab 100644 --- a/src/main/java/org/qortal/crosschain/PirateChainACCTv3.java +++ b/src/main/java/org/qortal/crosschain/PirateChainACCTv3.java @@ -768,12 +768,6 @@ public class PirateChainACCTv3 implements ACCT { return tradeData; } - /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */ - public static byte[] buildOfferMessage(byte[] partnerBitcoinPublicKey, byte[] hashOfSecretA, int lockTimeA) { - byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA); - return Bytes.concat(partnerBitcoinPublicKey, 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) diff --git a/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java b/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java index 866e2d6b..f027e9ca 100644 --- a/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java +++ b/src/main/java/org/qortal/crosschain/RavencoinACCTv3.java @@ -751,12 +751,6 @@ public class RavencoinACCTv3 implements ACCT { 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) diff --git a/src/test/java/org/qortal/test/api/CrossChainUtilsTests.java b/src/test/java/org/qortal/test/api/CrossChainUtilsTests.java new file mode 100644 index 00000000..0e4a6f07 --- /dev/null +++ b/src/test/java/org/qortal/test/api/CrossChainUtilsTests.java @@ -0,0 +1,140 @@ +package org.qortal.test.api; + +import org.json.simple.JSONObject; +import org.junit.Assert; +import org.junit.Test; +import org.qortal.api.resource.CrossChainUtils; +import org.qortal.test.common.ApiCommon; + +import java.util.HashMap; +import java.util.Map; + +public class CrossChainUtilsTests extends ApiCommon { + + @Test + public void testReduceDelimeters1() { + + String string = CrossChainUtils.reduceDelimeters("", 1, ','); + + Assert.assertEquals("", string); + } + + @Test + public void testReduceDelimeters2() { + + String string = CrossChainUtils.reduceDelimeters("0.17.0", 1, ','); + + Assert.assertEquals("0.17.0", string); + } + + @Test + public void testReduceDelimeters3() { + + String string = CrossChainUtils.reduceDelimeters("0.17.0", 1, '.'); + + Assert.assertEquals("0.17", string); + } + + @Test + public void testReduceDelimeters4() { + + String string = CrossChainUtils.reduceDelimeters("0.17.0", 2, '.'); + + Assert.assertEquals("0.17.0", string); + } + + @Test + public void testReduceDelimeters5() { + + String string = CrossChainUtils.reduceDelimeters("0.17.0", 10, '.'); + + Assert.assertEquals("0.17.0", string); + } + + @Test + public void testReduceDelimeters6() { + + String string = CrossChainUtils.reduceDelimeters("0.17.0", -1, '.'); + + Assert.assertEquals("0.17.0", string); + } + + @Test + public void testReduceDelimeters7() { + + String string = CrossChainUtils.reduceDelimeters("abcdef abcdef", 1, 'd'); + + Assert.assertEquals("abcdef abc", string); + } + + @Test + public void testGetVersionDecimalThrowNumberFormatExceptionTrue() { + + boolean thrown = false; + + try { + Map map = new HashMap<>(); + map.put("x", "v"); + double versionDecimal = CrossChainUtils.getVersionDecimal(new JSONObject(map), "x"); + } + catch( NumberFormatException e ) { + thrown = true; + } + + Assert.assertTrue(thrown); + } + + @Test + public void testGetVersionDecimalThrowNullPointerExceptionTrue() { + + boolean thrown = false; + + try { + Map map = new HashMap<>(); + + double versionDecimal = CrossChainUtils.getVersionDecimal(new JSONObject(map), "x"); + } + catch( NullPointerException e ) { + thrown = true; + } + + Assert.assertTrue(thrown); + } + + @Test + public void testGetVersionDecimalThrowAnyExceptionFalse() { + + boolean thrown = false; + + try { + Map map = new HashMap<>(); + map.put("x", "5"); + double versionDecimal = CrossChainUtils.getVersionDecimal(new JSONObject(map), "x"); + } + catch( NullPointerException | NumberFormatException e ) { + thrown = true; + } + + Assert.assertFalse(thrown); + } + + @Test + public void testGetVersionDecimal1() { + + boolean thrown = false; + + double versionDecimal = 0d; + + try { + Map map = new HashMap<>(); + map.put("x", "5.0.0"); + versionDecimal = CrossChainUtils.getVersionDecimal(new JSONObject(map), "x"); + } + catch( NullPointerException | NumberFormatException e ) { + thrown = true; + } + + Assert.assertEquals(5, versionDecimal, 0.001); + Assert.assertFalse(thrown); + } +} diff --git a/src/test/java/org/qortal/test/crosschain/apps/Common.java b/src/test/java/org/qortal/test/crosschain/apps/Common.java index dd3130b9..5bba0dc4 100644 --- a/src/test/java/org/qortal/test/crosschain/apps/Common.java +++ b/src/test/java/org/qortal/test/crosschain/apps/Common.java @@ -92,7 +92,7 @@ public abstract class Common { List unspentOutputs = Collections.emptyList(); try { - unspentOutputs = bitcoiny.getUnspentOutputs(address58); + unspentOutputs = bitcoiny.getUnspentOutputs(address58, false); } catch (ForeignBlockchainException e) { System.err.println(String.format("Can't find unspent outputs for %s: %s", address58, e.getMessage())); return unspentOutputs;