From bd9136fc97168866b68a99889133a651fecc3104 Mon Sep 17 00:00:00 2001 From: HenrikJannsen Date: Sun, 21 Jul 2024 17:39:00 +0700 Subject: [PATCH] Add explorer endpoints and dtos Signed-off-by: HenrikJannsen --- .../java/bisq/restapi/DaoExplorerService.java | 261 ++++++++++++++++++ .../src/main/java/bisq/restapi/RestApi.java | 23 ++ .../main/java/bisq/restapi/RestApiMain.java | 8 + .../bisq/restapi/dto/BondedReputationDto.java | 2 - .../dto/BondedRoleVerificationDto.java | 2 + .../java/bisq/restapi/dto/BsqStatsDto.java | 14 + .../main/java/bisq/restapi/dto/JsonBlock.java | 31 +++ .../java/bisq/restapi/dto/JsonBlocks.java | 28 ++ .../java/bisq/restapi/dto/JsonCurrency.java | 28 ++ .../java/bisq/restapi/dto/JsonDaoCycle.java | 31 +++ .../main/java/bisq/restapi/dto/JsonOffer.java | 29 ++ .../bisq/restapi/dto/JsonScriptPubKey.java | 43 +++ .../java/bisq/restapi/dto/JsonSpentInfo.java | 35 +++ .../java/bisq/restapi/dto/JsonTradeInfo.java | 34 +++ .../main/java/bisq/restapi/dto/JsonTx.java | 86 ++++++ .../java/bisq/restapi/dto/JsonTxInput.java | 33 +++ .../java/bisq/restapi/dto/JsonTxOutput.java | 134 +++++++++ .../bisq/restapi/dto/JsonTxOutputType.java | 51 ++++ .../java/bisq/restapi/dto/JsonTxType.java | 48 ++++ .../restapi/endpoints/ExplorerBlocksApi.java | 87 ++++++ .../restapi/endpoints/ExplorerDaoApi.java | 124 +++++++++ .../restapi/endpoints/ExplorerMarketsApi.java | 142 ++++++++++ .../endpoints/ExplorerTransactionsApi.java | 133 +++++++++ 23 files changed, 1405 insertions(+), 2 deletions(-) create mode 100644 restapi/src/main/java/bisq/restapi/DaoExplorerService.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/BsqStatsDto.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonBlock.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonBlocks.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonCurrency.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonDaoCycle.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonOffer.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonScriptPubKey.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonSpentInfo.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTradeInfo.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTx.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTxInput.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTxOutput.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTxOutputType.java create mode 100644 restapi/src/main/java/bisq/restapi/dto/JsonTxType.java create mode 100644 restapi/src/main/java/bisq/restapi/endpoints/ExplorerBlocksApi.java create mode 100644 restapi/src/main/java/bisq/restapi/endpoints/ExplorerDaoApi.java create mode 100644 restapi/src/main/java/bisq/restapi/endpoints/ExplorerMarketsApi.java create mode 100644 restapi/src/main/java/bisq/restapi/endpoints/ExplorerTransactionsApi.java diff --git a/restapi/src/main/java/bisq/restapi/DaoExplorerService.java b/restapi/src/main/java/bisq/restapi/DaoExplorerService.java new file mode 100644 index 0000000000..3c99e9ce43 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/DaoExplorerService.java @@ -0,0 +1,261 @@ +/* + * 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; + +import bisq.core.dao.DaoFacade; +import bisq.core.dao.state.DaoStateListener; +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.TxOutputKey; +import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.provider.price.PriceFeedService; + +import bisq.network.p2p.P2PService; +import bisq.network.p2p.P2PServiceListener; + +import bisq.common.UserThread; + +import com.google.inject.Inject; + +import com.google.common.io.BaseEncoding; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.Setter; +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; + +@Getter +@Slf4j +public class DaoExplorerService { + private final DaoStateService daoStateService; + private final DaoFacade daoFacade; + private final DaoStateListener daoStateListener; + private final Map> txIdsByAddress = new HashMap<>(); + @Setter + private int lastKnownBlockHeight = 0; + + @Inject + public DaoExplorerService(DaoStateService daoStateService, + DaoFacade daoFacade, + P2PService p2PService, + PriceFeedService priceFeedService) { + this.daoStateService = daoStateService; + this.daoFacade = daoFacade; + daoStateListener = new DaoStateListener() { + @Override + public void onParseBlockChainComplete() { + UserThread.execute(() -> updateTxIdsByAddress()); + } + + @Override + public void onDaoStateChanged(Block block) { + UserThread.execute(() -> updateTxIdsByAddress()); + } + + }; + daoFacade.addBsqStateListener(daoStateListener); + + p2PService.addP2PServiceListener(new P2PServiceListener() { + @Override + public void onDataReceived() { + } + + @Override + public void onNoSeedNodeAvailable() { + } + + @Override + public void onNoPeersAvailable() { + } + + @Override + public void onUpdatedDataReceived() { + } + + @Override + public void onTorNodeReady() { + // We want to get early connected to the price relay so we call it already now + priceFeedService.setCurrencyCodeOnInit(); + priceFeedService.initialRequestPriceFeed(); + } + + @Override + public void onHiddenServicePublished() { + } + }); + } + + public void updateTxIdsByAddress() { + Map txIdByTxOutputKey = new HashMap<>(); + txIdsByAddress.clear(); + daoStateService.getUnorderedTxStream() + .forEach(tx -> { + tx.getTxOutputs().forEach(txOutput -> { + String address = txOutput.getAddress(); + if (address != null && !address.isEmpty() && daoStateService.isBsqTxOutputType(txOutput)) { + Set txIdSet = txIdsByAddress.getOrDefault(address, new HashSet<>()); + String txId = tx.getId(); + txIdSet.add(txId); + txIdsByAddress.put(address, txIdSet); + tx.getTxInputs().forEach(txInput -> { + txIdByTxOutputKey.put(txInput.getConnectedTxOutputKey(), txId); + }); + } + }); + }); + log.info("txIdByTxOutputKey {}", txIdByTxOutputKey.size()); + // todo check if needed + daoStateService.getUnorderedTxOutputStream() + .filter(daoStateService::isBsqTxOutputType) + .filter(txOutput -> Objects.nonNull(txOutput.getAddress())) + .forEach(txOutput -> { + String txId = txIdByTxOutputKey.get(txOutput.getKey()); + if (txId != null) { + String address = txOutput.getAddress(); + Set txIdSet = txIdsByAddress.getOrDefault(address, new HashSet<>()); + txIdSet.add(txId); + txIdsByAddress.put(address, txIdSet); + } + }); + + log.info("result txIdByTxOutputKey {}", txIdByTxOutputKey.size()); + } + + public JsonBlock getJsonBlock(Block block) { + List jsonTxs = block.getTxs().stream() + .map(this::getJsonTx) + .collect(Collectors.toList()); + return new JsonBlock(block.getHeight(), + block.getTime(), + block.getHash(), + block.getPreviousBlockHash(), + jsonTxs); + } + + public JsonTx getJsonTx(Tx tx) { + JsonTxType jsonTxType = getJsonTxType(tx); + String jsonTxTypeDisplayString = getJsonTxTypeDisplayString(jsonTxType); + return new JsonTx(tx.getId(), + tx.getBlockHeight(), + tx.getBlockHash(), + tx.getTime(), + getJsonTxInputs(tx), + getJsonTxOutputs(tx), + jsonTxType, + jsonTxTypeDisplayString, + tx.getBurntFee(), + tx.getInvalidatedBsq(), + tx.getUnlockBlockHeight()); + } + + public int getNumAddresses() { + return txIdsByAddress.size(); + } + + private List getJsonTxInputs(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 List getJsonTxOutputs(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 String getJsonTxTypeDisplayString(JsonTxType jsonTxType) { + return jsonTxType != null ? jsonTxType.getDisplayString() : ""; + } + + private 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 76dd2b75f1..96d2cdad36 100644 --- a/restapi/src/main/java/bisq/restapi/RestApi.java +++ b/restapi/src/main/java/bisq/restapi/RestApi.java @@ -20,11 +20,16 @@ package bisq.restapi; 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.DaoStateService; import bisq.core.dao.state.DaoStateSnapshotService; +import bisq.core.offer.OfferBookService; +import bisq.core.trade.statistics.TradeStatisticsManager; import bisq.core.user.Preferences; import bisq.common.app.Version; @@ -47,6 +52,18 @@ public class RestApi extends ExecutableForAppWithP2p { private SignVerifyService signVerifyService; private DaoStateSnapshotService daoStateSnapshotService; private Preferences preferences; + @Getter + private DaoExplorerService daoExplorerService; + @Getter + private DaoFacade daoFacade; + @Getter + private ProposalService proposalService; + @Getter + private CycleService cycleService; + @Getter + private TradeStatisticsManager tradeStatisticsManager; + @Getter + private OfferBookService offerBookService; public RestApi() { super("Bisq Rest Api", "bisq_restapi", "bisq_restapi", Version.VERSION); @@ -74,6 +91,12 @@ public class RestApi extends ExecutableForAppWithP2p { bondedRolesRepository = injector.getInstance(BondedRolesRepository.class); signVerifyService = injector.getInstance(SignVerifyService.class); daoStateSnapshotService = injector.getInstance(DaoStateSnapshotService.class); + daoExplorerService = injector.getInstance(DaoExplorerService.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); } @Override diff --git a/restapi/src/main/java/bisq/restapi/RestApiMain.java b/restapi/src/main/java/bisq/restapi/RestApiMain.java index aa7cabd053..505e143adb 100644 --- a/restapi/src/main/java/bisq/restapi/RestApiMain.java +++ b/restapi/src/main/java/bisq/restapi/RestApiMain.java @@ -31,6 +31,10 @@ import lombok.extern.slf4j.Slf4j; 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 class RestApiMain extends ResourceConfig { .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/BondedReputationDto.java b/restapi/src/main/java/bisq/restapi/dto/BondedReputationDto.java index 0fe068545f..db4873ccf2 100644 --- a/restapi/src/main/java/bisq/restapi/dto/BondedReputationDto.java +++ b/restapi/src/main/java/bisq/restapi/dto/BondedReputationDto.java @@ -19,7 +19,6 @@ package bisq.restapi.dto; import lombok.Getter; import lombok.ToString; -import lombok.extern.slf4j.Slf4j; @@ -30,7 +29,6 @@ import io.swagger.v3.oas.annotations.media.Schema; * Need to be in sync with the Bisq 2 BondedReputationDto class. */ @Getter -@Slf4j @ToString @Schema(title = "BondedReputation") public class BondedReputationDto { diff --git a/restapi/src/main/java/bisq/restapi/dto/BondedRoleVerificationDto.java b/restapi/src/main/java/bisq/restapi/dto/BondedRoleVerificationDto.java index 6ab2b06a95..1a2f69353b 100644 --- a/restapi/src/main/java/bisq/restapi/dto/BondedRoleVerificationDto.java +++ b/restapi/src/main/java/bisq/restapi/dto/BondedRoleVerificationDto.java @@ -18,6 +18,7 @@ package bisq.restapi.dto; import lombok.Getter; +import lombok.ToString; import javax.annotation.Nullable; @@ -30,6 +31,7 @@ import io.swagger.v3.oas.annotations.media.Schema; * Need to be in sync with the Bisq 2 BondedRoleDto class. */ @Getter +@ToString @Schema(title = "BondedRoleVerification") public class BondedRoleVerificationDto { @Nullable 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 0000000000..d329081c63 --- /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 0000000000..e00b6f7c71 --- /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 0000000000..d4fad5329b --- /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 0000000000..cd435685d2 --- /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 0000000000..c474d9eb4f --- /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 0000000000..bb531f5b6c --- /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 0000000000..4b547e8f59 --- /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 0000000000..cd6e7307c9 --- /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 0000000000..5664d9c9d1 --- /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 0000000000..a0ee76f219 --- /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 0000000000..f1574ac7ae --- /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 0000000000..5f36d465c8 --- /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 0000000000..17033a22a8 --- /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 0000000000..df156ab435 --- /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 0000000000..2a0f2c5b76 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerBlocksApi.java @@ -0,0 +1,87 @@ +package bisq.restapi.endpoints; + +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.DaoExplorerService; +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 DaoExplorerService daoExplorerService; + private final RestApi restApi; + + public ExplorerBlocksApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoExplorerService = restApi.getDaoExplorerService(); + } + + // 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) { + List blocks = restApi.getDaoStateService().getBlocks(); + Optional jsonBlock = checkNotNull(blocks.stream()) + .filter(block -> block.getHeight() == blockHeight) + .map(this::getJsonBlock) + .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) { + List blocks = restApi.getDaoStateService().getBlocks(); + Optional jsonBlock = checkNotNull(blocks.stream()) + .filter(block -> block.getHash().equalsIgnoreCase(hash)) + .map(this::getJsonBlock) + .findFirst(); + if (jsonBlock.isPresent()) { + log.info("supplying block {} to client.", hash); + return jsonBlock.get(); + } + log.warn("block {} not found!", hash); + return null; + } + + private JsonBlock getJsonBlock(Block block) { + return daoExplorerService.getJsonBlock(block); + } +} 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 0000000000..0c0869d45c --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerDaoApi.java @@ -0,0 +1,124 @@ +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.DaoExplorerService; +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 RestApi restApi; + private final DaoStateService daoStateService; + private final DaoExplorerService daoExplorerService; + + public ExplorerDaoApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoStateService = restApi.getDaoStateService(); + daoExplorerService = restApi.getDaoExplorerService(); + } + + //http://localhost:8081/api/v1/explorer/dao/get-bsq-stats + @GET + @Path("get-bsq-stats") + public BsqStatsDto getBsqStats() { + DaoFacade daoFacade = restApi.getDaoFacade(); + if (daoExplorerService.getLastKnownBlockHeight() != daoFacade.getChainHeight()) { + log.info("we recalculate the BSQ address map {} / {}", daoExplorerService.getLastKnownBlockHeight(), daoFacade.getChainHeight()); + daoExplorerService.updateTxIdsByAddress(); + daoExplorerService.setLastKnownBlockHeight(daoFacade.getChainHeight()); + } + DaoStateService daoStateService = restApi.getDaoStateService(); + 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 = daoExplorerService.getNumAddresses(); + log.info("client requested BSQ stats, height={}", daoExplorerService.getLastKnownBlockHeight()); + return new BsqStatsDto(minted, burnt, numAddresses, unspentTxos, spentTxos, + daoExplorerService.getLastKnownBlockHeight(), daoFacade.getGenesisBlockHeight()); + } + + @GET + @Path("query-dao-cycles") + public List queryDaoCycles() { + Set cyclesAdded = new HashSet<>(); + List result = new ArrayList<>(); + ProposalService proposalService = restApi.getProposalService(); + CycleService cycleService = restApi.getCycleService(); + DaoFacade daoFacade = restApi.getDaoFacade(); + // 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 0000000000..c7628f4dc1 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerMarketsApi.java @@ -0,0 +1,142 @@ +package bisq.restapi.endpoints; + +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +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 RestApi restApi; + + public ExplorerMarketsApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + } + + // 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 = restApi.getOfferBookService().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 + TradeStatisticsManager tradeStatisticsManager = restApi.getTradeStatisticsManager(); + 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 0000000000..719b57c7c0 --- /dev/null +++ b/restapi/src/main/java/bisq/restapi/endpoints/ExplorerTransactionsApi.java @@ -0,0 +1,133 @@ +/* + * 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.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.blockchain.TxType; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import lombok.extern.slf4j.Slf4j; + + + +import bisq.restapi.DaoExplorerService; +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 RestApi restApi; + private final DaoExplorerService daoExplorerService; + + public ExplorerTransactionsApi(@Context Application application) { + restApi = ((RestApiMain) application).getRestApi(); + daoExplorerService = restApi.getDaoExplorerService(); + } + + @GET + @Path("get-bsq-tx/{txid}") + public JsonTx getTx(@Parameter(description = "TxId") + @PathParam("txid") String txId) { + Optional jsonTx = restApi.getDaoStateService().getUnorderedTxStream() + .filter(t -> t.getId().equals(txId)) + .map(this::getJsonTx) + .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) { + Map> addressToTxIds = daoExplorerService.getTxIdsByAddress(); + List result = new ArrayList<>(); + Set strings = addressToTxIds.get(address); + strings.forEach(txId -> { + restApi.getDaoStateService().getTx(txId).stream() + .map(this::getJsonTx) + .forEach(result::add); + }); + 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) { + log.info("filters: {}", filters); + List jsonTxs = restApi.getDaoStateService().getUnorderedTxStream() + .sorted(Comparator.comparing(BaseTx::getTime).reversed()) + .filter(tx -> hasMatchingTxType(tx, filters)) + .skip(start) + .limit(count) + .map(this::getJsonTx) + .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; + } + + private JsonTx getJsonTx(Tx tx) { + return daoExplorerService.getJsonTx(tx); + } +}