getEndStates() {
+ return this.endStates;
+ }
+
+ /**
+ * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for DOGE.
+ *
+ * Generates:
+ *
+ * - new 'trade' private key
+ *
+ * Derives:
+ *
+ * - 'native' (as in Qortal) public key, public key hash, address (starting with Q)
+ * - 'foreign' (as in Dogecoin) public key, public key hash
+ *
+ * A Qortal AT is then constructed including the following as constants in the 'data segment':
+ *
+ * - 'native'/Qortal 'trade' address - used as a MESSAGE contact
+ * - 'foreign'/Dogecoin public key hash - used by Alice's P2SH scripts to allow redeem
+ * - QORT amount on offer by Bob
+ * - DOGE amount expected in return by Bob (from Alice)
+ * - trading timeout, in case things go wrong and everyone needs to refund
+ *
+ * Returns a DEPLOY_AT transaction that needs to be signed and broadcast to the Qortal network.
+ *
+ * Trade-bot will wait for Bob's AT to be deployed before taking next step.
+ *
+ * @param repository
+ * @param tradeBotCreateRequest
+ * @return raw, unsigned DEPLOY_AT transaction
+ * @throws DataException
+ */
+ public byte[] createTrade(Repository repository, TradeBotCreateRequest tradeBotCreateRequest) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+
+ // Convert Dogecoin receiving address into public key hash (we only support P2PKH at this time)
+ Address dogecoinReceivingAddress;
+ try {
+ dogecoinReceivingAddress = Address.fromString(Dogecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress);
+ } catch (AddressFormatException e) {
+ throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
+ }
+ if (dogecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH)
+ throw new DataException("Unsupported Dogecoin receiving address: " + tradeBotCreateRequest.receivingAddress);
+
+ byte[] dogecoinReceivingAccountInfo = dogecoinReceivingAddress.getHash();
+
+ PublicKeyAccount creator = new PublicKeyAccount(repository, tradeBotCreateRequest.creatorPublicKey);
+
+ // Deploy AT
+ long timestamp = NTP.getTime();
+ byte[] reference = creator.getLastReference();
+ long fee = 0L;
+ byte[] signature = null;
+ BaseTransactionData baseTransactionData = new BaseTransactionData(timestamp, Group.NO_GROUP, reference, creator.getPublicKey(), fee, signature);
+
+ String name = "QORT/DOGE ACCT";
+ String description = "QORT/DOGE cross-chain trade";
+ String aTType = "ACCT";
+ String tags = "ACCT QORT DOGE";
+ byte[] creationBytes = DogecoinACCTv1.buildQortalAT(tradeNativeAddress, tradeForeignPublicKeyHash, tradeBotCreateRequest.qortAmount,
+ tradeBotCreateRequest.foreignAmount, tradeBotCreateRequest.tradeTimeout);
+ long amount = tradeBotCreateRequest.fundingQortAmount;
+
+ DeployAtTransactionData deployAtTransactionData = new DeployAtTransactionData(baseTransactionData, name, description, aTType, tags, creationBytes, amount, Asset.QORT);
+
+ DeployAtTransaction deployAtTransaction = new DeployAtTransaction(repository, deployAtTransactionData);
+ fee = deployAtTransaction.calcRecommendedFee();
+ deployAtTransactionData.setFee(fee);
+
+ DeployAtTransaction.ensureATAddress(deployAtTransactionData);
+ String atAddress = deployAtTransactionData.getAtAddress();
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv1.NAME,
+ State.BOB_WAITING_FOR_AT_CONFIRM.name(), State.BOB_WAITING_FOR_AT_CONFIRM.value,
+ creator.getAddress(), atAddress, timestamp, tradeBotCreateRequest.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ null, null,
+ SupportedBlockchain.DOGECOIN.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ tradeBotCreateRequest.foreignAmount, null, null, null, dogecoinReceivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress));
+
+ // Attempt to backup the trade bot data
+ TradeBot.backupTradeBotData(repository);
+
+ // Return to user for signing and broadcast as we don't have their Qortal private key
+ try {
+ return DeployAtTransactionTransformer.toBytes(deployAtTransactionData);
+ } catch (TransformationException e) {
+ throw new DataException("Failed to transform DEPLOY_AT transaction?", e);
+ }
+ }
+
+ /**
+ * Creates a trade-bot entry from the 'Alice' viewpoint, i.e. matching DOGE to an existing offer.
+ *
+ * Requires a chosen trade offer from Bob, passed by crossChainTradeData
+ * and access to a Dogecoin wallet via xprv58.
+ *
+ * The crossChainTradeData contains the current trade offer state
+ * as extracted from the AT's data segment.
+ *
+ * Access to a funded wallet is via a Dogecoin BIP32 hierarchical deterministic key,
+ * passed via xprv58.
+ * This key will be stored in your node's database
+ * to allow trade-bot to create/fund the necessary P2SH transactions!
+ * However, due to the nature of BIP32 keys, it is possible to give the trade-bot
+ * only a subset of wallet access (see BIP32 for more details).
+ *
+ * As an example, the xprv58 can be extract from a legacy, password-less
+ * Electrum wallet by going to the console tab and entering:
+ * wallet.keystore.xprv
+ * which should result in a base58 string starting with either 'xprv' (for Dogecoin main-net)
+ * or 'tprv' for (Dogecoin test-net).
+ *
+ * It is envisaged that the value in xprv58 will actually come from a Qortal-UI-managed wallet.
+ *
+ * If sufficient funds are available, this method will actually fund the P2SH-A
+ * with the Dogecoin amount expected by 'Bob'.
+ *
+ * If the Dogecoin transaction is successfully broadcast to the network then
+ * we also send a MESSAGE to Bob's trade-bot to let them know.
+ *
+ * The trade-bot entry is saved to the repository and the cross-chain trading process commences.
+ *
+ * @param repository
+ * @param crossChainTradeData chosen trade OFFER that Alice wants to match
+ * @param xprv58 funded wallet xprv in base58
+ * @return true if P2SH-A funding transaction successfully broadcast to Dogecoin network, false otherwise
+ * @throws DataException
+ */
+ public ResponseResult startResponse(Repository repository, ATData atData, ACCT acct, CrossChainTradeData crossChainTradeData, String xprv58, String receivingAddress) throws DataException {
+ byte[] tradePrivateKey = TradeBot.generateTradePrivateKey();
+ byte[] secretA = TradeBot.generateSecret();
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+
+ byte[] tradeNativePublicKey = TradeBot.deriveTradeNativePublicKey(tradePrivateKey);
+ byte[] tradeNativePublicKeyHash = Crypto.hash160(tradeNativePublicKey);
+ String tradeNativeAddress = Crypto.toAddress(tradeNativePublicKey);
+
+ byte[] tradeForeignPublicKey = TradeBot.deriveTradeForeignPublicKey(tradePrivateKey);
+ byte[] tradeForeignPublicKeyHash = Crypto.hash160(tradeForeignPublicKey);
+ byte[] receivingPublicKeyHash = Base58.decode(receivingAddress); // Actually the whole address, not just PKH
+
+ // We need to generate lockTime-A: add tradeTimeout to now
+ long now = NTP.getTime();
+ int lockTimeA = crossChainTradeData.tradeTimeout * 60 + (int) (now / 1000L);
+
+ TradeBotData tradeBotData = new TradeBotData(tradePrivateKey, DogecoinACCTv1.NAME,
+ State.ALICE_WAITING_FOR_AT_LOCK.name(), State.ALICE_WAITING_FOR_AT_LOCK.value,
+ receivingAddress, crossChainTradeData.qortalAtAddress, now, crossChainTradeData.qortAmount,
+ tradeNativePublicKey, tradeNativePublicKeyHash, tradeNativeAddress,
+ secretA, hashOfSecretA,
+ SupportedBlockchain.DOGECOIN.name(),
+ tradeForeignPublicKey, tradeForeignPublicKeyHash,
+ crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash);
+
+ // Attempt to backup the trade bot data
+ TradeBot.backupTradeBotData(repository);
+
+ // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount
+ long p2shFee;
+ try {
+ p2shFee = Dogecoin.getInstance().getP2shFee(now);
+ } catch (ForeignBlockchainException e) {
+ LOGGER.debug("Couldn't estimate Dogecoin fees?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ // Do not include fee for funding transaction as this is covered by buildSpend()
+ long amountA = crossChainTradeData.expectedForeignAmount + p2shFee /*redeeming/refunding P2SH-A*/;
+
+ // P2SH-A to be funded
+ byte[] redeemScriptBytes = BitcoinyHTLC.buildScript(tradeForeignPublicKeyHash, lockTimeA, crossChainTradeData.creatorForeignPKH, hashOfSecretA);
+ String p2shAddress = Dogecoin.getInstance().deriveP2shAddress(redeemScriptBytes);
+
+ // Build transaction for funding P2SH-A
+ Transaction p2shFundingTransaction = Dogecoin.getInstance().buildSpend(tradeBotData.getForeignKey(), p2shAddress, amountA);
+ if (p2shFundingTransaction == null) {
+ LOGGER.debug("Unable to build P2SH-A funding transaction - lack of funds?");
+ return ResponseResult.BALANCE_ISSUE;
+ }
+
+ try {
+ Dogecoin.getInstance().broadcastTransaction(p2shFundingTransaction);
+ } catch (ForeignBlockchainException e) {
+ // We couldn't fund P2SH-A at this time
+ LOGGER.debug("Couldn't broadcast P2SH-A funding transaction?");
+ return ResponseResult.NETWORK_ISSUE;
+ }
+
+ // Attempt to send MESSAGE to Bob's Qortal trade address
+ byte[] messageData = DogecoinACCTv1.buildOfferMessage(tradeBotData.getTradeForeignPublicKeyHash(), tradeBotData.getHashOfSecret(), tradeBotData.getLockTimeA());
+ String messageRecipient = crossChainTradeData.qortalCreatorTradeAddress;
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to Bob's trade-bot %s: %s", messageRecipient, result.name()));
+ return ResponseResult.NETWORK_ISSUE;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Funding P2SH-A %s. Messaged Bob. Waiting for AT-lock", p2shAddress));
+
+ return ResponseResult.OK;
+ }
+
+ @Override
+ public boolean canDelete(Repository repository, TradeBotData tradeBotData) throws DataException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null)
+ return true;
+
+ // If the AT doesn't exist then we might as well let the user tidy up
+ if (!repository.getATRepository().exists(tradeBotData.getAtAddress()))
+ return true;
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ case ALICE_DONE:
+ case BOB_DONE:
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ return true;
+
+ default:
+ return false;
+ }
+ }
+
+ @Override
+ public void progress(Repository repository, TradeBotData tradeBotData) throws DataException, ForeignBlockchainException {
+ State tradeBotState = State.valueOf(tradeBotData.getStateValue());
+ if (tradeBotState == null) {
+ LOGGER.info(() -> String.format("Trade-bot entry for AT %s has invalid state?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ ATData atData = null;
+ CrossChainTradeData tradeData = null;
+
+ if (tradeBotState.requiresAtData) {
+ // Attempt to fetch AT data
+ atData = repository.getATRepository().fromATAddress(tradeBotData.getAtAddress());
+ if (atData == null) {
+ LOGGER.debug(() -> String.format("Unable to fetch trade AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ if (tradeBotState.requiresTradeData) {
+ tradeData = DogecoinACCTv1.getInstance().populateTradeData(repository, atData);
+ if (tradeData == null) {
+ LOGGER.warn(() -> String.format("Unable to fetch ACCT trade data for AT %s from repository", tradeBotData.getAtAddress()));
+ return;
+ }
+ }
+ }
+
+ switch (tradeBotState) {
+ case BOB_WAITING_FOR_AT_CONFIRM:
+ handleBobWaitingForAtConfirm(repository, tradeBotData);
+ break;
+
+ case BOB_WAITING_FOR_MESSAGE:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForMessage(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_WAITING_FOR_AT_LOCK:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceWaitingForAtLock(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case BOB_WAITING_FOR_AT_REDEEM:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleBobWaitingForAtRedeem(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_DONE:
+ case BOB_DONE:
+ break;
+
+ case ALICE_REFUNDING_A:
+ TradeBot.getInstance().updatePresence(repository, tradeBotData, tradeData);
+ handleAliceRefundingP2shA(repository, tradeBotData, atData, tradeData);
+ break;
+
+ case ALICE_REFUNDED:
+ case BOB_REFUNDED:
+ break;
+ }
+ }
+
+ /**
+ * Trade-bot is waiting for Bob's AT to deploy.
+ *
+ * If AT is deployed, then trade-bot's next step is to wait for MESSAGE from Alice.
+ */
+ private void handleBobWaitingForAtConfirm(Repository repository, TradeBotData tradeBotData) throws DataException {
+ if (!repository.getATRepository().exists(tradeBotData.getAtAddress())) {
+ if (NTP.getTime() - tradeBotData.getTimestamp() <= MAX_AT_CONFIRMATION_PERIOD)
+ return;
+
+ // We've waited ages for AT to be confirmed into a block but something has gone awry.
+ // After this long we assume transaction loss so give up with trade-bot entry too.
+ tradeBotData.setState(State.BOB_REFUNDED.name());
+ tradeBotData.setStateValue(State.BOB_REFUNDED.value);
+ tradeBotData.setTimestamp(NTP.getTime());
+ // We delete trade-bot entry here instead of saving, hence not using updateTradeBotState()
+ repository.getCrossChainRepository().delete(tradeBotData.getTradePrivateKey());
+ repository.saveChanges();
+
+ LOGGER.info(() -> String.format("AT %s never confirmed. Giving up on trade", tradeBotData.getAtAddress()));
+ TradeBot.notifyStateChange(tradeBotData);
+ return;
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_MESSAGE,
+ () -> String.format("AT %s confirmed ready. Waiting for trade message", tradeBotData.getAtAddress()));
+ }
+
+ /**
+ * Trade-bot is waiting for MESSAGE from Alice's trade-bot, containing Alice's trade info.
+ *
+ * It's possible Bob has cancelling his trade offer, receiving an automatic QORT refund,
+ * in which case trade-bot is done with this specific trade and finalizes on refunded state.
+ *
+ * Assuming trade is still on offer, trade-bot checks the contents of MESSAGE from Alice's trade-bot.
+ *
+ * Details from Alice are used to derive P2SH-A address and this is checked for funding balance.
+ *
+ * Assuming P2SH-A has at least expected Dogecoin balance,
+ * Bob's trade-bot constructs a zero-fee, PoW MESSAGE to send to Bob's AT with more trade details.
+ *
+ * On processing this MESSAGE, Bob's AT should switch into 'TRADE' mode and only trade with Alice.
+ *
+ * Trade-bot's next step is to wait for Alice to redeem the AT, which will allow Bob to
+ * extract secret-A needed to redeem Alice's P2SH.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForMessage(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // If AT has finished then Bob likely cancelled his trade offer
+ if (atData.getIsFinished()) {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s cancelled - trading aborted", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ Dogecoin dogecoin = Dogecoin.getInstance();
+
+ String address = tradeBotData.getTradeNativeAddress();
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, address, null, null, null);
+
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ if (messageTransactionData.isText())
+ continue;
+
+ // We're expecting: HASH160(secret-A), Alice's Dogecoin pubkeyhash and lockTime-A
+ byte[] messageData = messageTransactionData.getData();
+ DogecoinACCTv1.OfferMessageData offerMessageData = DogecoinACCTv1.extractOfferMessageData(messageData);
+ if (offerMessageData == null)
+ continue;
+
+ byte[] aliceForeignPublicKeyHash = offerMessageData.partnerDogecoinPKH;
+ byte[] hashOfSecretA = offerMessageData.hashOfSecretA;
+ int lockTimeA = (int) offerMessageData.lockTimeA;
+ long messageTimestamp = messageTransactionData.getTimestamp();
+ int refundTimeout = DogecoinACCTv1.calcRefundTimeout(messageTimestamp, lockTimeA);
+
+ // Determine P2SH-A address and confirm funded
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA);
+ String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
+ final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // There might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // We've already redeemed this?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
+ () -> String.format("P2SH-A %s already spent? Assuming trade complete", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // This P2SH-A is burnt, but there might be another MESSAGE from someone else with an actually funded P2SH-A...
+ continue;
+
+ case FUNDED:
+ // Fall-through out of switch...
+ break;
+ }
+
+ // Good to go - send MESSAGE to AT
+
+ String aliceNativeAddress = Crypto.toAddress(messageTransactionData.getCreatorPublicKey());
+
+ // Build outgoing message, padding each part to 32 bytes to make it easier for AT to consume
+ byte[] outgoingMessageData = DogecoinACCTv1.buildTradeMessage(aliceNativeAddress, aliceForeignPublicKeyHash, hashOfSecretA, lockTimeA, refundTimeout);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, outgoingMessageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction outgoingMessageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, outgoingMessageData, false, false);
+
+ outgoingMessageTransaction.computeNonce();
+ outgoingMessageTransaction.sign(sender);
+
+ // reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = outgoingMessageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_WAITING_FOR_AT_REDEEM,
+ () -> String.format("Locked AT %s to %s. Waiting for AT redeem", tradeBotData.getAtAddress(), aliceNativeAddress));
+
+ return;
+ }
+ }
+
+ /**
+ * Trade-bot is waiting for Bob's AT to switch to TRADE mode and lock trade to Alice only.
+ *
+ * It's possible that Bob has cancelled his trade offer in the mean time, or that somehow
+ * this process has taken so long that we've reached P2SH-A's locktime, or that someone else
+ * has managed to trade with Bob. In any of these cases, trade-bot switches to begin the refunding process.
+ *
+ * Assuming Bob's AT is locked to Alice, trade-bot checks AT's state data to make sure it is correct.
+ *
+ * If all is well, trade-bot then redeems AT using Alice's secret-A, releasing Bob's QORT to Alice.
+ *
+ * In revealing a valid secret-A, Bob can then redeem the DOGE funds from P2SH-A.
+ *
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceWaitingForAtLock(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ if (aliceUnexpectedState(repository, tradeBotData, atData, crossChainTradeData))
+ return;
+
+ Dogecoin dogecoin = Dogecoin.getInstance();
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // Refund P2SH-A if we've passed lockTime-A
+ if (NTP.getTime() >= lockTimeA * 1000L) {
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
+
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ case FUNDED:
+ break;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Already redeemed?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent? Assuming trade completed", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("P2SH-A %s already refunded. Trade aborted", p2shAddressA));
+ return;
+
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> atData.getIsFinished()
+ ? String.format("AT %s cancelled. Refunding P2SH-A %s - aborting trade", tradeBotData.getAtAddress(), p2shAddressA)
+ : String.format("LockTime-A reached, refunding P2SH-A %s - aborting trade", p2shAddressA));
+
+ return;
+ }
+
+ // We're waiting for AT to be in TRADE mode
+ if (crossChainTradeData.mode != AcctMode.TRADING)
+ return;
+
+ // AT is in TRADE mode and locked to us as checked by aliceUnexpectedState() above
+
+ // Find our MESSAGE to AT from previous state
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(tradeBotData.getTradeNativePublicKey(),
+ crossChainTradeData.qortalCreatorTradeAddress, null, null, null);
+ if (messageTransactionsData == null || messageTransactionsData.isEmpty()) {
+ LOGGER.warn(() -> String.format("Unable to find our message to trade creator %s?", crossChainTradeData.qortalCreatorTradeAddress));
+ return;
+ }
+
+ long recipientMessageTimestamp = messageTransactionsData.get(0).getTimestamp();
+ int refundTimeout = DogecoinACCTv1.calcRefundTimeout(recipientMessageTimestamp, lockTimeA);
+
+ // Our calculated refundTimeout should match AT's refundTimeout
+ if (refundTimeout != crossChainTradeData.refundTimeout) {
+ LOGGER.debug(() -> String.format("Trade AT refundTimeout '%d' doesn't match our refundTimeout '%d'", crossChainTradeData.refundTimeout, refundTimeout));
+ // We'll eventually refund
+ return;
+ }
+
+ // We're good to redeem AT
+
+ // Send 'redeem' MESSAGE to AT using both secret
+ byte[] secretA = tradeBotData.getSecret();
+ String qortalReceivingAddress = Base58.encode(tradeBotData.getReceivingAccountInfo()); // Actually contains whole address, not just PKH
+ byte[] messageData = DogecoinACCTv1.buildRedeemMessage(secretA, qortalReceivingAddress);
+ String messageRecipient = tradeBotData.getAtAddress();
+
+ boolean isMessageAlreadySent = repository.getMessageRepository().exists(tradeBotData.getTradeNativePublicKey(), messageRecipient, messageData);
+ if (!isMessageAlreadySent) {
+ PrivateKeyAccount sender = new PrivateKeyAccount(repository, tradeBotData.getTradePrivateKey());
+ MessageTransaction messageTransaction = MessageTransaction.build(repository, sender, Group.NO_GROUP, messageRecipient, messageData, false, false);
+
+ messageTransaction.computeNonce();
+ messageTransaction.sign(sender);
+
+ // Reset repository state to prevent deadlock
+ repository.discardChanges();
+ ValidationResult result = messageTransaction.importAsUnconfirmed();
+
+ if (result != ValidationResult.OK) {
+ LOGGER.warn(() -> String.format("Unable to send MESSAGE to AT %s: %s", messageRecipient, result.name()));
+ return;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("Redeeming AT %s. Funds should arrive at %s",
+ tradeBotData.getAtAddress(), qortalReceivingAddress));
+ }
+
+ /**
+ * Trade-bot is waiting for Alice to redeem Bob's AT, thus revealing secret-A which is required to spend the DOGE funds from P2SH-A.
+ *
+ * It's possible that Bob's AT has reached its trading timeout and automatically refunded QORT back to Bob. In which case,
+ * trade-bot is done with this specific trade and finalizes in refunded state.
+ *
+ * Assuming trade-bot can extract a valid secret-A from Alice's MESSAGE then trade-bot uses that to redeem the DOGE funds from P2SH-A
+ * to Bob's 'foreign'/Dogecoin trade legacy-format address, as derived from trade private key.
+ *
+ * (This could potentially be 'improved' to send DOGE to any address of Bob's choosing by changing the transaction output).
+ *
+ * If trade-bot successfully broadcasts the transaction, then this specific trade is done.
+ * @throws ForeignBlockchainException
+ */
+ private void handleBobWaitingForAtRedeem(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // AT should be 'finished' once Alice has redeemed QORT funds
+ if (!atData.getIsFinished())
+ // Not finished yet
+ return;
+
+ // If AT is REFUNDED or CANCELLED then something has gone wrong
+ if (crossChainTradeData.mode == AcctMode.REFUNDED || crossChainTradeData.mode == AcctMode.CANCELLED) {
+ // Alice hasn't redeemed the QORT, so there is no point in trying to redeem the DOGE
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED,
+ () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress()));
+
+ return;
+ }
+
+ byte[] secretA = DogecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
+ if (secretA == null) {
+ LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
+ return;
+ }
+
+ // Use secret-A to redeem P2SH-A
+
+ Dogecoin dogecoin = Dogecoin.getInstance();
+
+ byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo();
+ int lockTimeA = crossChainTradeData.lockTimeA;
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA);
+ String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // P2SH-A suddenly not funded? Our best bet at this point is to hope for AT auto-refund
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Double-check that we have redeemed P2SH-A...
+ break;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ // Wait for AT to auto-refund
+ return;
+
+ case FUNDED: {
+ Coin redeemAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey redeemKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
+
+ Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(dogecoin.getNetworkParameters(), redeemAmount, redeemKey,
+ fundingOutputs, redeemScriptA, secretA, receivingAccountInfo);
+
+ dogecoin.broadcastTransaction(p2shRedeemTransaction);
+ break;
+ }
+ }
+
+ String receivingAddress = dogecoin.pkhToAddress(receivingAccountInfo);
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_DONE,
+ () -> String.format("P2SH-A %s redeemed. Funds should arrive at %s", tradeBotData.getAtAddress(), receivingAddress));
+ }
+
+ /**
+ * Trade-bot is attempting to refund P2SH-A.
+ * @throws ForeignBlockchainException
+ */
+ private void handleAliceRefundingP2shA(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ int lockTimeA = tradeBotData.getLockTimeA();
+
+ // We can't refund P2SH-A until lockTime-A has passed
+ if (NTP.getTime() <= lockTimeA * 1000L)
+ return;
+
+ Dogecoin dogecoin = Dogecoin.getInstance();
+
+ // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113)
+ int medianBlockTime = dogecoin.getMedianBlockTime();
+ if (medianBlockTime <= lockTimeA)
+ return;
+
+ byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret());
+ String p2shAddressA = dogecoin.deriveP2shAddress(redeemScriptA);
+
+ // Fee for redeem/refund is subtracted from P2SH-A balance.
+ long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout);
+ long p2shFee = Dogecoin.getInstance().getP2shFee(feeTimestamp);
+ long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee;
+ BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(dogecoin.getBlockchainProvider(), p2shAddressA, minimumAmountA);
+
+ switch (htlcStatusA) {
+ case UNFUNDED:
+ case FUNDING_IN_PROGRESS:
+ // Still waiting for P2SH-A to be funded...
+ return;
+
+ case REDEEM_IN_PROGRESS:
+ case REDEEMED:
+ // Too late!
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("P2SH-A %s already spent!", p2shAddressA));
+ return;
+
+ case REFUND_IN_PROGRESS:
+ case REFUNDED:
+ break;
+
+ case FUNDED:{
+ Coin refundAmount = Coin.valueOf(crossChainTradeData.expectedForeignAmount);
+ ECKey refundKey = ECKey.fromPrivate(tradeBotData.getTradePrivateKey());
+ List fundingOutputs = dogecoin.getUnspentOutputs(p2shAddressA);
+
+ // Determine receive address for refund
+ String receiveAddress = dogecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey());
+ Address receiving = Address.fromString(dogecoin.getNetworkParameters(), receiveAddress);
+
+ Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(dogecoin.getNetworkParameters(), refundAmount, refundKey,
+ fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash());
+
+ dogecoin.broadcastTransaction(p2shRefundTransaction);
+ break;
+ }
+ }
+
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDED,
+ () -> String.format("LockTime-A reached. Refunded P2SH-A %s. Trade aborted", p2shAddressA));
+ }
+
+ /**
+ * Returns true if Alice finds AT unexpectedly cancelled, refunded, redeemed or locked to someone else.
+ *
+ * Will automatically update trade-bot state to ALICE_REFUNDING_A or ALICE_DONE as necessary.
+ *
+ * @throws DataException
+ * @throws ForeignBlockchainException
+ */
+ private boolean aliceUnexpectedState(Repository repository, TradeBotData tradeBotData,
+ ATData atData, CrossChainTradeData crossChainTradeData) throws DataException, ForeignBlockchainException {
+ // This is OK
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.OFFERING)
+ return false;
+
+ boolean isAtLockedToUs = tradeBotData.getTradeNativeAddress().equals(crossChainTradeData.qortalPartnerAddress);
+
+ if (!atData.getIsFinished() && crossChainTradeData.mode == AcctMode.TRADING)
+ if (isAtLockedToUs) {
+ // AT is trading with us - OK
+ return false;
+ } else {
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s trading with someone else: %s. Refunding & aborting trade", tradeBotData.getAtAddress(), crossChainTradeData.qortalPartnerAddress));
+
+ return true;
+ }
+
+ if (atData.getIsFinished() && crossChainTradeData.mode == AcctMode.REDEEMED && isAtLockedToUs) {
+ // We've redeemed already?
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_DONE,
+ () -> String.format("AT %s already redeemed by us. Trade completed", tradeBotData.getAtAddress()));
+ } else {
+ // Any other state is not good, so start defensive refund
+ TradeBot.updateTradeBotState(repository, tradeBotData, State.ALICE_REFUNDING_A,
+ () -> String.format("AT %s cancelled/refunded/redeemed by someone else/invalid state. Refunding & aborting trade", tradeBotData.getAtAddress()));
+ }
+
+ return true;
+ }
+
+ private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) {
+ return (lockTimeA - tradeTimeout * 60) * 1000L;
+ }
+
+}
diff --git a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
index 0bd2972b..0246c199 100644
--- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv1TradeBot.java
@@ -725,7 +725,7 @@ public class LitecoinACCTv1TradeBot implements AcctTradeBot {
return;
}
- byte[] secretA = LitecoinACCTv1.findSecretA(repository, crossChainTradeData);
+ byte[] secretA = LitecoinACCTv1.getInstance().findSecretA(repository, crossChainTradeData);
if (secretA == null) {
LOGGER.debug(() -> String.format("Unable to find secret-A from redeem message to AT %s?", tradeBotData.getAtAddress()));
return;
diff --git a/src/main/java/org/qortal/controller/tradebot/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
index fa3b599e..6e9d1474 100644
--- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java
+++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java
@@ -17,11 +17,7 @@ import org.qortal.account.PrivateKeyAccount;
import org.qortal.api.model.crosschain.TradeBotCreateRequest;
import org.qortal.controller.Controller;
import org.qortal.controller.tradebot.AcctTradeBot.ResponseResult;
-import org.qortal.crosschain.ACCT;
-import org.qortal.crosschain.BitcoinACCTv1;
-import org.qortal.crosschain.ForeignBlockchainException;
-import org.qortal.crosschain.LitecoinACCTv1;
-import org.qortal.crosschain.SupportedBlockchain;
+import org.qortal.crosschain.*;
import org.qortal.data.at.ATData;
import org.qortal.data.crosschain.CrossChainTradeData;
import org.qortal.data.crosschain.TradeBotData;
@@ -80,6 +76,7 @@ public class TradeBot implements Listener {
static {
acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance);
acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance);
+ acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance);
}
private static TradeBot instance;
diff --git a/src/main/java/org/qortal/crosschain/ACCT.java b/src/main/java/org/qortal/crosschain/ACCT.java
index e557a3e2..de28cfce 100644
--- a/src/main/java/org/qortal/crosschain/ACCT.java
+++ b/src/main/java/org/qortal/crosschain/ACCT.java
@@ -20,4 +20,6 @@ public interface ACCT {
public byte[] buildCancelMessage(String creatorQortalAddress);
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException;
+
}
diff --git a/src/main/java/org/qortal/crosschain/Bitcoin.java b/src/main/java/org/qortal/crosschain/Bitcoin.java
index 28275d6a..2ce21d2f 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoin.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoin.java
@@ -67,7 +67,11 @@ public class Bitcoin extends Bitcoiny {
new Server("192.166.219.200", Server.ConnectionType.SSL, 50002),
new Server("2ex.digitaleveryware.com", Server.ConnectionType.SSL, 50002),
new Server("dxm.no-ip.biz", Server.ConnectionType.SSL, 50002),
- new Server("caleb.vegas", Server.ConnectionType.SSL, 50002));
+ new Server("caleb.vegas", Server.ConnectionType.SSL, 50002),
+ new Server("ecdsa.net", Server.ConnectionType.SSL, 110),
+ new Server("electrum.hsmiths.com", Server.ConnectionType.SSL, 995),
+ new Server("elec.luggs.co", Server.ConnectionType.SSL, 443),
+ new Server("btc.smsys.me", Server.ConnectionType.SSL, 995));
}
@Override
diff --git a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
index 5118e103..eea541ad 100644
--- a/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
+++ b/src/main/java/org/qortal/crosschain/BitcoinACCTv1.java
@@ -872,7 +872,8 @@ public class BitcoinACCTv1 implements ACCT {
return (int) ((lockTimeA + (offerMessageTimestamp / 1000L)) / 2L);
}
- public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
+ @Override
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
diff --git a/src/main/java/org/qortal/crosschain/Bitcoiny.java b/src/main/java/org/qortal/crosschain/Bitcoiny.java
index fc98f959..d4693818 100644
--- a/src/main/java/org/qortal/crosschain/Bitcoiny.java
+++ b/src/main/java/org/qortal/crosschain/Bitcoiny.java
@@ -169,6 +169,11 @@ public abstract class Bitcoiny implements ForeignBlockchain {
return this.bitcoinjContext.getFeePerKb();
}
+ /** Returns minimum order size in sats. To be overridden for coins that need to restrict order size. */
+ public long getMinimumOrderAmount() {
+ return 0L;
+ }
+
/**
* Returns fixed P2SH spending fee, in sats per 1000bytes, optionally for historic timestamp.
*
@@ -346,6 +351,10 @@ public abstract class Bitcoiny implements ForeignBlockchain {
Set walletTransactions = new HashSet<>();
Set keySet = new HashSet<>();
+ // Set the number of consecutive empty batches required before giving up
+ final int numberOfAdditionalBatchesToSearch = 5;
+
+ int unusedCounter = 0;
int ki = 0;
do {
boolean areAllKeysUnused = true;
@@ -369,9 +378,19 @@ public abstract class Bitcoiny implements ForeignBlockchain {
}
}
- if (areAllKeysUnused)
- // No transactions for this batch of keys so assume we're done searching.
- break;
+ if (areAllKeysUnused) {
+ // No transactions
+ if (unusedCounter >= numberOfAdditionalBatchesToSearch) {
+ // ... and we've hit our search limit
+ break;
+ }
+ // We haven't hit our search limit yet so increment the counter and keep looking
+ unusedCounter++;
+ }
+ else {
+ // Some keys in this batch were used, so reset the counter
+ unusedCounter = 0;
+ }
// Generate some more keys
keys.addAll(generateMoreKeys(keyChain));
diff --git a/src/main/java/org/qortal/crosschain/Dogecoin.java b/src/main/java/org/qortal/crosschain/Dogecoin.java
new file mode 100644
index 00000000..4acd95aa
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/Dogecoin.java
@@ -0,0 +1,171 @@
+package org.qortal.crosschain;
+
+import org.bitcoinj.core.Coin;
+import org.bitcoinj.core.Context;
+import org.bitcoinj.core.NetworkParameters;
+import org.libdohj.params.DogecoinMainNetParams;
+//import org.libdohj.params.DogecoinRegTestParams;
+import org.libdohj.params.DogecoinTestNet3Params;
+import org.qortal.crosschain.ElectrumX.Server;
+import org.qortal.crosschain.ElectrumX.Server.ConnectionType;
+import org.qortal.settings.Settings;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.EnumMap;
+import java.util.Map;
+
+public class Dogecoin extends Bitcoiny {
+
+ public static final String CURRENCY_CODE = "DOGE";
+
+ private static final Coin DEFAULT_FEE_PER_KB = Coin.valueOf(500000000); // 5 DOGE per 1000 bytes
+
+ private static final long MINIMUM_ORDER_AMOUNT = 300000000L; // 3 DOGE minimum order. The RPC dust threshold is around 2 DOGE
+
+ // Temporary values until a dynamic fee system is written.
+ private static final long MAINNET_FEE = 110000000L;
+ private static final long NON_MAINNET_FEE = 10000L; // TODO: calibrate this
+
+ private static final Map DEFAULT_ELECTRUMX_PORTS = new EnumMap<>(ConnectionType.class);
+ static {
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.TCP, 50001);
+ DEFAULT_ELECTRUMX_PORTS.put(ConnectionType.SSL, 50002);
+ }
+
+ public enum DogecoinNet {
+ MAIN {
+ @Override
+ public NetworkParameters getParams() {
+ return DogecoinMainNetParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ // Servers chosen on NO BASIS WHATSOEVER from various sources!
+ new Server("electrum1.cipig.net", ConnectionType.TCP, 10060),
+ new Server("electrum2.cipig.net", ConnectionType.TCP, 10060),
+ new Server("electrum3.cipig.net", ConnectionType.TCP, 10060));
+ // TODO: add more mainnet servers. It's too centralized.
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "1a91e3dace36e2be3bf030a65679fe821aa1d6ef92e7c9902eb318182c355691";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ // TODO: This will need to be replaced with something better in the near future!
+ return MAINNET_FEE;
+ }
+ },
+ TEST3 {
+ @Override
+ public NetworkParameters getParams() {
+ return DogecoinTestNet3Params.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(); // TODO: find testnet servers
+ }
+
+ @Override
+ public String getGenesisHash() {
+ return "4966625a4b2851d9fdee139e56211a0d88575f59ed816ff5e6a63deb4e3e29a0";
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ },
+ REGTEST {
+ @Override
+ public NetworkParameters getParams() {
+ return null; // TODO: DogecoinRegTestParams.get();
+ }
+
+ @Override
+ public Collection getServers() {
+ return Arrays.asList(
+ new Server("localhost", ConnectionType.TCP, 50001),
+ new Server("localhost", ConnectionType.SSL, 50002));
+ }
+
+ @Override
+ public String getGenesisHash() {
+ // This is unique to each regtest instance
+ return null;
+ }
+
+ @Override
+ public long getP2shFee(Long timestamp) {
+ return NON_MAINNET_FEE;
+ }
+ };
+
+ public abstract NetworkParameters getParams();
+ public abstract Collection getServers();
+ public abstract String getGenesisHash();
+ public abstract long getP2shFee(Long timestamp) throws ForeignBlockchainException;
+ }
+
+ private static Dogecoin instance;
+
+ private final DogecoinNet dogecoinNet;
+
+ // Constructors and instance
+
+ private Dogecoin(DogecoinNet dogecoinNet, BitcoinyBlockchainProvider blockchain, Context bitcoinjContext, String currencyCode) {
+ super(blockchain, bitcoinjContext, currencyCode);
+ this.dogecoinNet = dogecoinNet;
+
+ LOGGER.info(() -> String.format("Starting Dogecoin support using %s", this.dogecoinNet.name()));
+ }
+
+ public static synchronized Dogecoin getInstance() {
+ if (instance == null) {
+ DogecoinNet dogecoinNet = Settings.getInstance().getDogecoinNet();
+
+ BitcoinyBlockchainProvider electrumX = new ElectrumX("Dogecoin-" + dogecoinNet.name(), dogecoinNet.getGenesisHash(), dogecoinNet.getServers(), DEFAULT_ELECTRUMX_PORTS);
+ Context bitcoinjContext = new Context(dogecoinNet.getParams());
+
+ instance = new Dogecoin(dogecoinNet, electrumX, bitcoinjContext, CURRENCY_CODE);
+ }
+
+ return instance;
+ }
+
+ // Getters & setters
+
+ public static synchronized void resetForTesting() {
+ instance = null;
+ }
+
+ // Actual useful methods for use by other classes
+
+ @Override
+ public Coin getFeePerKb() {
+ return DEFAULT_FEE_PER_KB;
+ }
+
+ @Override
+ public long getMinimumOrderAmount() {
+ return MINIMUM_ORDER_AMOUNT;
+ }
+
+ /**
+ * Returns estimated DOGE fee, in sats per 1000bytes, optionally for historic timestamp.
+ *
+ * @param timestamp optional milliseconds since epoch, or null for 'now'
+ * @return sats per 1000bytes, or throws ForeignBlockchainException if something went wrong
+ */
+ @Override
+ public long getP2shFee(Long timestamp) throws ForeignBlockchainException {
+ return this.dogecoinNet.getP2shFee(timestamp);
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java b/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java
new file mode 100644
index 00000000..36ff7c5c
--- /dev/null
+++ b/src/main/java/org/qortal/crosschain/DogecoinACCTv1.java
@@ -0,0 +1,855 @@
+package org.qortal.crosschain;
+
+import com.google.common.hash.HashCode;
+import com.google.common.primitives.Bytes;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.ciyam.at.*;
+import org.qortal.account.Account;
+import org.qortal.asset.Asset;
+import org.qortal.at.QortalFunctionCode;
+import org.qortal.crypto.Crypto;
+import org.qortal.data.at.ATData;
+import org.qortal.data.at.ATStateData;
+import org.qortal.data.crosschain.CrossChainTradeData;
+import org.qortal.data.transaction.MessageTransactionData;
+import org.qortal.repository.DataException;
+import org.qortal.repository.Repository;
+import org.qortal.utils.Base58;
+import org.qortal.utils.BitTwiddling;
+
+import java.nio.ByteBuffer;
+import java.util.Arrays;
+import java.util.List;
+
+import static org.ciyam.at.OpCode.calcOffset;
+
+/**
+ * Cross-chain trade AT
+ *
+ *
+ *
+ * - Bob generates Dogecoin & Qortal 'trade' keys
+ *
+ * - private key required to sign P2SH redeem tx
+ * - private key could be used to create 'secret' (e.g. double-SHA256)
+ * - encrypted private key could be stored in Qortal AT for access by Bob from any node
+ *
+ *
+ * - Bob deploys Qortal AT
+ *
+ *
+ * - Alice finds Qortal AT and wants to trade
+ *
+ * - Alice generates Dogecoin & Qortal 'trade' keys
+ * - Alice funds Dogecoin P2SH-A
+ * - Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing:
+ *
+ * - hash-of-secret-A
+ * - her 'trade' Dogecoin PKH
+ *
+ *
+ *
+ *
+ * - Bob receives "offer" MESSAGE
+ *
+ * - Checks Alice's P2SH-A
+ * - Sends 'trade' MESSAGE to Qortal AT from his trade address, containing:
+ *
+ * - Alice's trade Qortal address
+ * - Alice's trade Dogecoin PKH
+ * - hash-of-secret-A
+ *
+ *
+ *
+ *
+ * - Alice checks Qortal AT to confirm it's locked to her
+ *
+ * - Alice sends 'redeem' MESSAGE to Qortal AT from her trade address, containing:
+ *
+ * - secret-A
+ * - Qortal receiving address of her chosing
+ *
+ *
+ * - AT's QORT funds are sent to Qortal receiving address
+ *
+ *
+ * - Bob checks AT, extracts secret-A
+ *
+ * - Bob redeems P2SH-A using his Dogecoin trade key and secret-A
+ * - P2SH-A DOGE funds end up at Dogecoin address determined by redeem transaction output(s)
+ *
+ *
+ *
+ */
+public class DogecoinACCTv1 implements ACCT {
+
+ private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv1.class);
+
+ public static final String NAME = DogecoinACCTv1.class.getSimpleName();
+ public static final byte[] CODE_BYTES_HASH = HashCode.fromString("0eb49b0313ff3855a29d860c2a8203faa2ef62e28ea30459321f176079cfa3a5").asBytes(); // SHA256 of AT code bytes
+
+ public static final int SECRET_LENGTH = 32;
+
+ /** Value offset into AT segment where 'mode' variable (long) is stored. (Multiply by MachineState.VALUE_SIZE for byte offset). */
+ private static final int MODE_VALUE_OFFSET = 61;
+ /** Byte offset into AT state data where 'mode' variable (long) is stored. */
+ public static final int MODE_BYTE_OFFSET = MachineState.HEADER_LENGTH + (MODE_VALUE_OFFSET * MachineState.VALUE_SIZE);
+
+ public static class OfferMessageData {
+ public byte[] partnerDogecoinPKH;
+ public byte[] hashOfSecretA;
+ public long lockTimeA;
+ }
+ public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerDogecoinPKH*/ + 20 /*hashOfSecretA*/ + 8 /*lockTimeA*/;
+ public static final int TRADE_MESSAGE_LENGTH = 32 /*partner's Qortal trade address (padded from 25 to 32)*/
+ + 24 /*partner's Dogecoin PKH (padded from 20 to 24)*/
+ + 8 /*AT trade timeout (minutes)*/
+ + 24 /*hash of secret-A (padded from 20 to 24)*/
+ + 8 /*lockTimeA*/;
+ public static final int REDEEM_MESSAGE_LENGTH = 32 /*secret-A*/ + 32 /*partner's Qortal receiving address padded from 25 to 32*/;
+ public static final int CANCEL_MESSAGE_LENGTH = 32 /*AT creator's Qortal address*/;
+
+ private static DogecoinACCTv1 instance;
+
+ private DogecoinACCTv1() {
+ }
+
+ public static synchronized DogecoinACCTv1 getInstance() {
+ if (instance == null)
+ instance = new DogecoinACCTv1();
+
+ return instance;
+ }
+
+ @Override
+ public byte[] getCodeBytesHash() {
+ return CODE_BYTES_HASH;
+ }
+
+ @Override
+ public int getModeByteOffset() {
+ return MODE_BYTE_OFFSET;
+ }
+
+ @Override
+ public ForeignBlockchain getBlockchain() {
+ return Dogecoin.getInstance();
+ }
+
+ /**
+ * Returns Qortal AT creation bytes for cross-chain trading AT.
+ *
+ * tradeTimeout (minutes) is the time window for the trade partner to send the
+ * 32-byte secret to the AT, before the AT automatically refunds the AT's creator.
+ *
+ * @param creatorTradeAddress AT creator's trade Qortal address
+ * @param dogecoinPublicKeyHash 20-byte HASH160 of creator's trade Dogecoin public key
+ * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT
+ * @param dogecoinAmount how much DOGE the AT creator is expecting to trade
+ * @param tradeTimeout suggested timeout for entire trade
+ */
+ public static byte[] buildQortalAT(String creatorTradeAddress, byte[] dogecoinPublicKeyHash, long qortAmount, long dogecoinAmount, int tradeTimeout) {
+ if (dogecoinPublicKeyHash.length != 20)
+ throw new IllegalArgumentException("Dogecoin public key hash should be 20 bytes");
+
+ // Labels for data segment addresses
+ int addrCounter = 0;
+
+ // Constants (with corresponding dataByteBuffer.put*() calls below)
+
+ final int addrCreatorTradeAddress1 = addrCounter++;
+ final int addrCreatorTradeAddress2 = addrCounter++;
+ final int addrCreatorTradeAddress3 = addrCounter++;
+ final int addrCreatorTradeAddress4 = addrCounter++;
+
+ final int addrDogecoinPublicKeyHash = addrCounter;
+ addrCounter += 4;
+
+ final int addrQortAmount = addrCounter++;
+ final int addrDogecoinAmount = addrCounter++;
+ final int addrTradeTimeout = addrCounter++;
+
+ final int addrMessageTxnType = addrCounter++;
+ final int addrExpectedTradeMessageLength = addrCounter++;
+ final int addrExpectedRedeemMessageLength = addrCounter++;
+
+ final int addrCreatorAddressPointer = addrCounter++;
+ final int addrQortalPartnerAddressPointer = addrCounter++;
+ final int addrMessageSenderPointer = addrCounter++;
+
+ final int addrTradeMessagePartnerDogecoinPKHOffset = addrCounter++;
+ final int addrPartnerDogecoinPKHPointer = addrCounter++;
+ final int addrTradeMessageHashOfSecretAOffset = addrCounter++;
+ final int addrHashOfSecretAPointer = addrCounter++;
+
+ final int addrRedeemMessageReceivingAddressOffset = addrCounter++;
+
+ final int addrMessageDataPointer = addrCounter++;
+ final int addrMessageDataLength = addrCounter++;
+
+ final int addrPartnerReceivingAddressPointer = addrCounter++;
+
+ final int addrEndOfConstants = addrCounter;
+
+ // Variables
+
+ final int addrCreatorAddress1 = addrCounter++;
+ final int addrCreatorAddress2 = addrCounter++;
+ final int addrCreatorAddress3 = addrCounter++;
+ final int addrCreatorAddress4 = addrCounter++;
+
+ final int addrQortalPartnerAddress1 = addrCounter++;
+ final int addrQortalPartnerAddress2 = addrCounter++;
+ final int addrQortalPartnerAddress3 = addrCounter++;
+ final int addrQortalPartnerAddress4 = addrCounter++;
+
+ final int addrLockTimeA = addrCounter++;
+ final int addrRefundTimeout = addrCounter++;
+ final int addrRefundTimestamp = addrCounter++;
+ final int addrLastTxnTimestamp = addrCounter++;
+ final int addrBlockTimestamp = addrCounter++;
+ final int addrTxnType = addrCounter++;
+ final int addrResult = addrCounter++;
+
+ final int addrMessageSender1 = addrCounter++;
+ final int addrMessageSender2 = addrCounter++;
+ final int addrMessageSender3 = addrCounter++;
+ final int addrMessageSender4 = addrCounter++;
+
+ final int addrMessageLength = addrCounter++;
+
+ final int addrMessageData = addrCounter;
+ addrCounter += 4;
+
+ final int addrHashOfSecretA = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerDogecoinPKH = addrCounter;
+ addrCounter += 4;
+
+ final int addrPartnerReceivingAddress = addrCounter;
+ addrCounter += 4;
+
+ final int addrMode = addrCounter++;
+ assert addrMode == MODE_VALUE_OFFSET : String.format("addrMode %d does not match MODE_VALUE_OFFSET %d", addrMode, MODE_VALUE_OFFSET);
+
+ // Data segment
+ ByteBuffer dataByteBuffer = ByteBuffer.allocate(addrCounter * MachineState.VALUE_SIZE);
+
+ // AT creator's trade Qortal address, decoded from Base58
+ assert dataByteBuffer.position() == addrCreatorTradeAddress1 * MachineState.VALUE_SIZE : "addrCreatorTradeAddress1 incorrect";
+ byte[] creatorTradeAddressBytes = Base58.decode(creatorTradeAddress);
+ dataByteBuffer.put(Bytes.ensureCapacity(creatorTradeAddressBytes, 32, 0));
+
+ // Dogecoin public key hash
+ assert dataByteBuffer.position() == addrDogecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrDogecoinPublicKeyHash incorrect";
+ dataByteBuffer.put(Bytes.ensureCapacity(dogecoinPublicKeyHash, 32, 0));
+
+ // Redeem Qort amount
+ assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect";
+ dataByteBuffer.putLong(qortAmount);
+
+ // Expected Dogecoin amount
+ assert dataByteBuffer.position() == addrDogecoinAmount * MachineState.VALUE_SIZE : "addrDogecoinAmount incorrect";
+ dataByteBuffer.putLong(dogecoinAmount);
+
+ // Suggested trade timeout (minutes)
+ assert dataByteBuffer.position() == addrTradeTimeout * MachineState.VALUE_SIZE : "addrTradeTimeout incorrect";
+ dataByteBuffer.putLong(tradeTimeout);
+
+ // We're only interested in MESSAGE transactions
+ assert dataByteBuffer.position() == addrMessageTxnType * MachineState.VALUE_SIZE : "addrMessageTxnType incorrect";
+ dataByteBuffer.putLong(API.ATTransactionType.MESSAGE.value);
+
+ // Expected length of 'trade' MESSAGE data from AT creator
+ assert dataByteBuffer.position() == addrExpectedTradeMessageLength * MachineState.VALUE_SIZE : "addrExpectedTradeMessageLength incorrect";
+ dataByteBuffer.putLong(TRADE_MESSAGE_LENGTH);
+
+ // Expected length of 'redeem' MESSAGE data from trade partner
+ assert dataByteBuffer.position() == addrExpectedRedeemMessageLength * MachineState.VALUE_SIZE : "addrExpectedRedeemMessageLength incorrect";
+ dataByteBuffer.putLong(REDEEM_MESSAGE_LENGTH);
+
+ // Index into data segment of AT creator's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrCreatorAddressPointer * MachineState.VALUE_SIZE : "addrCreatorAddressPointer incorrect";
+ dataByteBuffer.putLong(addrCreatorAddress1);
+
+ // Index into data segment of partner's Qortal address, used by SET_B_IND
+ assert dataByteBuffer.position() == addrQortalPartnerAddressPointer * MachineState.VALUE_SIZE : "addrQortalPartnerAddressPointer incorrect";
+ dataByteBuffer.putLong(addrQortalPartnerAddress1);
+
+ // Index into data segment of (temporary) transaction's sender's address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrMessageSenderPointer * MachineState.VALUE_SIZE : "addrMessageSenderPointer incorrect";
+ dataByteBuffer.putLong(addrMessageSender1);
+
+ // Offset into 'trade' MESSAGE data payload for extracting partner's Dogecoin PKH
+ assert dataByteBuffer.position() == addrTradeMessagePartnerDogecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerDogecoinPKHOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Index into data segment of partner's Dogecoin PKH, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerDogecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerDogecoinPKHPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerDogecoinPKH);
+
+ // Offset into 'trade' MESSAGE data payload for extracting hash-of-secret-A
+ assert dataByteBuffer.position() == addrTradeMessageHashOfSecretAOffset * MachineState.VALUE_SIZE : "addrTradeMessageHashOfSecretAOffset incorrect";
+ dataByteBuffer.putLong(64L);
+
+ // Index into data segment to hash of secret A, used by GET_B_IND
+ assert dataByteBuffer.position() == addrHashOfSecretAPointer * MachineState.VALUE_SIZE : "addrHashOfSecretAPointer incorrect";
+ dataByteBuffer.putLong(addrHashOfSecretA);
+
+ // Offset into 'redeem' MESSAGE data payload for extracting Qortal receiving address
+ assert dataByteBuffer.position() == addrRedeemMessageReceivingAddressOffset * MachineState.VALUE_SIZE : "addrRedeemMessageReceivingAddressOffset incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Source location and length for hashing any passed secret
+ assert dataByteBuffer.position() == addrMessageDataPointer * MachineState.VALUE_SIZE : "addrMessageDataPointer incorrect";
+ dataByteBuffer.putLong(addrMessageData);
+ assert dataByteBuffer.position() == addrMessageDataLength * MachineState.VALUE_SIZE : "addrMessageDataLength incorrect";
+ dataByteBuffer.putLong(32L);
+
+ // Pointer into data segment of where to save partner's receiving Qortal address, used by GET_B_IND
+ assert dataByteBuffer.position() == addrPartnerReceivingAddressPointer * MachineState.VALUE_SIZE : "addrPartnerReceivingAddressPointer incorrect";
+ dataByteBuffer.putLong(addrPartnerReceivingAddress);
+
+ assert dataByteBuffer.position() == addrEndOfConstants * MachineState.VALUE_SIZE : "dataByteBuffer position not at end of constants";
+
+ // Code labels
+ Integer labelRefund = null;
+
+ Integer labelTradeTxnLoop = null;
+ Integer labelCheckTradeTxn = null;
+ Integer labelCheckCancelTxn = null;
+ Integer labelNotTradeNorCancelTxn = null;
+ Integer labelCheckNonRefundTradeTxn = null;
+ Integer labelTradeTxnExtract = null;
+ Integer labelRedeemTxnLoop = null;
+ Integer labelCheckRedeemTxn = null;
+ Integer labelCheckRedeemTxnSender = null;
+ Integer labelPayout = null;
+
+ ByteBuffer codeByteBuffer = ByteBuffer.allocate(768);
+
+ // Two-pass version
+ for (int pass = 0; pass < 2; ++pass) {
+ codeByteBuffer.clear();
+
+ try {
+ /* Initialization */
+
+ /* NOP - to ensure DOGECOIN ACCT is unique */
+ codeByteBuffer.put(OpCode.NOP.compile());
+
+ // Use AT creation 'timestamp' as starting point for finding transactions sent to AT
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_CREATION_TIMESTAMP, addrLastTxnTimestamp));
+
+ // Load B register with AT creator's address so we can save it into addrCreatorAddress1-4
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_CREATOR_INTO_B));
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrCreatorAddressPointer));
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for message from AT creator's trade address containing trade partner details, or AT owner's address to cancel offer */
+
+ /* Transaction processing loop */
+ labelTradeTxnLoop = codeByteBuffer.position();
+
+ // Find next transaction (if any) to this AT since the last one (referenced by addrLastTxnTimestamp)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrResult to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckTradeTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckTradeTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelTradeTxnLoop)));
+
+ /* Check transaction's sender. We're expecting AT creator's trade address for 'trade' message, or AT creator's own address for 'cancel' message. */
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of message sender's address with AT creator's trade address. If they don't match, check for cancel situation.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorTradeAddress1, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorTradeAddress2, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorTradeAddress3, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorTradeAddress4, calcOffset(codeByteBuffer, labelCheckCancelTxn)));
+ // Message sender's address matches AT creator's trade address so go process 'trade' message
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelCheckNonRefundTradeTxn == null ? 0 : labelCheckNonRefundTradeTxn));
+
+ /* Checking message sender for possible cancel message */
+ labelCheckCancelTxn = codeByteBuffer.position();
+
+ // Compare each part of message sender's address with AT creator's address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrCreatorAddress1, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrCreatorAddress2, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrCreatorAddress3, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrCreatorAddress4, calcOffset(codeByteBuffer, labelNotTradeNorCancelTxn)));
+ // Partner address is AT creator's address, so cancel offer and finish.
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.CANCELLED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ /* Not trade nor cancel message */
+ labelNotTradeNorCancelTxn = codeByteBuffer.position();
+
+ // Loop to find another transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Possible switch-to-trade-mode message */
+ labelCheckNonRefundTradeTxn = codeByteBuffer.position();
+
+ // Check 'trade' message we received has expected number of message bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to info extraction code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedTradeMessageLength, calcOffset(codeByteBuffer, labelTradeTxnExtract)));
+ // Message length didn't match - go back to finding another 'trade' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelTradeTxnLoop == null ? 0 : labelTradeTxnLoop));
+
+ /* Extracting info from 'trade' MESSAGE transaction */
+ labelTradeTxnExtract = codeByteBuffer.position();
+
+ // Extract message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrQortalPartnerAddress1 (as pointed to by addrQortalPartnerAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrQortalPartnerAddressPointer));
+
+ // Extract trade partner's Dogecoin public key hash (PKH) from message into B
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessagePartnerDogecoinPKHOffset));
+ // Store partner's Dogecoin PKH (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerDogecoinPKHPointer));
+ // Extract AT trade timeout (minutes) (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrRefundTimeout));
+
+ // Grab next 32 bytes
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrTradeMessageHashOfSecretAOffset));
+
+ // Extract hash-of-secret-A (we only really use values from B1-B3)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrHashOfSecretAPointer));
+ // Extract lockTime-A (from B4)
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_B4, addrLockTimeA));
+
+ // Calculate trade timeout refund 'timestamp' by adding addrRefundTimeout minutes to this transaction's 'timestamp', then save into addrRefundTimestamp
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.ADD_MINUTES_TO_TIMESTAMP, addrRefundTimestamp, addrLastTxnTimestamp, addrRefundTimeout));
+
+ /* We are in 'trade mode' */
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.TRADING.value));
+
+ // Set restart position to after this opcode
+ codeByteBuffer.put(OpCode.SET_PCS.compile());
+
+ /* Loop, waiting for trade timeout or 'redeem' MESSAGE from Qortal trade partner */
+
+ // Fetch current block 'timestamp'
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_BLOCK_TIMESTAMP, addrBlockTimestamp));
+ // If we're not past refund 'timestamp' then look for next transaction
+ codeByteBuffer.put(OpCode.BLT_DAT.compile(addrBlockTimestamp, addrRefundTimestamp, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ // We're past refund 'timestamp' so go refund everything back to AT creator
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRefund == null ? 0 : labelRefund));
+
+ /* Transaction processing loop */
+ labelRedeemTxnLoop = codeByteBuffer.position();
+
+ // Find next transaction to this AT since the last one (if any)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PUT_TX_AFTER_TIMESTAMP_INTO_A, addrLastTxnTimestamp));
+ // If no transaction found, A will be zero. If A is zero, set addrComparator to 1, otherwise 0.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.CHECK_A_IS_ZERO, addrResult));
+ // If addrResult is zero (i.e. A is non-zero, transaction was found) then go check transaction
+ codeByteBuffer.put(OpCode.BZR_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelCheckRedeemTxn)));
+ // Stop and wait for next block
+ codeByteBuffer.put(OpCode.STP_IMD.compile());
+
+ /* Check transaction */
+ labelCheckRedeemTxn = codeByteBuffer.position();
+
+ // Update our 'last found transaction's timestamp' using 'timestamp' from transaction
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TIMESTAMP_FROM_TX_IN_A, addrLastTxnTimestamp));
+ // Extract transaction type (message/payment) from transaction and save type in addrTxnType
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(FunctionCode.GET_TYPE_FROM_TX_IN_A, addrTxnType));
+ // If transaction type is not MESSAGE type then go look for another transaction
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrTxnType, addrMessageTxnType, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check message payload length */
+ codeByteBuffer.put(OpCode.EXT_FUN_RET.compile(QortalFunctionCode.GET_MESSAGE_LENGTH_FROM_TX_IN_A.value, addrMessageLength));
+ // If message length matches, branch to sender checking code
+ codeByteBuffer.put(OpCode.BEQ_DAT.compile(addrMessageLength, addrExpectedRedeemMessageLength, calcOffset(codeByteBuffer, labelCheckRedeemTxnSender)));
+ // Message length didn't match - go back to finding another 'redeem' MESSAGE transaction
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Check transaction's sender */
+ labelCheckRedeemTxnSender = codeByteBuffer.position();
+
+ // Extract sender address from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_ADDRESS_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageSender1 (as pointed to by addrMessageSenderPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageSenderPointer));
+ // Compare each part of transaction's sender's address with expected address. If they don't match, look for another transaction.
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender1, addrQortalPartnerAddress1, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender2, addrQortalPartnerAddress2, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender3, addrQortalPartnerAddress3, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+ codeByteBuffer.put(OpCode.BNE_DAT.compile(addrMessageSender4, addrQortalPartnerAddress4, calcOffset(codeByteBuffer, labelRedeemTxnLoop)));
+
+ /* Check 'secret-A' in transaction's message */
+
+ // Extract secret-A from first 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN.compile(FunctionCode.PUT_MESSAGE_FROM_TX_IN_A_INTO_B));
+ // Save B register into data segment starting at addrMessageData (as pointed to by addrMessageDataPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrMessageDataPointer));
+ // Load B register with expected hash result (as pointed to by addrHashOfSecretAPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.SET_B_IND, addrHashOfSecretAPointer));
+ // Perform HASH160 using source data at addrMessageData. (Location and length specified via addrMessageDataPointer and addrMessageDataLength).
+ // Save the equality result (1 if they match, 0 otherwise) into addrResult.
+ codeByteBuffer.put(OpCode.EXT_FUN_RET_DAT_2.compile(FunctionCode.CHECK_HASH160_WITH_B, addrResult, addrMessageDataPointer, addrMessageDataLength));
+ // If hashes don't match, addrResult will be zero so go find another transaction
+ codeByteBuffer.put(OpCode.BNZ_DAT.compile(addrResult, calcOffset(codeByteBuffer, labelPayout)));
+ codeByteBuffer.put(OpCode.JMP_ADR.compile(labelRedeemTxnLoop == null ? 0 : labelRedeemTxnLoop));
+
+ /* Success! Pay arranged amount to receiving address */
+ labelPayout = codeByteBuffer.position();
+
+ // Extract Qortal receiving address from next 32 bytes of message from transaction into B register
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.PUT_PARTIAL_MESSAGE_FROM_TX_IN_A_INTO_B.value, addrRedeemMessageReceivingAddressOffset));
+ // Save B register into data segment starting at addrPartnerReceivingAddress (as pointed to by addrPartnerReceivingAddressPointer)
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerReceivingAddressPointer));
+ // Pay AT's balance to receiving address
+ codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.PAY_TO_ADDRESS_IN_B, addrQortAmount));
+ // Set redeemed mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REDEEMED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+
+ // Fall-through to refunding any remaining balance back to AT creator
+
+ /* Refund balance back to AT creator */
+ labelRefund = codeByteBuffer.position();
+
+ // Set refunded mode
+ codeByteBuffer.put(OpCode.SET_VAL.compile(addrMode, AcctMode.REFUNDED.value));
+ // We're finished forever (finishing auto-refunds remaining balance to AT creator)
+ codeByteBuffer.put(OpCode.FIN_IMD.compile());
+ } catch (CompilationException e) {
+ throw new IllegalStateException("Unable to compile DOGE-QORT ACCT?", e);
+ }
+ }
+
+ codeByteBuffer.flip();
+
+ byte[] codeBytes = new byte[codeByteBuffer.limit()];
+ codeByteBuffer.get(codeBytes);
+
+ assert Arrays.equals(Crypto.digest(codeBytes), DogecoinACCTv1.CODE_BYTES_HASH)
+ : String.format("BTCACCT.CODE_BYTES_HASH mismatch: expected %s, actual %s", HashCode.fromBytes(CODE_BYTES_HASH), HashCode.fromBytes(Crypto.digest(codeBytes)));
+
+ final short ciyamAtVersion = 2;
+ final short numCallStackPages = 0;
+ final short numUserStackPages = 0;
+ final long minActivationAmount = 0L;
+
+ return MachineState.toCreationBytes(ciyamAtVersion, codeBytes, dataByteBuffer.array(), numCallStackPages, numUserStackPages, minActivationAmount);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATData atData) throws DataException {
+ ATStateData atStateData = repository.getATRepository().getLatestATState(atData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ @Override
+ public CrossChainTradeData populateTradeData(Repository repository, ATStateData atStateData) throws DataException {
+ ATData atData = repository.getATRepository().fromATAddress(atStateData.getATAddress());
+ return populateTradeData(repository, atData.getCreatorPublicKey(), atData.getCreation(), atStateData);
+ }
+
+ /**
+ * Returns CrossChainTradeData with useful info extracted from AT.
+ */
+ public CrossChainTradeData populateTradeData(Repository repository, byte[] creatorPublicKey, long creationTimestamp, ATStateData atStateData) throws DataException {
+ byte[] addressBytes = new byte[25]; // for general use
+ String atAddress = atStateData.getATAddress();
+
+ CrossChainTradeData tradeData = new CrossChainTradeData();
+
+ tradeData.foreignBlockchain = SupportedBlockchain.DOGECOIN.name();
+ tradeData.acctName = NAME;
+
+ tradeData.qortalAtAddress = atAddress;
+ tradeData.qortalCreator = Crypto.toAddress(creatorPublicKey);
+ tradeData.creationTimestamp = creationTimestamp;
+
+ Account atAccount = new Account(repository, atAddress);
+ tradeData.qortBalance = atAccount.getConfirmedBalance(Asset.QORT);
+
+ byte[] stateData = atStateData.getStateData();
+ ByteBuffer dataByteBuffer = ByteBuffer.wrap(stateData);
+ dataByteBuffer.position(MachineState.HEADER_LENGTH);
+
+ /* Constants */
+
+ // Skip creator's trade address
+ dataByteBuffer.get(addressBytes);
+ tradeData.qortalCreatorTradeAddress = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Creator's Dogecoin/foreign public key hash
+ tradeData.creatorForeignPKH = new byte[20];
+ dataByteBuffer.get(tradeData.creatorForeignPKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - tradeData.creatorForeignPKH.length); // skip to 32 bytes
+
+ // We don't use secret-B
+ tradeData.hashOfSecretB = null;
+
+ // Redeem payout
+ tradeData.qortAmount = dataByteBuffer.getLong();
+
+ // Expected DOGE amount
+ tradeData.expectedForeignAmount = dataByteBuffer.getLong();
+
+ // Trade timeout
+ tradeData.tradeTimeout = (int) dataByteBuffer.getLong();
+
+ // Skip MESSAGE transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'trade' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip expected 'redeem' message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Qortal trade address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for partner's Dogecoin PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's Dogecoin PKH
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'trade' message data offset for hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to hash-of-secret-A
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip 'redeem' message data offset for partner's Qortal receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip message data length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip pointer to partner's receiving address
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ /* End of constants / begin variables */
+
+ // Skip AT creator's address
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Partner's trade address (if present)
+ dataByteBuffer.get(addressBytes);
+ String qortalRecipient = Base58.encode(addressBytes);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - addressBytes.length);
+
+ // Potential lockTimeA (if in trade mode)
+ int lockTimeA = (int) dataByteBuffer.getLong();
+
+ // AT refund timeout (probably only useful for debugging)
+ int refundTimeout = (int) dataByteBuffer.getLong();
+
+ // Trade-mode refund timestamp (AT 'timestamp' converted to Qortal block height)
+ long tradeRefundTimestamp = dataByteBuffer.getLong();
+
+ // Skip last transaction timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip block timestamp
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip transaction type
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary result
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message sender
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Skip message length
+ dataByteBuffer.position(dataByteBuffer.position() + 8);
+
+ // Skip temporary message data
+ dataByteBuffer.position(dataByteBuffer.position() + 8 * 4);
+
+ // Potential hash160 of secret A
+ byte[] hashOfSecretA = new byte[20];
+ dataByteBuffer.get(hashOfSecretA);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - hashOfSecretA.length); // skip to 32 bytes
+
+ // Potential partner's Dogecoin PKH
+ byte[] partnerDogecoinPKH = new byte[20];
+ dataByteBuffer.get(partnerDogecoinPKH);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerDogecoinPKH.length); // skip to 32 bytes
+
+ // Partner's receiving address (if present)
+ byte[] partnerReceivingAddress = new byte[25];
+ dataByteBuffer.get(partnerReceivingAddress);
+ dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerReceivingAddress.length); // skip to 32 bytes
+
+ // Trade AT's 'mode'
+ long modeValue = dataByteBuffer.getLong();
+ AcctMode mode = AcctMode.valueOf((int) (modeValue & 0xffL));
+
+ /* End of variables */
+
+ if (mode != null && mode != AcctMode.OFFERING) {
+ tradeData.mode = mode;
+ tradeData.refundTimeout = refundTimeout;
+ tradeData.tradeRefundHeight = new Timestamp(tradeRefundTimestamp).blockHeight;
+ tradeData.qortalPartnerAddress = qortalRecipient;
+ tradeData.hashOfSecretA = hashOfSecretA;
+ tradeData.partnerForeignPKH = partnerDogecoinPKH;
+ tradeData.lockTimeA = lockTimeA;
+
+ if (mode == AcctMode.REDEEMED)
+ tradeData.qortalPartnerReceivingAddress = Base58.encode(partnerReceivingAddress);
+ } else {
+ tradeData.mode = AcctMode.OFFERING;
+ }
+
+ tradeData.duplicateDeprecated();
+
+ return tradeData;
+ }
+
+ /** Returns 'offer' MESSAGE payload for trade partner to send to AT creator's trade address. */
+ public static byte[] buildOfferMessage(byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA) {
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ return Bytes.concat(partnerBitcoinPKH, hashOfSecretA, lockTimeABytes);
+ }
+
+ /** Returns info extracted from 'offer' MESSAGE payload sent by trade partner to AT creator's trade address, or null if not valid. */
+ public static OfferMessageData extractOfferMessageData(byte[] messageData) {
+ if (messageData == null || messageData.length != OFFER_MESSAGE_LENGTH)
+ return null;
+
+ OfferMessageData offerMessageData = new OfferMessageData();
+ offerMessageData.partnerDogecoinPKH = Arrays.copyOfRange(messageData, 0, 20);
+ offerMessageData.hashOfSecretA = Arrays.copyOfRange(messageData, 20, 40);
+ offerMessageData.lockTimeA = BitTwiddling.longFromBEBytes(messageData, 40);
+
+ return offerMessageData;
+ }
+
+ /** Returns 'trade' MESSAGE payload for AT creator to send to AT. */
+ public static byte[] buildTradeMessage(String partnerQortalTradeAddress, byte[] partnerBitcoinPKH, byte[] hashOfSecretA, int lockTimeA, int refundTimeout) {
+ byte[] data = new byte[TRADE_MESSAGE_LENGTH];
+ byte[] partnerQortalAddressBytes = Base58.decode(partnerQortalTradeAddress);
+ byte[] lockTimeABytes = BitTwiddling.toBEByteArray((long) lockTimeA);
+ byte[] refundTimeoutBytes = BitTwiddling.toBEByteArray((long) refundTimeout);
+
+ System.arraycopy(partnerQortalAddressBytes, 0, data, 0, partnerQortalAddressBytes.length);
+ System.arraycopy(partnerBitcoinPKH, 0, data, 32, partnerBitcoinPKH.length);
+ System.arraycopy(refundTimeoutBytes, 0, data, 56, refundTimeoutBytes.length);
+ System.arraycopy(hashOfSecretA, 0, data, 64, hashOfSecretA.length);
+ System.arraycopy(lockTimeABytes, 0, data, 88, lockTimeABytes.length);
+
+ return data;
+ }
+
+ /** Returns 'cancel' MESSAGE payload for AT creator to cancel trade AT. */
+ @Override
+ public byte[] buildCancelMessage(String creatorQortalAddress) {
+ byte[] data = new byte[CANCEL_MESSAGE_LENGTH];
+ byte[] creatorQortalAddressBytes = Base58.decode(creatorQortalAddress);
+
+ System.arraycopy(creatorQortalAddressBytes, 0, data, 0, creatorQortalAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns 'redeem' MESSAGE payload for trade partner to send to AT. */
+ public static byte[] buildRedeemMessage(byte[] secretA, String qortalReceivingAddress) {
+ byte[] data = new byte[REDEEM_MESSAGE_LENGTH];
+ byte[] qortalReceivingAddressBytes = Base58.decode(qortalReceivingAddress);
+
+ System.arraycopy(secretA, 0, data, 0, secretA.length);
+ System.arraycopy(qortalReceivingAddressBytes, 0, data, 32, qortalReceivingAddressBytes.length);
+
+ return data;
+ }
+
+ /** Returns refund timeout (minutes) based on trade partner's 'offer' MESSAGE timestamp and P2SH-A locktime. */
+ public static int calcRefundTimeout(long offerMessageTimestamp, int lockTimeA) {
+ // refund should be triggered halfway between offerMessageTimestamp and lockTimeA
+ return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
+ }
+
+ @Override
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
+ String atAddress = crossChainTradeData.qortalAtAddress;
+ String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
+
+ // We don't have partner's public key so we check every message to AT
+ List messageTransactionsData = repository.getMessageRepository().getMessagesByParticipants(null, atAddress, null, null, null);
+ if (messageTransactionsData == null)
+ return null;
+
+ // Find 'redeem' message
+ for (MessageTransactionData messageTransactionData : messageTransactionsData) {
+ // Check message payload type/encryption
+ if (messageTransactionData.isText() || messageTransactionData.isEncrypted())
+ continue;
+
+ // Check message payload size
+ byte[] messageData = messageTransactionData.getData();
+ if (messageData.length != REDEEM_MESSAGE_LENGTH)
+ // Wrong payload length
+ continue;
+
+ // Check sender
+ if (!Crypto.toAddress(messageTransactionData.getSenderPublicKey()).equals(redeemerAddress))
+ // Wrong sender;
+ continue;
+
+ // Extract secretA
+ byte[] secretA = new byte[32];
+ System.arraycopy(messageData, 0, secretA, 0, secretA.length);
+
+ byte[] hashOfSecretA = Crypto.hash160(secretA);
+ if (!Arrays.equals(hashOfSecretA, crossChainTradeData.hashOfSecretA))
+ continue;
+
+ return secretA;
+ }
+
+ return null;
+ }
+
+}
diff --git a/src/main/java/org/qortal/crosschain/ElectrumX.java b/src/main/java/org/qortal/crosschain/ElectrumX.java
index b34aa199..8f41ed86 100644
--- a/src/main/java/org/qortal/crosschain/ElectrumX.java
+++ b/src/main/java/org/qortal/crosschain/ElectrumX.java
@@ -33,6 +33,7 @@ import org.qortal.crypto.TrustlessSSLSocketFactory;
import com.google.common.hash.HashCode;
import com.google.common.primitives.Bytes;
+import org.qortal.utils.BitTwiddling;
/** ElectrumX network support for querying Bitcoiny-related info like block headers, transaction outputs, etc. */
public class ElectrumX extends BitcoinyBlockchainProvider {
@@ -171,13 +172,41 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
Long returnedCount = (Long) countObj;
String hex = (String) hexObj;
- byte[] raw = HashCode.fromString(hex).asBytes();
- if (raw.length != returnedCount * BLOCK_HEADER_LENGTH)
- throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
-
List rawBlockHeaders = new ArrayList<>(returnedCount.intValue());
- for (int i = 0; i < returnedCount; ++i)
- rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
+
+ byte[] raw = HashCode.fromString(hex).asBytes();
+
+ // Most chains use a fixed length 80 byte header, so block headers can be split up by dividing the hex into
+ // 80-byte segments. However, some chains such as DOGE use variable length headers due to AuxPoW or other
+ // reasons. In these cases we can identify the start of each block header by the location of the block version
+ // numbers. Each block starts with a version number, and for DOGE this is easily identifiable (6422788) at the
+ // time of writing (Jul 2021). If we encounter a chain that is using more generic version numbers (e.g. 1)
+ // and can't be used to accurately identify block indexes, then there are sufficient checks to ensure an
+ // exception is thrown.
+
+ if (raw.length == returnedCount * BLOCK_HEADER_LENGTH) {
+ // Fixed-length header (BTC, LTC, etc)
+ for (int i = 0; i < returnedCount; ++i) {
+ rawBlockHeaders.add(Arrays.copyOfRange(raw, i * BLOCK_HEADER_LENGTH, (i + 1) * BLOCK_HEADER_LENGTH));
+ }
+ }
+ else if (raw.length > returnedCount * BLOCK_HEADER_LENGTH) {
+ // Assume AuxPoW variable length header (DOGE)
+ int referenceVersion = BitTwiddling.intFromLEBytes(raw, 0); // DOGE uses 6422788 at time of commit (Jul 2021)
+ for (int i = 0; i < raw.length - 4; ++i) {
+ // Locate the start of each block by its version number
+ if (BitTwiddling.intFromLEBytes(raw, i) == referenceVersion) {
+ rawBlockHeaders.add(Arrays.copyOfRange(raw, i, i + BLOCK_HEADER_LENGTH));
+ }
+ }
+ // Ensure that we found the correct number of block headers
+ if (rawBlockHeaders.size() != count) {
+ throw new ForeignBlockchainException.NetworkException("Unexpected raw header contents in JSON from ElectrumX blockchain.block.headers RPC.");
+ }
+ }
+ else if (raw.length != returnedCount * BLOCK_HEADER_LENGTH) {
+ throw new ForeignBlockchainException.NetworkException("Unexpected raw header length in JSON from ElectrumX blockchain.block.headers RPC");
+ }
return rawBlockHeaders;
}
@@ -518,6 +547,7 @@ public class ElectrumX extends BitcoinyBlockchainProvider {
}
// Failed to perform RPC - maybe lack of servers?
+ LOGGER.info("Error: No connected Electrum servers when trying to make RPC call");
throw new ForeignBlockchainException.NetworkException(String.format("Failed to perform ElectrumX RPC %s", method));
}
}
diff --git a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java b/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
index 0a71e9d9..fe64ab83 100644
--- a/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
+++ b/src/main/java/org/qortal/crosschain/ForeignBlockchain.java
@@ -6,4 +6,6 @@ public interface ForeignBlockchain {
public boolean isValidWalletKey(String walletKey);
+ public long getMinimumOrderAmount();
+
}
diff --git a/src/main/java/org/qortal/crosschain/Litecoin.java b/src/main/java/org/qortal/crosschain/Litecoin.java
index 5cbe4044..0c04243c 100644
--- a/src/main/java/org/qortal/crosschain/Litecoin.java
+++ b/src/main/java/org/qortal/crosschain/Litecoin.java
@@ -51,7 +51,10 @@ public class Litecoin extends Bitcoiny {
new Server("ltc.rentonisk.com", Server.ConnectionType.TCP, 50001),
new Server("ltc.rentonisk.com", Server.ConnectionType.SSL, 50002),
new Server("electrum-ltc.petrkr.net", Server.ConnectionType.SSL, 60002),
- new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022));
+ new Server("ltc.litepay.ch", Server.ConnectionType.SSL, 50022),
+ new Server("electrum-ltc-bysh.me", Server.ConnectionType.TCP, 50002),
+ new Server("electrum.jochen-hoenicke.de", Server.ConnectionType.TCP, 50005),
+ new Server("node.ispol.sk", Server.ConnectionType.TCP, 50004));
}
@Override
diff --git a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
index 454e80c2..efd7043e 100644
--- a/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
+++ b/src/main/java/org/qortal/crosschain/LitecoinACCTv1.java
@@ -810,7 +810,8 @@ public class LitecoinACCTv1 implements ACCT {
return (int) ((lockTimeA - (offerMessageTimestamp / 1000L)) / 2L / 60L);
}
- public static byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
+ @Override
+ public byte[] findSecretA(Repository repository, CrossChainTradeData crossChainTradeData) throws DataException {
String atAddress = crossChainTradeData.qortalAtAddress;
String redeemerAddress = crossChainTradeData.qortalPartnerAddress;
diff --git a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
index 7b6f91f5..1fc8d149 100644
--- a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
+++ b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java
@@ -39,6 +39,20 @@ public enum SupportedBlockchain {
public ACCT getLatestAcct() {
return LitecoinACCTv1.getInstance();
}
+ },
+
+ DOGECOIN(Arrays.asList(
+ Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance)
+ )) {
+ @Override
+ public ForeignBlockchain getInstance() {
+ return Dogecoin.getInstance();
+ }
+
+ @Override
+ public ACCT getLatestAcct() {
+ return DogecoinACCTv1.getInstance();
+ }
};
private static final Map> supportedAcctsByCodeHash = Arrays.stream(SupportedBlockchain.values())
@@ -110,4 +124,4 @@ public enum SupportedBlockchain {
return acctInstanceSupplier.get();
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/qortal/gui/SplashFrame.java b/src/main/java/org/qortal/gui/SplashFrame.java
index e0859030..37d20ec5 100644
--- a/src/main/java/org/qortal/gui/SplashFrame.java
+++ b/src/main/java/org/qortal/gui/SplashFrame.java
@@ -1,15 +1,11 @@
package org.qortal.gui;
-import java.awt.BorderLayout;
-import java.awt.Image;
+import java.awt.*;
import java.util.ArrayList;
import java.util.List;
-import java.awt.Dimension;
-import java.awt.Graphics;
import java.awt.image.BufferedImage;
-import javax.swing.JDialog;
-import javax.swing.JPanel;
+import javax.swing.*;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@@ -19,46 +15,53 @@ public class SplashFrame {
protected static final Logger LOGGER = LogManager.getLogger(SplashFrame.class);
private static SplashFrame instance;
- private JDialog splashDialog;
+ private JFrame splashDialog;
@SuppressWarnings("serial")
public static class SplashPanel extends JPanel {
private BufferedImage image;
+ private String defaultSplash = "Qlogo_512.png";
+
public SplashPanel() {
- image = Gui.loadImage("splash.png");
- this.setPreferredSize(new Dimension(image.getWidth(), image.getHeight()));
- this.setLayout(new BorderLayout());
+ image = Gui.loadImage(defaultSplash);
+
+ setOpaque(false);
+ setLayout(new GridBagLayout());
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
- g.drawImage(image, 0, 0, null);
+ g.drawImage(image, 0, 0, getWidth(), getHeight(), this);
+ }
+
+ @Override
+ public Dimension getPreferredSize() {
+ return new Dimension(500, 500);
}
}
private SplashFrame() {
- this.splashDialog = new JDialog();
+ this.splashDialog = new JFrame();
List icons = new ArrayList<>();
icons.add(Gui.loadImage("icons/icon16.png"));
- icons.add(Gui.loadImage("icons/icon32.png"));
+ icons.add(Gui.loadImage("icons/qortal_ui_tray_synced.png"));
+ icons.add(Gui.loadImage("icons/qortal_ui_tray_syncing_time-alt.png"));
+ icons.add(Gui.loadImage("icons/qortal_ui_tray_minting.png"));
+ icons.add(Gui.loadImage("icons/qortal_ui_tray_syncing.png"));
icons.add(Gui.loadImage("icons/icon64.png"));
- icons.add(Gui.loadImage("icons/icon128.png"));
+ icons.add(Gui.loadImage("icons/Qlogo_128.png"));
this.splashDialog.setIconImages(icons);
- this.splashDialog.setDefaultCloseOperation(JDialog.DO_NOTHING_ON_CLOSE);
- this.splashDialog.setTitle("qortal");
- this.splashDialog.setContentPane(new SplashPanel());
-
+ this.splashDialog.getContentPane().add(new SplashPanel());
+ this.splashDialog.setDefaultCloseOperation(JFrame.DO_NOTHING_ON_CLOSE);
this.splashDialog.setUndecorated(true);
- this.splashDialog.setModal(false);
this.splashDialog.pack();
this.splashDialog.setLocationRelativeTo(null);
- this.splashDialog.toFront();
+ this.splashDialog.setBackground(new Color(0,0,0,0));
this.splashDialog.setVisible(true);
- this.splashDialog.repaint();
}
public static SplashFrame getInstance() {
diff --git a/src/main/java/org/qortal/gui/SysTray.java b/src/main/java/org/qortal/gui/SysTray.java
index c456d6fe..6fc994bf 100644
--- a/src/main/java/org/qortal/gui/SysTray.java
+++ b/src/main/java/org/qortal/gui/SysTray.java
@@ -61,7 +61,7 @@ public class SysTray {
this.popupMenu = createJPopupMenu();
// Build TrayIcon without AWT PopupMenu (which doesn't support Unicode)...
- this.trayIcon = new TrayIcon(Gui.loadImage("icons/icon32.png"), "qortal", null);
+ this.trayIcon = new TrayIcon(Gui.loadImage("icons/qortal_ui_tray_synced.png"), "qortal", null);
// ...and attach mouse listener instead so we can use JPopupMenu (which does support Unicode)
this.trayIcon.addMouseListener(new MouseAdapter() {
@Override
@@ -289,6 +289,25 @@ public class SysTray {
this.trayIcon.setToolTip(text);
}
+ public void setTrayIcon(int iconid) {
+ if (trayIcon != null) {
+ switch (iconid) {
+ case 1:
+ this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_syncing_time-alt.png"));
+ break;
+ case 2:
+ this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_minting.png"));
+ break;
+ case 3:
+ this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_syncing.png"));
+ break;
+ case 4:
+ this.trayIcon.setImage(Gui.loadImage("icons/qortal_ui_tray_synced.png"));
+ break;
+ }
+ }
+ }
+
public void dispose() {
if (trayIcon != null)
SystemTray.getSystemTray().remove(this.trayIcon);
diff --git a/src/main/java/org/qortal/network/Network.java b/src/main/java/org/qortal/network/Network.java
index 772a96d5..c4fd10d9 100644
--- a/src/main/java/org/qortal/network/Network.java
+++ b/src/main/java/org/qortal/network/Network.java
@@ -72,7 +72,8 @@ public class Network {
private static final String[] INITIAL_PEERS = new String[]{
"node1.qortal.org", "node2.qortal.org", "node3.qortal.org", "node4.qortal.org", "node5.qortal.org",
"node6.qortal.org", "node7.qortal.org", "node8.qortal.org", "node9.qortal.org", "node10.qortal.org",
- "node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk"
+ "node.qortal.ru", "node2.qortal.ru", "node3.qortal.ru", "node.qortal.uk", "node22.qortal.org",
+ "cinfu1.crowetic.com", "node.cwd.systems"
};
private static final long NETWORK_EPC_KEEPALIVE = 10L; // seconds
@@ -80,6 +81,8 @@ public class Network {
public static final int MAX_SIGNATURES_PER_REPLY = 500;
public static final int MAX_BLOCK_SUMMARIES_PER_REPLY = 500;
+ private static final long DISCONNECTION_CHECK_INTERVAL = 10 * 1000L; // milliseconds
+
// Generate our node keys / ID
private final Ed25519PrivateKeyParameters edPrivateKeyParams = new Ed25519PrivateKeyParameters(new SecureRandom());
private final Ed25519PublicKeyParameters edPublicKeyParams = edPrivateKeyParams.generatePublicKey();
@@ -89,6 +92,8 @@ public class Network {
private final int minOutboundPeers;
private final int maxPeers;
+ private long nextDisconnectionCheck = 0L;
+
private final List allKnownPeers = new ArrayList<>();
private final List connectedPeers = new ArrayList<>();
private final List selfPeers = new ArrayList<>();
@@ -611,6 +616,8 @@ public class Network {
// Don't consider already connected peers (resolved address match)
// XXX This might be too slow if we end up waiting a long time for hostnames to resolve via DNS
peers.removeIf(isResolvedAsConnectedPeer);
+
+ this.checkLongestConnection(now);
}
// Any left?
@@ -668,6 +675,29 @@ public class Network {
return null;
}
+ private void checkLongestConnection(Long now) {
+ if (now == null || now < nextDisconnectionCheck) {
+ return;
+ }
+
+ // Find peers that have reached their maximum connection age, and disconnect them
+ List peersToDisconnect = this.connectedPeers.stream()
+ .filter(peer -> !peer.isSyncInProgress())
+ .filter(peer -> peer.hasReachedMaxConnectionAge())
+ .collect(Collectors.toList());
+
+ if (peersToDisconnect != null && peersToDisconnect.size() > 0) {
+ for (Peer peer : peersToDisconnect) {
+ LOGGER.info("Forcing disconnection of peer {} because connection age ({} ms) " +
+ "has reached the maximum ({} ms)", peer, peer.getConnectionAge(), peer.getMaxConnectionAge());
+ peer.disconnect("Connection age too old");
+ }
+ }
+
+ // Check again after a minimum fixed interval
+ nextDisconnectionCheck = now + DISCONNECTION_CHECK_INTERVAL;
+ }
+
// Peer callbacks
protected void wakeupChannelSelector() {
diff --git a/src/main/java/org/qortal/network/Peer.java b/src/main/java/org/qortal/network/Peer.java
index bb6dd148..8763c114 100644
--- a/src/main/java/org/qortal/network/Peer.java
+++ b/src/main/java/org/qortal/network/Peer.java
@@ -84,6 +84,7 @@ public class Peer {
private Handshake handshakeStatus = Handshake.STARTED;
private volatile boolean handshakeMessagePending = false;
private long handshakeComplete = -1L;
+ private long maxConnectionAge = 0L;
/**
* Timestamp of when socket was accepted, or connected.
@@ -101,6 +102,8 @@ public class Peer {
byte[] ourChallenge;
+ private boolean syncInProgress = false;
+
// Versioning
public static final Pattern VERSION_PATTERN = Pattern.compile(Controller.VERSION_PREFIX
+ "(\\d{1,3})\\.(\\d{1,5})\\.(\\d{1,5})");
@@ -197,10 +200,24 @@ public class Peer {
this.handshakeStatus = handshakeStatus;
if (handshakeStatus.equals(Handshake.COMPLETED)) {
this.handshakeComplete = System.currentTimeMillis();
+ this.generateRandomMaxConnectionAge();
}
}
}
+ private void generateRandomMaxConnectionAge() {
+ // Retrieve the min and max connection time from the settings, and calculate the range
+ final int minPeerConnectionTime = Settings.getInstance().getMinPeerConnectionTime();
+ final int maxPeerConnectionTime = Settings.getInstance().getMaxPeerConnectionTime();
+ final int peerConnectionTimeRange = maxPeerConnectionTime - minPeerConnectionTime;
+
+ // Generate a random number between the min and the max
+ Random random = new Random();
+ this.maxConnectionAge = (random.nextInt(peerConnectionTimeRange) + minPeerConnectionTime) * 1000L;
+ LOGGER.debug(String.format("[%s] Generated max connection age for peer %s. Min: %ds, max: %ds, range: %ds, random max: %dms", this.peerConnectionId, this, minPeerConnectionTime, maxPeerConnectionTime, peerConnectionTimeRange, this.maxConnectionAge));
+
+ }
+
protected void resetHandshakeMessagePending() {
this.handshakeMessagePending = false;
}
@@ -330,6 +347,14 @@ public class Peer {
}
}
+ public boolean isSyncInProgress() {
+ return this.syncInProgress;
+ }
+
+ public void setSyncInProgress(boolean syncInProgress) {
+ this.syncInProgress = syncInProgress;
+ }
+
@Override
public String toString() {
// Easier, and nicer output, than peer.getRemoteSocketAddress()
@@ -812,4 +837,12 @@ public class Peer {
}
return handshakeComplete;
}
+
+ public long getMaxConnectionAge() {
+ return maxConnectionAge;
+ }
+
+ public boolean hasReachedMaxConnectionAge() {
+ return this.getConnectionAge() > this.getMaxConnectionAge();
+ }
}
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
index 09c6a6d4..4d8e5043 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBRepository.java
@@ -55,7 +55,7 @@ public class HSQLDBRepository implements Repository {
private static final Logger LOGGER = LogManager.getLogger(HSQLDBRepository.class);
- private static final Object CHECKPOINT_LOCK = new Object();
+ public static final Object CHECKPOINT_LOCK = new Object();
// "serialization failure"
private static final Integer DEADLOCK_ERROR_CODE = Integer.valueOf(-4861);
@@ -703,8 +703,11 @@ public class HSQLDBRepository implements Repository {
private ResultSet checkedExecuteResultSet(PreparedStatement preparedStatement, Object... objects) throws SQLException {
bindStatementParams(preparedStatement, objects);
- if (!preparedStatement.execute())
- throw new SQLException("Fetching from database produced no results");
+ // synchronize to block new executions if checkpointing in progress
+ synchronized (CHECKPOINT_LOCK) {
+ if (!preparedStatement.execute())
+ throw new SQLException("Fetching from database produced no results");
+ }
ResultSet resultSet = preparedStatement.getResultSet();
if (resultSet == null)
@@ -1056,4 +1059,4 @@ public class HSQLDBRepository implements Repository {
return DEADLOCK_ERROR_CODE.equals(e.getErrorCode());
}
-}
\ No newline at end of file
+}
diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java
index c1b6ee9b..acf24c54 100644
--- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java
+++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBSaver.java
@@ -61,13 +61,15 @@ public class HSQLDBSaver {
public boolean execute(HSQLDBRepository repository) throws SQLException {
String sql = this.formatInsertWithPlaceholders();
- try {
- PreparedStatement preparedStatement = repository.prepareStatement(sql);
- this.bindValues(preparedStatement);
+ synchronized (HSQLDBRepository.CHECKPOINT_LOCK) {
+ try {
+ PreparedStatement preparedStatement = repository.prepareStatement(sql);
+ this.bindValues(preparedStatement);
- return preparedStatement.execute();
- } catch (SQLException e) {
- throw repository.examineException(e);
+ return preparedStatement.execute();
+ } catch (SQLException e) {
+ throw repository.examineException(e);
+ }
}
}
diff --git a/src/main/java/org/qortal/settings/Settings.java b/src/main/java/org/qortal/settings/Settings.java
index 7c26fc22..7cd0f941 100644
--- a/src/main/java/org/qortal/settings/Settings.java
+++ b/src/main/java/org/qortal/settings/Settings.java
@@ -26,6 +26,7 @@ import org.eclipse.persistence.jaxb.UnmarshallerProperties;
import org.qortal.block.BlockChain;
import org.qortal.crosschain.Bitcoin.BitcoinNet;
import org.qortal.crosschain.Litecoin.LitecoinNet;
+import org.qortal.crosschain.Dogecoin.DogecoinNet;
// All properties to be converted to JSON via JAXB
@XmlAccessorType(XmlAccessType.FIELD)
@@ -144,6 +145,11 @@ public class Settings {
* If false, sync will be blocked both ways, and they will not appear in the peers list */
private boolean allowConnectionsWithOlderPeerVersions = true;
+ /** Minimum time (in seconds) that we should attempt to remain connected to a peer for */
+ private int minPeerConnectionTime = 2 * 60; // seconds
+ /** Maximum time (in seconds) that we should attempt to remain connected to a peer for */
+ private int maxPeerConnectionTime = 20 * 60; // seconds
+
/** Whether to sync multiple blocks at once in normal operation */
private boolean fastSyncEnabled = true;
/** Whether to sync multiple blocks at once when the peer has a different chain */
@@ -159,6 +165,7 @@ public class Settings {
private String blockchainConfig = null; // use default from resources
private BitcoinNet bitcoinNet = BitcoinNet.MAIN;
private LitecoinNet litecoinNet = LitecoinNet.MAIN;
+ private DogecoinNet dogecoinNet = DogecoinNet.MAIN;
// Also crosschain-related:
/** Whether to show SysTray pop-up notifications when trade-bot entries change state */
private boolean tradebotSystrayEnabled = false;
@@ -507,6 +514,10 @@ public class Settings {
public boolean getAllowConnectionsWithOlderPeerVersions() { return this.allowConnectionsWithOlderPeerVersions; }
+ public int getMinPeerConnectionTime() { return this.minPeerConnectionTime; }
+
+ public int getMaxPeerConnectionTime() { return this.maxPeerConnectionTime; }
+
public String getBlockchainConfig() {
return this.blockchainConfig;
}
@@ -519,6 +530,10 @@ public class Settings {
return this.litecoinNet;
}
+ public DogecoinNet getDogecoinNet() {
+ return this.dogecoinNet;
+ }
+
public boolean isTradebotSystrayEnabled() {
return this.tradebotSystrayEnabled;
}
diff --git a/src/main/java/org/qortal/utils/BIP39.java b/src/main/java/org/qortal/utils/BIP39.java
deleted file mode 100644
index 488396eb..00000000
--- a/src/main/java/org/qortal/utils/BIP39.java
+++ /dev/null
@@ -1,86 +0,0 @@
-package org.qortal.utils;
-
-import java.util.ArrayList;
-import java.util.List;
-
-import org.qortal.globalization.BIP39WordList;
-
-public class BIP39 {
-
- private static final int BITS_PER_WORD = 11;
-
- /** Convert BIP39 mnemonic to binary 'entropy' */
- public static byte[] decode(String[] phraseWords, String lang) {
- if (lang == null)
- lang = "en";
-
- List wordList = BIP39WordList.INSTANCE.getByLang(lang);
- if (wordList == null)
- throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
-
- byte[] entropy = new byte[(phraseWords.length * BITS_PER_WORD + 7) / 8];
- int byteIndex = 0;
- int bitShift = 3;
-
- for (int i = 0; i < phraseWords.length; ++i) {
- int wordListIndex = wordList.indexOf(phraseWords[i]);
- if (wordListIndex == -1)
- // Word not found
- return null;
-
- entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
-
- bitShift = 8 - bitShift;
- if (bitShift >= 0) {
- // Leftover fits inside one byte
- entropy[byteIndex] |= (byte) ((wordListIndex << bitShift));
- bitShift = BITS_PER_WORD - bitShift;
- } else {
- // Leftover spread over next two bytes
- bitShift = - bitShift;
- entropy[byteIndex++] |= (byte) (wordListIndex >> bitShift);
-
- entropy[byteIndex] |= (byte) (wordListIndex << (8 - bitShift));
- bitShift = bitShift + BITS_PER_WORD - 8;
- }
- }
-
- return entropy;
- }
-
- /** Convert binary entropy to BIP39 mnemonic */
- public static String encode(byte[] entropy, String lang) {
- if (lang == null)
- lang = "en";
-
- List wordList = BIP39WordList.INSTANCE.getByLang(lang);
- if (wordList == null)
- throw new IllegalStateException("BIP39 word list for lang '" + lang + "' unavailable");
-
- List phraseWords = new ArrayList<>();
-
- int bitMask = 128; // MSB first
- int byteIndex = 0;
- while (true) {
- int wordListIndex = 0;
- for (int bitCount = 0; bitCount < BITS_PER_WORD; ++bitCount) {
- wordListIndex <<= 1;
-
- if ((entropy[byteIndex] & bitMask) != 0)
- ++wordListIndex;
-
- bitMask >>= 1;
- if (bitMask == 0) {
- bitMask = 128;
- ++byteIndex;
-
- if (byteIndex >= entropy.length)
- return String.join(" ", phraseWords);
- }
- }
-
- phraseWords.add(wordList.get(wordListIndex));
- }
- }
-
-}
diff --git a/src/main/resources/BIP39/wordlist_en.txt b/src/main/resources/BIP39/wordlist_en.txt
deleted file mode 100644
index 942040ed..00000000
--- a/src/main/resources/BIP39/wordlist_en.txt
+++ /dev/null
@@ -1,2048 +0,0 @@
-abandon
-ability
-able
-about
-above
-absent
-absorb
-abstract
-absurd
-abuse
-access
-accident
-account
-accuse
-achieve
-acid
-acoustic
-acquire
-across
-act
-action
-actor
-actress
-actual
-adapt
-add
-addict
-address
-adjust
-admit
-adult
-advance
-advice
-aerobic
-affair
-afford
-afraid
-again
-age
-agent
-agree
-ahead
-aim
-air
-airport
-aisle
-alarm
-album
-alcohol
-alert
-alien
-all
-alley
-allow
-almost
-alone
-alpha
-already
-also
-alter
-always
-amateur
-amazing
-among
-amount
-amused
-analyst
-anchor
-ancient
-anger
-angle
-angry
-animal
-ankle
-announce
-annual
-another
-answer
-antenna
-antique
-anxiety
-any
-apart
-apology
-appear
-apple
-approve
-april
-arch
-arctic
-area
-arena
-argue
-arm
-armed
-armor
-army
-around
-arrange
-arrest
-arrive
-arrow
-art
-artefact
-artist
-artwork
-ask
-aspect
-assault
-asset
-assist
-assume
-asthma
-athlete
-atom
-attack
-attend
-attitude
-attract
-auction
-audit
-august
-aunt
-author
-auto
-autumn
-average
-avocado
-avoid
-awake
-aware
-away
-awesome
-awful
-awkward
-axis
-baby
-bachelor
-bacon
-badge
-bag
-balance
-balcony
-ball
-bamboo
-banana
-banner
-bar
-barely
-bargain
-barrel
-base
-basic
-basket
-battle
-beach
-bean
-beauty
-because
-become
-beef
-before
-begin
-behave
-behind
-believe
-below
-belt
-bench
-benefit
-best
-betray
-better
-between
-beyond
-bicycle
-bid
-bike
-bind
-biology
-bird
-birth
-bitter
-black
-blade
-blame
-blanket
-blast
-bleak
-bless
-blind
-blood
-blossom
-blouse
-blue
-blur
-blush
-board
-boat
-body
-boil
-bomb
-bone
-bonus
-book
-boost
-border
-boring
-borrow
-boss
-bottom
-bounce
-box
-boy
-bracket
-brain
-brand
-brass
-brave
-bread
-breeze
-brick
-bridge
-brief
-bright
-bring
-brisk
-broccoli
-broken
-bronze
-broom
-brother
-brown
-brush
-bubble
-buddy
-budget
-buffalo
-build
-bulb
-bulk
-bullet
-bundle
-bunker
-burden
-burger
-burst
-bus
-business
-busy
-butter
-buyer
-buzz
-cabbage
-cabin
-cable
-cactus
-cage
-cake
-call
-calm
-camera
-camp
-can
-canal
-cancel
-candy
-cannon
-canoe
-canvas
-canyon
-capable
-capital
-captain
-car
-carbon
-card
-cargo
-carpet
-carry
-cart
-case
-cash
-casino
-castle
-casual
-cat
-catalog
-catch
-category
-cattle
-caught
-cause
-caution
-cave
-ceiling
-celery
-cement
-census
-century
-cereal
-certain
-chair
-chalk
-champion
-change
-chaos
-chapter
-charge
-chase
-chat
-cheap
-check
-cheese
-chef
-cherry
-chest
-chicken
-chief
-child
-chimney
-choice
-choose
-chronic
-chuckle
-chunk
-churn
-cigar
-cinnamon
-circle
-citizen
-city
-civil
-claim
-clap
-clarify
-claw
-clay
-clean
-clerk
-clever
-click
-client
-cliff
-climb
-clinic
-clip
-clock
-clog
-close
-cloth
-cloud
-clown
-club
-clump
-cluster
-clutch
-coach
-coast
-coconut
-code
-coffee
-coil
-coin
-collect
-color
-column
-combine
-come
-comfort
-comic
-common
-company
-concert
-conduct
-confirm
-congress
-connect
-consider
-control
-convince
-cook
-cool
-copper
-copy
-coral
-core
-corn
-correct
-cost
-cotton
-couch
-country
-couple
-course
-cousin
-cover
-coyote
-crack
-cradle
-craft
-cram
-crane
-crash
-crater
-crawl
-crazy
-cream
-credit
-creek
-crew
-cricket
-crime
-crisp
-critic
-crop
-cross
-crouch
-crowd
-crucial
-cruel
-cruise
-crumble
-crunch
-crush
-cry
-crystal
-cube
-culture
-cup
-cupboard
-curious
-current
-curtain
-curve
-cushion
-custom
-cute
-cycle
-dad
-damage
-damp
-dance
-danger
-daring
-dash
-daughter
-dawn
-day
-deal
-debate
-debris
-decade
-december
-decide
-decline
-decorate
-decrease
-deer
-defense
-define
-defy
-degree
-delay
-deliver
-demand
-demise
-denial
-dentist
-deny
-depart
-depend
-deposit
-depth
-deputy
-derive
-describe
-desert
-design
-desk
-despair
-destroy
-detail
-detect
-develop
-device
-devote
-diagram
-dial
-diamond
-diary
-dice
-diesel
-diet
-differ
-digital
-dignity
-dilemma
-dinner
-dinosaur
-direct
-dirt
-disagree
-discover
-disease
-dish
-dismiss
-disorder
-display
-distance
-divert
-divide
-divorce
-dizzy
-doctor
-document
-dog
-doll
-dolphin
-domain
-donate
-donkey
-donor
-door
-dose
-double
-dove
-draft
-dragon
-drama
-drastic
-draw
-dream
-dress
-drift
-drill
-drink
-drip
-drive
-drop
-drum
-dry
-duck
-dumb
-dune
-during
-dust
-dutch
-duty
-dwarf
-dynamic
-eager
-eagle
-early
-earn
-earth
-easily
-east
-easy
-echo
-ecology
-economy
-edge
-edit
-educate
-effort
-egg
-eight
-either
-elbow
-elder
-electric
-elegant
-element
-elephant
-elevator
-elite
-else
-embark
-embody
-embrace
-emerge
-emotion
-employ
-empower
-empty
-enable
-enact
-end
-endless
-endorse
-enemy
-energy
-enforce
-engage
-engine
-enhance
-enjoy
-enlist
-enough
-enrich
-enroll
-ensure
-enter
-entire
-entry
-envelope
-episode
-equal
-equip
-era
-erase
-erode
-erosion
-error
-erupt
-escape
-essay
-essence
-estate
-eternal
-ethics
-evidence
-evil
-evoke
-evolve
-exact
-example
-excess
-exchange
-excite
-exclude
-excuse
-execute
-exercise
-exhaust
-exhibit
-exile
-exist
-exit
-exotic
-expand
-expect
-expire
-explain
-expose
-express
-extend
-extra
-eye
-eyebrow
-fabric
-face
-faculty
-fade
-faint
-faith
-fall
-false
-fame
-family
-famous
-fan
-fancy
-fantasy
-farm
-fashion
-fat
-fatal
-father
-fatigue
-fault
-favorite
-feature
-february
-federal
-fee
-feed
-feel
-female
-fence
-festival
-fetch
-fever
-few
-fiber
-fiction
-field
-figure
-file
-film
-filter
-final
-find
-fine
-finger
-finish
-fire
-firm
-first
-fiscal
-fish
-fit
-fitness
-fix
-flag
-flame
-flash
-flat
-flavor
-flee
-flight
-flip
-float
-flock
-floor
-flower
-fluid
-flush
-fly
-foam
-focus
-fog
-foil
-fold
-follow
-food
-foot
-force
-forest
-forget
-fork
-fortune
-forum
-forward
-fossil
-foster
-found
-fox
-fragile
-frame
-frequent
-fresh
-friend
-fringe
-frog
-front
-frost
-frown
-frozen
-fruit
-fuel
-fun
-funny
-furnace
-fury
-future
-gadget
-gain
-galaxy
-gallery
-game
-gap
-garage
-garbage
-garden
-garlic
-garment
-gas
-gasp
-gate
-gather
-gauge
-gaze
-general
-genius
-genre
-gentle
-genuine
-gesture
-ghost
-giant
-gift
-giggle
-ginger
-giraffe
-girl
-give
-glad
-glance
-glare
-glass
-glide
-glimpse
-globe
-gloom
-glory
-glove
-glow
-glue
-goat
-goddess
-gold
-good
-goose
-gorilla
-gospel
-gossip
-govern
-gown
-grab
-grace
-grain
-grant
-grape
-grass
-gravity
-great
-green
-grid
-grief
-grit
-grocery
-group
-grow
-grunt
-guard
-guess
-guide
-guilt
-guitar
-gun
-gym
-habit
-hair
-half
-hammer
-hamster
-hand
-happy
-harbor
-hard
-harsh
-harvest
-hat
-have
-hawk
-hazard
-head
-health
-heart
-heavy
-hedgehog
-height
-hello
-helmet
-help
-hen
-hero
-hidden
-high
-hill
-hint
-hip
-hire
-history
-hobby
-hockey
-hold
-hole
-holiday
-hollow
-home
-honey
-hood
-hope
-horn
-horror
-horse
-hospital
-host
-hotel
-hour
-hover
-hub
-huge
-human
-humble
-humor
-hundred
-hungry
-hunt
-hurdle
-hurry
-hurt
-husband
-hybrid
-ice
-icon
-idea
-identify
-idle
-ignore
-ill
-illegal
-illness
-image
-imitate
-immense
-immune
-impact
-impose
-improve
-impulse
-inch
-include
-income
-increase
-index
-indicate
-indoor
-industry
-infant
-inflict
-inform
-inhale
-inherit
-initial
-inject
-injury
-inmate
-inner
-innocent
-input
-inquiry
-insane
-insect
-inside
-inspire
-install
-intact
-interest
-into
-invest
-invite
-involve
-iron
-island
-isolate
-issue
-item
-ivory
-jacket
-jaguar
-jar
-jazz
-jealous
-jeans
-jelly
-jewel
-job
-join
-joke
-journey
-joy
-judge
-juice
-jump
-jungle
-junior
-junk
-just
-kangaroo
-keen
-keep
-ketchup
-key
-kick
-kid
-kidney
-kind
-kingdom
-kiss
-kit
-kitchen
-kite
-kitten
-kiwi
-knee
-knife
-knock
-know
-lab
-label
-labor
-ladder
-lady
-lake
-lamp
-language
-laptop
-large
-later
-latin
-laugh
-laundry
-lava
-law
-lawn
-lawsuit
-layer
-lazy
-leader
-leaf
-learn
-leave
-lecture
-left
-leg
-legal
-legend
-leisure
-lemon
-lend
-length
-lens
-leopard
-lesson
-letter
-level
-liar
-liberty
-library
-license
-life
-lift
-light
-like
-limb
-limit
-link
-lion
-liquid
-list
-little
-live
-lizard
-load
-loan
-lobster
-local
-lock
-logic
-lonely
-long
-loop
-lottery
-loud
-lounge
-love
-loyal
-lucky
-luggage
-lumber
-lunar
-lunch
-luxury
-lyrics
-machine
-mad
-magic
-magnet
-maid
-mail
-main
-major
-make
-mammal
-man
-manage
-mandate
-mango
-mansion
-manual
-maple
-marble
-march
-margin
-marine
-market
-marriage
-mask
-mass
-master
-match
-material
-math
-matrix
-matter
-maximum
-maze
-meadow
-mean
-measure
-meat
-mechanic
-medal
-media
-melody
-melt
-member
-memory
-mention
-menu
-mercy
-merge
-merit
-merry
-mesh
-message
-metal
-method
-middle
-midnight
-milk
-million
-mimic
-mind
-minimum
-minor
-minute
-miracle
-mirror
-misery
-miss
-mistake
-mix
-mixed
-mixture
-mobile
-model
-modify
-mom
-moment
-monitor
-monkey
-monster
-month
-moon
-moral
-more
-morning
-mosquito
-mother
-motion
-motor
-mountain
-mouse
-move
-movie
-much
-muffin
-mule
-multiply
-muscle
-museum
-mushroom
-music
-must
-mutual
-myself
-mystery
-myth
-naive
-name
-napkin
-narrow
-nasty
-nation
-nature
-near
-neck
-need
-negative
-neglect
-neither
-nephew
-nerve
-nest
-net
-network
-neutral
-never
-news
-next
-nice
-night
-noble
-noise
-nominee
-noodle
-normal
-north
-nose
-notable
-note
-nothing
-notice
-novel
-now
-nuclear
-number
-nurse
-nut
-oak
-obey
-object
-oblige
-obscure
-observe
-obtain
-obvious
-occur
-ocean
-october
-odor
-off
-offer
-office
-often
-oil
-okay
-old
-olive
-olympic
-omit
-once
-one
-onion
-online
-only
-open
-opera
-opinion
-oppose
-option
-orange
-orbit
-orchard
-order
-ordinary
-organ
-orient
-original
-orphan
-ostrich
-other
-outdoor
-outer
-output
-outside
-oval
-oven
-over
-own
-owner
-oxygen
-oyster
-ozone
-pact
-paddle
-page
-pair
-palace
-palm
-panda
-panel
-panic
-panther
-paper
-parade
-parent
-park
-parrot
-party
-pass
-patch
-path
-patient
-patrol
-pattern
-pause
-pave
-payment
-peace
-peanut
-pear
-peasant
-pelican
-pen
-penalty
-pencil
-people
-pepper
-perfect
-permit
-person
-pet
-phone
-photo
-phrase
-physical
-piano
-picnic
-picture
-piece
-pig
-pigeon
-pill
-pilot
-pink
-pioneer
-pipe
-pistol
-pitch
-pizza
-place
-planet
-plastic
-plate
-play
-please
-pledge
-pluck
-plug
-plunge
-poem
-poet
-point
-polar
-pole
-police
-pond
-pony
-pool
-popular
-portion
-position
-possible
-post
-potato
-pottery
-poverty
-powder
-power
-practice
-praise
-predict
-prefer
-prepare
-present
-pretty
-prevent
-price
-pride
-primary
-print
-priority
-prison
-private
-prize
-problem
-process
-produce
-profit
-program
-project
-promote
-proof
-property
-prosper
-protect
-proud
-provide
-public
-pudding
-pull
-pulp
-pulse
-pumpkin
-punch
-pupil
-puppy
-purchase
-purity
-purpose
-purse
-push
-put
-puzzle
-pyramid
-quality
-quantum
-quarter
-question
-quick
-quit
-quiz
-quote
-rabbit
-raccoon
-race
-rack
-radar
-radio
-rail
-rain
-raise
-rally
-ramp
-ranch
-random
-range
-rapid
-rare
-rate
-rather
-raven
-raw
-razor
-ready
-real
-reason
-rebel
-rebuild
-recall
-receive
-recipe
-record
-recycle
-reduce
-reflect
-reform
-refuse
-region
-regret
-regular
-reject
-relax
-release
-relief
-rely
-remain
-remember
-remind
-remove
-render
-renew
-rent
-reopen
-repair
-repeat
-replace
-report
-require
-rescue
-resemble
-resist
-resource
-response
-result
-retire
-retreat
-return
-reunion
-reveal
-review
-reward
-rhythm
-rib
-ribbon
-rice
-rich
-ride
-ridge
-rifle
-right
-rigid
-ring
-riot
-ripple
-risk
-ritual
-rival
-river
-road
-roast
-robot
-robust
-rocket
-romance
-roof
-rookie
-room
-rose
-rotate
-rough
-round
-route
-royal
-rubber
-rude
-rug
-rule
-run
-runway
-rural
-sad
-saddle
-sadness
-safe
-sail
-salad
-salmon
-salon
-salt
-salute
-same
-sample
-sand
-satisfy
-satoshi
-sauce
-sausage
-save
-say
-scale
-scan
-scare
-scatter
-scene
-scheme
-school
-science
-scissors
-scorpion
-scout
-scrap
-screen
-script
-scrub
-sea
-search
-season
-seat
-second
-secret
-section
-security
-seed
-seek
-segment
-select
-sell
-seminar
-senior
-sense
-sentence
-series
-service
-session
-settle
-setup
-seven
-shadow
-shaft
-shallow
-share
-shed
-shell
-sheriff
-shield
-shift
-shine
-ship
-shiver
-shock
-shoe
-shoot
-shop
-short
-shoulder
-shove
-shrimp
-shrug
-shuffle
-shy
-sibling
-sick
-side
-siege
-sight
-sign
-silent
-silk
-silly
-silver
-similar
-simple
-since
-sing
-siren
-sister
-situate
-six
-size
-skate
-sketch
-ski
-skill
-skin
-skirt
-skull
-slab
-slam
-sleep
-slender
-slice
-slide
-slight
-slim
-slogan
-slot
-slow
-slush
-small
-smart
-smile
-smoke
-smooth
-snack
-snake
-snap
-sniff
-snow
-soap
-soccer
-social
-sock
-soda
-soft
-solar
-soldier
-solid
-solution
-solve
-someone
-song
-soon
-sorry
-sort
-soul
-sound
-soup
-source
-south
-space
-spare
-spatial
-spawn
-speak
-special
-speed
-spell
-spend
-sphere
-spice
-spider
-spike
-spin
-spirit
-split
-spoil
-sponsor
-spoon
-sport
-spot
-spray
-spread
-spring
-spy
-square
-squeeze
-squirrel
-stable
-stadium
-staff
-stage
-stairs
-stamp
-stand
-start
-state
-stay
-steak
-steel
-stem
-step
-stereo
-stick
-still
-sting
-stock
-stomach
-stone
-stool
-story
-stove
-strategy
-street
-strike
-strong
-struggle
-student
-stuff
-stumble
-style
-subject
-submit
-subway
-success
-such
-sudden
-suffer
-sugar
-suggest
-suit
-summer
-sun
-sunny
-sunset
-super
-supply
-supreme
-sure
-surface
-surge
-surprise
-surround
-survey
-suspect
-sustain
-swallow
-swamp
-swap
-swarm
-swear
-sweet
-swift
-swim
-swing
-switch
-sword
-symbol
-symptom
-syrup
-system
-table
-tackle
-tag
-tail
-talent
-talk
-tank
-tape
-target
-task
-taste
-tattoo
-taxi
-teach
-team
-tell
-ten
-tenant
-tennis
-tent
-term
-test
-text
-thank
-that
-theme
-then
-theory
-there
-they
-thing
-this
-thought
-three
-thrive
-throw
-thumb
-thunder
-ticket
-tide
-tiger
-tilt
-timber
-time
-tiny
-tip
-tired
-tissue
-title
-toast
-tobacco
-today
-toddler
-toe
-together
-toilet
-token
-tomato
-tomorrow
-tone
-tongue
-tonight
-tool
-tooth
-top
-topic
-topple
-torch
-tornado
-tortoise
-toss
-total
-tourist
-toward
-tower
-town
-toy
-track
-trade
-traffic
-tragic
-train
-transfer
-trap
-trash
-travel
-tray
-treat
-tree
-trend
-trial
-tribe
-trick
-trigger
-trim
-trip
-trophy
-trouble
-truck
-true
-truly
-trumpet
-trust
-truth
-try
-tube
-tuition
-tumble
-tuna
-tunnel
-turkey
-turn
-turtle
-twelve
-twenty
-twice
-twin
-twist
-two
-type
-typical
-ugly
-umbrella
-unable
-unaware
-uncle
-uncover
-under
-undo
-unfair
-unfold
-unhappy
-uniform
-unique
-unit
-universe
-unknown
-unlock
-until
-unusual
-unveil
-update
-upgrade
-uphold
-upon
-upper
-upset
-urban
-urge
-usage
-use
-used
-useful
-useless
-usual
-utility
-vacant
-vacuum
-vague
-valid
-valley
-valve
-van
-vanish
-vapor
-various
-vast
-vault
-vehicle
-velvet
-vendor
-venture
-venue
-verb
-verify
-version
-very
-vessel
-veteran
-viable
-vibrant
-vicious
-victory
-video
-view
-village
-vintage
-violin
-virtual
-virus
-visa
-visit
-visual
-vital
-vivid
-vocal
-voice
-void
-volcano
-volume
-vote
-voyage
-wage
-wagon
-wait
-walk
-wall
-walnut
-want
-warfare
-warm
-warrior
-wash
-wasp
-waste
-water
-wave
-way
-wealth
-weapon
-wear
-weasel
-weather
-web
-wedding
-weekend
-weird
-welcome
-west
-wet
-whale
-what
-wheat
-wheel
-when
-where
-whip
-whisper
-wide
-width
-wife
-wild
-will
-win
-window
-wine
-wing
-wink
-winner
-winter
-wire
-wisdom
-wise
-wish
-witness
-wolf
-woman
-wonder
-wood
-wool
-word
-work
-world
-worry
-worth
-wrap
-wreck
-wrestle
-wrist
-write
-wrong
-yard
-year
-yellow
-you
-young
-youth
-zebra
-zero
-zone
-zoo
diff --git a/src/main/resources/i18n/ApiError_en.properties b/src/main/resources/i18n/ApiError_en.properties
index 2a6ec002..6b083ae7 100644
--- a/src/main/resources/i18n/ApiError_en.properties
+++ b/src/main/resources/i18n/ApiError_en.properties
@@ -65,6 +65,8 @@ TRANSFORMATION_ERROR = could not transform JSON into transaction
UNAUTHORIZED = API call unauthorized
+ORDER_SIZE_TOO_SMALL = order size too small
+
FILE_NOT_FOUND = file not found
NO_REPLY = peer didn't reply within the allowed time
diff --git a/src/main/resources/i18n/ApiError_nl.properties b/src/main/resources/i18n/ApiError_nl.properties
new file mode 100644
index 00000000..60faa0f6
--- /dev/null
+++ b/src/main/resources/i18n/ApiError_nl.properties
@@ -0,0 +1,66 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# Keys are from api.ApiError enum
+
+ADDRESS_UNKNOWN = account adres onbekend
+
+BLOCKCHAIN_NEEDS_SYNC = blockchain dient eerst gesynchronizeerd te worden
+
+# Blocks
+BLOCK_UNKNOWN = blok onbekend
+
+BTC_BALANCE_ISSUE = onvoldoende Bitcoin balans
+
+BTC_NETWORK_ISSUE = Bitcoin/ElectrumX netwerk probleem
+
+BTC_TOO_SOON = te vroeg om Bitcoin transactie te versturen (vergrendelingstijd/gemiddelde bloktijd)
+
+CANNOT_MINT = account kan niet munten
+
+GROUP_UNKNOWN = onbekende groep
+
+INVALID_ADDRESS = ongeldig adres
+
+# Assets
+INVALID_ASSET_ID = ongeldige asset ID
+
+INVALID_CRITERIA = ongeldige zoekcriteria
+
+INVALID_DATA = ongeldige gegevens
+
+INVALID_HEIGHT = ongeldige blokhoogte
+
+INVALID_NETWORK_ADDRESS = ongeldig netwerkadres
+
+INVALID_ORDER_ID = ongeldige asset order ID
+
+INVALID_PRIVATE_KEY = ongeldige private key
+
+INVALID_PUBLIC_KEY = ongeldige public key
+
+INVALID_REFERENCE = ongeldige verwijzing
+
+# Validation
+INVALID_SIGNATURE = ongeldige handtekening
+
+JSON = lezen van JSON bericht gefaald
+
+NAME_UNKNOWN = onbekende naam
+
+NON_PRODUCTION = deze API call is niet toegestaan voor productiesystemen
+
+NO_TIME_SYNC = klok nog niet gesynchronizeerd
+
+ORDER_UNKNOWN = onbekende asset order ID
+
+PUBLIC_KEY_NOT_FOUND = public key niet gevonden
+
+REPOSITORY_ISSUE = repository fout
+
+# This one is special in that caller expected to pass two additional strings, hence the two %s
+TRANSACTION_INVALID = ongeldige transactie: %s (%s)
+
+TRANSACTION_UNKNOWN = onbekende transactie
+
+TRANSFORMATION_ERROR = JSON kon niet omgezet worden in transactie
+
+UNAUTHORIZED = ongeautoriseerde API call
diff --git a/src/main/resources/i18n/SysTray_nl.properties b/src/main/resources/i18n/SysTray_nl.properties
new file mode 100644
index 00000000..4e3e48ec
--- /dev/null
+++ b/src/main/resources/i18n/SysTray_nl.properties
@@ -0,0 +1,45 @@
+Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# SysTray pop-up menu
+
+APPLYING_UPDATE_AND_RESTARTING = Automatische update en herstart worden uitgevoerd...
+
+AUTO_UPDATE = Automatische Update
+
+BLOCK_HEIGHT = hoogte
+
+CHECK_TIME_ACCURACY = Controleer accuraatheid van de tijd
+
+CONNECTING = Verbinden
+
+CONNECTION = verbinding
+
+CONNECTIONS = verbindingen
+
+CREATING_BACKUP_OF_DB_FILES = Backup van databasebestanden wordt gemaakt...
+
+DB_BACKUP = Database Backup
+
+DB_CHECKPOINT = Database Controlepunt
+
+EXIT = Verlaten
+
+MINTING_DISABLED = NIET muntend
+
+MINTING_ENABLED = \u2714 Muntend
+
+# Nagging about lack of NTP time sync
+NTP_NAG_CAPTION = Klok van de computer is inaccuraat!
+
+NTP_NAG_TEXT_UNIX = Installeer NTP service voor een accurate klok.
+
+NTP_NAG_TEXT_WINDOWS = Selecteer "Synchronizeer klok" uit het menu om op te lossen.
+
+OPEN_UI = Open UI
+
+PERFORMING_DB_CHECKPOINT = Nieuwe veranderingen aan database worden opgeslagen...
+
+SYNCHRONIZE_CLOCK = Synchronizeer klok
+
+SYNCHRONIZING_BLOCKCHAIN = Aan het synchronizeren
+
+SYNCHRONIZING_CLOCK = Klok wordt gesynchronizeerd
diff --git a/src/main/resources/i18n/SysTray_zh.properties b/src/main/resources/i18n/SysTray_zh_SC.properties
similarity index 53%
rename from src/main/resources/i18n/SysTray_zh.properties
rename to src/main/resources/i18n/SysTray_zh_SC.properties
index 0aaa2e33..caba49cf 100644
--- a/src/main/resources/i18n/SysTray_zh.properties
+++ b/src/main/resources/i18n/SysTray_zh_SC.properties
@@ -1,31 +1,31 @@
#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
# SysTray pop-up menu
-BLOCK_HEIGHT = 块高度
+BLOCK_HEIGHT = 区块高度
CHECK_TIME_ACCURACY = 检查时间准确性
-CONNECTION = 个连接
+CONNECTION = 个链接
-CONNECTIONS = 个连接
+CONNECTIONS = 个链接
-EXIT = 退出软件
+EXIT = 退出核心
MINTING_DISABLED = 没有铸币
MINTING_ENABLED = ✔ 铸币
# Nagging about lack of NTP time sync
-NTP_NAG_CAPTION = 电脑的时钟不准确!
+NTP_NAG_CAPTION = 电脑的时间不准确!
-NTP_NAG_TEXT_UNIX = 安装NTP服务以获得准确的时钟。
+NTP_NAG_TEXT_UNIX = 安装NTP服务以获取准确的时间。
NTP_NAG_TEXT_WINDOWS = 从菜单中选择“同步时钟”进行修复。
-OPEN_UI = 开启界面
+OPEN_UI = 开启Qortal界面
SYNCHRONIZE_CLOCK = 同步时钟
-SYNCHRONIZING_BLOCKCHAIN = 同步区块链
+SYNCHRONIZING_BLOCKCHAIN = 正在同步区块链
-SYNCHRONIZING_CLOCK = 同步着时钟
+SYNCHRONIZING_CLOCK = 正在同步时钟
diff --git a/src/main/resources/i18n/SysTray_zh_TC.properties b/src/main/resources/i18n/SysTray_zh_TC.properties
new file mode 100644
index 00000000..ac768846
--- /dev/null
+++ b/src/main/resources/i18n/SysTray_zh_TC.properties
@@ -0,0 +1,31 @@
+#Generated by ResourceBundle Editor (http://essiembre.github.io/eclipse-rbe/)
+# SysTray pop-up menu
+
+BLOCK_HEIGHT = 區塊高度
+
+CHECK_TIME_ACCURACY = 檢查時間準確性
+
+CONNECTION = 個鏈接
+
+CONNECTIONS = 個鏈接
+
+EXIT = 退出核心
+
+MINTING_DISABLED = 沒有鑄幣
+
+MINTING_ENABLED = ✔ 鑄幣
+
+# Nagging about lack of NTP time sync
+NTP_NAG_CAPTION = 電腦的時間不準確!
+
+NTP_NAG_TEXT_UNIX = 安装NTP服務以獲取準確的時間。
+
+NTP_NAG_TEXT_WINDOWS = 從菜單中選擇“同步時鐘”進行修復。
+
+OPEN_UI = 開啓Qortal界面
+
+SYNCHRONIZE_CLOCK = 同步時鐘
+
+SYNCHRONIZING_BLOCKCHAIN = 正在同步區塊鏈
+
+SYNCHRONIZING_CLOCK = 正在同步時鐘
diff --git a/src/main/resources/i18n/TransactionValidity_nl.properties b/src/main/resources/i18n/TransactionValidity_nl.properties
new file mode 100644
index 00000000..7afaad89
--- /dev/null
+++ b/src/main/resources/i18n/TransactionValidity_nl.properties
@@ -0,0 +1,184 @@
+
+ACCOUNT_ALREADY_EXISTS = account bestaat al
+
+ACCOUNT_CANNOT_REWARD_SHARE = account kan geen beloningen delen
+
+ALREADY_GROUP_ADMIN = reeds groepsadministrator
+
+ALREADY_GROUP_MEMBER = reeds groepslid
+
+ALREADY_VOTED_FOR_THAT_OPTION = reeds gestemd voor die optie
+
+ASSET_ALREADY_EXISTS = asset bestaat al
+
+ASSET_DOES_NOT_EXIST = asset bestaat niet
+
+ASSET_DOES_NOT_MATCH_AT = asset matcht niet met de asset van de AT
+
+ASSET_NOT_SPENDABLE = asset is niet uitgeefbaar
+
+AT_ALREADY_EXISTS = AT bestaat al
+
+AT_IS_FINISHED = AT is afgelopen
+
+AT_UNKNOWN = AT onbekend
+
+BANNED_FROM_GROUP = verbannen uit groep
+
+BAN_EXISTS = ban bestaat al
+
+BAN_UNKNOWN = ban onbekend
+
+BUYER_ALREADY_OWNER = koper is al eigenaar
+
+CHAT = CHAT transacties zijn nooit geldig voor opname in blokken
+
+CLOCK_NOT_SYNCED = klok is niet gesynchronizeerd
+
+DUPLICATE_OPTION = dubbele optie
+
+GROUP_ALREADY_EXISTS = groep bestaat reeds
+
+GROUP_APPROVAL_DECIDED = groepsgoedkeuring reeds afgewezen
+
+GROUP_APPROVAL_NOT_REQUIRED = groepsgoedkeuring niet vereist
+
+GROUP_DOES_NOT_EXIST = groep bestaat niet
+
+GROUP_ID_MISMATCH = ongeldige match met groep-ID
+
+GROUP_OWNER_CANNOT_LEAVE = groepseigenaar kan de groep niet verlaten
+
+HAVE_EQUALS_WANT = have-asset is gelijk aan want-asset
+
+INCORRECT_NONCE = incorrecte PoW nonce
+
+INSUFFICIENT_FEE = vergoeding te laag
+
+INVALID_ADDRESS = ongeldig adres
+
+INVALID_AMOUNT = ongeldige hoeveelheid
+
+INVALID_ASSET_OWNER = ongeldige asset-eigenaar
+
+INVALID_AT_TRANSACTION = ongeldige AT-transactie
+
+INVALID_AT_TYPE_LENGTH = ongeldige lengte voor AT 'type'
+
+INVALID_CREATION_BYTES = ongeldige creation bytes
+
+INVALID_DATA_LENGTH = ongeldige lengte voor data
+
+INVALID_DESCRIPTION_LENGTH = ongeldige lengte voor beschrijving
+
+INVALID_GROUP_APPROVAL_THRESHOLD = ongeldige drempelwaarde voor groepsgoedkeuring
+
+INVALID_GROUP_BLOCK_DELAY = ongeldige groepsgoedkeuring voor blokvertraging
+
+INVALID_GROUP_ID = ongeldige groep-ID
+
+INVALID_GROUP_OWNER = ongeldige groepseigenaar
+
+INVALID_LIFETIME = ongeldige levensduur
+
+INVALID_NAME_LENGTH = ongeldige lengte voor naam
+
+INVALID_NAME_OWNER = ongeldige naam voor eigenaar
+
+INVALID_OPTIONS_COUNT = ongeldige hoeveelheid opties
+
+INVALID_OPTION_LENGTH = ongeldige lengte voor opties
+
+INVALID_ORDER_CREATOR = ongeldige aanmaker voor order
+
+INVALID_PAYMENTS_COUNT = ongeldige hoeveelheid betalingen
+
+INVALID_PUBLIC_KEY = ongeldige public key
+
+INVALID_QUANTITY = ongeldige hoeveelheid
+
+INVALID_REFERENCE = ongeldige verwijzing
+
+INVALID_RETURN = ongeldige return
+
+INVALID_REWARD_SHARE_PERCENT = ongeldig percentage voor beloningsdeling
+
+INVALID_SELLER = ongeldige verkoper
+
+INVALID_TAGS_LENGTH = ongeldige lengte voor 'tags'
+
+INVALID_TX_GROUP_ID = ongeldige transactiegroep-ID
+
+INVALID_VALUE_LENGTH = ongeldige lengte voor 'waarde'
+
+INVITE_UNKNOWN = onbekende groepsuitnodiging
+
+JOIN_REQUEST_EXISTS = aanvraag om lid van groep te worden bestaat al
+
+MAXIMUM_REWARD_SHARES = limiet aan beloningsdelingen voor dit account is bereikt
+
+MISSING_CREATOR = ontbrekende aanmaker
+
+MULTIPLE_NAMES_FORBIDDEN = het registreren van meerdere namen op een account is niet toegestaan
+
+NAME_ALREADY_FOR_SALE = naam reeds te koop
+
+NAME_ALREADY_REGISTERED = naam reeds geregistreerd
+
+NAME_DOES_NOT_EXIST = naam bestaat niet
+
+NAME_NOT_FOR_SALE = naam is niet te koop
+
+NAME_NOT_NORMALIZED = naam is niet in 'genormalizeerde' Unicode-vorm
+
+NEGATIVE_AMOUNT = ongeldige/negatieve hoeveelheid
+
+NEGATIVE_FEE = ongeldige/negatieve vergoeding
+
+NEGATIVE_PRICE = ongeldige/negatieve prijs
+
+NOT_GROUP_ADMIN = account is geen groepsadministrator
+
+NOT_GROUP_MEMBER = account is geen groepslid
+
+NOT_MINTING_ACCOUNT = account kan niet munten
+
+NOT_YET_RELEASED = functie nog niet uitgebracht
+
+NO_BALANCE = onvoldoende balans
+
+NO_BLOCKCHAIN_LOCK = blockchain van node is momenteel bezig
+
+NO_FLAG_PERMISSION = account heeft hier geen toestemming voor
+
+OK = Oke
+
+ORDER_ALREADY_CLOSED = asset handelsorder is al gesloten
+
+ORDER_DOES_NOT_EXIST = asset handelsorder bestaat niet
+
+POLL_ALREADY_EXISTS = peiling bestaat al
+
+POLL_DOES_NOT_EXIST = peiling bestaat niet
+
+POLL_OPTION_DOES_NOT_EXIST = peilingsoptie bestaat niet
+
+PUBLIC_KEY_UNKNOWN = public key onbekend
+
+REWARD_SHARE_UNKNOWN = beloningsdeling onbekend
+
+SELF_SHARE_EXISTS = zelfdeling (beloningsdeling) bestaat reeds
+
+TIMESTAMP_TOO_NEW = tijdstempel te nieuw
+
+TIMESTAMP_TOO_OLD = tijdstempel te oud
+
+TOO_MANY_UNCONFIRMED = account heeft te veel onbevestigde transacties in afwachting
+
+TRANSACTION_ALREADY_CONFIRMED = transactie is reeds bevestigd
+
+TRANSACTION_ALREADY_EXISTS = transactie bestaat al
+
+TRANSACTION_UNKNOWN = transactie onbekend
+
+TX_GROUP_ID_MISMATCH = groep-ID van transactie matcht niet
diff --git a/src/main/resources/images/Qlogo_512.png b/src/main/resources/images/Qlogo_512.png
new file mode 100644
index 00000000..81508bb7
Binary files /dev/null and b/src/main/resources/images/Qlogo_512.png differ
diff --git a/src/main/resources/images/icons/Qlogo_128.png b/src/main/resources/images/icons/Qlogo_128.png
new file mode 100644
index 00000000..463bb527
Binary files /dev/null and b/src/main/resources/images/icons/Qlogo_128.png differ
diff --git a/src/main/resources/images/icons/icon128.png b/src/main/resources/images/icons/icon128.png
deleted file mode 100644
index ddb869bd..00000000
Binary files a/src/main/resources/images/icons/icon128.png and /dev/null differ
diff --git a/src/main/resources/images/icons/icon32.png b/src/main/resources/images/icons/icon32.png
deleted file mode 100644
index 43a37510..00000000
Binary files a/src/main/resources/images/icons/icon32.png and /dev/null differ
diff --git a/src/main/resources/images/icons/qortal_ui_tray_minting.png b/src/main/resources/images/icons/qortal_ui_tray_minting.png
new file mode 100644
index 00000000..567e784b
Binary files /dev/null and b/src/main/resources/images/icons/qortal_ui_tray_minting.png differ
diff --git a/src/main/resources/images/icons/qortal_ui_tray_synced.png b/src/main/resources/images/icons/qortal_ui_tray_synced.png
new file mode 100644
index 00000000..f944bad9
Binary files /dev/null and b/src/main/resources/images/icons/qortal_ui_tray_synced.png differ
diff --git a/src/main/resources/images/icons/qortal_ui_tray_syncing.png b/src/main/resources/images/icons/qortal_ui_tray_syncing.png
new file mode 100644
index 00000000..82d39bbb
Binary files /dev/null and b/src/main/resources/images/icons/qortal_ui_tray_syncing.png differ
diff --git a/src/main/resources/images/icons/qortal_ui_tray_syncing_time-alt.png b/src/main/resources/images/icons/qortal_ui_tray_syncing_time-alt.png
new file mode 100644
index 00000000..608be51e
Binary files /dev/null and b/src/main/resources/images/icons/qortal_ui_tray_syncing_time-alt.png differ
diff --git a/src/main/resources/images/splash.png b/src/main/resources/images/splash.png
old mode 100755
new mode 100644
diff --git a/src/test/java/org/qortal/test/RepositoryTests.java b/src/test/java/org/qortal/test/RepositoryTests.java
index 434e03f0..91dd03c2 100644
--- a/src/test/java/org/qortal/test/RepositoryTests.java
+++ b/src/test/java/org/qortal/test/RepositoryTests.java
@@ -261,11 +261,11 @@ public class RepositoryTests extends Common {
/** Check that the sub-query used to fetch highest block height is optimized by HSQLDB. */
@Test
public void testBlockHeightSpeed() throws DataException, SQLException {
- final int mintBlockCount = 30000;
+ final int mintBlockCount = 10000;
try (final Repository repository = RepositoryManager.getRepository()) {
// Mint some blocks
- System.out.println(String.format("Minting %d test blocks - should take approx. 30 seconds...", mintBlockCount));
+ System.out.println(String.format("Minting %d test blocks - should take approx. 10 seconds...", mintBlockCount));
long beforeBigMint = System.currentTimeMillis();
for (int i = 0; i < mintBlockCount; ++i)
diff --git a/src/test/java/org/qortal/test/apps/VanityGen.java b/src/test/java/org/qortal/test/apps/VanityGen.java
index f697087f..2c22ea0b 100644
--- a/src/test/java/org/qortal/test/apps/VanityGen.java
+++ b/src/test/java/org/qortal/test/apps/VanityGen.java
@@ -10,7 +10,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider;
import org.qortal.account.PrivateKeyAccount;
import org.qortal.crypto.Crypto;
-import org.qortal.utils.BIP39;
import org.qortal.utils.Base58;
import com.google.common.primitives.Bytes;
@@ -44,15 +43,13 @@ public class VanityGen {
byte checksum = (byte) (hash[0] & 0xf0);
byte[] entropy132 = Bytes.concat(entropy, new byte[] { checksum });
- String mnemonic = BIP39.encode(entropy132, "en");
-
PrivateKeyAccount account = new PrivateKeyAccount(null, hash);
if (!account.getAddress().startsWith(prefix))
continue;
- System.out.println(String.format("Address: %s, public key: %s, private key: %s, mnemonic: %s",
- account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash), mnemonic));
+ System.out.println(String.format("Address: %s, public key: %s, private key: %s",
+ account.getAddress(), Base58.encode(account.getPublicKey()), Base58.encode(hash)));
System.out.flush();
}
}
diff --git a/src/test/java/org/qortal/test/crosschain/DogecoinTests.java b/src/test/java/org/qortal/test/crosschain/DogecoinTests.java
new file mode 100644
index 00000000..b6d21315
--- /dev/null
+++ b/src/test/java/org/qortal/test/crosschain/DogecoinTests.java
@@ -0,0 +1,114 @@
+package org.qortal.test.crosschain;
+
+import org.bitcoinj.core.Transaction;
+import org.bitcoinj.store.BlockStoreException;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Test;
+import org.qortal.crosschain.BitcoinyHTLC;
+import org.qortal.crosschain.ForeignBlockchainException;
+import org.qortal.crosschain.Dogecoin;
+import org.qortal.repository.DataException;
+import org.qortal.test.common.Common;
+
+import java.util.Arrays;
+
+import static org.junit.Assert.*;
+
+public class DogecoinTests extends Common {
+
+ private Dogecoin dogecoin;
+
+ @Before
+ public void beforeTest() throws DataException {
+ Common.useDefaultSettings(); // TestNet3
+ dogecoin = Dogecoin.getInstance();
+ }
+
+ @After
+ public void afterTest() {
+ Dogecoin.resetForTesting();
+ dogecoin = null;
+ }
+
+ @Test
+ public void testGetMedianBlockTime() throws BlockStoreException, ForeignBlockchainException {
+ long before = System.currentTimeMillis();
+ System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
+ long afterFirst = System.currentTimeMillis();
+
+ System.out.println(String.format("Bitcoin median blocktime: %d", dogecoin.getMedianBlockTime()));
+ long afterSecond = System.currentTimeMillis();
+
+ long firstPeriod = afterFirst - before;
+ long secondPeriod = afterSecond - afterFirst;
+
+ System.out.println(String.format("1st call: %d ms, 2nd call: %d ms", firstPeriod, secondPeriod));
+
+ assertTrue("2nd call should be quicker than 1st", secondPeriod < firstPeriod);
+ assertTrue("2nd call should take less than 5 seconds", secondPeriod < 5000L);
+ }
+
+ @Test
+ @Ignore(value = "Doesn't work, to be fixed later")
+ public void testFindHtlcSecret() throws ForeignBlockchainException {
+ // This actually exists on TEST3 but can take a while to fetch
+ String p2shAddress = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+
+ byte[] expectedSecret = "This string is exactly 32 bytes!".getBytes();
+ byte[] secret = BitcoinyHTLC.findHtlcSecret(dogecoin, p2shAddress);
+
+ assertNotNull("secret not found", secret);
+ assertTrue("secret incorrect", Arrays.equals(expectedSecret, secret));
+ }
+
+ @Test
+ public void testBuildSpend() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String recipient = "2N8WCg52ULCtDSMjkgVTm5mtPdCsUptkHWE";
+ long amount = 1000L;
+
+ Transaction transaction = dogecoin.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+
+ // Check spent key caching doesn't affect outcome
+
+ transaction = dogecoin.buildSpend(xprv58, recipient, amount);
+ assertNotNull("insufficient funds", transaction);
+ }
+
+ @Test
+ public void testGetWalletBalance() {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ Long balance = dogecoin.getWalletBalance(xprv58);
+
+ assertNotNull(balance);
+
+ System.out.println(dogecoin.format(balance));
+
+ // Check spent key caching doesn't affect outcome
+
+ Long repeatBalance = dogecoin.getWalletBalance(xprv58);
+
+ assertNotNull(repeatBalance);
+
+ System.out.println(dogecoin.format(repeatBalance));
+
+ assertEquals(balance, repeatBalance);
+ }
+
+ @Test
+ public void testGetUnusedReceiveAddress() throws ForeignBlockchainException {
+ String xprv58 = "tprv8ZgxMBicQKsPdahhFSrCdvC1bsWyzHHZfTneTVqUXN6s1wEtZLwAkZXzFP6TYLg2aQMecZLXLre5bTVGajEB55L1HYJcawpdFG66STVAWPJ";
+
+ String address = dogecoin.getUnusedReceiveAddress(xprv58);
+
+ assertNotNull(address);
+
+ System.out.println(address);
+ }
+
+}
diff --git a/tools/build-release.sh b/tools/build-release.sh
index f78ec1b0..28b289f7 100755
--- a/tools/build-release.sh
+++ b/tools/build-release.sh
@@ -67,7 +67,7 @@ git_url=https://github.com/${git_url##*:}
git_url=${git_url%%.git}
# Check for EXE
-exe=${project}-${git_tag#v}.exe
+exe=${project}.exe
exe_src="${WINDOWS_INSTALLER_DIR}/${exe}"
if [ ! -r "${exe_src}" ]; then
echo "Cannot find EXE installer at ${exe_src}"
@@ -75,7 +75,7 @@ if [ ! -r "${exe_src}" ]; then
fi
# Check for ZIP
-zip_filename=${project}-${git_tag#v}.zip
+zip_filename=${project}.zip
zip_src=${saved_pwd}/${zip_filename}
if [ ! -r "${zip_src}" ]; then
echo "Cannot find ZIP at ${zip_src}"
diff --git a/tools/build-zip.sh b/tools/build-zip.sh
index 47fdd373..b52b5da7 100755
--- a/tools/build-zip.sh
+++ b/tools/build-zip.sh
@@ -63,4 +63,4 @@ printf "{\n}\n" > ${build_dir}/settings.json
gtouch -d ${commit_ts%%+??:??} ${build_dir} ${build_dir}/*
rm -f ${saved_pwd}/${project}.zip
-(cd ${build_dir}/..; 7z a -r -tzip ${saved_pwd}/${project}-${git_tag#v}.zip ${project}/)
+(cd ${build_dir}/..; 7z a -r -tzip ${saved_pwd}/${project}.zip ${project}/)