diff --git a/src/main/java/org/qortal/api/resource/AdminResource.java b/src/main/java/org/qortal/api/resource/AdminResource.java index 8a9d8025..9cff1bbb 100644 --- a/src/main/java/org/qortal/api/resource/AdminResource.java +++ b/src/main/java/org/qortal/api/resource/AdminResource.java @@ -728,6 +728,49 @@ public class AdminResource { } } + @POST + @Path("/repository/importarchivedtrades") + @Operation( + summary = "Imports archived trades from TradeBotStatesArchive.json", + description = "This can be used to recover trades that exist in the archive only, which may be needed if a
" + + "problem occurred during the proof-of-work computation stage of a buy request.", + responses = { + @ApiResponse( + content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) + ) + } + ) + @ApiErrors({ApiError.REPOSITORY_ISSUE}) + @SecurityRequirement(name = "apiKey") + public boolean importArchivedTrades(@HeaderParam(Security.API_KEY_HEADER) String apiKey) { + Security.checkApiCallAllowed(request); + + try (final Repository repository = RepositoryManager.getRepository()) { + ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); + + blockchainLock.lockInterruptibly(); + + try { + repository.importDataFromFile("qortal-backup/TradeBotStatesArchive.json"); + repository.saveChanges(); + + return true; + + } catch (IOException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e); + + } finally { + blockchainLock.unlock(); + } + } catch (InterruptedException e) { + // We couldn't lock blockchain to perform import + return false; + } catch (DataException e) { + throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); + } + } + + @POST @Path("/apikey/generate") diff --git a/src/main/java/org/qortal/api/resource/ArbitraryResource.java b/src/main/java/org/qortal/api/resource/ArbitraryResource.java index dad941e6..978183c0 100644 --- a/src/main/java/org/qortal/api/resource/ArbitraryResource.java +++ b/src/main/java/org/qortal/api/resource/ArbitraryResource.java @@ -15,6 +15,7 @@ import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Arrays; import java.util.List; import javax.servlet.ServletContext; @@ -44,6 +45,7 @@ import org.qortal.data.arbitrary.*; import org.qortal.data.naming.NameData; import org.qortal.data.transaction.ArbitraryTransactionData; import org.qortal.data.transaction.TransactionData; +import org.qortal.list.ResourceListManager; import org.qortal.repository.DataException; import org.qortal.repository.Repository; import org.qortal.repository.RepositoryManager; @@ -91,6 +93,7 @@ public class ArbitraryResource { @Parameter(ref = "limit") @QueryParam("limit") Integer limit, @Parameter(ref = "offset") @QueryParam("offset") Integer offset, @Parameter(ref = "reverse") @QueryParam("reverse") Boolean reverse, + @Parameter(description = "Filter names by list") @QueryParam("namefilter") String nameFilter, @Parameter(description = "Include status") @QueryParam("includestatus") Boolean includeStatus, @Parameter(description = "Include metadata") @QueryParam("includemetadata") Boolean includeMetadata) { @@ -107,8 +110,18 @@ public class ArbitraryResource { throw ApiExceptionFactory.INSTANCE.createCustomException(request, ApiError.INVALID_CRITERIA, "identifier cannot be specified when requesting a default resource"); } + // Load filter from list if needed + List names = null; + if (nameFilter != null) { + names = ResourceListManager.getInstance().getStringsInList(nameFilter); + if (names.isEmpty()) { + // List doesn't exist or is empty - so there will be no matches + return new ArrayList<>(); + } + } + List resources = repository.getArbitraryRepository() - .getArbitraryResources(service, identifier, null, defaultRes, limit, offset, reverse); + .getArbitraryResources(service, identifier, names, defaultRes, limit, offset, reverse); if (resources == null) { return new ArrayList<>(); @@ -216,7 +229,7 @@ public class ArbitraryResource { String name = creatorName.name; if (name != null) { List resources = repository.getArbitraryRepository() - .getArbitraryResources(service, identifier, name, defaultRes, null, null, reverse); + .getArbitraryResources(service, identifier, Arrays.asList(name), defaultRes, null, null, reverse); if (includeStatus != null && includeStatus) { resources = this.addStatusToResources(resources); diff --git a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java index 5c77f212..cf098f53 100644 --- a/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java +++ b/src/main/java/org/qortal/api/resource/CrossChainHtlcResource.java @@ -665,48 +665,6 @@ public class CrossChainHtlcResource { return false; } - @POST - @Path("/importarchivedtrades") - @Operation( - summary = "Imports archived trades from TradeBotStatesArchive.json", - description = "This can be used to recover trades that exist in the archive only, which may be needed if a
" + - "problem occurred during the proof-of-work computation stage of a buy request.", - responses = { - @ApiResponse( - content = @Content(mediaType = MediaType.TEXT_PLAIN, schema = @Schema(type = "boolean")) - ) - } - ) - @ApiErrors({ApiError.REPOSITORY_ISSUE}) - @SecurityRequirement(name = "apiKey") - public boolean importArchivedTrades(@HeaderParam(Security.API_KEY_HEADER) String apiKey) { - Security.checkApiCallAllowed(request); - - try (final Repository repository = RepositoryManager.getRepository()) { - ReentrantLock blockchainLock = Controller.getInstance().getBlockchainLock(); - - blockchainLock.lockInterruptibly(); - - try { - repository.importDataFromFile("qortal-backup/TradeBotStatesArchive.json"); - repository.saveChanges(); - - return true; - - } catch (IOException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.INVALID_CRITERIA, e); - - } finally { - blockchainLock.unlock(); - } - } catch (InterruptedException e) { - // We couldn't lock blockchain to perform import - return false; - } catch (DataException e) { - throw ApiExceptionFactory.INSTANCE.createException(request, ApiError.REPOSITORY_ISSUE, e); - } - } - private long calcFeeTimestamp(int lockTimeA, int tradeTimeout) { return (lockTimeA - tradeTimeout * 60) * 1000L; } diff --git a/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java index 55969c6b..8d7a13cd 100644 --- a/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/TradeBotWebSocket.java @@ -2,10 +2,7 @@ package org.qortal.api.websocket; import java.io.IOException; import java.io.StringWriter; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.stream.Collectors; import org.eclipse.jetty.websocket.api.Session; @@ -85,6 +82,7 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener { @Override public void onWebSocketConnect(Session session) { Map> queryParams = session.getUpgradeRequest().getParameterMap(); + final boolean excludeInitialData = queryParams.get("excludeInitialData") != null; List foreignBlockchains = queryParams.get("foreignBlockchain"); final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0); @@ -98,15 +96,22 @@ public class TradeBotWebSocket extends ApiWebSocket implements Listener { // save session's preferred blockchain (if any) sessionBlockchain.put(session, foreignBlockchain); - // Send all known trade-bot entries - try (final Repository repository = RepositoryManager.getRepository()) { - List tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData(); - // Optional filtering - if (foreignBlockchain != null) - tradeBotEntries = tradeBotEntries.stream() - .filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain)) - .collect(Collectors.toList()); + + // Maybe send all known trade-bot entries + try (final Repository repository = RepositoryManager.getRepository()) { + List tradeBotEntries = new ArrayList<>(); + + // We might need to exclude the initial data from the response + if (!excludeInitialData) { + tradeBotEntries = repository.getCrossChainRepository().getAllTradeBotData(); + + // Optional filtering + if (foreignBlockchain != null) + tradeBotEntries = tradeBotEntries.stream() + .filter(tradeBotData -> tradeBotData.getForeignBlockchain().equals(foreignBlockchain)) + .collect(Collectors.toList()); + } if (!sendEntries(session, tradeBotEntries)) { session.close(4002, "websocket issue"); diff --git a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java index 35fc4691..78c53dc3 100644 --- a/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/TradeOffersWebSocket.java @@ -173,6 +173,7 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { public void onWebSocketConnect(Session session) { Map> queryParams = session.getUpgradeRequest().getParameterMap(); final boolean includeHistoric = queryParams.get("includeHistoric") != null; + final boolean excludeInitialData = queryParams.get("excludeInitialData") != null; List foreignBlockchains = queryParams.get("foreignBlockchain"); final String foreignBlockchain = foreignBlockchains == null ? null : foreignBlockchains.get(0); @@ -189,20 +190,23 @@ public class TradeOffersWebSocket extends ApiWebSocket implements Listener { List crossChainOfferSummaries = new ArrayList<>(); - synchronized (cachedInfoByBlockchain) { - Collection cachedInfos; + // We might need to exclude the initial data from the response + if (!excludeInitialData) { + synchronized (cachedInfoByBlockchain) { + Collection cachedInfos; - if (foreignBlockchain == null) - // No preferred blockchain, so iterate through all of them - cachedInfos = cachedInfoByBlockchain.values(); - else - cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo())); + if (foreignBlockchain == null) + // No preferred blockchain, so iterate through all of them + cachedInfos = cachedInfoByBlockchain.values(); + else + cachedInfos = Collections.singleton(cachedInfoByBlockchain.computeIfAbsent(foreignBlockchain, k -> new CachedOfferInfo())); - for (CachedOfferInfo cachedInfo : cachedInfos) { - crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values()); + for (CachedOfferInfo cachedInfo : cachedInfos) { + crossChainOfferSummaries.addAll(cachedInfo.currentSummaries.values()); - if (includeHistoric) - crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values()); + if (includeHistoric) + crossChainOfferSummaries.addAll(cachedInfo.historicSummaries.values()); + } } } diff --git a/src/main/java/org/qortal/api/websocket/TradePresenceWebSocket.java b/src/main/java/org/qortal/api/websocket/TradePresenceWebSocket.java index e9558599..ba9a8085 100644 --- a/src/main/java/org/qortal/api/websocket/TradePresenceWebSocket.java +++ b/src/main/java/org/qortal/api/websocket/TradePresenceWebSocket.java @@ -65,11 +65,15 @@ public class TradePresenceWebSocket extends ApiWebSocket implements Listener { @Override public void onWebSocketConnect(Session session) { Map> queryParams = session.getUpgradeRequest().getParameterMap(); + final boolean excludeInitialData = queryParams.get("excludeInitialData") != null; - List tradePresences; + List tradePresences = new ArrayList<>(); - synchronized (currentEntries) { - tradePresences = List.copyOf(currentEntries.values()); + // We might need to exclude the initial data from the response + if (!excludeInitialData) { + synchronized (currentEntries) { + tradePresences = List.copyOf(currentEntries.values()); + } } if (!sendTradePresences(session, tradePresences)) { diff --git a/src/main/java/org/qortal/block/Block.java b/src/main/java/org/qortal/block/Block.java index 0d2deb45..5c482c39 100644 --- a/src/main/java/org/qortal/block/Block.java +++ b/src/main/java/org/qortal/block/Block.java @@ -185,8 +185,11 @@ public class Block { if (accountLevel <= 0) return null; // level 0 isn't included in any share bins + // Select the correct set of share bins based on block height final BlockChain blockChain = BlockChain.getInstance(); - final AccountLevelShareBin[] shareBinsByLevel = blockChain.getShareBinsByAccountLevel(); + final AccountLevelShareBin[] shareBinsByLevel = (blockHeight >= blockChain.getSharesByLevelV2Height()) ? + blockChain.getShareBinsByAccountLevelV2() : blockChain.getShareBinsByAccountLevelV1(); + if (accountLevel > shareBinsByLevel.length) return null; @@ -1919,10 +1922,14 @@ public class Block { final List onlineFounderAccounts = expandedAccounts.stream().filter(expandedAccount -> expandedAccount.isMinterFounder).collect(Collectors.toList()); final boolean haveFounders = !onlineFounderAccounts.isEmpty(); + // Select the correct set of share bins based on block height + List accountLevelShareBinsForBlock = (this.blockData.getHeight() >= BlockChain.getInstance().getSharesByLevelV2Height()) ? + BlockChain.getInstance().getAccountLevelShareBinsV2() : BlockChain.getInstance().getAccountLevelShareBinsV1(); + // Determine reward candidates based on account level // This needs a deep copy, so the shares can be modified when tiers aren't activated yet List accountLevelShareBins = new ArrayList<>(); - for (AccountLevelShareBin accountLevelShareBin : BlockChain.getInstance().getAccountLevelShareBins()) { + for (AccountLevelShareBin accountLevelShareBin : accountLevelShareBinsForBlock) { accountLevelShareBins.add((AccountLevelShareBin) accountLevelShareBin.clone()); } @@ -1998,7 +2005,7 @@ public class Block { // Fetch list of legacy QORA holders who haven't reached their cap of QORT reward. List qoraHolders = this.repository.getAccountRepository().getEligibleLegacyQoraHolders(isProcessingNotOrphaning ? null : this.blockData.getHeight()); final boolean haveQoraHolders = !qoraHolders.isEmpty(); - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(this.blockData.getHeight()); // Perform account-level-based reward scaling if appropriate if (!haveFounders) { diff --git a/src/main/java/org/qortal/block/BlockChain.java b/src/main/java/org/qortal/block/BlockChain.java index 59b71e3c..42692a18 100644 --- a/src/main/java/org/qortal/block/BlockChain.java +++ b/src/main/java/org/qortal/block/BlockChain.java @@ -68,6 +68,7 @@ public class BlockChain { atFindNextTransactionFix, newBlockSigHeight, shareBinFix, + sharesByLevelV2Height, rewardShareLimitTimestamp, calcChainWeightTimestamp, transactionV5Timestamp, @@ -121,13 +122,19 @@ public class BlockChain { return shareBinCopy; } } - private List sharesByLevel; + private List sharesByLevelV1; + private List sharesByLevelV2; /** Generated lookup of share-bin by account level */ - private AccountLevelShareBin[] shareBinsByLevel; + private AccountLevelShareBin[] shareBinsByLevelV1; + private AccountLevelShareBin[] shareBinsByLevelV2; - /** Share of block reward/fees to legacy QORA coin holders */ - @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) - private Long qoraHoldersShare; + /** Share of block reward/fees to legacy QORA coin holders, by block height */ + public static class ShareByHeight { + public int height; + @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) + public long share; + } + private List qoraHoldersShareByHeight; /** How many legacy QORA per 1 QORT of block reward. */ @XmlJavaTypeAdapter(value = org.qortal.api.AmountTypeAdapter.class) @@ -373,12 +380,20 @@ public class BlockChain { return this.rewardsByHeight; } - public List getAccountLevelShareBins() { - return this.sharesByLevel; + public List getAccountLevelShareBinsV1() { + return this.sharesByLevelV1; } - public AccountLevelShareBin[] getShareBinsByAccountLevel() { - return this.shareBinsByLevel; + public List getAccountLevelShareBinsV2() { + return this.sharesByLevelV2; + } + + public AccountLevelShareBin[] getShareBinsByAccountLevelV1() { + return this.shareBinsByLevelV1; + } + + public AccountLevelShareBin[] getShareBinsByAccountLevelV2() { + return this.shareBinsByLevelV2; } public List getBlocksNeededByLevel() { @@ -389,10 +404,6 @@ public class BlockChain { return this.cumulativeBlocksByLevel; } - public long getQoraHoldersShare() { - return this.qoraHoldersShare; - } - public long getQoraPerQortReward() { return this.qoraPerQortReward; } @@ -451,6 +462,10 @@ public class BlockChain { return this.featureTriggers.get(FeatureTrigger.shareBinFix.name()).intValue(); } + public int getSharesByLevelV2Height() { + return this.featureTriggers.get(FeatureTrigger.sharesByLevelV2Height.name()).intValue(); + } + public long getRewardShareLimitTimestamp() { return this.featureTriggers.get(FeatureTrigger.rewardShareLimitTimestamp.name()).longValue(); } @@ -508,6 +523,15 @@ public class BlockChain { return 0; } + public long getQoraHoldersShareAtHeight(int ourHeight) { + // Scan through for QORA share at our height + for (int i = qoraHoldersShareByHeight.size() - 1; i >= 0; --i) + if (qoraHoldersShareByHeight.get(i).height <= ourHeight) + return qoraHoldersShareByHeight.get(i).share; + + return 0; + } + /** Validate blockchain config read from JSON */ private void validateConfig() { if (this.genesisInfo == null) @@ -516,11 +540,14 @@ public class BlockChain { if (this.rewardsByHeight == null) Settings.throwValidationError("No \"rewardsByHeight\" entry found in blockchain config"); - if (this.sharesByLevel == null) - Settings.throwValidationError("No \"sharesByLevel\" entry found in blockchain config"); + if (this.sharesByLevelV1 == null) + Settings.throwValidationError("No \"sharesByLevelV1\" entry found in blockchain config"); - if (this.qoraHoldersShare == null) - Settings.throwValidationError("No \"qoraHoldersShare\" entry found in blockchain config"); + if (this.sharesByLevelV2 == null) + Settings.throwValidationError("No \"sharesByLevelV2\" entry found in blockchain config"); + + if (this.qoraHoldersShareByHeight == null) + Settings.throwValidationError("No \"qoraHoldersShareByHeight\" entry found in blockchain config"); if (this.qoraPerQortReward == null) Settings.throwValidationError("No \"qoraPerQortReward\" entry found in blockchain config"); @@ -557,13 +584,22 @@ public class BlockChain { if (!this.featureTriggers.containsKey(featureTrigger.name())) Settings.throwValidationError(String.format("Missing feature trigger \"%s\" in blockchain config", featureTrigger.name())); - // Check block reward share bounds - long totalShare = this.qoraHoldersShare; + // Check block reward share bounds (V1) + long totalShareV1 = this.qoraHoldersShareByHeight.get(0).share; // Add share percents for account-level-based rewards - for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevel) - totalShare += accountLevelShareBin.share; + for (AccountLevelShareBin accountLevelShareBin : this.sharesByLevelV1) + totalShareV1 += accountLevelShareBin.share; - if (totalShare < 0 || totalShare > 1_00000000L) + if (totalShareV1 < 0 || totalShareV1 > 1_00000000L) + Settings.throwValidationError("Total non-founder share out of bounds (0 1_00000000L) Settings.throwValidationError("Total non-founder share out of bounds (0 - * We deal with three different independent state-spaces here: - *
    - *
  • Qortal blockchain
  • - *
  • Foreign blockchain
  • - *
  • Trade-bot entries
  • - *
- */ -public class DogecoinACCTv2TradeBot implements AcctTradeBot { - - private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2TradeBot.class); - - public enum State implements TradeBot.StateNameAndValueSupplier { - BOB_WAITING_FOR_AT_CONFIRM(10, false, false), - BOB_WAITING_FOR_MESSAGE(15, true, true), - BOB_WAITING_FOR_AT_REDEEM(25, true, true), - BOB_DONE(30, false, false), - BOB_REFUNDED(35, false, false), - - ALICE_WAITING_FOR_AT_LOCK(85, true, true), - ALICE_DONE(95, false, false), - ALICE_REFUNDING_A(105, true, true), - ALICE_REFUNDED(110, false, false); - - private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state)); - - public final int value; - public final boolean requiresAtData; - public final boolean requiresTradeData; - - State(int value, boolean requiresAtData, boolean requiresTradeData) { - this.value = value; - this.requiresAtData = requiresAtData; - this.requiresTradeData = requiresTradeData; - } - - public static State valueOf(int value) { - return map.get(value); - } - - @Override - public String getState() { - return this.name(); - } - - @Override - public int getStateValue() { - return this.value; - } - } - - /** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */ - private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms - - private static DogecoinACCTv2TradeBot instance; - - private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream() - .map(State::name) - .collect(Collectors.toUnmodifiableList()); - - private DogecoinACCTv2TradeBot() { - } - - public static synchronized DogecoinACCTv2TradeBot getInstance() { - if (instance == null) - instance = new DogecoinACCTv2TradeBot(); - - return instance; - } - - @Override - public List 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 = DogecoinACCTv2.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, DogecoinACCTv2.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, null); - - // 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, DogecoinACCTv2.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 - // Include tradeBotData as an additional parameter, since it's not in the repository yet - TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData)); - - // 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 = DogecoinACCTv2.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 = DogecoinACCTv2.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(); - DogecoinACCTv2.OfferMessageData offerMessageData = DogecoinACCTv2.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 = DogecoinACCTv2.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 = DogecoinACCTv2.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 = DogecoinACCTv2.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 = DogecoinACCTv2.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 = DogecoinACCTv2.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/LitecoinACCTv2TradeBot.java b/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv2TradeBot.java deleted file mode 100644 index 6261339a..00000000 --- a/src/main/java/org/qortal/controller/tradebot/LitecoinACCTv2TradeBot.java +++ /dev/null @@ -1,885 +0,0 @@ -package org.qortal.controller.tradebot; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; -import org.bitcoinj.core.*; -import org.bitcoinj.script.Script.ScriptType; -import org.qortal.account.PrivateKeyAccount; -import org.qortal.account.PublicKeyAccount; -import org.qortal.api.model.crosschain.TradeBotCreateRequest; -import org.qortal.asset.Asset; -import org.qortal.crosschain.*; -import org.qortal.crypto.Crypto; -import org.qortal.data.at.ATData; -import org.qortal.data.crosschain.CrossChainTradeData; -import org.qortal.data.crosschain.TradeBotData; -import org.qortal.data.transaction.BaseTransactionData; -import org.qortal.data.transaction.DeployAtTransactionData; -import org.qortal.data.transaction.MessageTransactionData; -import org.qortal.group.Group; -import org.qortal.repository.DataException; -import org.qortal.repository.Repository; -import org.qortal.transaction.DeployAtTransaction; -import org.qortal.transaction.MessageTransaction; -import org.qortal.transaction.Transaction.ValidationResult; -import org.qortal.transform.TransformationException; -import org.qortal.transform.transaction.DeployAtTransactionTransformer; -import org.qortal.utils.Base58; -import org.qortal.utils.NTP; - -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.stream.Collectors; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toMap; - -/** - * Performing cross-chain trading steps on behalf of user. - *

