diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 0f6d42de729..698cfa3205c 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -37,6 +37,7 @@ import bisq.core.dao.governance.param.Param; import bisq.core.dao.governance.period.CycleService; import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proposal.IssuanceProposal; import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.dao.governance.proposal.ProposalConsensus; import bisq.core.dao.governance.proposal.ProposalListPresentation; @@ -66,6 +67,7 @@ import bisq.core.dao.state.model.governance.BondedRoleType; import bisq.core.dao.state.model.governance.Cycle; import bisq.core.dao.state.model.governance.DaoPhase; +import bisq.core.dao.state.model.governance.EvaluatedProposal; import bisq.core.dao.state.model.governance.IssuanceType; import bisq.core.dao.state.model.governance.Proposal; import bisq.core.dao.state.model.governance.Role; @@ -805,4 +807,14 @@ public Set getAllDonationAddresses() { public boolean isParseBlockChainComplete() { return daoStateService.isParseBlockChainComplete(); } + + public long getIssuanceForCycle(Cycle cycle) { + return daoStateService.getEvaluatedProposalList().stream() + .filter(EvaluatedProposal::isAccepted) + .filter(evaluatedProposal -> cycleService.isTxInCycle(cycle, evaluatedProposal.getProposal().getTxId())) + .filter(e -> e.getProposal() instanceof IssuanceProposal) + .map(e -> (IssuanceProposal) e.getProposal()) + .mapToLong(e -> e.getRequestedBsq().value) + .sum(); + } } diff --git a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java index af1db2fcb12..2fe016783cb 100644 --- a/core/src/main/java/bisq/core/dao/governance/period/CycleService.java +++ b/core/src/main/java/bisq/core/dao/governance/period/CycleService.java @@ -97,16 +97,16 @@ public boolean isTxInCycle(Cycle cycle, String txId) { return daoStateService.getTx(txId).filter(tx -> isBlockHeightInCycle(tx.getBlockHeight(), cycle)).isPresent(); } + public boolean isBlockHeightInCycle(int blockHeight, Cycle cycle) { + return blockHeight >= cycle.getHeightOfFirstBlock() && + blockHeight <= cycle.getHeightOfLastBlock(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// - private boolean isBlockHeightInCycle(int blockHeight, Cycle cycle) { - return blockHeight >= cycle.getHeightOfFirstBlock() && - blockHeight <= cycle.getHeightOfLastBlock(); - } - private Optional maybeCreateNewCycle(int blockHeight, LinkedList cycles) { // We want to set the correct phase and cycle before we start parsing a new block. // For Genesis block we did it already in the start method. diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java index c48cce3dead..a1969ef515a 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/ProposalService.java @@ -202,6 +202,11 @@ public double getRequiredThreshold(Proposal proposal) { return daoStateService.getParamValueAsPercentDouble(proposal.getThresholdParam(), chainHeight); } + // We use it in the RestApi do not have a dependency to javafx/ + public List getTempProposalsAsArrayList() { + return new ArrayList<>(tempProposals); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index a77b01b9433..90e2df9af86 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -46,9 +46,11 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Comparator; +import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.Set; import java.util.TreeMap; @@ -75,6 +77,7 @@ public class DaoStateService implements DaoSetupService { @Getter private boolean parseBlockChainComplete; private boolean allowDaoStateChange; + private final Map> cachedTxIdSetByAddress = new HashMap<>(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -299,6 +302,10 @@ public void onParseBlockComplete(Block block) { // generate a hash of the state. allowDaoStateChange = false; daoStateListeners.forEach(l -> l.onDaoStateChanged(block)); + + if (!block.getTxs().isEmpty()) { + cachedTxIdSetByAddress.clear(); + } } // Called after parsing of all pending blocks is completed @@ -493,6 +500,10 @@ public TreeMap getUnspentTxOutputMap() { return daoState.getUnspentTxOutputMap(); } + public TreeMap getSpentInfoMap() { + return daoState.getSpentInfoMap(); + } + public void addUnspentTxOutput(TxOutput txOutput) { assertDaoStateChange(); getUnspentTxOutputMap().put(txOutput.getKey(), txOutput); @@ -1027,6 +1038,54 @@ public Optional getSpentInfo(TxOutput txOutput) { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Addresses + /////////////////////////////////////////////////////////////////////////////////////////// + + public Map> getTxIdSetByAddress() { + // We clear it at each new (non-empty) block, so it gets recreated + if (!cachedTxIdSetByAddress.isEmpty()) { + return cachedTxIdSetByAddress; + } + + Map txIdByConnectedTxOutputKey = new HashMap<>(); + // Add tx ids and addresses from tx outputs + getUnorderedTxStream() + .forEach(tx -> { + tx.getTxOutputs().stream() + .filter(this::isBsqTxOutputType) + .filter(txOutput -> txOutput.getAddress() != null) + .filter(txOutput -> !txOutput.getAddress().isEmpty()) + .forEach(txOutput -> { + String address = txOutput.getAddress(); + Set txIdSet = cachedTxIdSetByAddress.getOrDefault(address, new HashSet<>()); + String txId = tx.getId(); + txIdSet.add(txId); + cachedTxIdSetByAddress.put(address, txIdSet); + tx.getTxInputs().forEach(txInput -> { + txIdByConnectedTxOutputKey.put(txInput.getConnectedTxOutputKey(), txId); + }); + }); + }); + + // Add tx ids and addresses from connected outputs (inputs) + getUnorderedTxOutputStream() + .filter(this::isBsqTxOutputType) + .filter(txOutput -> txOutput.getAddress() != null) + .filter(txOutput -> !txOutput.getAddress().isEmpty()) + .forEach(txOutput -> { + String txId = txIdByConnectedTxOutputKey.get(txOutput.getKey()); + if (txId != null) { + String address = txOutput.getAddress(); + Set txIdSet = cachedTxIdSetByAddress.getOrDefault(address, new HashSet<>()); + txIdSet.add(txId); + cachedTxIdSetByAddress.put(address, txIdSet); + } + }); + + return cachedTxIdSetByAddress; + } + /////////////////////////////////////////////////////////////////////////////////////////// // Vote result data /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/offer/OfferBookService.java b/core/src/main/java/bisq/core/offer/OfferBookService.java index 653e87541da..093c37b334c 100644 --- a/core/src/main/java/bisq/core/offer/OfferBookService.java +++ b/core/src/main/java/bisq/core/offer/OfferBookService.java @@ -18,6 +18,7 @@ package bisq.core.offer; import bisq.core.filter.FilterManager; +import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.provider.price.PriceFeedService; import bisq.core.util.JsonUtil; @@ -213,6 +214,32 @@ public void addOfferBookChangedListener(OfferBookChangedListener offerBookChange offerBookChangedListeners.add(offerBookChangedListener); } + public List getOfferForJsonList() { + return getOffers().stream() + .map(offer -> { + try { + OfferDirection inverseDirection = offer.getDirection() == OfferDirection.BUY ? OfferDirection.SELL : OfferDirection.BUY; + OfferDirection offerDirection = CurrencyUtil.isCryptoCurrency(offer.getCurrencyCode()) ? inverseDirection : offer.getDirection(); + return new OfferForJson(offerDirection, + offer.getCurrencyCode(), + offer.getMinAmount(), + offer.getAmount(), + offer.getPrice(), + offer.getDate(), + offer.getId(), + offer.isUseMarketBasedPrice(), + offer.getMarketPriceMargin(), + offer.getPaymentMethod() + ); + } catch (Throwable t) { + // In case an offer was corrupted with null values we ignore it + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Private diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java index 6c250f8c6eb..0c070e928ec 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -135,6 +135,13 @@ public ObservableSet getObservableTradeStatisticsSet() { return observableTradeStatisticsSet; } + public List getTradeStatisticsList(long dateStart, long dateEnd) { + return observableTradeStatisticsSet.stream() + .filter(x -> x.getDateAsLong() > dateStart && x.getDateAsLong() <= dateEnd) + .sorted((o1, o2) -> (Long.compare(o2.getDateAsLong(), o1.getDateAsLong()))) + .collect(Collectors.toList()); + } + private void maybeDumpStatistics() { if (!dumpStatistics) { return; diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 19a2bf37f37..1c524ee49cc 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -863,11 +863,27 @@ + + + + + + + + + + + + + diff --git a/restapi/build.gradle b/restapi/build.gradle index 042d22d2517..e4166c6068d 100644 --- a/restapi/build.gradle +++ b/restapi/build.gradle @@ -17,6 +17,14 @@ dependencies { implementation(libs.google.guice) { exclude(module: 'guava') } + implementation(libs.bitcoinj) { + exclude(module: 'bcprov-jdk15on') + exclude(module: 'guava') + exclude(module: 'jsr305') + exclude(module: 'okhttp') + exclude(module: 'okio') + exclude(module: 'slf4j-api') + } implementation libs.google.guava implementation libs.google.guice diff --git a/restapi/src/main/java/bisq/restapi/BlockDataToJsonConverter.java b/restapi/src/main/java/bisq/restapi/BlockDataToJsonConverter.java new file mode 100644 index 00000000000..8356fdb7874 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/BlockDataToJsonConverter.java @@ -0,0 +1,128 @@ +package bisq.restapi; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.PubKeyScript; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; +import bisq.core.dao.state.model.blockchain.TxType; + +import com.google.common.io.BaseEncoding; + +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.restapi.dto.JsonBlock; +import bisq.restapi.dto.JsonScriptPubKey; +import bisq.restapi.dto.JsonSpentInfo; +import bisq.restapi.dto.JsonTx; +import bisq.restapi.dto.JsonTxInput; +import bisq.restapi.dto.JsonTxOutput; +import bisq.restapi.dto.JsonTxOutputType; +import bisq.restapi.dto.JsonTxType; + +@Slf4j +public class BlockDataToJsonConverter { + public static JsonBlock getJsonBlock(DaoStateService daoStateService, Block block) { + List jsonTxs = block.getTxs().stream() + .map(tx -> getJsonTx(daoStateService, tx)) + .collect(Collectors.toList()); + return new JsonBlock(block.getHeight(), + block.getTime(), + block.getHash(), + block.getPreviousBlockHash(), + jsonTxs); + } + + public static JsonTx getJsonTx(DaoStateService daoStateService, Tx tx) { + JsonTxType jsonTxType = getJsonTxType(tx); + String jsonTxTypeDisplayString = getJsonTxTypeDisplayString(jsonTxType); + return new JsonTx(tx.getId(), + tx.getBlockHeight(), + tx.getBlockHash(), + tx.getTime(), + getJsonTxInputs(daoStateService, tx), + getJsonTxOutputs(daoStateService, tx), + jsonTxType, + jsonTxTypeDisplayString, + tx.getBurntFee(), + tx.getInvalidatedBsq(), + tx.getUnlockBlockHeight()); + } + + private static List getJsonTxInputs(DaoStateService daoStateService, Tx tx) { + return tx.getTxInputs().stream() + .map(txInput -> { + Optional optionalTxOutput = daoStateService.getConnectedTxOutput(txInput); + if (optionalTxOutput.isPresent()) { + TxOutput connectedTxOutput = optionalTxOutput.get(); + boolean isBsqTxOutputType = daoStateService.isBsqTxOutputType(connectedTxOutput); + return new JsonTxInput(txInput.getConnectedTxOutputIndex(), + txInput.getConnectedTxOutputTxId(), + connectedTxOutput.getValue(), + isBsqTxOutputType, + connectedTxOutput.getAddress(), + tx.getTime()); + } else { + return null; + } + }) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + } + + private static List getJsonTxOutputs(DaoStateService daoStateService, Tx tx) { + JsonTxType jsonTxType = getJsonTxType(tx); + String jsonTxTypeDisplayString = getJsonTxTypeDisplayString(jsonTxType); + return tx.getTxOutputs().stream() + .map(txOutput -> { + boolean isBsqTxOutputType = daoStateService.isBsqTxOutputType(txOutput); + long bsqAmount = isBsqTxOutputType ? txOutput.getValue() : 0; + long btcAmount = !isBsqTxOutputType ? txOutput.getValue() : 0; + PubKeyScript pubKeyScript = txOutput.getPubKeyScript(); + JsonScriptPubKey scriptPubKey = pubKeyScript != null ? new JsonScriptPubKey(pubKeyScript) : null; + JsonSpentInfo spentInfo = daoStateService.getSpentInfo(txOutput).map(JsonSpentInfo::new).orElse(null); + JsonTxOutputType txOutputType = JsonTxOutputType.valueOf(txOutput.getTxOutputType().name()); + int lockTime = txOutput.getLockTime(); + BaseEncoding HEX = BaseEncoding.base16().lowerCase(); + String opReturn = txOutput.getOpReturnData() != null ? HEX.encode(txOutput.getOpReturnData()) : null; + boolean isUnspent = daoStateService.isUnspent(txOutput.getKey()); + return new JsonTxOutput(tx.getId(), + txOutput.getIndex(), + bsqAmount, + btcAmount, + tx.getBlockHeight(), + isBsqTxOutputType, + tx.getBurntFee(), + tx.getInvalidatedBsq(), + txOutput.getAddress(), + scriptPubKey, + spentInfo, + tx.getTime(), + jsonTxType, + jsonTxTypeDisplayString, + txOutputType, + txOutputType.getDisplayString(), + opReturn, + lockTime, + isUnspent + ); + }) + .collect(Collectors.toList()); + } + + private static String getJsonTxTypeDisplayString(JsonTxType jsonTxType) { + return jsonTxType != null ? jsonTxType.getDisplayString() : ""; + } + + private static JsonTxType getJsonTxType(Tx tx) { + TxType txType = tx.getTxType(); + return txType != null ? JsonTxType.valueOf(txType.name()) : null; + } +} diff --git a/restapi/src/main/java/bisq/restapi/RestApi.java b/restapi/src/main/java/bisq/restapi/RestApi.java index 76dd2b75f19..ad684812933 100644 --- a/restapi/src/main/java/bisq/restapi/RestApi.java +++ b/restapi/src/main/java/bisq/restapi/RestApi.java @@ -20,11 +20,19 @@ import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.app.misc.ExecutableForAppWithP2p; +import bisq.core.dao.DaoFacade; import bisq.core.dao.SignVerifyService; import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; import bisq.core.dao.governance.bond.role.BondedRolesRepository; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.offer.OfferBookService; +import bisq.core.provider.price.PriceFeedService; +import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.common.app.Version; @@ -33,6 +41,8 @@ import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + @Slf4j public class RestApi extends ExecutableForAppWithP2p { @Getter @@ -47,6 +57,19 @@ public class RestApi extends ExecutableForAppWithP2p { private SignVerifyService signVerifyService; private DaoStateSnapshotService daoStateSnapshotService; private Preferences preferences; + @Getter + private DaoFacade daoFacade; + @Getter + private ProposalService proposalService; + @Getter + private CycleService cycleService; + @Getter + private TradeStatisticsManager tradeStatisticsManager; + @Getter + private OfferBookService offerBookService; + private PriceFeedService priceFeedService; + @Getter + private boolean parseBlockCompleteAfterBatchProcessing; public RestApi() { super("Bisq Rest Api", "bisq_restapi", "bisq_restapi", Version.VERSION); @@ -74,6 +97,20 @@ protected void applyInjector() { bondedRolesRepository = injector.getInstance(BondedRolesRepository.class); signVerifyService = injector.getInstance(SignVerifyService.class); daoStateSnapshotService = injector.getInstance(DaoStateSnapshotService.class); + daoFacade = injector.getInstance(DaoFacade.class); + proposalService = injector.getInstance(ProposalService.class); + cycleService = injector.getInstance(CycleService.class); + tradeStatisticsManager = injector.getInstance(TradeStatisticsManager.class); + offerBookService = injector.getInstance(OfferBookService.class); + priceFeedService = injector.getInstance(PriceFeedService.class); + + daoStateService.addDaoStateListener(new DaoStateListener() { + @Override + public void onParseBlockCompleteAfterBatchProcessing(Block block) { + log.error("onParseBlockCompleteAfterBatchProcessing"); + parseBlockCompleteAfterBatchProcessing = true; + } + }); } @Override @@ -88,5 +125,11 @@ protected void onHiddenServicePublished() { super.onHiddenServicePublished(); accountAgeWitnessService.onAllServicesInitialized(); + priceFeedService.setCurrencyCodeOnInit(); + priceFeedService.initialRequestPriceFeed(); + } + + public void checkDaoReady() { + checkArgument(parseBlockCompleteAfterBatchProcessing, "DAO not ready yet"); } } diff --git a/restapi/src/main/java/bisq/restapi/RestApiMain.java b/restapi/src/main/java/bisq/restapi/RestApiMain.java index aa7cabd053b..505e143adb1 100644 --- a/restapi/src/main/java/bisq/restapi/RestApiMain.java +++ b/restapi/src/main/java/bisq/restapi/RestApiMain.java @@ -31,6 +31,10 @@ import bisq.restapi.endpoints.AccountAgeApi; import bisq.restapi.endpoints.BondedReputationApi; import bisq.restapi.endpoints.BondedRoleVerificationApi; +import bisq.restapi.endpoints.ExplorerBlocksApi; +import bisq.restapi.endpoints.ExplorerDaoApi; +import bisq.restapi.endpoints.ExplorerMarketsApi; +import bisq.restapi.endpoints.ExplorerTransactionsApi; import bisq.restapi.endpoints.ProofOfBurnApi; import bisq.restapi.endpoints.SignedWitnessApi; import bisq.restapi.error.CustomExceptionMapper; @@ -63,6 +67,10 @@ public static void main(String[] args) throws Exception { .register(BondedRoleVerificationApi.class) .register(AccountAgeApi.class) .register(SignedWitnessApi.class) + .register(ExplorerMarketsApi.class) + .register(ExplorerDaoApi.class) + .register(ExplorerBlocksApi.class) + .register(ExplorerTransactionsApi.class) .register(SwaggerResolution.class); daoNodeApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort); }); diff --git a/restapi/src/main/java/bisq/restapi/dto/BsqStatsDto.java b/restapi/src/main/java/bisq/restapi/dto/BsqStatsDto.java new file mode 100644 index 00000000000..d329081c639 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/BsqStatsDto.java @@ -0,0 +1,14 @@ +package bisq.restapi.dto; + +import lombok.Value; + +@Value +public class BsqStatsDto { + long minted; + long burnt; + int addresses; + int unspent_txos; + int spent_txos; + int height; + int genesisHeight; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonBlock.java b/restapi/src/main/java/bisq/restapi/dto/JsonBlock.java new file mode 100644 index 00000000000..e00b6f7c716 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonBlock.java @@ -0,0 +1,31 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import java.util.List; + +import lombok.Value; + +@Value +public class JsonBlock { + int height; + long time; // in ms + String hash; + String previousBlockHash; + List txs; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonBlocks.java b/restapi/src/main/java/bisq/restapi/dto/JsonBlocks.java new file mode 100644 index 00000000000..d4fad5329b0 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonBlocks.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import java.util.List; + +import lombok.Value; + +@Value +class JsonBlocks { + int chainHeight; + List blocks; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonCurrency.java b/restapi/src/main/java/bisq/restapi/dto/JsonCurrency.java new file mode 100644 index 00000000000..cd435685d20 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonCurrency.java @@ -0,0 +1,28 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Value; + +@Value +public class JsonCurrency { + String code; + String name; + int precision; + String _type; // "fiat" or "crypto" +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonDaoCycle.java b/restapi/src/main/java/bisq/restapi/dto/JsonDaoCycle.java new file mode 100644 index 00000000000..c474d9eb4f1 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonDaoCycle.java @@ -0,0 +1,31 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Value; + +@Value +public class JsonDaoCycle { + int heightOfFirstBlock; + int cycleIndex; + long startDate; // in ms + int proposalCount; + long burnedAmount; + long issuedAmount; + Boolean inProgress; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonOffer.java b/restapi/src/main/java/bisq/restapi/dto/JsonOffer.java new file mode 100644 index 00000000000..bb531f5b6ce --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonOffer.java @@ -0,0 +1,29 @@ +package bisq.restapi.dto; + +import lombok.Value; + +@Value +public class JsonOffer { + String direction; + String currencyCode; + long minAmount; + long amount; + long price; + long date; + boolean useMarketBasedPrice; + double marketPriceMargin; + String paymentMethod; + String id; + String currencyPair; + String primaryMarketDirection; + String priceDisplayString; + String primaryMarketAmountDisplayString; + String primaryMarketMinAmountDisplayString; + String primaryMarketVolumeDisplayString; + String primaryMarketMinVolumeDisplayString; + long primaryMarketPrice; + long primaryMarketAmount; + long primaryMarketMinAmount; + long primaryMarketVolume; + long primaryMarketMinVolume; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonScriptPubKey.java b/restapi/src/main/java/bisq/restapi/dto/JsonScriptPubKey.java new file mode 100644 index 00000000000..4b547e8f591 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonScriptPubKey.java @@ -0,0 +1,43 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import bisq.core.dao.state.model.blockchain.PubKeyScript; + +import java.util.List; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Value +public class JsonScriptPubKey { + List addresses; + String asm; + String hex; + int reqSigs; + String type; + + public JsonScriptPubKey(PubKeyScript pubKeyScript) { + addresses = pubKeyScript.getAddresses(); + asm = pubKeyScript.getAsm(); + hex = pubKeyScript.getHex(); + reqSigs = pubKeyScript.getReqSigs(); + type = pubKeyScript.getScriptType().toString(); + } +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonSpentInfo.java b/restapi/src/main/java/bisq/restapi/dto/JsonSpentInfo.java new file mode 100644 index 00000000000..cd6e7307c9b --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonSpentInfo.java @@ -0,0 +1,35 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import bisq.core.dao.state.model.blockchain.SpentInfo; + +import lombok.Value; + +@Value +public class JsonSpentInfo { + long height; + int inputIndex; + String txId; + + public JsonSpentInfo(SpentInfo spentInfo) { + height = spentInfo.getBlockHeight(); + inputIndex = spentInfo.getInputIndex(); + txId = spentInfo.getTxId(); + } +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTradeInfo.java b/restapi/src/main/java/bisq/restapi/dto/JsonTradeInfo.java new file mode 100644 index 00000000000..5664d9c9d14 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTradeInfo.java @@ -0,0 +1,34 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Value; + +// equivalent of bisq.core.trade.statistics.TradeStatisticsForJson +@Value +public class JsonTradeInfo { + String currency; + long tradePrice; + long tradeAmount; + long tradeDate; + String paymentMethod; + String currencyPair; + long primaryMarketTradePrice; + long primaryMarketTradeAmount; + long primaryMarketTradeVolume; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTx.java b/restapi/src/main/java/bisq/restapi/dto/JsonTx.java new file mode 100644 index 00000000000..a0ee76f219e --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTx.java @@ -0,0 +1,86 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import bisq.common.app.Version; + +import java.util.List; +import java.util.Objects; + +import lombok.Value; + +@Value +public class JsonTx { + String txVersion = Version.BSQ_TX_VERSION; + String id; + int blockHeight; + String blockHash; + long time; + List inputs; + List outputs; + JsonTxType txType; + String txTypeDisplayString; + long burntFee; + long invalidatedBsq; + // If not set it is -1. LockTime of 0 is a valid value. + int unlockBlockHeight; + + public JsonTx(String id, int blockHeight, String blockHash, long time, List inputs, + List outputs, JsonTxType txType, String txTypeDisplayString, long burntFee, + long invalidatedBsq, int unlockBlockHeight) { + this.id = id; + this.blockHeight = blockHeight; + this.blockHash = blockHash; + this.time = time; + this.inputs = inputs; + this.outputs = outputs; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.unlockBlockHeight = unlockBlockHeight; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JsonTx)) return false; + if (!super.equals(o)) return false; + JsonTx jsonTx = (JsonTx) o; + return blockHeight == jsonTx.blockHeight && + time == jsonTx.time && + burntFee == jsonTx.burntFee && + invalidatedBsq == jsonTx.invalidatedBsq && + unlockBlockHeight == jsonTx.unlockBlockHeight && + Objects.equals(txVersion, jsonTx.txVersion) && + Objects.equals(id, jsonTx.id) && + Objects.equals(blockHash, jsonTx.blockHash) && + Objects.equals(inputs, jsonTx.inputs) && + Objects.equals(outputs, jsonTx.outputs) && + txType.name().equals(jsonTx.txType.name()) && + Objects.equals(txTypeDisplayString, jsonTx.txTypeDisplayString); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txVersion, id, blockHeight, blockHash, time, inputs, outputs, + txType.name(), txTypeDisplayString, burntFee, invalidatedBsq, unlockBlockHeight); + } +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTxInput.java b/restapi/src/main/java/bisq/restapi/dto/JsonTxInput.java new file mode 100644 index 00000000000..f1574ac7ae3 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTxInput.java @@ -0,0 +1,33 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Value; + +import javax.annotation.concurrent.Immutable; + +@Value +@Immutable +public class JsonTxInput { + int spendingTxOutputIndex; // connectedTxOutputIndex + String spendingTxId; // connectedTxOutputTxId + long bsqAmount; + Boolean isVerified; // isBsqTxOutputType + String address; + long time; +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTxOutput.java b/restapi/src/main/java/bisq/restapi/dto/JsonTxOutput.java new file mode 100644 index 00000000000..5f36d465c83 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTxOutput.java @@ -0,0 +1,134 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import bisq.common.app.Version; + +import java.util.Objects; + +import lombok.Value; + +import javax.annotation.Nullable; + +@Value +public class JsonTxOutput { + String txVersion = Version.BSQ_TX_VERSION; + String txId; + int index; + long bsqAmount; + long btcAmount; + int height; + Boolean isVerified; // isBsqTxOutputType + long burntFee; + long invalidatedBsq; + String address; + @Nullable + JsonScriptPubKey scriptPubKey; + @Nullable + JsonSpentInfo spentInfo; + long time; + JsonTxType txType; + String txTypeDisplayString; + JsonTxOutputType txOutputType; + String txOutputTypeDisplayString; + @Nullable + String opReturn; + int lockTime; + Boolean isUnspent; + + public JsonTxOutput(String txId, + int index, + long bsqAmount, + long btcAmount, + int height, + boolean isVerified, + long burntFee, + long invalidatedBsq, + String address, + JsonScriptPubKey scriptPubKey, + JsonSpentInfo spentInfo, + long time, + JsonTxType txType, + String txTypeDisplayString, + JsonTxOutputType txOutputType, + String txOutputTypeDisplayString, + String opReturn, + int lockTime, + boolean isUnspent) { + this.txId = txId; + this.index = index; + this.bsqAmount = bsqAmount; + this.btcAmount = btcAmount; + this.height = height; + this.isVerified = isVerified; + this.burntFee = burntFee; + this.invalidatedBsq = invalidatedBsq; + this.address = address; + this.scriptPubKey = scriptPubKey; + this.spentInfo = spentInfo; + this.time = time; + this.txType = txType; + this.txTypeDisplayString = txTypeDisplayString; + this.txOutputType = txOutputType; + this.txOutputTypeDisplayString = txOutputTypeDisplayString; + this.opReturn = opReturn; + this.lockTime = lockTime; + this.isUnspent = isUnspent; + } + + String getId() { + return txId + ":" + index; + } + + // Enums must not be used directly for hashCode or equals as it delivers the Object.hashCode (internal address)! + // The equals and hashCode methods cannot be overwritten in Enums. + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof JsonTxOutput)) return false; + if (!super.equals(o)) return false; + JsonTxOutput that = (JsonTxOutput) o; + return index == that.index && + bsqAmount == that.bsqAmount && + btcAmount == that.btcAmount && + height == that.height && + isVerified == that.isVerified && + burntFee == that.burntFee && + invalidatedBsq == that.invalidatedBsq && + time == that.time && + lockTime == that.lockTime && + isUnspent == that.isUnspent && + Objects.equals(txVersion, that.txVersion) && + Objects.equals(txId, that.txId) && + Objects.equals(address, that.address) && + Objects.equals(scriptPubKey, that.scriptPubKey) && + Objects.equals(spentInfo, that.spentInfo) && + txType.name().equals(that.txType.name()) && + Objects.equals(txTypeDisplayString, that.txTypeDisplayString) && + txOutputType == that.txOutputType && + Objects.equals(txOutputTypeDisplayString, that.txOutputTypeDisplayString) && + Objects.equals(opReturn, that.opReturn); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txVersion, txId, index, bsqAmount, btcAmount, height, isVerified, + burntFee, invalidatedBsq, address, scriptPubKey, spentInfo, time, txType.name(), txTypeDisplayString, + txOutputType, txOutputTypeDisplayString, opReturn, lockTime, isUnspent); + } +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTxOutputType.java b/restapi/src/main/java/bisq/restapi/dto/JsonTxOutputType.java new file mode 100644 index 00000000000..17033a22a8b --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTxOutputType.java @@ -0,0 +1,51 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Getter; + +@Getter +// Need to be in sync with TxOutputType +public enum JsonTxOutputType { + UNDEFINED("Undefined"), + UNDEFINED_OUTPUT("Undefined output"), + GENESIS_OUTPUT("Genesis"), + BSQ_OUTPUT("BSQ"), + BTC_OUTPUT("BTC"), + PROPOSAL_OP_RETURN_OUTPUT("Proposal opReturn"), + COMP_REQ_OP_RETURN_OUTPUT("Compensation request opReturn"), + REIMBURSEMENT_OP_RETURN_OUTPUT("Reimbursement request opReturn"), + CONFISCATE_BOND_OP_RETURN_OUTPUT("Confiscate bond opReturn"), + ISSUANCE_CANDIDATE_OUTPUT("Issuance candidate"), + BLIND_VOTE_LOCK_STAKE_OUTPUT("Blind vote lock stake"), + BLIND_VOTE_OP_RETURN_OUTPUT("Blind vote opReturn"), + VOTE_REVEAL_UNLOCK_STAKE_OUTPUT("Vote reveal unlock stake"), + VOTE_REVEAL_OP_RETURN_OUTPUT("Vote reveal opReturn"), + ASSET_LISTING_FEE_OP_RETURN_OUTPUT("Asset listing fee OpReturn"), + PROOF_OF_BURN_OP_RETURN_OUTPUT("Proof of burn opReturn"), + LOCKUP_OUTPUT("Lockup"), + LOCKUP_OP_RETURN_OUTPUT("Lockup opReturn"), + UNLOCK_OUTPUT("Unlock"), + INVALID_OUTPUT("Invalid"); + + private final String displayString; + + JsonTxOutputType(String displayString) { + this.displayString = displayString; + } +} diff --git a/restapi/src/main/java/bisq/restapi/dto/JsonTxType.java b/restapi/src/main/java/bisq/restapi/dto/JsonTxType.java new file mode 100644 index 00000000000..df156ab435f --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/dto/JsonTxType.java @@ -0,0 +1,48 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.dto; + +import lombok.Getter; + +@Getter +// Need to be in sync with TxOutputType +public enum JsonTxType { + UNDEFINED("Undefined"), + UNDEFINED_TX_TYPE("Undefined tx type"), + UNVERIFIED("Unverified"), + INVALID("Invalid"), + GENESIS("Genesis"), + TRANSFER_BSQ("Transfer BSQ"), + PAY_TRADE_FEE("Pay trade fee"), + PROPOSAL("Proposal"), + COMPENSATION_REQUEST("Compensation request"), + REIMBURSEMENT_REQUEST("Reimbursement request"), + BLIND_VOTE("Blind vote"), + VOTE_REVEAL("Vote reveal"), + LOCKUP("Lockup"), + UNLOCK("Unlock"), + ASSET_LISTING_FEE("Asset listing fee"), + PROOF_OF_BURN("Proof of burn"), + IRREGULAR("Irregular"); + + private final String displayString; + + JsonTxType(String displayString) { + this.displayString = displayString; + } +} diff --git a/restapi/src/main/java/bisq/restapi/endpoints/ExplorerBlocksApi.java b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerBlocksApi.java new file mode 100644 index 00000000000..2d65506f104 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerBlocksApi.java @@ -0,0 +1,86 @@ +package bisq.restapi.endpoints; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Block; + +import java.util.List; +import java.util.Optional; + +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; + + + +import bisq.restapi.BlockDataToJsonConverter; +import bisq.restapi.RestApi; +import bisq.restapi.RestApiMain; +import bisq.restapi.dto.JsonBlock; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.Content; +import io.swagger.v3.oas.annotations.media.Schema; +import io.swagger.v3.oas.annotations.responses.ApiResponse; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +@Slf4j +@Path("/explorer/blocks") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "BLOCKS API") +public class ExplorerBlocksApi { + private final DaoStateService daoStateService; + private final RestApi restApi; + + public ExplorerBlocksApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoStateService = restApi.getDaoStateService(); + } + + // http://localhost:8081/api/v1/explorer/blocks/get-bsq-block-by-height/139 + @Operation(description = "Request BSQ block details") + @ApiResponse(responseCode = "200", description = "The BSQ block", + content = {@Content(mediaType = MediaType.APPLICATION_JSON, + schema = @Schema(allOf = JsonBlock.class))} + ) + @GET + @Path("get-bsq-block-by-height/{block-height}") + public JsonBlock getBsqBlockByHeight(@Parameter(description = "Block Height") @PathParam("block-height") int blockHeight) { + restApi.checkDaoReady(); + List blocks = daoStateService.getBlocks(); + Optional jsonBlock = checkNotNull(blocks.stream()) + .filter(block -> block.getHeight() == blockHeight) + .map(block -> BlockDataToJsonConverter.getJsonBlock(daoStateService, block)) + .findFirst(); + if (jsonBlock.isPresent()) { + log.info("supplying block at height {} to client.", blockHeight); + return jsonBlock.get(); + } + log.warn("block {} not found!", blockHeight); + return null; + } + + //http://localhost:8081/api/v1/explorer/blocks/get-bsq-block-by-hash/2e90186bd0958e8d4821e0b2546e018d70e3b4f136af8676e3571ca2363ce7f8 + @GET + @Path("get-bsq-block-by-hash/{block-hash}") + public JsonBlock getBsqBlockByHash(@Parameter(description = "Block Hash") @PathParam("block-hash") String hash) { + restApi.checkDaoReady(); + List blocks = daoStateService.getBlocks(); + Optional jsonBlock = checkNotNull(blocks.stream()) + .filter(block -> block.getHash().equalsIgnoreCase(hash)) + .map(block -> BlockDataToJsonConverter.getJsonBlock(daoStateService, block)) + .findFirst(); + if (jsonBlock.isPresent()) { + log.info("supplying block {} to client.", hash); + return jsonBlock.get(); + } + log.warn("block {} not found!", hash); + return null; + } +} diff --git a/restapi/src/main/java/bisq/restapi/endpoints/ExplorerDaoApi.java b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerDaoApi.java new file mode 100644 index 00000000000..8c761cd138e --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerDaoApi.java @@ -0,0 +1,119 @@ +package bisq.restapi.endpoints; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.proposal.ProposalService; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.Issuance; +import bisq.core.dao.state.model.governance.IssuanceType; +import bisq.core.dao.state.model.governance.Proposal; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.restapi.RestApi; +import bisq.restapi.RestApiMain; +import bisq.restapi.dto.BsqStatsDto; +import bisq.restapi.dto.JsonDaoCycle; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +@Slf4j +@Path("/explorer/dao") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "EXPLORER API") +public class ExplorerDaoApi { + private final DaoStateService daoStateService; + private final DaoFacade daoFacade; + private final ProposalService proposalService; + private final CycleService cycleService; + private final RestApi restApi; + + public ExplorerDaoApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoStateService = restApi.getDaoStateService(); + proposalService = restApi.getProposalService(); + cycleService = restApi.getCycleService(); + daoFacade = restApi.getDaoFacade(); + } + + //http://localhost:8081/api/v1/explorer/dao/get-bsq-stats + @GET + @Path("get-bsq-stats") + public BsqStatsDto getBsqStats() { + restApi.checkDaoReady(); + long genesisSupply = daoFacade.getGenesisTotalSupply().getValue(); + long issuedByCompensations = daoStateService.getIssuanceSetForType(IssuanceType.COMPENSATION).stream().mapToLong(Issuance::getAmount).sum(); + long issuedByReimbursements = daoStateService.getIssuanceSetForType(IssuanceType.REIMBURSEMENT).stream().mapToLong(Issuance::getAmount).sum(); + long minted = genesisSupply + issuedByCompensations + issuedByReimbursements; + long burnt = daoStateService.getTotalAmountOfBurntBsq(); + int unspentTxos = daoStateService.getUnspentTxOutputMap().size(); + int spentTxos = daoStateService.getSpentInfoMap().size(); + int numAddresses = daoStateService.getTxIdSetByAddress().size(); + log.info("client requested BSQ stats, height={}", daoFacade.getChainHeight()); + return new BsqStatsDto(minted, burnt, numAddresses, unspentTxos, spentTxos, + daoFacade.getChainHeight(), daoFacade.getGenesisBlockHeight()); + } + + @GET + @Path("query-dao-cycles") + public List queryDaoCycles() { + restApi.checkDaoReady(); + Set cyclesAdded = new HashSet<>(); + List result = new ArrayList<>(); + // Creating our data structure is a bit expensive so we ensure to only create the CycleListItems once. + daoStateService.getCycles().stream() + .filter(cycle -> !cyclesAdded.contains(cycle.getHeightOfFirstBlock())) + .filter(cycle -> cycleService.getCycleIndex(cycle) >= 0) // change this if you only need the latest n cycles + .forEach(cycle -> { + long cycleStartTime = daoStateService.getBlockTimeAtBlockHeight(cycle.getHeightOfFirstBlock()); + int cycleIndex = cycleService.getCycleIndex(cycle); + boolean isCycleInProgress = cycleService.isBlockHeightInCycle(daoFacade.getChainHeight(), cycle); + log.info("Cycle {} {}", cycleIndex, isCycleInProgress ? "pending" : "complete"); + List proposalsForCycle = proposalService.getValidatedProposals().stream() + .filter(proposal -> cycleService.isTxInCycle(cycle, proposal.getTxId())) + .collect(Collectors.toList()); + int tempProposalCount = 0; + if (isCycleInProgress) { + tempProposalCount = (int) proposalService.getTempProposalsAsArrayList().stream() + .filter(proposal -> cycleService.isTxInCycle(cycle, proposal.getTxId())) + .count(); + } + + long burnedAmount = daoFacade.getBurntFeeTxs().stream() + .filter(e -> cycleService.isBlockHeightInCycle(e.getBlockHeight(), cycle)) + .mapToLong(Tx::getBurntFee) + .sum(); + + int proposalCount = proposalsForCycle.size() + tempProposalCount; + long issuedAmount = daoFacade.getIssuanceForCycle(cycle); + JsonDaoCycle resultsOfCycle = new JsonDaoCycle( + cycle.getHeightOfFirstBlock(), + cycleIndex + 1, + cycleStartTime, + proposalCount, + burnedAmount, + issuedAmount, + isCycleInProgress); + cyclesAdded.add(resultsOfCycle.getHeightOfFirstBlock()); + result.add(resultsOfCycle); + }); + result.sort(Comparator.comparing(e -> ((JsonDaoCycle) e).getCycleIndex()).reversed()); + log.info("client requested dao cycles, returning {} records", result.size()); + return result; + } +} diff --git a/restapi/src/main/java/bisq/restapi/endpoints/ExplorerMarketsApi.java b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerMarketsApi.java new file mode 100644 index 00000000000..8211ebf12ac --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerMarketsApi.java @@ -0,0 +1,145 @@ +package bisq.restapi.endpoints; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.offer.OfferBookService; +import bisq.core.trade.statistics.TradeStatistics3; +import bisq.core.trade.statistics.TradeStatisticsManager; + +import bisq.common.util.MathUtils; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.restapi.RestApi; +import bisq.restapi.RestApiMain; +import bisq.restapi.dto.JsonCurrency; +import bisq.restapi.dto.JsonOffer; +import bisq.restapi.dto.JsonTradeInfo; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +@Slf4j +@Path("/explorer/markets") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "EXPLORER API") +public class ExplorerMarketsApi { + private static final long MONTH = TimeUnit.DAYS.toMillis(30); + + private final OfferBookService offerBookService; + private final TradeStatisticsManager tradeStatisticsManager; + + public ExplorerMarketsApi(@Context Application application) { + RestApi restApi = ((RestApiMain) application).getRestApi(); + offerBookService = restApi.getOfferBookService(); + tradeStatisticsManager = restApi.getTradeStatisticsManager(); + } + + // http://localhost:8081/api/v1/explorer/markets/get-currencies + @GET + @Path("get-currencies") + public List getBisqCurrencies() { + ArrayList fiatCurrencyList = CurrencyUtil.getMatureMarketCurrencies().stream() + .map(e -> new JsonCurrency(e.getCode(), e.getName(), 8, "fiat")) + .collect(Collectors.toCollection(ArrayList::new)); + ArrayList cryptoCurrencyList = CurrencyUtil.getMainCryptoCurrencies().stream() + .map(e -> new JsonCurrency(e.getCode(), e.getName(), 8, "crypto")) + .collect(Collectors.toCollection(ArrayList::new)); + List result = Stream.concat(fiatCurrencyList.stream(), cryptoCurrencyList.stream()).collect(Collectors.toList()); + log.info("client requested currencies, returning {} currencies", result.size()); + return result; + } + + @GET + @Path("get-offers") + public List getBisqOffers() { + List result = offerBookService.getOfferForJsonList().stream() + .map(offerForJson -> new JsonOffer( + offerForJson.direction.name(), + offerForJson.currencyCode, + offerForJson.minAmount, + offerForJson.amount, + offerForJson.price, + offerForJson.date, + offerForJson.useMarketBasedPrice, + offerForJson.marketPriceMargin, + offerForJson.paymentMethod, + offerForJson.id, + offerForJson.currencyPair, + offerForJson.direction.name(), + offerForJson.priceDisplayString, + offerForJson.primaryMarketAmountDisplayString, + offerForJson.primaryMarketMinAmountDisplayString, + offerForJson.primaryMarketVolumeDisplayString, + offerForJson.primaryMarketMinVolumeDisplayString, + offerForJson.primaryMarketPrice, + offerForJson.primaryMarketAmount, + offerForJson.primaryMarketMinAmount, + offerForJson.primaryMarketVolume, + offerForJson.primaryMarketMinVolume) + ) + .collect(Collectors.toList()); + log.info("client requested offers, returning {} offers", result.size()); + return result; + } + + @GET + @Path("get-trades/{newestTimestamp}/{oldestTimestamp}") + public List getBisqTrades(@PathParam("newestTimestamp") long newestTimestamp, + @PathParam("oldestTimestamp") long oldestTimestamp) { + log.info("newestTimestamp: {} oldestTimestamp: {}", newestTimestamp, oldestTimestamp); + + long to = new Date().getTime(); + long from = newestTimestamp > 0 ? newestTimestamp : to - MONTH; // 30 days default + ArrayList result = new ArrayList<>(); + List tradeStatisticsList = tradeStatisticsManager.getTradeStatisticsList(from, to); + log.info("requesting a fresh batch of trades {}", tradeStatisticsList.size()); + if (tradeStatisticsList.size() < 200 && oldestTimestamp > 0) { + to = oldestTimestamp; + from = to - MONTH; + List additional = tradeStatisticsManager.getTradeStatisticsList(from, to); + tradeStatisticsList.addAll(additional); + log.info("requesting an additional older batch of trades {}", additional.size()); + } + tradeStatisticsList.forEach(x -> { + try { + String currencyPair = Res.getBaseCurrencyCode() + "/" + x.getCurrency(); + // we use precision 4 for fiat based price but on the markets api we use precision 8 so we scale up by 10000 + long primaryMarketTradePrice = (long) MathUtils.scaleUpByPowerOf10(x.getTradePrice().getValue(), 4); + long primaryMarketTradeAmount = x.getAmount(); + // we use precision 4 for fiat but on the markets api we use precision 8 so we scale up by 10000 + long primaryMarketTradeVolume = x.getTradeVolume() != null ? + (long) MathUtils.scaleUpByPowerOf10(x.getTradeVolume().getValue(), 4) : 0; + + if (CurrencyUtil.isCryptoCurrency(x.getCurrency())) { + currencyPair = x.getCurrency() + "/" + Res.getBaseCurrencyCode(); + primaryMarketTradePrice = x.getTradePrice().getValue(); + primaryMarketTradeAmount = x.getTradeVolume().getValue(); // getVolumeByAmount? + primaryMarketTradeVolume = x.getAmount(); + } + JsonTradeInfo jsonTradeInfo = new JsonTradeInfo(x.getCurrency(), x.getPrice(), x.getAmount(), + x.getDateAsLong(), x.getPaymentMethodId(), currencyPair, primaryMarketTradePrice, + primaryMarketTradeAmount, primaryMarketTradeVolume); + result.add(jsonTradeInfo); + } catch (Throwable t) { + log.error("Iterating tradeStatisticsList failed", t); + } + }); + log.info("client requested trades, returning {} trades", result.size()); + return result; + } +} diff --git a/restapi/src/main/java/bisq/restapi/endpoints/ExplorerTransactionsApi.java b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerTransactionsApi.java new file mode 100644 index 00000000000..3ce58aabdf9 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerTransactionsApi.java @@ -0,0 +1,137 @@ +/* + * This file is part of Bisq. + * + * Bisq is free software: you can redistribute it and/or modify it + * under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or (at + * your option) any later version. + * + * Bisq is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public + * License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with Bisq. If not, see . + */ + +package bisq.restapi.endpoints; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxType; + +import java.util.Collection; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.restapi.BlockDataToJsonConverter; +import bisq.restapi.RestApi; +import bisq.restapi.RestApiMain; +import bisq.restapi.dto.JsonTx; +import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Application; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; + +@Slf4j +@Path("/explorer/transactions") +@Produces(MediaType.APPLICATION_JSON) +@Tag(name = "TRANSACTIONS API") +public class ExplorerTransactionsApi { + private final DaoStateService daoStateService; + private final RestApi restApi; + + public ExplorerTransactionsApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoStateService = restApi.getDaoStateService(); + } + + @GET + @Path("get-bsq-tx/{txid}") + public JsonTx getTx(@Parameter(description = "TxId") + @PathParam("txid") String txId) { + restApi.checkDaoReady(); + Optional jsonTx = daoStateService.getUnorderedTxStream() + .filter(t -> t.getId().equals(txId)) + .map(tx -> BlockDataToJsonConverter.getJsonTx(daoStateService, tx)) + .findFirst(); + if (jsonTx.isPresent()) { + log.info("supplying tx {} to client.", txId); + return jsonTx.get(); + } + log.warn("txid {} not found!", txId); + return null; + } + + @GET + @Path("get-bsq-tx-for-addr/{addr}") + public List getBisqTxForAddr(@PathParam("addr") String address) { + restApi.checkDaoReady(); + // In case we get a prefixed address marking BSQ addresses we remove the prefix + if (address.startsWith("B")) { + address = address.substring(1, address.length()); + } + String finalAddress = address; + List result = daoStateService.getTxIdSetByAddress().entrySet().stream() + .filter(e -> e.getKey().equals(finalAddress)) + .map(Map.Entry::getValue) + .flatMap(Collection::stream) + .flatMap(txId -> daoStateService.getTx(txId).stream()) + .map(tx -> BlockDataToJsonConverter.getJsonTx(daoStateService, tx)) + .collect(Collectors.toList()); + log.info("getBisqTxForAddr: returning {} items.", result.size()); + return result; + } + + @GET + @Path("query-txs-paginated/{start}/{count}/{filters}") + public List queryTxsPaginated(@PathParam("start") int start, + @PathParam("count") int count, + @PathParam("filters") String filters) { + restApi.checkDaoReady(); + log.info("filters: {}", filters); + List jsonTxs = daoStateService.getUnorderedTxStream() + .sorted(Comparator.comparing(BaseTx::getTime).reversed()) + .filter(tx -> hasMatchingTxType(tx, filters)) + .skip(start) + .limit(count) + .map(tx -> BlockDataToJsonConverter.getJsonTx(daoStateService, tx)) + .collect(Collectors.toList()); + log.info("supplying {} jsonTxs to client from index {}", jsonTxs.size(), start); + return jsonTxs; + } + + private boolean hasMatchingTxType(Tx tx, String filters) { + String[] filterTokens = filters.split("~"); + if (filterTokens.length < 1 || filters.equalsIgnoreCase("~")) { + return true; + } + for (String filter : filterTokens) { + try { + TxType txType = Enum.valueOf(TxType.class, filter); + if (tx.getTxType() == txType) { + return true; + } + } catch (Exception e) { + log.error("Could not resolve TxType Enum from " + filter, e); + return false; + } + } + return false; + } + +}