Add explorer endpoints and dtos

Signed-off-by: HenrikJannsen <boilingfrog@gmx.com>
This commit is contained in:
HenrikJannsen 2024-07-21 17:39:00 +07:00
parent 9e0845e7f7
commit bd9136fc97
No known key found for this signature in database
GPG Key ID: 02AA2BAE387C8307
23 changed files with 1405 additions and 2 deletions

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String, Set<String>> 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<TxOutputKey, String> 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<String> 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<String> 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<JsonTx> 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<JsonTxInput> getJsonTxInputs(Tx tx) {
return tx.getTxInputs().stream()
.map(txInput -> {
Optional<TxOutput> 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<JsonTxOutput> 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;
}
}

View File

@ -20,11 +20,16 @@ package bisq.restapi;
import bisq.core.account.witness.AccountAgeWitnessService; import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.app.misc.ExecutableForAppWithP2p; import bisq.core.app.misc.ExecutableForAppWithP2p;
import bisq.core.dao.DaoFacade;
import bisq.core.dao.SignVerifyService; import bisq.core.dao.SignVerifyService;
import bisq.core.dao.governance.bond.reputation.BondedReputationRepository; import bisq.core.dao.governance.bond.reputation.BondedReputationRepository;
import bisq.core.dao.governance.bond.role.BondedRolesRepository; 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.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService; import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.offer.OfferBookService;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences; import bisq.core.user.Preferences;
import bisq.common.app.Version; import bisq.common.app.Version;
@ -47,6 +52,18 @@ public class RestApi extends ExecutableForAppWithP2p {
private SignVerifyService signVerifyService; private SignVerifyService signVerifyService;
private DaoStateSnapshotService daoStateSnapshotService; private DaoStateSnapshotService daoStateSnapshotService;
private Preferences preferences; 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() { public RestApi() {
super("Bisq Rest Api", "bisq_restapi", "bisq_restapi", Version.VERSION); super("Bisq Rest Api", "bisq_restapi", "bisq_restapi", Version.VERSION);
@ -74,6 +91,12 @@ public class RestApi extends ExecutableForAppWithP2p {
bondedRolesRepository = injector.getInstance(BondedRolesRepository.class); bondedRolesRepository = injector.getInstance(BondedRolesRepository.class);
signVerifyService = injector.getInstance(SignVerifyService.class); signVerifyService = injector.getInstance(SignVerifyService.class);
daoStateSnapshotService = injector.getInstance(DaoStateSnapshotService.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 @Override

View File

@ -31,6 +31,10 @@ import lombok.extern.slf4j.Slf4j;
import bisq.restapi.endpoints.AccountAgeApi; import bisq.restapi.endpoints.AccountAgeApi;
import bisq.restapi.endpoints.BondedReputationApi; import bisq.restapi.endpoints.BondedReputationApi;
import bisq.restapi.endpoints.BondedRoleVerificationApi; 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.ProofOfBurnApi;
import bisq.restapi.endpoints.SignedWitnessApi; import bisq.restapi.endpoints.SignedWitnessApi;
import bisq.restapi.error.CustomExceptionMapper; import bisq.restapi.error.CustomExceptionMapper;
@ -63,6 +67,10 @@ public class RestApiMain extends ResourceConfig {
.register(BondedRoleVerificationApi.class) .register(BondedRoleVerificationApi.class)
.register(AccountAgeApi.class) .register(AccountAgeApi.class)
.register(SignedWitnessApi.class) .register(SignedWitnessApi.class)
.register(ExplorerMarketsApi.class)
.register(ExplorerDaoApi.class)
.register(ExplorerBlocksApi.class)
.register(ExplorerTransactionsApi.class)
.register(SwaggerResolution.class); .register(SwaggerResolution.class);
daoNodeApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort); daoNodeApplication.startServer(config.daoNodeApiUrl, config.daoNodeApiPort);
}); });

View File

@ -19,7 +19,6 @@ package bisq.restapi.dto;
import lombok.Getter; import lombok.Getter;
import lombok.ToString; 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. * Need to be in sync with the Bisq 2 BondedReputationDto class.
*/ */
@Getter @Getter
@Slf4j
@ToString @ToString
@Schema(title = "BondedReputation") @Schema(title = "BondedReputation")
public class BondedReputationDto { public class BondedReputationDto {

View File

@ -18,6 +18,7 @@
package bisq.restapi.dto; package bisq.restapi.dto;
import lombok.Getter; import lombok.Getter;
import lombok.ToString;
import javax.annotation.Nullable; 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. * Need to be in sync with the Bisq 2 BondedRoleDto class.
*/ */
@Getter @Getter
@ToString
@Schema(title = "BondedRoleVerification") @Schema(title = "BondedRoleVerification")
public class BondedRoleVerificationDto { public class BondedRoleVerificationDto {
@Nullable @Nullable

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<JsonTx> txs;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.dto;
import java.util.List;
import lombok.Value;
@Value
class JsonBlocks {
int chainHeight;
List<JsonBlock> blocks;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.restapi.dto;
import lombok.Value;
@Value
public class JsonCurrency {
String code;
String name;
int precision;
String _type; // "fiat" or "crypto"
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

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

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<String> 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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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();
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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<JsonTxInput> inputs;
List<JsonTxOutput> 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<JsonTxInput> inputs,
List<JsonTxOutput> 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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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);
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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;
}
}

View File

@ -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<Block> blocks = restApi.getDaoStateService().getBlocks();
Optional<JsonBlock> 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<Block> blocks = restApi.getDaoStateService().getBlocks();
Optional<JsonBlock> 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);
}
}

View File

@ -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<JsonDaoCycle> queryDaoCycles() {
Set<Integer> cyclesAdded = new HashSet<>();
List<JsonDaoCycle> 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<Proposal> 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;
}
}

View File

@ -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<JsonCurrency> getBisqCurrencies() {
ArrayList<JsonCurrency> fiatCurrencyList = CurrencyUtil.getMatureMarketCurrencies().stream()
.map(e -> new JsonCurrency(e.getCode(), e.getName(), 8, "fiat"))
.collect(Collectors.toCollection(ArrayList::new));
ArrayList<JsonCurrency> cryptoCurrencyList = CurrencyUtil.getMainCryptoCurrencies().stream()
.map(e -> new JsonCurrency(e.getCode(), e.getName(), 8, "crypto"))
.collect(Collectors.toCollection(ArrayList::new));
List<JsonCurrency> 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<JsonOffer> getBisqOffers() {
List<JsonOffer> 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<JsonTradeInfo> 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<JsonTradeInfo> result = new ArrayList<>();
List<TradeStatistics3> 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<TradeStatistics3> 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;
}
}

View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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> 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<JsonTx> getBisqTxForAddr(@PathParam("addr") String address) {
Map<String, Set<String>> addressToTxIds = daoExplorerService.getTxIdsByAddress();
List<JsonTx> result = new ArrayList<>();
Set<String> 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<JsonTx> queryTxsPaginated(@PathParam("start") int start,
@PathParam("count") int count,
@PathParam("filters") String filters) {
log.info("filters: {}", filters);
List<JsonTx> 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);
}
}