- * We deal with three different independent state-spaces here: - *

    - *
  • Qortal blockchain
  • - *
  • Foreign blockchain
  • - *
  • Trade-bot entries
  • - *
- */ -public class LitecoinACCTv2TradeBot implements AcctTradeBot { - - private static final Logger LOGGER = LogManager.getLogger(LitecoinACCTv2TradeBot.class); - - public enum State implements TradeBot.StateNameAndValueSupplier { - BOB_WAITING_FOR_AT_CONFIRM(10, false, false), - BOB_WAITING_FOR_MESSAGE(15, true, true), - BOB_WAITING_FOR_AT_REDEEM(25, true, true), - BOB_DONE(30, false, false), - BOB_REFUNDED(35, false, false), - - ALICE_WAITING_FOR_AT_LOCK(85, true, true), - ALICE_DONE(95, false, false), - ALICE_REFUNDING_A(105, true, true), - ALICE_REFUNDED(110, false, false); - - private static final Map map = stream(State.values()).collect(toMap(state -> state.value, state -> state)); - - public final int value; - public final boolean requiresAtData; - public final boolean requiresTradeData; - - State(int value, boolean requiresAtData, boolean requiresTradeData) { - this.value = value; - this.requiresAtData = requiresAtData; - this.requiresTradeData = requiresTradeData; - } - - public static State valueOf(int value) { - return map.get(value); - } - - @Override - public String getState() { - return this.name(); - } - - @Override - public int getStateValue() { - return this.value; - } - } - - /** Maximum time Bob waits for his AT creation transaction to be confirmed into a block. (milliseconds) */ - private static final long MAX_AT_CONFIRMATION_PERIOD = 24 * 60 * 60 * 1000L; // ms - - private static LitecoinACCTv2TradeBot instance; - - private final List endStates = Arrays.asList(State.BOB_DONE, State.BOB_REFUNDED, State.ALICE_DONE, State.ALICE_REFUNDING_A, State.ALICE_REFUNDED).stream() - .map(State::name) - .collect(Collectors.toUnmodifiableList()); - - private LitecoinACCTv2TradeBot() { - } - - public static synchronized LitecoinACCTv2TradeBot getInstance() { - if (instance == null) - instance = new LitecoinACCTv2TradeBot(); - - return instance; - } - - @Override - public List getEndStates() { - return this.endStates; - } - - /** - * Creates a new trade-bot entry from the "Bob" viewpoint, i.e. OFFERing QORT in exchange for LTC. - *

- * Generates: - *

    - *
  • new 'trade' private key
  • - *
- * Derives: - *
    - *
  • 'native' (as in Qortal) public key, public key hash, address (starting with Q)
  • - *
  • 'foreign' (as in Litecoin) 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'/Litecoin public key hash - used by Alice's P2SH scripts to allow redeem
  • - *
  • QORT amount on offer by Bob
  • - *
  • LTC 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 Litecoin receiving address into public key hash (we only support P2PKH at this time) - Address litecoinReceivingAddress; - try { - litecoinReceivingAddress = Address.fromString(Litecoin.getInstance().getNetworkParameters(), tradeBotCreateRequest.receivingAddress); - } catch (AddressFormatException e) { - throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress); - } - if (litecoinReceivingAddress.getOutputScriptType() != ScriptType.P2PKH) - throw new DataException("Unsupported Litecoin receiving address: " + tradeBotCreateRequest.receivingAddress); - - byte[] litecoinReceivingAccountInfo = litecoinReceivingAddress.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/LTC ACCT"; - String description = "QORT/LTC cross-chain trade"; - String aTType = "ACCT"; - String tags = "ACCT QORT LTC"; - byte[] creationBytes = LitecoinACCTv2.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, LitecoinACCTv2.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.LITECOIN.name(), - tradeForeignPublicKey, tradeForeignPublicKeyHash, - tradeBotCreateRequest.foreignAmount, null, null, null, litecoinReceivingAccountInfo); - - TradeBot.updateTradeBotState(repository, tradeBotData, () -> String.format("Built AT %s. Waiting for deployment", atAddress)); - - // Attempt to backup the trade bot data - TradeBot.backupTradeBotData(repository, null); - - // 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 LTC to an existing offer. - *

