Merge branch 'Qortal:master' into master

This commit is contained in:
AlphaX 2024-09-11 20:47:00 +02:00 committed by GitHub
commit 0266248ca6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
36 changed files with 2262 additions and 367 deletions

View File

@ -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 +
'}';
}
}

View File

@ -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 +
'}';
}
}

View File

@ -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);

View File

@ -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",

View File

@ -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);
}
}

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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());

View File

@ -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);

View File

@ -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());

View File

@ -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)

View 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;
}
}

View File

@ -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;
}
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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();

View 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 );
}
}

View File

@ -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));
}
}

View 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) );
}
}

View 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 { }
}

View File

@ -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)

View File

@ -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)

View File

@ -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)

View File

@ -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) );

View File

@ -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)

View File

@ -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)

View 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;
}
}

View File

@ -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)

View File

@ -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)

View File

@ -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;

View 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);
}
}

View File

@ -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;