From f5cd664ddecb039137dc0e62f8433b3954bd4fa5 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 12:52:25 -0700 Subject: [PATCH 01/17] I'm adding this in now for later use. These are the parameters needed for future foreign blockchain support. --- .../model/crosschain/BitcoinyTBDRequest.java | 692 ++++++++++++++++++ 1 file changed, 692 insertions(+) create mode 100644 src/main/java/org/qortal/api/model/crosschain/BitcoinyTBDRequest.java 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 + + '}'; + } +} From 1cd5dccbd6b95f281414ef7b6ee8f8418bb6b3eb Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 12:56:03 -0700 Subject: [PATCH 02/17] Adding support for BitcoinyTBD, version parsing and crosschain offer message building. --- .../qortal/api/resource/CrossChainUtils.java | 87 +++++++++++ .../qortal/test/api/CrossChainUtilsTests.java | 140 ++++++++++++++++++ 2 files changed, 227 insertions(+) create mode 100644 src/test/java/org/qortal/test/api/CrossChainUtilsTests.java 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/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); + } +} From 661827f92af87f3b035a3ed38fedb1544f81f4b5 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 13:04:04 -0700 Subject: [PATCH 03/17] Adding for later use. --- .../org/qortal/crosschain/BitcoinyTBD.java | 151 ++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/main/java/org/qortal/crosschain/BitcoinyTBD.java 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 From 4cf157ba6406241b5b76aea622b6b43357991674 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 13:07:04 -0700 Subject: [PATCH 04/17] Adding for later use. --- .../java/org/qortal/crosschain/NetTBD.java | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 src/main/java/org/qortal/crosschain/NetTBD.java 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 From da1ea9fe2cc3d536538ddc3f30609be22e3ac8f9 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 13:11:12 -0700 Subject: [PATCH 05/17] Adding for later use. --- .../crosschain/DeterminedNetworkParams.java | 387 ++++++++++++++++++ 1 file changed, 387 insertions(+) create mode 100644 src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java 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 { } +} From d4b0d47c908a330550bd6b89d0f352016146263d Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 15:18:11 -0700 Subject: [PATCH 06/17] Support for responding to multiple crosschain sell offers. --- .../resource/CrossChainTradeBotResource.java | 127 ++++++++++++++++++ .../qortal/controller/tradebot/TradeBot.java | 35 +++++ .../java/org/qortal/crosschain/Bitcoiny.java | 39 ++++++ 3 files changed, 201 insertions(+) diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java index 5a50222a..9d33cd22 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java @@ -17,12 +17,14 @@ 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.SupportedBlockchain; import org.qortal.crypto.Crypto; @@ -42,8 +44,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 +191,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.", + 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 +294,96 @@ 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 and fill the optional + else if( acct.isEmpty() ) { + if( !(acctUsingAtData.getBlockchain() instanceof Bitcoiny) ) + 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/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/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 7f624e20..b1938639 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -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 * From 780bfe6249c730184381e65260dc5b4915526673 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 15:30:08 -0700 Subject: [PATCH 07/17] Support for responding to multiple crosschain sell offers. --- .../crosschain/TradeBotRespondRequests.java | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 src/main/java/org/qortal/api/model/crosschain/TradeBotRespondRequests.java 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 From a07052161a37a40eb3d1d1aeaae08e58c7685cf5 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 15:36:22 -0700 Subject: [PATCH 08/17] Support for responding to multiple crosschain sell offers. --- .../controller/tradebot/TradeBotUtils.java | 217 ++++++++++++++++++ 1 file changed, 217 insertions(+) create mode 100644 src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java 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 From 4f05b61a8ee3d156dff821d5a4a1a0f86789b008 Mon Sep 17 00:00:00 2001 From: kennycud Date: Thu, 25 Jul 2024 15:38:56 -0700 Subject: [PATCH 09/17] Support for responding to multiple crosschain sell offers. --- .../controller/tradebot/TradeStates.java | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 src/main/java/org/qortal/controller/tradebot/TradeStates.java 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; + } + } +} From 8c1251d7166d2e08f889dfdfac97d547bf8fc7e3 Mon Sep 17 00:00:00 2001 From: kennycud Date: Fri, 26 Jul 2024 10:40:27 -0700 Subject: [PATCH 10/17] Delegated buildOfferMessage functionality to CrossChainUtils and removed copies of it from each individual class. Delegated trade bot state enumerations to the TradeStates class. --- .../tradebot/BitcoinACCTv1TradeBot.java | 3 +- .../tradebot/BitcoinACCTv3TradeBot.java | 47 ++----------------- .../tradebot/DigibyteACCTv3TradeBot.java | 46 ++---------------- .../tradebot/DogecoinACCTv1TradeBot.java | 3 +- .../tradebot/DogecoinACCTv3TradeBot.java | 46 ++---------------- .../tradebot/LitecoinACCTv1TradeBot.java | 3 +- .../tradebot/LitecoinACCTv3TradeBot.java | 47 ++----------------- .../tradebot/PirateChainACCTv3TradeBot.java | 46 ++---------------- .../tradebot/RavencoinACCTv3TradeBot.java | 46 ++---------------- .../org/qortal/crosschain/BitcoinACCTv1.java | 6 --- .../org/qortal/crosschain/BitcoinACCTv3.java | 6 --- .../org/qortal/crosschain/DigibyteACCTv3.java | 6 --- .../org/qortal/crosschain/DogecoinACCTv3.java | 6 --- .../org/qortal/crosschain/LitecoinACCTv1.java | 6 --- .../org/qortal/crosschain/LitecoinACCTv3.java | 6 --- .../qortal/crosschain/PirateChainACCTv3.java | 6 --- .../qortal/crosschain/RavencoinACCTv3.java | 6 --- 17 files changed, 24 insertions(+), 311 deletions(-) diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java index 259a16b8..6963f11c 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); diff --git a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java index 9ab97be9..48f089d2 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); diff --git a/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java index 4b1ba7bb..b8ce66e5 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); diff --git a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java index 52e7bb24..1d03bec7 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); diff --git a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java index b57b9354..84e00a14 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); diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java index 0b612d11..43009421 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); diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java index b5631f0b..5359d436 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); 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..2b640927 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); 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/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/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/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/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) From 51feb9682497f58241c303f657c24ea0d27bd46b Mon Sep 17 00:00:00 2001 From: kennycud Date: Fri, 26 Jul 2024 13:50:43 -0700 Subject: [PATCH 11/17] Delegated buildOfferMessage functionality to CrossChainUtils and removed copies of it from each individual class. Delegated trade bot state enumerations to the TradeStates class. --- src/main/java/org/qortal/crosschain/DogecoinACCTv1.java | 6 ------ 1 file changed, 6 deletions(-) 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) From 211fc0d5a4fda9fbc827bfbb2c13905d9d38c306 Mon Sep 17 00:00:00 2001 From: kennycud Date: Fri, 26 Jul 2024 13:53:05 -0700 Subject: [PATCH 12/17] Protocol version error handling improvements. --- .../java/org/qortal/crosschain/ElectrumX.java | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) 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) ); From da889f2905dcc474291704caa3e31c7235090d0c Mon Sep 17 00:00:00 2001 From: kennycud Date: Sat, 27 Jul 2024 12:50:22 -0700 Subject: [PATCH 13/17] Including unconfirmed transactions for wallet balances while spending foreign coin. This is for sends and for responding to trade sell orders. This is not for any other stages in the trading process after the initial response. --- .../api/resource/CrossChainHtlcResource.java | 6 +++--- .../controller/tradebot/BitcoinACCTv1TradeBot.java | 8 ++++---- .../controller/tradebot/BitcoinACCTv3TradeBot.java | 4 ++-- .../tradebot/DigibyteACCTv3TradeBot.java | 4 ++-- .../tradebot/DogecoinACCTv1TradeBot.java | 4 ++-- .../tradebot/DogecoinACCTv3TradeBot.java | 4 ++-- .../tradebot/LitecoinACCTv1TradeBot.java | 4 ++-- .../tradebot/LitecoinACCTv3TradeBot.java | 4 ++-- .../tradebot/RavencoinACCTv3TradeBot.java | 4 ++-- src/main/java/org/qortal/crosschain/Bitcoiny.java | 14 +++++++------- .../qortal/crosschain/BitcoinyUTXOProvider.java | 2 +- .../org/qortal/test/crosschain/apps/Common.java | 2 +- 12 files changed, 30 insertions(+), 30 deletions(-) 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/controller/tradebot/BitcoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java index 6963f11c..e7cb0fb8 100644 --- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv1TradeBot.java @@ -894,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, @@ -1064,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); @@ -1136,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()); @@ -1202,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 48f089d2..18f79b81 100644 --- a/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/BitcoinACCTv3TradeBot.java @@ -752,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); @@ -816,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 b8ce66e5..5b65c9a1 100644 --- a/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DigibyteACCTv3TradeBot.java @@ -753,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); @@ -817,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 1d03bec7..6c9f5a29 100644 --- a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv1TradeBot.java @@ -794,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); @@ -858,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 84e00a14..6a2ef700 100644 --- a/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/DogecoinACCTv3TradeBot.java @@ -753,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); @@ -817,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 43009421..cef93d12 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java @@ -757,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); @@ -821,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 5359d436..aa791e96 100644 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv3TradeBot.java @@ -752,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); @@ -816,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/RavencoinACCTv3TradeBot.java b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java index 2b640927..a383dfd8 100644 --- a/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/RavencoinACCTv3TradeBot.java @@ -753,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); @@ -817,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/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index b1938639..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) { @@ -430,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()) { @@ -504,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; @@ -608,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; @@ -842,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)); } @@ -932,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/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/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; From 145191075aed85e65b717796653e073a8a1fcd0a Mon Sep 17 00:00:00 2001 From: kennycud Date: Fri, 30 Aug 2024 04:53:38 -0700 Subject: [PATCH 14/17] Responding to multiple trade offers on Pirate Chain now throws an invalid criteria error. The Pirate Chain API we are using does not support multiple spends. --- .../api/resource/CrossChainTradeBotResource.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java index 9d33cd22..de646a9f 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainTradeBotResource.java @@ -26,6 +26,7 @@ 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; @@ -198,7 +199,7 @@ public class CrossChainTradeBotResource { @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.", + 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( @@ -323,9 +324,12 @@ public class CrossChainTradeBotResource { 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 and fill the optional + // 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) ) + if( !(acctUsingAtData.getBlockchain() instanceof Bitcoiny) || + acctUsingAtData.getBlockchain() instanceof PirateChain ) throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA); acct = Optional.of(acctUsingAtData); } From 454c471dfef7eef0d97604e82358dcfefad1bf7b Mon Sep 17 00:00:00 2001 From: kennycud Date: Tue, 3 Sep 2024 18:39:41 -0700 Subject: [PATCH 15/17] Changed gapLimit from 24 to 3 since we have mitigated the gap problem. --- src/main/java/org/qortal/settings/Settings.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index de0ce5ed..90dab19b 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -323,7 +323,7 @@ public class Settings { /* Foreign chains */ /** The number of consecutive empty addresses required before treating a wallet's transaction set as complete */ - private int gapLimit = 24; + private int gapLimit = 3; /** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */ private int bitcoinjLookaheadSize = 50; From acc37cef0e90536c319842456fc9cecbc28d5659 Mon Sep 17 00:00:00 2001 From: kennycud Date: Tue, 3 Sep 2024 18:42:27 -0700 Subject: [PATCH 16/17] storing blockchain data in a cache to reduce redundant RPCs to the ElectrumX servers --- .../java/org/qortal/crosschain/Bitcoiny.java | 70 ++++++++++---- .../qortal/crosschain/BlockchainCache.java | 91 +++++++++++++++++++ 2 files changed, 144 insertions(+), 17 deletions(-) create mode 100644 src/main/java/org/qortal/crosschain/BlockchainCache.java diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java index 4a819209..a4f5a2af 100644 --- a/src/main/java/org/qortal/crosschain/Bitcoiny.java +++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java @@ -55,6 +55,13 @@ public abstract class Bitcoiny implements ForeignBlockchain { protected Coin feePerKb; + /** + * Blockchain Cache + * + * To store blockchain data and reduce redundant RPCs to the ElectrumX servers + */ + private final BlockchainCache blockchainCache = new BlockchainCache(); + // Constructors and instance protected Bitcoiny(BitcoinyBlockchainProvider blockchainProvider, Context bitcoinjContext, String currencyCode, Coin feePerKb) { @@ -509,8 +516,22 @@ public abstract class Bitcoiny implements ForeignBlockchain { if (!historicTransactionHashes.isEmpty()) { areAllKeysUnused = false; - for (TransactionHash transactionHash : historicTransactionHashes) - walletTransactions.add(this.getTransaction(transactionHash.txHash)); + for (TransactionHash transactionHash : historicTransactionHashes) { + + Optional walletTransaction + = this.blockchainCache.getTransactionByHash( transactionHash.txHash ); + + // if the wallet transaction is already cached + if(walletTransaction.isPresent() ) { + walletTransactions.add( walletTransaction.get() ); + } + // otherwise get the transaction from the blockchain server + else { + BitcoinyTransaction transaction = getTransaction(transactionHash.txHash); + walletTransactions.add( transaction ); + this.blockchainCache.addTransactionByHash(transactionHash.txHash, transaction); + } + } } } @@ -602,17 +623,25 @@ public abstract class Bitcoiny implements ForeignBlockchain { for (; ki < keys.size(); ++ki) { DeterministicKey dKey = keys.get(ki); - // Check for transactions Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); keySet.add(address.toString()); - byte[] script = ScriptBuilder.createOutputScript(address).getProgram(); - // Ask for transaction history - if it's empty then key has never been used - List historicTransactionHashes = this.getAddressTransactions(script, true); - - if (!historicTransactionHashes.isEmpty()) { + // if the key already has a verified transaction history + if( this.blockchainCache.keyHasHistory( dKey ) ){ areAllKeysUnused = false; } + else { + // Check for transactions + 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, true); + + if (!historicTransactionHashes.isEmpty()) { + areAllKeysUnused = false; + this.blockchainCache.addKeyWithHistory(dKey); + } + } } if (areAllKeysUnused) { @@ -667,19 +696,26 @@ public abstract class Bitcoiny implements ForeignBlockchain { do { boolean areAllKeysUnused = true; - for (; ki < keys.size(); ++ki) { + for (; areAllKeysUnused && ki < keys.size(); ++ki) { DeterministicKey dKey = keys.get(ki); - // Check for transactions - Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); - 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); - - if (!historicTransactionHashes.isEmpty()) { + // if the key already has a verified transaction history + if( this.blockchainCache.keyHasHistory(dKey)) { areAllKeysUnused = false; } + else { + // Check for transactions + Address address = Address.fromKey(this.params, dKey, ScriptType.P2PKH); + 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, true); + + if (!historicTransactionHashes.isEmpty()) { + areAllKeysUnused = false; + this.blockchainCache.addKeyWithHistory(dKey); + } + } } if (areAllKeysUnused) { diff --git a/src/main/java/org/qortal/crosschain/BlockchainCache.java b/src/main/java/org/qortal/crosschain/BlockchainCache.java new file mode 100644 index 00000000..bfc6f3c6 --- /dev/null +++ b/src/main/java/org/qortal/crosschain/BlockchainCache.java @@ -0,0 +1,91 @@ +package org.qortal.crosschain; + +import org.bitcoinj.crypto.DeterministicKey; + +import java.util.Optional; +import java.util.Queue; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentLinkedDeque; + +/** + * Class BlockchainCache + * + * Cache blockchain information to reduce redundant RPCs to the ElectrumX servers. + */ +public class BlockchainCache { + + /** + * Keys With History + * + * Deterministic Keys with any transaction history. + */ + private Queue keysWithHistory = new ConcurrentLinkedDeque<>(); + + /** + * Transactions By Hash + * + * Transaction Hash -> Transaction + */ + private ConcurrentHashMap transactionByHash = new ConcurrentHashMap<>(); + + /** + * Key History Limit + * + * If this limit is reached, the cache will be reduced. + */ + private static final int KEY_HISTORY_LIMIT = 10000; + + /** + * Transaction Limit + * + * If this limit is reached, the cache will be cleared. + */ + private static final int TRANSACTION_LIMIT = 10000; + + /** + * Add Key With History + * + * @param key a deterministic key with a verified history + */ + public void addKeyWithHistory(DeterministicKey key) { + + if( this.keysWithHistory.size() > KEY_HISTORY_LIMIT ) this.keysWithHistory.remove(); + + this.keysWithHistory.add(key); + } + + /** + * Key Has History? + * + * @param key the deterministic key + * + * @return true if the key has a history, otherwise false + */ + public boolean keyHasHistory( DeterministicKey key ) { + return this.keysWithHistory.contains(key); + } + + /** + * Add Transaction By Hash + * + * @param hash the transaction hash + * @param transaction the transaction + */ + public void addTransactionByHash( String hash, BitcoinyTransaction transaction ) { + + if( this.transactionByHash.size() > TRANSACTION_LIMIT ) this.transactionByHash.clear(); + + this.transactionByHash.put(hash, transaction); + } + + /** + * Get Transaction By Hash + * + * @param hash the transaction hash + * + * @return the transaction, empty if the hash is not in the cache + */ + public Optional getTransactionByHash( String hash ) { + return Optional.ofNullable( this.transactionByHash.get(hash) ); + } +} \ No newline at end of file From a64e9052dd0b6ad294377c238a74f403713b2f6e Mon Sep 17 00:00:00 2001 From: kennycud Date: Wed, 4 Sep 2024 16:24:08 -0700 Subject: [PATCH 17/17] consolidated the cache limits into an attribute in Settings.java --- .../qortal/crosschain/BlockchainCache.java | 22 +++++++++---------- .../java/org/qortal/settings/Settings.java | 6 +++++ 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/main/java/org/qortal/crosschain/BlockchainCache.java b/src/main/java/org/qortal/crosschain/BlockchainCache.java index bfc6f3c6..f6a1acf6 100644 --- a/src/main/java/org/qortal/crosschain/BlockchainCache.java +++ b/src/main/java/org/qortal/crosschain/BlockchainCache.java @@ -1,6 +1,7 @@ package org.qortal.crosschain; import org.bitcoinj.crypto.DeterministicKey; +import org.qortal.settings.Settings; import java.util.Optional; import java.util.Queue; @@ -29,18 +30,11 @@ public class BlockchainCache { private ConcurrentHashMap transactionByHash = new ConcurrentHashMap<>(); /** - * Key History Limit + * Cache Limit * - * If this limit is reached, the cache will be reduced. + * If this limit is reached, the cache will be cleared or reduced. */ - private static final int KEY_HISTORY_LIMIT = 10000; - - /** - * Transaction Limit - * - * If this limit is reached, the cache will be cleared. - */ - private static final int TRANSACTION_LIMIT = 10000; + private static final int CACHE_LIMIT = Settings.getInstance().getBlockchainCacheLimit(); /** * Add Key With History @@ -49,7 +43,9 @@ public class BlockchainCache { */ public void addKeyWithHistory(DeterministicKey key) { - if( this.keysWithHistory.size() > KEY_HISTORY_LIMIT ) this.keysWithHistory.remove(); + if( this.keysWithHistory.size() > CACHE_LIMIT ) { + this.keysWithHistory.remove(); + } this.keysWithHistory.add(key); } @@ -73,7 +69,9 @@ public class BlockchainCache { */ public void addTransactionByHash( String hash, BitcoinyTransaction transaction ) { - if( this.transactionByHash.size() > TRANSACTION_LIMIT ) this.transactionByHash.clear(); + if( this.transactionByHash.size() > CACHE_LIMIT ) { + this.transactionByHash.clear(); + } this.transactionByHash.put(hash, transaction); } diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java index 90dab19b..f18ccd88 100644 --- a/src/main/java/org/qortal/settings/Settings.java +++ b/src/main/java/org/qortal/settings/Settings.java @@ -328,6 +328,9 @@ public class Settings { /** How many wallet keys to generate when using bitcoinj as the blockchain interface (e.g. when sending coins) */ private int bitcoinjLookaheadSize = 50; + /** How many units of data to be kept in a blockchain cache before the cache should be reduced or cleared. */ + private int blockchainCacheLimit = 1000; + // Data storage (QDN) /** Data storage enabled/disabled*/ @@ -1049,6 +1052,9 @@ public class Settings { return bitcoinjLookaheadSize; } + public int getBlockchainCacheLimit() { + return blockchainCacheLimit; + } public boolean isQdnEnabled() { return this.qdnEnabled;