- * Requires a chosen trade offer from Bob, passed by crossChainTradeData - * and access to a Litecoin 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 Litecoin 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 Litecoin main-net) - * or 'tprv' for (Litecoin 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 Litecoin amount expected by 'Bob'. - *

- * If the Litecoin 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 Litecoin 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, LitecoinACCTv2.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.LITECOIN.name(), - tradeForeignPublicKey, tradeForeignPublicKeyHash, - crossChainTradeData.expectedForeignAmount, xprv58, null, lockTimeA, receivingPublicKeyHash); - - // Attempt to backup the trade bot data - // Include tradeBotData as an additional parameter, since it's not in the repository yet - TradeBot.backupTradeBotData(repository, Arrays.asList(tradeBotData)); - - // Check we have enough funds via xprv58 to fund P2SH to cover expectedForeignAmount - long p2shFee; - try { - p2shFee = Litecoin.getInstance().getP2shFee(now); - } catch (ForeignBlockchainException e) { - LOGGER.debug("Couldn't estimate Litecoin 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 = Litecoin.getInstance().deriveP2shAddress(redeemScriptBytes); - - // Build transaction for funding P2SH-A - Transaction p2shFundingTransaction = Litecoin.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 { - Litecoin.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 = LitecoinACCTv2.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: - case ALICE_REFUNDING_A: - 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 = LitecoinACCTv2.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 Litecoin 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; - } - - Litecoin litecoin = Litecoin.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 Litecoin pubkeyhash and lockTime-A - byte[] messageData = messageTransactionData.getData(); - LitecoinACCTv2.OfferMessageData offerMessageData = LitecoinACCTv2.extractOfferMessageData(messageData); - if (offerMessageData == null) - continue; - - byte[] aliceForeignPublicKeyHash = offerMessageData.partnerLitecoinPKH; - byte[] hashOfSecretA = offerMessageData.hashOfSecretA; - int lockTimeA = (int) offerMessageData.lockTimeA; - long messageTimestamp = messageTransactionData.getTimestamp(); - int refundTimeout = LitecoinACCTv2.calcRefundTimeout(messageTimestamp, lockTimeA); - - // Determine P2SH-A address and confirm funded - byte[] redeemScriptA = BitcoinyHTLC.buildScript(aliceForeignPublicKeyHash, lockTimeA, tradeBotData.getTradeForeignPublicKeyHash(), hashOfSecretA); - String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA); - - long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); - long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp); - final long minimumAmountA = tradeBotData.getForeignAmount() + p2shFee; - - BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = LitecoinACCTv2.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 LTC 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; - - Litecoin litecoin = Litecoin.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 = litecoin.deriveP2shAddress(redeemScriptA); - - long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); - long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp); - long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - - BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = LitecoinACCTv2.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 = LitecoinACCTv2.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 LTC 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 LTC funds from P2SH-A - * to Bob's 'foreign'/Litecoin trade legacy-format address, as derived from trade private key. - *

