mirror of
https://github.com/Qortal/qortal.git
synced 2025-03-13 11:12:31 +00:00
Merge branch 'Qortal:master' into master
This commit is contained in:
commit
0266248ca6
@ -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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -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<String> addresses;
|
||||
|
||||
@Schema(description = "Qortal address for receiving QORT from AT", example = "Qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq")
|
||||
public String receivingAddress;
|
||||
|
||||
public TradeBotRespondRequests() {
|
||||
}
|
||||
|
||||
public TradeBotRespondRequests(String foreignKey, List<String> 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 +
|
||||
'}';
|
||||
}
|
||||
}
|
@ -157,7 +157,7 @@ public class CrossChainHtlcResource {
|
||||
htlcStatus.bitcoinP2shAddress = p2shAddress;
|
||||
htlcStatus.bitcoinP2shBalance = BigDecimal.valueOf(p2shBalance, 8);
|
||||
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddress.toString());
|
||||
List<TransactionOutput> 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<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> 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<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoiny.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Validate the destination foreign blockchain address
|
||||
Address receiving = Address.fromString(bitcoiny.getNetworkParameters(), receiveAddress);
|
||||
|
@ -17,13 +17,16 @@ import org.qortal.api.ApiExceptionFactory;
|
||||
import org.qortal.api.Security;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.api.model.crosschain.TradeBotRespondRequest;
|
||||
import org.qortal.api.model.crosschain.TradeBotRespondRequests;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.controller.Controller;
|
||||
import org.qortal.controller.tradebot.AcctTradeBot;
|
||||
import org.qortal.controller.tradebot.TradeBot;
|
||||
import org.qortal.crosschain.ACCT;
|
||||
import org.qortal.crosschain.AcctMode;
|
||||
import org.qortal.crosschain.Bitcoiny;
|
||||
import org.qortal.crosschain.ForeignBlockchain;
|
||||
import org.qortal.crosschain.PirateChain;
|
||||
import org.qortal.crosschain.SupportedBlockchain;
|
||||
import org.qortal.crypto.Crypto;
|
||||
import org.qortal.data.at.ATData;
|
||||
@ -42,8 +45,10 @@ import javax.servlet.http.HttpServletRequest;
|
||||
import javax.ws.rs.*;
|
||||
import javax.ws.rs.core.Context;
|
||||
import javax.ws.rs.core.MediaType;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Path("/crosschain/tradebot")
|
||||
@ -187,6 +192,39 @@ public class CrossChainTradeBotResource {
|
||||
public String tradeBotResponder(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequest tradeBotRespondRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return createTradeBotResponse(tradeBotRespondRequest);
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/respondmultiple")
|
||||
@Operation(
|
||||
summary = "Respond to multiple trade offers. NOTE: WILL SPEND FUNDS!)",
|
||||
description = "Start a new trade-bot entry to respond to chosen trade offers. Pirate Chain is not supported and will throw an invalid criteria error.",
|
||||
requestBody = @RequestBody(
|
||||
required = true,
|
||||
content = @Content(
|
||||
mediaType = MediaType.APPLICATION_JSON,
|
||||
schema = @Schema(
|
||||
implementation = TradeBotRespondRequests.class
|
||||
)
|
||||
)
|
||||
),
|
||||
responses = {
|
||||
@ApiResponse(
|
||||
content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "string"))
|
||||
)
|
||||
}
|
||||
)
|
||||
@ApiErrors({ApiError.INVALID_PRIVATE_KEY, ApiError.INVALID_ADDRESS, ApiError.INVALID_CRITERIA, ApiError.FOREIGN_BLOCKCHAIN_BALANCE_ISSUE, ApiError.FOREIGN_BLOCKCHAIN_NETWORK_ISSUE, ApiError.REPOSITORY_ISSUE})
|
||||
@SuppressWarnings("deprecation")
|
||||
@SecurityRequirement(name = "apiKey")
|
||||
public String tradeBotResponderMultiple(@HeaderParam(Security.API_KEY_HEADER) String apiKey, TradeBotRespondRequests tradeBotRespondRequest) {
|
||||
Security.checkApiCallAllowed(request);
|
||||
|
||||
return createTradeBotResponseMultiple(tradeBotRespondRequest);
|
||||
}
|
||||
|
||||
private String createTradeBotResponse(TradeBotRespondRequest tradeBotRespondRequest) {
|
||||
final String atAddress = tradeBotRespondRequest.atAddress;
|
||||
|
||||
// We prefer foreignKey to deprecated xprv58
|
||||
@ -257,6 +295,99 @@ public class CrossChainTradeBotResource {
|
||||
}
|
||||
}
|
||||
|
||||
private String createTradeBotResponseMultiple(TradeBotRespondRequests respondRequests) {
|
||||
try (final Repository repository = RepositoryManager.getRepository()) {
|
||||
|
||||
if (respondRequests.foreignKey == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
List<CrossChainTradeData> crossChainTradeDataList = new ArrayList<>(respondRequests.addresses.size());
|
||||
Optional<ACCT> acct = Optional.empty();
|
||||
|
||||
for(String atAddress : respondRequests.addresses ) {
|
||||
|
||||
if (atAddress == null || !Crypto.isValidAtAddress(atAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (respondRequests.receivingAddress == null || !Crypto.isValidAddress(respondRequests.receivingAddress))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
final Long minLatestBlockTimestamp = NTP.getTime() - (60 * 60 * 1000L);
|
||||
if (!Controller.getInstance().isUpToDate(minLatestBlockTimestamp))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.BLOCKCHAIN_NEEDS_SYNC);
|
||||
|
||||
// Extract data from cross-chain trading AT
|
||||
ATData atData = fetchAtDataWithChecking(repository, atAddress);
|
||||
|
||||
// TradeBot uses AT's code hash to map to ACCT
|
||||
ACCT acctUsingAtData = TradeBot.getInstance().getAcctUsingAtData(atData);
|
||||
if (acctUsingAtData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
// if the optional is empty,
|
||||
// then ensure the ACCT blockchain is a Bitcoiny blockchain, but not Pirate Chain and fill the optional
|
||||
// Even though the Pirate Chain protocol does support multi send,
|
||||
// the Pirate Chain API we are using does not support multi send
|
||||
else if( acct.isEmpty() ) {
|
||||
if( !(acctUsingAtData.getBlockchain() instanceof Bitcoiny) ||
|
||||
acctUsingAtData.getBlockchain() instanceof PirateChain )
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
acct = Optional.of(acctUsingAtData);
|
||||
}
|
||||
// if the optional is filled, then ensure it is equal to the AT in this iteration
|
||||
else if( !acctUsingAtData.getCodeBytesHash().equals(acct.get().getCodeBytesHash()) )
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
if (!acctUsingAtData.getBlockchain().isValidWalletKey(respondRequests.foreignKey))
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_PRIVATE_KEY);
|
||||
|
||||
CrossChainTradeData crossChainTradeData = acctUsingAtData.populateTradeData(repository, atData);
|
||||
if (crossChainTradeData == null)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_ADDRESS);
|
||||
|
||||
if (crossChainTradeData.mode != AcctMode.OFFERING)
|
||||
throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA);
|
||||
|
||||
// Check if there is a buy or a cancel request in progress for this trade
|
||||
List<Transaction.TransactionType> txTypes = List.of(Transaction.TransactionType.MESSAGE);
|
||||
List<TransactionData> 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",
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -7,6 +7,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -527,7 +528,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
// P2SH-A funding confirmed
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = BitcoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -893,7 +894,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
// Redeem P2SH-B using secret-B
|
||||
Coin redeemAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees. The real funds are in P2SH-A.
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB, false);
|
||||
byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
@ -1063,7 +1064,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -1135,7 +1136,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(P2SH_B_OUTPUT_AMOUNT); // An actual amount to avoid dust filter, remaining used as fees.
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressB, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
@ -1201,7 +1202,7 @@ public class BitcoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount - P2SH_B_OUTPUT_AMOUNT);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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.
|
||||
* <p>
|
||||
@ -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<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
@ -313,7 +272,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = BitcoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +752,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(bitcoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +816,7 @@ public class BitcoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = bitcoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = bitcoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
@ -313,7 +273,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = DigibyteACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +753,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(digibyte.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +817,7 @@ public class DigibyteACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = digibyte.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = digibyte.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -7,6 +7,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -313,7 +314,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = DogecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +794,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +858,7 @@ public class DogecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
@ -313,7 +273,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = DogecoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +753,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +817,7 @@ public class DogecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -7,6 +7,7 @@ import org.bitcoinj.script.Script.ScriptType;
|
||||
import org.qortal.account.PrivateKeyAccount;
|
||||
import org.qortal.account.PublicKeyAccount;
|
||||
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
|
||||
import org.qortal.api.resource.CrossChainUtils;
|
||||
import org.qortal.asset.Asset;
|
||||
import org.qortal.crosschain.*;
|
||||
import org.qortal.crypto.Crypto;
|
||||
@ -312,7 +313,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = LitecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -756,7 +757,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -820,7 +821,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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.
|
||||
* <p>
|
||||
@ -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<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
@ -313,7 +272,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = LitecoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +752,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +816,7 @@ public class LitecoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = litecoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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<Integer, State> 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);
|
||||
|
@ -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<Integer, State> map = stream(State.values()).collect(toMap(state -> state.value, state -> state));
|
||||
|
||||
public final int value;
|
||||
public final boolean requiresAtData;
|
||||
public final boolean requiresTradeData;
|
||||
|
||||
State(int value, boolean requiresAtData, boolean requiresTradeData) {
|
||||
this.value = value;
|
||||
this.requiresAtData = requiresAtData;
|
||||
this.requiresTradeData = requiresTradeData;
|
||||
}
|
||||
|
||||
public static State valueOf(int value) {
|
||||
return map.get(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getState() {
|
||||
return this.name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getStateValue() {
|
||||
return this.value;
|
||||
}
|
||||
}
|
||||
|
||||
/** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */
|
||||
private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms
|
||||
|
||||
@ -313,7 +273,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
}
|
||||
|
||||
// Attempt to send MESSAGE to Bob's Qortal trade address
|
||||
byte[] messageData = RavencoinACCTv3.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
byte[] messageData = CrossChainUtils.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
|
||||
String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
|
||||
|
||||
boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
|
||||
@ -793,7 +753,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED: {
|
||||
Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(ravencoin.getNetworkParameters(), redeemAmount, redeemKey,
|
||||
fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
|
||||
@ -857,7 +817,7 @@ public class RavencoinACCTv3TradeBot implements AcctTradeBot {
|
||||
case FUNDED:{
|
||||
Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
|
||||
ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA);
|
||||
List<TransactionOutput> fundingOutputs = ravencoin.getUnspentOutputs(p2shAddressA, false);
|
||||
|
||||
// Determine receive address for refund
|
||||
String receiveAddress = ravencoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
|
||||
|
@ -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.
|
||||
* <p>
|
||||
* Requires chosen trade offers from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a foreign blockchain wallet via <tt>foreignKey</tt>.
|
||||
* <p>
|
||||
* @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<CrossChainTradeData> 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)
|
||||
|
217
src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java
Normal file
217
src/main/java/org/qortal/controller/tradebot/TradeBotUtils.java
Normal file
@ -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.
|
||||
* <p>
|
||||
* Requires chosen trade offers from Bob, passed by <tt>crossChainTradeData</tt>
|
||||
* and access to a Blockchain wallet via <tt>foreignKey</tt>.
|
||||
* <p>
|
||||
* The <tt>crossChainTradeData</tt> contains the current trade offers state
|
||||
* as extracted from the AT's data segment.
|
||||
* <p>
|
||||
* Access to a funded wallet is via a Blockchain BIP32 hierarchical deterministic key,
|
||||
* passed via <tt>foreignKey</tt>.
|
||||
* <b>This key will be stored in your node's database</b>
|
||||
* 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).
|
||||
* <p>
|
||||
* As an example, the foreignKey can be extract from a <i>legacy, password-less</i>
|
||||
* Electrum wallet by going to the console tab and entering:<br>
|
||||
* <tt>wallet.keystore.xprv</tt><br>
|
||||
* which should result in a base58 string starting with either 'xprv' (for Blockchain main-net)
|
||||
* or 'tprv' for (Blockchain test-net).
|
||||
* <p>
|
||||
* It is envisaged that the value in <tt>foreignKey</tt> will actually come from a Qortal-UI-managed wallet.
|
||||
* <p>
|
||||
* If sufficient funds are available, <b>this method will actually fund the P2SH-A</b>
|
||||
* with the Blockchain amount expected by 'Bob'.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* The trade-bot entries are saved to the repository and the cross-chain trading process commences.
|
||||
* <p>
|
||||
*
|
||||
* @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<CrossChainTradeData> 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<String, Long> 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<DataCombiner> 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;
|
||||
}
|
||||
}
|
@ -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<Integer, State> 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) {
|
||||
@ -208,8 +215,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<TransactionOutput> getUnspentOutputs(String base58Address) throws ForeignBlockchainException {
|
||||
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), false);
|
||||
public List<TransactionOutput> getUnspentOutputs(String base58Address, boolean includeUnconfirmed) throws ForeignBlockchainException {
|
||||
List<UnspentOutput> unspentOutputs = this.blockchainProvider.getUnspentOutputs(addressToScriptPubKey(base58Address), includeUnconfirmed);
|
||||
|
||||
List<TransactionOutput> unspentTransactionOutputs = new ArrayList<>();
|
||||
for (UnspentOutput unspentOutput : unspentOutputs) {
|
||||
@ -343,6 +350,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<String, Long> 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<String, Long> amountForRecipient : amountByRecipient.entrySet()) {
|
||||
Address destination = Address.fromString(this.params, amountForRecipient.getKey());
|
||||
transaction.addOutput(Coin.valueOf(amountForRecipient.getValue()), destination);
|
||||
}
|
||||
|
||||
SendRequest sendRequest = SendRequest.forTx(transaction);
|
||||
|
||||
if (feePerByte != null)
|
||||
sendRequest.feePerKb = Coin.valueOf(feePerByte * 1000L); // Note: 1000 not 1024
|
||||
else
|
||||
// Allow override of default for TestNet3, etc.
|
||||
sendRequest.feePerKb = this.getFeePerKb();
|
||||
|
||||
try {
|
||||
wallet.completeTx(sendRequest);
|
||||
return sendRequest.tx;
|
||||
} catch (InsufficientMoneyException e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Spending Candidate Addresses
|
||||
*
|
||||
@ -391,7 +437,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
List<TransactionOutput> allUnspentOutputs = new ArrayList<>();
|
||||
Set<String> walletAddresses = this.getWalletAddresses(key58);
|
||||
for (String address : walletAddresses) {
|
||||
allUnspentOutputs.addAll(this.getUnspentOutputs(address));
|
||||
allUnspentOutputs.addAll(this.getUnspentOutputs(address, true));
|
||||
}
|
||||
for (TransactionOutput output : allUnspentOutputs) {
|
||||
if (!output.isAvailableForSpending()) {
|
||||
@ -465,13 +511,27 @@ 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<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, false);
|
||||
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, true);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
|
||||
for (TransactionHash transactionHash : historicTransactionHashes)
|
||||
walletTransactions.add(this.getTransaction(transactionHash.txHash));
|
||||
for (TransactionHash transactionHash : historicTransactionHashes) {
|
||||
|
||||
Optional<BitcoinyTransaction> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -563,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<TransactionHash> 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
|
||||
byte[] script = ScriptBuilder.createOutputScript(address).getProgram();
|
||||
|
||||
// Ask for transaction history - if it's empty then key has never been used
|
||||
List<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, true);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
this.blockchainCache.addKeyWithHistory(dKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
@ -628,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<TransactionHash> 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<TransactionHash> historicTransactionHashes = this.getAddressTransactions(script, true);
|
||||
|
||||
if (!historicTransactionHashes.isEmpty()) {
|
||||
areAllKeysUnused = false;
|
||||
this.blockchainCache.addKeyWithHistory(dKey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (areAllKeysUnused) {
|
||||
@ -803,7 +878,7 @@ public abstract class Bitcoiny implements ForeignBlockchain {
|
||||
|
||||
List<UnspentOutput> unspentOutputs;
|
||||
try {
|
||||
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, false);
|
||||
unspentOutputs = this.bitcoiny.blockchainProvider.getUnspentOutputs(script, true);
|
||||
} catch (ForeignBlockchainException e) {
|
||||
throw new UTXOProviderException(String.format("Unable to fetch unspent outputs for %s", address));
|
||||
}
|
||||
@ -893,7 +968,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();
|
||||
|
151
src/main/java/org/qortal/crosschain/BitcoinyTBD.java
Normal file
151
src/main/java/org/qortal/crosschain/BitcoinyTBD.java
Normal file
@ -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<String, BitcoinyTBDRequest> requestsById = new HashMap<>();
|
||||
|
||||
private long minimumOrderAmount;
|
||||
|
||||
private static Map<String, BitcoinyTBD> instanceByCode = new HashMap<>();
|
||||
|
||||
private final NetTBD netTBD;
|
||||
|
||||
/**
|
||||
* Default ElectrumX Ports
|
||||
*
|
||||
* These are the defualts for all Bitcoin forks.
|
||||
*/
|
||||
private static final Map<ConnectionType, Integer> 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<BitcoinyTBD> 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<BitcoinyTBDRequest> getRequests() {
|
||||
|
||||
Collection<BitcoinyTBDRequest> requests = requestsById.values();
|
||||
|
||||
List<BitcoinyTBDRequest> 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 );
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
89
src/main/java/org/qortal/crosschain/BlockchainCache.java
Normal file
89
src/main/java/org/qortal/crosschain/BlockchainCache.java
Normal file
@ -0,0 +1,89 @@
|
||||
package org.qortal.crosschain;
|
||||
|
||||
import org.bitcoinj.crypto.DeterministicKey;
|
||||
import org.qortal.settings.Settings;
|
||||
|
||||
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<DeterministicKey> keysWithHistory = new ConcurrentLinkedDeque<>();
|
||||
|
||||
/**
|
||||
* Transactions By Hash
|
||||
*
|
||||
* Transaction Hash -> Transaction
|
||||
*/
|
||||
private ConcurrentHashMap<String, BitcoinyTransaction> transactionByHash = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* Cache Limit
|
||||
*
|
||||
* If this limit is reached, the cache will be cleared or reduced.
|
||||
*/
|
||||
private static final int CACHE_LIMIT = Settings.getInstance().getBlockchainCacheLimit();
|
||||
|
||||
/**
|
||||
* Add Key With History
|
||||
*
|
||||
* @param key a deterministic key with a verified history
|
||||
*/
|
||||
public void addKeyWithHistory(DeterministicKey key) {
|
||||
|
||||
if( this.keysWithHistory.size() > CACHE_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() > CACHE_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<BitcoinyTransaction> getTransactionByHash( String hash ) {
|
||||
return Optional.ofNullable( this.transactionByHash.get(hash) );
|
||||
}
|
||||
}
|
387
src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java
Normal file
387
src/main/java/org/qortal/crosschain/DeterminedNetworkParams.java
Normal file
@ -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 { }
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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) );
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
52
src/main/java/org/qortal/crosschain/NetTBD.java
Normal file
52
src/main/java/org/qortal/crosschain/NetTBD.java
Normal file
@ -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<ElectrumX.Server> servers;
|
||||
private String genesisHash;
|
||||
|
||||
public NetTBD(String name, long feeCeiling, NetworkParameters params, Collection<ElectrumX.Server> 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<ElectrumX.Server> getServers() {
|
||||
|
||||
return this.servers;
|
||||
}
|
||||
|
||||
public String getGenesisHash() {
|
||||
|
||||
return this.genesisHash;
|
||||
}
|
||||
}
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -323,11 +323,14 @@ 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;
|
||||
|
||||
/** 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;
|
||||
|
140
src/test/java/org/qortal/test/api/CrossChainUtilsTests.java
Normal file
140
src/test/java/org/qortal/test/api/CrossChainUtilsTests.java
Normal file
@ -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<String, String> 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<String, String> 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<String, String> 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<String, String> 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);
|
||||
}
|
||||
}
|
@ -92,7 +92,7 @@ public abstract class Common {
|
||||
List<TransactionOutput> 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;
|
||||
|
Loading…
x
Reference in New Issue
Block a user