- * (This could potentially be 'improved' to send LTC 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 LTC - TradeBot.updateTradeBotState(repository, tradeBotData, State.BOB_REFUNDED, - () -> String.format("AT %s has auto-refunded - trade aborted", tradeBotData.getAtAddress())); - - return; - } - - byte[] secretA = LitecoinACCTv2.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 - - Litecoin litecoin = Litecoin.getInstance(); - - byte[] receivingAccountInfo = tradeBotData.getReceivingAccountInfo(); - int lockTimeA = crossChainTradeData.lockTimeA; - byte[] redeemScriptA = BitcoinyHTLC.buildScript(crossChainTradeData.partnerForeignPKH, lockTimeA, crossChainTradeData.creatorForeignPKH, crossChainTradeData.hashOfSecretA); - String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA); - - // Fee for redeem/refund is subtracted from P2SH-A balance. - long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); - long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp); - long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = litecoin.getUnspentOutputs(p2shAddressA); - - Transaction p2shRedeemTransaction = BitcoinyHTLC.buildRedeemTransaction(litecoin.getNetworkParameters(), redeemAmount, redeemKey, - fundingOutputs, redeemScriptA, secretA, receivingAccountInfo); - - litecoin.broadcastTransaction(p2shRedeemTransaction); - break; - } - } - - String receivingAddress = litecoin.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; - - Litecoin litecoin = Litecoin.getInstance(); - - // We can't refund P2SH-A until median block time has passed lockTime-A (see BIP113) - int medianBlockTime = litecoin.getMedianBlockTime(); - if (medianBlockTime <= lockTimeA) - return; - - byte[] redeemScriptA = BitcoinyHTLC.buildScript(tradeBotData.getTradeForeignPublicKeyHash(), lockTimeA, crossChainTradeData.creatorForeignPKH, tradeBotData.getHashOfSecret()); - String p2shAddressA = litecoin.deriveP2shAddress(redeemScriptA); - - // Fee for redeem/refund is subtracted from P2SH-A balance. - long feeTimestamp = calcFeeTimestamp(lockTimeA, crossChainTradeData.tradeTimeout); - long p2shFee = Litecoin.getInstance().getP2shFee(feeTimestamp); - long minimumAmountA = crossChainTradeData.expectedForeignAmount + p2shFee; - BitcoinyHTLC.Status htlcStatusA = BitcoinyHTLC.determineHtlcStatus(litecoin.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 = litecoin.getUnspentOutputs(p2shAddressA); - - // Determine receive address for refund - String receiveAddress = litecoin.getUnusedReceiveAddress(tradeBotData.getForeignKey()); - Address receiving = Address.fromString(litecoin.getNetworkParameters(), receiveAddress); - - Transaction p2shRefundTransaction = BitcoinyHTLC.buildRefundTransaction(litecoin.getNetworkParameters(), refundAmount, refundKey, - fundingOutputs, redeemScriptA, lockTimeA, receiving.getHash()); - - litecoin.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/TradeBot.java b/src/main/java/org/qortal/controller/tradebot/TradeBot.java index 05ba8f1b..c7ae1db3 100644 --- a/src/main/java/org/qortal/controller/tradebot/TradeBot.java +++ b/src/main/java/org/qortal/controller/tradebot/TradeBot.java @@ -96,10 +96,8 @@ public class TradeBot implements Listener { acctTradeBotSuppliers.put(BitcoinACCTv1.class, BitcoinACCTv1TradeBot::getInstance); acctTradeBotSuppliers.put(BitcoinACCTv3.class, BitcoinACCTv3TradeBot::getInstance); acctTradeBotSuppliers.put(LitecoinACCTv1.class, LitecoinACCTv1TradeBot::getInstance); - acctTradeBotSuppliers.put(LitecoinACCTv2.class, LitecoinACCTv2TradeBot::getInstance); acctTradeBotSuppliers.put(LitecoinACCTv3.class, LitecoinACCTv3TradeBot::getInstance); acctTradeBotSuppliers.put(DogecoinACCTv1.class, DogecoinACCTv1TradeBot::getInstance); - acctTradeBotSuppliers.put(DogecoinACCTv2.class, DogecoinACCTv2TradeBot::getInstance); acctTradeBotSuppliers.put(DogecoinACCTv3.class, DogecoinACCTv3TradeBot::getInstance); acctTradeBotSuppliers.put(DigibyteACCTv3.class, DigibyteACCTv3TradeBot::getInstance); acctTradeBotSuppliers.put(RavencoinACCTv3.class, RavencoinACCTv3TradeBot::getInstance); diff --git a/src/main/java/org/qortal/crosschain/DogecoinACCTv2.java b/src/main/java/org/qortal/crosschain/DogecoinACCTv2.java deleted file mode 100644 index c4b0edb3..00000000 --- a/src/main/java/org/qortal/crosschain/DogecoinACCTv2.java +++ /dev/null @@ -1,861 +0,0 @@ -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 DogecoinACCTv2 implements ACCT { - - private static final Logger LOGGER = LogManager.getLogger(DogecoinACCTv2.class); - - public static final String NAME = DogecoinACCTv2.class.getSimpleName(); - public static final byte[] CODE_BYTES_HASH = HashCode.fromString("6fff38d6eeb06568a9c879c5628527730319844aa0de53f5f4ffab5506efe885").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 DogecoinACCTv2 instance; - - private DogecoinACCTv2() { - } - - public static synchronized DogecoinACCTv2 getInstance() { - if (instance == null) - instance = new DogecoinACCTv2(); - - 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(); - - /* Sleep until message arrives */ - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp)); - - // 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(); - - /* Sleep until message arrives */ - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp)); - - // 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), DogecoinACCTv2.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/LitecoinACCTv2.java b/src/main/java/org/qortal/crosschain/LitecoinACCTv2.java deleted file mode 100644 index c5728953..00000000 --- a/src/main/java/org/qortal/crosschain/LitecoinACCTv2.java +++ /dev/null @@ -1,854 +0,0 @@ -package org.qortal.crosschain; - -import com.google.common.hash.HashCode; -import com.google.common.primitives.Bytes; -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 Litecoin & 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 Litecoin & Qortal 'trade' keys
    • - *
    • Alice funds Litecoin P2SH-A
    • - *
    • Alice sends 'offer' MESSAGE to Bob from her Qortal trade address, containing: - *
        - *
      • hash-of-secret-A
      • - *
      • her 'trade' Litecoin 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 Litecoin 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 Litecoin trade key and secret-A
    • - *
    • P2SH-A LTC funds end up at Litecoin address determined by redeem transaction output(s)
    • - *
    - *
  • - *
- */ -public class LitecoinACCTv2 implements ACCT { - - public static final String NAME = LitecoinACCTv2.class.getSimpleName(); - public static final byte[] CODE_BYTES_HASH = HashCode.fromString("d5ea386a41441180c854ca8d7bbc620bfd53a97df2650a2b162b52324caf6e19").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[] partnerLitecoinPKH; - public byte[] hashOfSecretA; - public long lockTimeA; - } - public static final int OFFER_MESSAGE_LENGTH = 20 /*partnerLitecoinPKH*/ + 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 Litecoin 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 LitecoinACCTv2 instance; - - private LitecoinACCTv2() { - } - - public static synchronized LitecoinACCTv2 getInstance() { - if (instance == null) - instance = new LitecoinACCTv2(); - - return instance; - } - - @Override - public byte[] getCodeBytesHash() { - return CODE_BYTES_HASH; - } - - @Override - public int getModeByteOffset() { - return MODE_BYTE_OFFSET; - } - - @Override - public ForeignBlockchain getBlockchain() { - return Litecoin.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 litecoinPublicKeyHash 20-byte HASH160 of creator's trade Litecoin public key - * @param qortAmount how much QORT to pay trade partner if they send correct 32-byte secrets to AT - * @param litecoinAmount how much LTC the AT creator is expecting to trade - * @param tradeTimeout suggested timeout for entire trade - */ - public static byte[] buildQortalAT(String creatorTradeAddress, byte[] litecoinPublicKeyHash, long qortAmount, long litecoinAmount, int tradeTimeout) { - if (litecoinPublicKeyHash.length != 20) - throw new IllegalArgumentException("Litecoin 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 addrLitecoinPublicKeyHash = addrCounter; - addrCounter += 4; - - final int addrQortAmount = addrCounter++; - final int addrLitecoinAmount = 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 addrTradeMessagePartnerLitecoinPKHOffset = addrCounter++; - final int addrPartnerLitecoinPKHPointer = 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 addrPartnerLitecoinPKH = 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)); - - // Litecoin public key hash - assert dataByteBuffer.position() == addrLitecoinPublicKeyHash * MachineState.VALUE_SIZE : "addrLitecoinPublicKeyHash incorrect"; - dataByteBuffer.put(Bytes.ensureCapacity(litecoinPublicKeyHash, 32, 0)); - - // Redeem Qort amount - assert dataByteBuffer.position() == addrQortAmount * MachineState.VALUE_SIZE : "addrQortAmount incorrect"; - dataByteBuffer.putLong(qortAmount); - - // Expected Litecoin amount - assert dataByteBuffer.position() == addrLitecoinAmount * MachineState.VALUE_SIZE : "addrLitecoinAmount incorrect"; - dataByteBuffer.putLong(litecoinAmount); - - // 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 Litecoin PKH - assert dataByteBuffer.position() == addrTradeMessagePartnerLitecoinPKHOffset * MachineState.VALUE_SIZE : "addrTradeMessagePartnerLitecoinPKHOffset incorrect"; - dataByteBuffer.putLong(32L); - - // Index into data segment of partner's Litecoin PKH, used by GET_B_IND - assert dataByteBuffer.position() == addrPartnerLitecoinPKHPointer * MachineState.VALUE_SIZE : "addrPartnerLitecoinPKHPointer incorrect"; - dataByteBuffer.putLong(addrPartnerLitecoinPKH); - - // 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 */ - - // 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(); - - /* Sleep until message arrives */ - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp)); - - // 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 Litecoin 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, addrTradeMessagePartnerLitecoinPKHOffset)); - // Store partner's Litecoin PKH (we only really use values from B1-B3) - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(FunctionCode.GET_B_IND, addrPartnerLitecoinPKHPointer)); - // 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(); - - /* Sleep until message arrives */ - codeByteBuffer.put(OpCode.EXT_FUN_DAT.compile(QortalFunctionCode.SLEEP_UNTIL_MESSAGE.value, addrLastTxnTimestamp)); - - // 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 LTC-QORT ACCT?", e); - } - } - - codeByteBuffer.flip(); - - byte[] codeBytes = new byte[codeByteBuffer.limit()]; - codeByteBuffer.get(codeBytes); - - assert Arrays.equals(Crypto.digest(codeBytes), LitecoinACCTv2.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.LITECOIN.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 Litecoin/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 LTC 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 Litecoin PKH - dataByteBuffer.position(dataByteBuffer.position() + 8); - - // Skip pointer to partner's Litecoin 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 Litecoin PKH - byte[] partnerLitecoinPKH = new byte[20]; - dataByteBuffer.get(partnerLitecoinPKH); - dataByteBuffer.position(dataByteBuffer.position() + 32 - partnerLitecoinPKH.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 = partnerLitecoinPKH; - 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.partnerLitecoinPKH = 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/SupportedBlockchain.java b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java index c1f68831..5ddb6aec 100644 --- a/src/main/java/org/qortal/crosschain/SupportedBlockchain.java +++ b/src/main/java/org/qortal/crosschain/SupportedBlockchain.java @@ -29,7 +29,6 @@ public enum SupportedBlockchain { LITECOIN(Arrays.asList( Triple.valueOf(LitecoinACCTv1.NAME, LitecoinACCTv1.CODE_BYTES_HASH, LitecoinACCTv1::getInstance), - Triple.valueOf(LitecoinACCTv2.NAME, LitecoinACCTv2.CODE_BYTES_HASH, LitecoinACCTv2::getInstance), Triple.valueOf(LitecoinACCTv3.NAME, LitecoinACCTv3.CODE_BYTES_HASH, LitecoinACCTv3::getInstance) )) { @Override @@ -45,7 +44,6 @@ public enum SupportedBlockchain { DOGECOIN(Arrays.asList( Triple.valueOf(DogecoinACCTv1.NAME, DogecoinACCTv1.CODE_BYTES_HASH, DogecoinACCTv1::getInstance), - Triple.valueOf(DogecoinACCTv2.NAME, DogecoinACCTv2.CODE_BYTES_HASH, DogecoinACCTv2::getInstance), Triple.valueOf(DogecoinACCTv3.NAME, DogecoinACCTv3.CODE_BYTES_HASH, DogecoinACCTv3::getInstance) )) { @Override diff --git a/src/main/java/org/qortal/repository/ArbitraryRepository.java b/src/main/java/org/qortal/repository/ArbitraryRepository.java index 7a31f40e..75fb0509 100644 --- a/src/main/java/org/qortal/repository/ArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/ArbitraryRepository.java @@ -24,7 +24,7 @@ public interface ArbitraryRepository { public ArbitraryTransactionData getLatestTransaction(String name, Service service, Method method, String identifier) throws DataException; - public List getArbitraryResources(Service service, String identifier, String name, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException; + public List getArbitraryResources(Service service, String identifier, List names, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException; public List searchArbitraryResources(Service service, String query, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException; diff --git a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java index 2c88b089..c21dd038 100644 --- a/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java +++ b/src/main/java/org/qortal/repository/hsqldb/HSQLDBArbitraryRepository.java @@ -301,7 +301,7 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { } @Override - public List getArbitraryResources(Service service, String identifier, String name, + public List getArbitraryResources(Service service, String identifier, List names, boolean defaultResource, Integer limit, Integer offset, Boolean reverse) throws DataException { StringBuilder sql = new StringBuilder(512); List bindParams = new ArrayList<>(); @@ -325,9 +325,16 @@ public class HSQLDBArbitraryRepository implements ArbitraryRepository { bindParams.add(identifier); } - if (name != null) { - sql.append(" AND name = ?"); - bindParams.add(name); + if (names != null && !names.isEmpty()) { + sql.append(" AND name IN (?"); + bindParams.add(names.get(0)); + + for (int i = 1; i < names.size(); ++i) { + sql.append(", ?"); + bindParams.add(names.get(i)); + } + + sql.append(")"); } sql.append(" GROUP BY name, service, identifier ORDER BY name COLLATE SQL_TEXT_UCC_NO_PAD"); diff --git a/src/main/resources/blockchain.json b/src/main/resources/blockchain.json index f1041485..8d1600ed 100644 --- a/src/main/resources/blockchain.json +++ b/src/main/resources/blockchain.json @@ -40,14 +40,24 @@ { "height": 2851201, "reward": 2.25 }, { "height": 3110401, "reward": 2.00 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 9999999, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -65,6 +75,7 @@ "atFindNextTransactionFix": 275000, "newBlockSigHeight": 320000, "shareBinFix": 399000, + "sharesByLevelV2Height": 9999999, "rewardShareLimitTimestamp": 1657382400000, "calcChainWeightTimestamp": 1620579600000, "transactionV5Timestamp": 1642176000000, diff --git a/src/test/java/org/qortal/test/minting/RewardTests.java b/src/test/java/org/qortal/test/minting/RewardTests.java index 4aee2de1..1689db5b 100644 --- a/src/test/java/org/qortal/test/minting/RewardTests.java +++ b/src/test/java/org/qortal/test/minting/RewardTests.java @@ -13,6 +13,7 @@ import org.junit.Before; import org.junit.Test; import org.qortal.account.PrivateKeyAccount; import org.qortal.asset.Asset; +import org.qortal.block.Block; import org.qortal.block.BlockChain; import org.qortal.block.BlockChain.RewardByHeight; import org.qortal.controller.BlockMinter; @@ -108,7 +109,7 @@ public class RewardTests extends Common { public void testLegacyQoraReward() throws DataException { Common.useSettings("test-settings-v2-qora-holder-extremes.json"); - long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); BigInteger qoraHoldersShareBI = BigInteger.valueOf(qoraHoldersShare); long qoraPerQort = BlockChain.getInstance().getQoraPerQortReward(); @@ -189,6 +190,47 @@ public class RewardTests extends Common { } } + @Test + public void testLegacyQoraRewardReduction() throws DataException { + Common.useSettings("test-settings-v2-qora-holder-reduction.json"); + + // Make sure that the QORA share reduces between blocks 4 and 5 + assertTrue(BlockChain.getInstance().getQoraHoldersShareAtHeight(5) < BlockChain.getInstance().getQoraHoldersShareAtHeight(4)); + + // Keep track of balance deltas at each height + Map chloeQortBalanceDeltaAtEachHeight = new HashMap<>(); + + try (final Repository repository = RepositoryManager.getRepository()) { + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + long chloeLastQortBalance = initialBalances.get("chloe").get(Asset.QORT); + + for (int i=2; i<=10; i++) { + + Block block = BlockUtils.mintBlock(repository); + + // Add to map of balance deltas at each height + long chloeNewQortBalance = AccountUtils.getBalance(repository, "chloe", Asset.QORT); + chloeQortBalanceDeltaAtEachHeight.put(block.getBlockData().getHeight(), chloeNewQortBalance - chloeLastQortBalance); + chloeLastQortBalance = chloeNewQortBalance; + } + + // Ensure blocks 2-4 paid out the same rewards to Chloe + assertEquals(chloeQortBalanceDeltaAtEachHeight.get(2), chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Ensure block 5 paid a lower reward + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) < chloeQortBalanceDeltaAtEachHeight.get(4)); + + // Check that the reward was 20x lower + assertTrue(chloeQortBalanceDeltaAtEachHeight.get(5) == chloeQortBalanceDeltaAtEachHeight.get(4) / 20); + + // Orphan to block 4 and ensure that Chloe's balance hasn't been incorrectly affected by the reward reduction + BlockUtils.orphanToBlock(repository, 4); + long expectedChloeQortBalance = initialBalances.get("chloe").get(Asset.QORT) + chloeQortBalanceDeltaAtEachHeight.get(2) + + chloeQortBalanceDeltaAtEachHeight.get(3) + chloeQortBalanceDeltaAtEachHeight.get(4); + assertEquals(expectedChloeQortBalance, AccountUtils.getBalance(repository, "chloe", Asset.QORT)); + } + } + /** Use Alice-Chloe reward-share to bump Chloe from level 0 to level 1, then check orphaning works as expected. */ @Test public void testLevel1() throws DataException { @@ -294,7 +336,7 @@ public class RewardTests extends Common { * So Dilbert should receive 100% - legacy QORA holder's share. */ - final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShare(); + final long qoraHoldersShare = BlockChain.getInstance().getQoraHoldersShareAtHeight(1); final long remainingShare = 1_00000000 - qoraHoldersShare; long dilbertExpectedBalance = initialBalances.get("dilbert").get(Asset.QORT); @@ -1140,6 +1182,250 @@ public class RewardTests extends Common { } } + /** Test rewards for level 1 and 2 accounts with V2 share-bin layout (post QORA reduction) */ + @Test + public void testLevel1And2RewardsShareBinsV2() throws DataException { + Common.useSettings("test-settings-v2-reward-levels.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + byte[] chloeRewardSharePrivateKey; + // Bob self-share NOT online + + // Mint some blocks, to get us close to the V2 activation, but with some room for Chloe and Dilbert to start minting some blocks + for (int i=0; i<990; i++) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Chloe self share comes online + try { + chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + } catch (IllegalArgumentException ex) { + LOGGER.error("FAILED {}", ex.getLocalizedMessage(), ex); + throw ex; + } + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share comes online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint 6 more blocks, so that V2 share bins are nearly activated + for (int i=0; i<6; i++) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(10, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(2, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Ensure that only Alice is a founder + assertEquals(1, getFlags(repository, "alice")); + assertEquals(0, getFlags(repository, "bob")); + assertEquals(0, getFlags(repository, "chloe")); + assertEquals(0, getFlags(repository, "dilbert")); + + // Now that everyone is at level 1 or 2, we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are at the correct height and block reward value + assertEquals(1000, (int) repository.getBlockRepository().getLastBlock().getHeight()); + assertEquals(100000000L, blockReward); + + // We are past the sharesByLevelV2Height feature trigger, so we expect level 1 and 2 to share the increased reward (6%) + final int level1And2SharePercent = 6_00; // 6% + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long expectedLevel1And2RewardV2 = level1And2ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderRewardV2 = blockReward - level1And2ShareAmount; // Alice should receive the remainder + + // Validate the balances + assertEquals(6000000, level1And2ShareAmount); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderRewardV2); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance+expectedLevel1And2RewardV2); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel1And2RewardV2); + + // Now orphan the latest block. This brings us to the threshold of the sharesByLevelV2Height feature trigger + BlockUtils.orphanBlocks(repository, 1); + assertEquals(999, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Ensure the latest block rewards have been subtracted and they have returned to their initial values + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + + // Orphan another block. This time, the block that was orphaned was prior to the sharesByLevelV2Height feature trigger. + BlockUtils.orphanBlocks(repository, 1); + assertEquals(998, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // In V1 of share-bins, level 1-2 pays out 5% instead of 6% + final int level1And2SharePercentV1 = 5_00; // 5% + final long level1And2ShareAmountV1 = (blockReward * level1And2SharePercentV1) / 100L / 100L; + final long expectedLevel1And2RewardV1 = level1And2ShareAmountV1 / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderRewardV1 = blockReward - level1And2ShareAmountV1; // Alice should receive the remainder + + // Validate the share amounts and balances + assertEquals(5000000, level1And2ShareAmountV1); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-expectedFounderRewardV1); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-expectedLevel1And2RewardV1); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-expectedLevel1And2RewardV1); + + // Orphan the latest block one last time + BlockUtils.orphanBlocks(repository, 1); + assertEquals(997, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Validate balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-(expectedFounderRewardV1*2)); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance-(expectedLevel1And2RewardV1*2)); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-(expectedLevel1And2RewardV1*2)); + + } + } + + /** Test rewards for level 1 and 2 accounts with V2 share-bin layout (post QORA reduction) + * plus some legacy QORA holders */ + @Test + public void testLevel1And2RewardsShareBinsV2WithQoraHolders() throws DataException { + Common.useSettings("test-settings-v2-qora-holder-extremes.json"); + + try (final Repository repository = RepositoryManager.getRepository()) { + + List mintingAndOnlineAccounts = new ArrayList<>(); + + // Some legacy QORA holders exist (Bob and Chloe) + + // Alice self share online + PrivateKeyAccount aliceSelfShare = Common.getTestAccount(repository, "alice-reward-share"); + mintingAndOnlineAccounts.add(aliceSelfShare); + byte[] chloeRewardSharePrivateKey; + // Bob self-share NOT online + + // Mint some blocks, to get us close to the V2 activation, but with some room for Chloe and Dilbert to start minting some blocks + for (int i=0; i<990; i++) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Chloe self share comes online + try { + chloeRewardSharePrivateKey = AccountUtils.rewardShare(repository, "chloe", "chloe", 0); + } catch (IllegalArgumentException ex) { + LOGGER.error("FAILED {}", ex.getLocalizedMessage(), ex); + throw ex; + } + PrivateKeyAccount chloeRewardShareAccount = new PrivateKeyAccount(repository, chloeRewardSharePrivateKey); + mintingAndOnlineAccounts.add(chloeRewardShareAccount); + + // Dilbert self share comes online + byte[] dilbertRewardSharePrivateKey = AccountUtils.rewardShare(repository, "dilbert", "dilbert", 0); + PrivateKeyAccount dilbertRewardShareAccount = new PrivateKeyAccount(repository, dilbertRewardSharePrivateKey); + mintingAndOnlineAccounts.add(dilbertRewardShareAccount); + + // Mint 6 more blocks, so that V2 share bins are nearly activated + for (int i=0; i<6; i++) + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure that the levels are as we expect + assertEquals(10, (int) Common.getTestAccount(repository, "alice").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "bob").getLevel()); + assertEquals(1, (int) Common.getTestAccount(repository, "chloe").getLevel()); + assertEquals(2, (int) Common.getTestAccount(repository, "dilbert").getLevel()); + + // Ensure that only Alice is a founder + assertEquals(1, getFlags(repository, "alice")); + assertEquals(0, getFlags(repository, "bob")); + assertEquals(0, getFlags(repository, "chloe")); + assertEquals(0, getFlags(repository, "dilbert")); + + // Now that everyone is at level 1 or 2, we can capture initial balances + Map> initialBalances = AccountUtils.getBalances(repository, Asset.QORT, Asset.LEGACY_QORA, Asset.QORT_FROM_QORA); + final long aliceInitialBalance = initialBalances.get("alice").get(Asset.QORT); + final long bobInitialBalance = initialBalances.get("bob").get(Asset.QORT); + final long chloeInitialBalance = initialBalances.get("chloe").get(Asset.QORT); + final long dilbertInitialBalance = initialBalances.get("dilbert").get(Asset.QORT); + + // Mint a block + final long blockReward = BlockUtils.getNextBlockReward(repository); + BlockMinter.mintTestingBlock(repository, mintingAndOnlineAccounts.toArray(new PrivateKeyAccount[0])); + + // Ensure we are at the correct height and block reward value + assertEquals(1000, (int) repository.getBlockRepository().getLastBlock().getHeight()); + assertEquals(100000000L, blockReward); + + // We are past the sharesByLevelV2Height feature trigger, so we expect level 1 and 2 to share the increased reward (6%) + // and the QORA share will be 1% + final int level1And2SharePercent = 6_00; // 6% + final int qoraSharePercentV2 = 1_00; // 1% + final long qoraShareAmountV2 = (blockReward * qoraSharePercentV2) / 100L / 100L; + final long level1And2ShareAmount = (blockReward * level1And2SharePercent) / 100L / 100L; + final long expectedLevel1And2RewardV2 = level1And2ShareAmount / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderRewardV2 = blockReward - level1And2ShareAmount - qoraShareAmountV2; // Alice should receive the remainder + + // Validate the balances + assertEquals(6000000, level1And2ShareAmount); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance+expectedFounderRewardV2); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + // Chloe is a QORA holder and will receive additional QORT, so it's not easy to pre-calculate her balance + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance+expectedLevel1And2RewardV2); + + // Now orphan the latest block. This brings us to the threshold of the sharesByLevelV2Height feature trigger + BlockUtils.orphanBlocks(repository, 1); + assertEquals(999, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Ensure the latest block rewards have been subtracted and they have returned to their initial values + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + AccountUtils.assertBalance(repository, "chloe", Asset.QORT, chloeInitialBalance); + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance); + + // Orphan another block. This time, the block that was orphaned was prior to the sharesByLevelV2Height feature trigger. + BlockUtils.orphanBlocks(repository, 1); + assertEquals(998, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // In V1 of share-bins, level 1-2 pays out 5% instead of 6%, and the QORA share is higher at 20% + final int level1And2SharePercentV1 = 5_00; // 5% + final int qoraSharePercentV1 = 20_00; // 20% + final long qoraShareAmountV1 = (blockReward * qoraSharePercentV1) / 100L / 100L; + final long level1And2ShareAmountV1 = (blockReward * level1And2SharePercentV1) / 100L / 100L; + final long expectedLevel1And2RewardV1 = level1And2ShareAmountV1 / 2; // The reward is split between Chloe and Dilbert + final long expectedFounderRewardV1 = blockReward - level1And2ShareAmountV1 - qoraShareAmountV1; // Alice should receive the remainder + + // Validate the share amounts and balances + assertEquals(5000000, level1And2ShareAmountV1); + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-expectedFounderRewardV1); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + // Chloe is a QORA holder and will receive additional QORT, so it's not easy to pre-calculate her balance + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-expectedLevel1And2RewardV1); + + // Orphan the latest block one last time + BlockUtils.orphanBlocks(repository, 1); + assertEquals(997, (int) repository.getBlockRepository().getLastBlock().getHeight()); + + // Validate balances + AccountUtils.assertBalance(repository, "alice", Asset.QORT, aliceInitialBalance-(expectedFounderRewardV1*2)); + AccountUtils.assertBalance(repository, "bob", Asset.QORT, bobInitialBalance); // Bob not online so his balance remains the same + // Chloe is a QORA holder and will receive additional QORT, so it's not easy to pre-calculate her balance + AccountUtils.assertBalance(repository, "dilbert", Asset.QORT, dilbertInitialBalance-(expectedLevel1And2RewardV1*2)); + + } + } + private int getFlags(Repository repository, String name) throws DataException { TestAccount testAccount = Common.getTestAccount(repository, name); diff --git a/src/test/resources/test-chain-v2-block-timestamps.json b/src/test/resources/test-chain-v2-block-timestamps.json index bad908eb..37224684 100644 --- a/src/test/resources/test-chain-v2-block-timestamps.json +++ b/src/test/resources/test-chain-v2-block-timestamps.json @@ -19,14 +19,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -54,6 +64,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-disable-reference.json b/src/test/resources/test-chain-v2-disable-reference.json index e7be1ac7..7ea0b86d 100644 --- a/src/test/resources/test-chain-v2-disable-reference.json +++ b/src/test/resources/test-chain-v2-disable-reference.json @@ -23,14 +23,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -57,6 +67,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-founder-rewards.json b/src/test/resources/test-chain-v2-founder-rewards.json index 37c79ce5..85a50f83 100644 --- a/src/test/resources/test-chain-v2-founder-rewards.json +++ b/src/test/resources/test-chain-v2-founder-rewards.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-leftover-reward.json b/src/test/resources/test-chain-v2-leftover-reward.json index fbb7c553..ebc3ccfa 100644 --- a/src/test/resources/test-chain-v2-leftover-reward.json +++ b/src/test/resources/test-chain-v2-leftover-reward.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-minting.json b/src/test/resources/test-chain-v2-minting.json index 085152d2..cc91f993 100644 --- a/src/test/resources/test-chain-v2-minting.json +++ b/src/test/resources/test-chain-v2-minting.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-qora-holder-extremes.json b/src/test/resources/test-chain-v2-qora-holder-extremes.json index b8ec21a4..085d1dbf 100644 --- a/src/test/resources/test-chain-v2-qora-holder-extremes.json +++ b/src/test/resources/test-chain-v2-qora-holder-extremes.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -57,7 +67,8 @@ "groupApprovalTimestamp": 0, "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, - "shareBinFix": 999999, + "shareBinFix": 0, + "sharesByLevelV2Height": 1000, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, @@ -89,7 +100,10 @@ { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }, - { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 } + { "type": "ACCOUNT_LEVEL", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 2 }, + { "type": "ACCOUNT_LEVEL", "target": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "level": 1 }, + { "type": "ACCOUNT_LEVEL", "target": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "level": 1 } ] } } diff --git a/src/test/resources/test-chain-v2-qora-holder-reduction.json b/src/test/resources/test-chain-v2-qora-holder-reduction.json new file mode 100644 index 00000000..75858057 --- /dev/null +++ b/src/test/resources/test-chain-v2-qora-holder-reduction.json @@ -0,0 +1,107 @@ +{ + "isTestChain": true, + "blockTimestampMargin": 500, + "transactionExpiryPeriod": 86400000, + "maxBlockSize": 2097152, + "maxBytesPerUnitFee": 1024, + "unitFee": "0.1", + "nameRegistrationUnitFees": [ + { "timestamp": 1645372800000, "fee": "5" } + ], + "requireGroupForApproval": false, + "minAccountLevelToRewardShare": 5, + "maxRewardSharesPerFounderMintingAccount": 6, + "maxRewardSharesByTimestamp": [ + { "timestamp": 0, "maxShares": 6 }, + { "timestamp": 9999999999999, "maxShares": 3 } + ], + "founderEffectiveMintingLevel": 10, + "onlineAccountSignaturesMinLifetime": 3600000, + "onlineAccountSignaturesMaxLifetime": 86400000, + "onlineAccountsModulusV2Timestamp": 9999999999999, + "rewardsByHeight": [ + { "height": 1, "reward": 100 }, + { "height": 11, "reward": 10 }, + { "height": 21, "reward": 1 } + ], + "sharesByLevelV1": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } + ], + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 5, "share": 0.01 } + ], + "qoraPerQortReward": 250, + "minAccountsToActivateShareBin": 30, + "shareBinActivationMinLevel": 7, + "blocksNeededByLevel": [ 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 ], + "blockTimingsByHeight": [ + { "height": 1, "target": 60000, "deviation": 30000, "power": 0.2 } + ], + "ciyamAtSettings": { + "feePerStep": "0.0001", + "maxStepsPerRound": 500, + "stepsPerFunctionCall": 10, + "minutesPerBlock": 1 + }, + "featureTriggers": { + "messageHeight": 0, + "atHeight": 0, + "assetsTimestamp": 0, + "votingTimestamp": 0, + "arbitraryTimestamp": 0, + "powfixTimestamp": 0, + "qortalTimestamp": 0, + "newAssetPricingTimestamp": 0, + "groupApprovalTimestamp": 0, + "atFindNextTransactionFix": 0, + "newBlockSigHeight": 999999, + "shareBinFix": 999999, + "sharesByLevelV2Height": 5, + "rewardShareLimitTimestamp": 9999999999999, + "calcChainWeightTimestamp": 0, + "transactionV5Timestamp": 0, + "transactionV6Timestamp": 0, + "disableReferenceTimestamp": 9999999999999, + "aggregateSignatureTimestamp": 0 + }, + "genesisInfo": { + "version": 4, + "timestamp": 0, + "transactions": [ + { "type": "ISSUE_ASSET", "assetName": "QORT", "description": "QORT native coin", "data": "", "quantity": 0, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "assetName": "Legacy-QORA", "description": "Representative legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + { "type": "ISSUE_ASSET", "assetName": "QORT-from-QORA", "description": "QORT gained from holding legacy QORA", "quantity": 0, "isDivisible": true, "data": "{}", "isUnspendable": true }, + + { "type": "GENESIS", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "amount": "1000000000" }, + { "type": "GENESIS", "recipient": "QixPbJUwsaHsVEofJdozU9zgVqkK6aYhrK", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "1000000" }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "1000000" }, + + { "type": "GENESIS", "recipient": "QaUpHNhT3Ygx6avRiKobuLdusppR5biXjL", "amount": "637557960.49687541", "assetId": 1 }, + { "type": "GENESIS", "recipient": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "amount": "0.666", "assetId": 1 }, + + { "type": "CREATE_GROUP", "creatorPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "groupName": "dev-group", "description": "developer group", "isOpen": false, "approvalThreshold": "PCT100", "minimumBlockDelay": 0, "maximumBlockDelay": 1440 }, + + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "TEST", "description": "test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "C6wuddsBV3HzRrXUtezE7P5MoRXp5m3mEDokRDGZB6ry", "assetName": "OTHER", "description": "other test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, + { "type": "ISSUE_ASSET", "issuerPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "assetName": "GOLD", "description": "gold test asset", "data": "", "quantity": 1000000, "isDivisible": true, "fee": 0 }, + + { "type": "ACCOUNT_FLAGS", "target": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "andMask": -1, "orMask": 1, "xorMask": 0 }, + { "type": "REWARD_SHARE", "minterPublicKey": "2tiMr5LTpaWCgbRvkPK8TFd7k63DyHJMMFFsz9uBf1ZP", "recipient": "QgV4s3xnzLhVBEJxcYui4u4q11yhUHsd9v", "rewardSharePublicKey": "7PpfnvLSG7y4HPh8hE7KoqAjLCkv7Ui6xw4mKAkbZtox", "sharePercent": 100 }, + + { "type": "ACCOUNT_LEVEL", "target": "Qci5m9k4rcwe4ruKrZZQKka4FzUUMut3er", "level": 8 } + ] + } +} diff --git a/src/test/resources/test-chain-v2-qora-holder.json b/src/test/resources/test-chain-v2-qora-holder.json index 8a1acd0d..0706c5bb 100644 --- a/src/test/resources/test-chain-v2-qora-holder.json +++ b/src/test/resources/test-chain-v2-qora-holder.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 1000, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-levels.json b/src/test/resources/test-chain-v2-reward-levels.json index 8461a593..b3644d6b 100644 --- a/src/test/resources/test-chain-v2-reward-levels.json +++ b/src/test/resources/test-chain-v2-reward-levels.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 1, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 6, + "sharesByLevelV2Height": 1000, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-scaling.json b/src/test/resources/test-chain-v2-reward-scaling.json index 4d432224..1c68dda4 100644 --- a/src/test/resources/test-chain-v2-reward-scaling.json +++ b/src/test/resources/test-chain-v2-reward-scaling.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-chain-v2-reward-shares.json b/src/test/resources/test-chain-v2-reward-shares.json index 9dd3c223..10d2aab3 100644 --- a/src/test/resources/test-chain-v2-reward-shares.json +++ b/src/test/resources/test-chain-v2-reward-shares.json @@ -23,14 +23,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -57,6 +67,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 0, "calcChainWeightTimestamp": 0, "newConsensusTimestamp": 0, diff --git a/src/test/resources/test-chain-v2.json b/src/test/resources/test-chain-v2.json index f6376be4..5f439602 100644 --- a/src/test/resources/test-chain-v2.json +++ b/src/test/resources/test-chain-v2.json @@ -24,14 +24,24 @@ { "height": 11, "reward": 10 }, { "height": 21, "reward": 1 } ], - "sharesByLevel": [ + "sharesByLevelV1": [ { "id": 1, "levels": [ 1, 2 ], "share": 0.05 }, { "id": 2, "levels": [ 3, 4 ], "share": 0.10 }, { "id": 3, "levels": [ 5, 6 ], "share": 0.15 }, { "id": 4, "levels": [ 7, 8 ], "share": 0.20 }, { "id": 5, "levels": [ 9, 10 ], "share": 0.25 } ], - "qoraHoldersShare": 0.20, + "sharesByLevelV2": [ + { "id": 1, "levels": [ 1, 2 ], "share": 0.06 }, + { "id": 2, "levels": [ 3, 4 ], "share": 0.13 }, + { "id": 3, "levels": [ 5, 6 ], "share": 0.19 }, + { "id": 4, "levels": [ 7, 8 ], "share": 0.26 }, + { "id": 5, "levels": [ 9, 10 ], "share": 0.32 } + ], + "qoraHoldersShareByHeight": [ + { "height": 1, "share": 0.20 }, + { "height": 1000000, "share": 0.01 } + ], "qoraPerQortReward": 250, "minAccountsToActivateShareBin": 30, "shareBinActivationMinLevel": 7, @@ -58,6 +68,7 @@ "atFindNextTransactionFix": 0, "newBlockSigHeight": 999999, "shareBinFix": 999999, + "sharesByLevelV2Height": 999999, "rewardShareLimitTimestamp": 9999999999999, "calcChainWeightTimestamp": 0, "transactionV5Timestamp": 0, diff --git a/src/test/resources/test-settings-v2-qora-holder-reduction.json b/src/test/resources/test-settings-v2-qora-holder-reduction.json new file mode 100644 index 00000000..a489cc12 --- /dev/null +++ b/src/test/resources/test-settings-v2-qora-holder-reduction.json @@ -0,0 +1,11 @@ +{ + "repositoryPath": "testdb", + "restrictedApi": false, + "blockchainConfig": "src/test/resources/test-chain-v2-qora-holder-reduction.json", + "exportPath": "qortal-backup-test", + "bootstrap": false, + "wipeUnconfirmedOnStart": false, + "testNtpOffset": 0, + "minPeers": 0, + "pruneBlockLimit": 100 +}