diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateService.java b/core/src/main/java/bisq/core/dao/state/DaoStateService.java index 9204215a98e..4fd6e1e7fc5 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -30,6 +30,7 @@ import bisq.core.dao.state.model.blockchain.TxOutputKey; import bisq.core.dao.state.model.blockchain.TxOutputType; import bisq.core.dao.state.model.blockchain.TxType; +import bisq.core.dao.state.model.governance.BsqSupplyChange; import bisq.core.dao.state.model.governance.Cycle; import bisq.core.dao.state.model.governance.DecryptedBallotsWithMerits; import bisq.core.dao.state.model.governance.EvaluatedProposal; @@ -44,6 +45,7 @@ import javax.inject.Inject; import java.util.ArrayList; +import java.util.Collection; import java.util.Comparator; import java.util.HashSet; import java.util.LinkedList; @@ -616,8 +618,12 @@ public void addIssuance(Issuance issuance) { daoState.getIssuanceMap().put(issuance.getTxId(), issuance); } + public Collection getIssuanceItems() { + return daoState.getIssuanceMap().values(); + } + public Set getIssuanceSetForType(IssuanceType issuanceType) { - return daoState.getIssuanceMap().values().stream() + return getIssuanceItems().stream() .filter(issuance -> issuance.getIssuanceType() == issuanceType) .collect(Collectors.toSet()); } @@ -1044,6 +1050,18 @@ public Set getProofOfBurnOpReturnTxOutputs() { return getTxOutputsByTxOutputType(TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT); } + public Stream getBsqSupplyChanges() { + Stream issued = getIssuanceItems() + .stream() + .map(i -> new BsqSupplyChange(getBlockTime(i.getChainHeight()), i.getAmount())); + + Stream burned = getUnorderedTxStream() + .filter(tx -> tx.getTxType() == TxType.PROOF_OF_BURN || tx.getTxType() == TxType.PAY_TRADE_FEE) + .map(i -> new BsqSupplyChange(i.getTime(), -i.getBurntBsq())); + + return Stream.concat(issued, burned); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners diff --git a/core/src/main/java/bisq/core/dao/state/model/governance/BsqSupplyChange.java b/core/src/main/java/bisq/core/dao/state/model/governance/BsqSupplyChange.java new file mode 100644 index 00000000000..a37950f100b --- /dev/null +++ b/core/src/main/java/bisq/core/dao/state/model/governance/BsqSupplyChange.java @@ -0,0 +1,30 @@ +/* + * 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.core.dao.state.model.governance; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@AllArgsConstructor +public class BsqSupplyChange { + @Getter + long time; + + @Getter + long value; +} diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 7440d9716bb..482aef0fa32 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -2600,6 +2600,7 @@ dao.factsAndFigures.supply.compRequestIssueAmount=BSQ issued for compensation re dao.factsAndFigures.supply.reimbursementAmount=BSQ issued for reimbursement requests dao.factsAndFigures.supply.totalIssued=Total issued BSQ dao.factsAndFigures.supply.totalBurned=Total burned BSQ +dao.factsAndFigures.supply.totalBsqSupply=Total BSQ supply dao.factsAndFigures.supply.chart.tradeFee.toolTip={0}\n{1} dao.factsAndFigures.supply.burnt=BSQ burnt diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java index 610ed63b03c..0b3399f39e3 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartDataModel.java @@ -22,6 +22,7 @@ import java.time.Instant; import java.time.temporal.TemporalAdjuster; +import java.util.Comparator; import java.util.Map; import java.util.function.BinaryOperator; import java.util.function.Predicate; diff --git a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java index bcc114bf9b5..2348e78f2ec 100644 --- a/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java +++ b/desktop/src/main/java/bisq/desktop/components/chart/ChartView.java @@ -87,7 +87,7 @@ public abstract class ChartView chart; - private HBox timelineLabels, legendBox2; + private HBox timelineLabels, legendBox2, legendBox3; private final ToggleGroup timeIntervalToggleGroup = new ToggleGroup(); protected final Set> activeSeries = new HashSet<>(); @@ -157,6 +157,11 @@ public void initialize() { legendBox2 = initLegendsAndGetLegendBox(seriesForLegend2); } + Collection> seriesForLegend3 = getSeriesForLegend3(); + if (seriesForLegend3 != null && !seriesForLegend3.isEmpty()) { + legendBox3 = initLegendsAndGetLegendBox(seriesForLegend3); + } + // Set active series/legends defineAndAddActiveSeries(); @@ -176,6 +181,9 @@ public void initialize() { if (legendBox2 != null) { VBox.setMargin(legendBox2, new Insets(-20, rightPadding, 0, paddingLeft)); } + if (legendBox3 != null) { + VBox.setMargin(legendBox3, new Insets(-20, rightPadding, 0, paddingLeft)); + } if (model.getDividerPositions()[0] == 0 && model.getDividerPositions()[1] == 1) { resetTimeNavigation(); @@ -192,6 +200,10 @@ public void initialize() { VBox.setMargin(legendBox2, new Insets(-20, paddingRight, 0, paddingLeft)); timelineNavigationBox.getChildren().add(legendBox2); } + if (legendBox3 != null) { + VBox.setMargin(legendBox3, new Insets(-20, paddingRight, 0, paddingLeft)); + timelineNavigationBox.getChildren().add(legendBox3); + } root.getChildren().addAll(timeIntervalBox, chart, timelineNavigationBox); // Listeners @@ -241,6 +253,7 @@ public void activate() { addLegendToggleActionHandlers(getSeriesForLegend1()); addLegendToggleActionHandlers(getSeriesForLegend2()); + addLegendToggleActionHandlers(getSeriesForLegend3()); addActionHandlersToDividers(); } @@ -258,6 +271,7 @@ public void deactivate() { removeLegendToggleActionHandlers(getSeriesForLegend1()); removeLegendToggleActionHandlers(getSeriesForLegend2()); + removeLegendToggleActionHandlers(getSeriesForLegend3()); removeActionHandlersToDividers(); // clear data, reset states. We keep timeInterval state though @@ -541,6 +555,10 @@ protected Collection> getSeriesForLegend2() { return null; } + protected Collection> getSeriesForLegend3() { + return null; + } + protected abstract void defineAndAddActiveSeries(); protected void activateSeries(XYChart.Series series) { diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java index 52b90f72bab..c4f61c028bb 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartDataModel.java @@ -21,6 +21,7 @@ import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.BsqSupplyChange; import bisq.core.dao.state.model.governance.Issuance; import bisq.core.dao.state.model.governance.IssuanceType; @@ -30,13 +31,16 @@ import java.time.Instant; import java.util.Collection; +import java.util.Comparator; import java.util.HashMap; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.extern.slf4j.Slf4j; @@ -45,7 +49,7 @@ public class DaoChartDataModel extends ChartDataModel { private final DaoStateService daoStateService; private final Function blockTimeOfIssuanceFunction; - private Map totalIssuedByInterval, compensationByInterval, reimbursementByInterval, + private Map totalSupplyByInterval, totalIssuedByInterval, compensationByInterval, reimbursementByInterval, totalBurnedByInterval, bsqTradeFeeByInterval, proofOfBurnByInterval; @@ -68,6 +72,7 @@ public DaoChartDataModel(DaoStateService daoStateService) { @Override protected void invalidateCache() { + totalSupplyByInterval = null; totalIssuedByInterval = null; compensationByInterval = null; reimbursementByInterval = null; @@ -111,6 +116,19 @@ long getProofOfBurnAmount() { // Data for chart /////////////////////////////////////////////////////////////////////////////////////////// + Map getTotalSupplyByInterval() { + if (totalSupplyByInterval != null) { + return totalSupplyByInterval; + } + + totalSupplyByInterval = getTotalBsqSupplyByInterval( + daoStateService.getBsqSupplyChanges(), + getDateFilter() + ); + + return totalSupplyByInterval; + } + Map getTotalIssuedByInterval() { if (totalIssuedByInterval != null) { return totalIssuedByInterval; @@ -179,6 +197,29 @@ Map getProofOfBurnByInterval() { // Aggregated collection data by interval /////////////////////////////////////////////////////////////////////////////////////////// + private Map getTotalBsqSupplyByInterval(Stream bsqSupplyChanges, Predicate dateFilter) { + AtomicLong supply = new AtomicLong( + DaoEconomyHistoricalData.TOTAL_SUPPLY_BY_CYCLE_DATE.get(1555340856L) + ); + + return bsqSupplyChanges + .collect(Collectors.groupingBy(tx -> toTimeInterval(Instant.ofEpochMilli(tx.getTime())))) + .entrySet() + .stream() + .sorted(Comparator.comparingLong(Map.Entry::getKey)) + .map(e -> new BsqSupplyChange( + e.getKey(), + supply.addAndGet(e + .getValue() + .stream() + .mapToLong(BsqSupplyChange::getValue) + .sum() + )) + ) + .filter(t -> dateFilter.test(t.getTime())) + .collect(Collectors.toMap(BsqSupplyChange::getTime, BsqSupplyChange::getValue)); + } + private Map getIssuedBsqByInterval(Set issuanceSet, Predicate dateFilter) { return issuanceSet.stream() .collect(Collectors.groupingBy(issuance -> @@ -199,7 +240,7 @@ private Map getHistoricalIssuedBsqByInterval(Map histori .filter(e -> dateFilter.test(e.getKey())) .collect(Collectors.toMap(e -> toTimeInterval(Instant.ofEpochSecond(e.getKey())), Map.Entry::getValue, - (a, b) -> a + b)); + Long::sum)); } private Map getBurntBsqByInterval(Collection txs, Predicate dateFilter) { @@ -236,6 +277,7 @@ private static class DaoEconomyHistoricalData { // Key is start date of the cycle in epoch seconds, value is reimbursement amount public final static Map REIMBURSEMENTS_BY_CYCLE_DATE = new HashMap<>(); public final static Map COMPENSATIONS_BY_CYCLE_DATE = new HashMap<>(); + public final static Map TOTAL_SUPPLY_BY_CYCLE_DATE = new HashMap<>(); static { REIMBURSEMENTS_BY_CYCLE_DATE.put(1571349571L, 60760L); @@ -271,6 +313,8 @@ private static class DaoEconomyHistoricalData { COMPENSATIONS_BY_CYCLE_DATE.put(1599175867L, 6086442L); COMPENSATIONS_BY_CYCLE_DATE.put(1601861442L, 5615973L); COMPENSATIONS_BY_CYCLE_DATE.put(1604845863L, 7782667L); + + TOTAL_SUPPLY_BY_CYCLE_DATE.put(1555340856L, 372540100L); } } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java index 200777090f9..75894fafbe4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartView.java @@ -46,7 +46,7 @@ public class DaoChartView extends ChartView { private final LongProperty proofOfBurnAmountProperty = new SimpleLongProperty(); private XYChart.Series seriesBsqTradeFee, seriesProofOfBurn, seriesCompensation, - seriesReimbursement, seriesTotalIssued, seriesTotalBurned; + seriesReimbursement, seriesTotalSupply, seriesTotalIssued, seriesTotalBurned; @Inject @@ -90,6 +90,11 @@ protected Collection> getSeriesForLegend2() { return List.of(seriesTotalBurned, seriesBsqTradeFee, seriesProofOfBurn); } + @Override + protected Collection> getSeriesForLegend3() { + return List.of(seriesTotalSupply); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Timeline navigation @@ -130,6 +135,10 @@ protected void createSeries() { seriesProofOfBurn = new XYChart.Series<>(); seriesProofOfBurn.setName(Res.get("dao.factsAndFigures.supply.proofOfBurn")); seriesIndexMap.put(getSeriesId(seriesProofOfBurn), 5); + + seriesTotalSupply = new XYChart.Series<>(); + seriesTotalSupply.setName(Res.get("dao.factsAndFigures.supply.totalBsqSupply")); + seriesIndexMap.put(getSeriesId(seriesTotalSupply), 6); } @Override @@ -176,6 +185,11 @@ protected CompletableFuture applyData() { allFutures.add(task6Done); applyProofOfBurn(task6Done); } + if (activeSeries.contains(seriesTotalSupply)) { + CompletableFuture task6ADone = new CompletableFuture<>(); + allFutures.add(task6ADone); + applyTotalSupply(task6ADone); + } CompletableFuture task7Done = new CompletableFuture<>(); allFutures.add(task7Done); @@ -216,6 +230,15 @@ protected CompletableFuture applyData() { return CompletableFutureUtils.allOf(allFutures).thenApply(e -> true); } + private void applyTotalSupply(CompletableFuture completeFuture) { + model.getTotalSupplyChartData() + .whenComplete((data, t) -> + mapToUserThread(() -> { + seriesTotalSupply.getData().setAll(data); + completeFuture.complete(true); + })); + } + private void applyTotalIssued(CompletableFuture completeFuture) { model.getTotalIssuedChartData() .whenComplete((data, t) -> diff --git a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java index 70429d33e25..44af8d106cc 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/economy/supply/dao/DaoChartViewModel.java @@ -54,6 +54,10 @@ public DaoChartViewModel(DaoChartDataModel dataModel, BsqFormatter bsqFormatter) // Chart data /////////////////////////////////////////////////////////////////////////////////////////// + CompletableFuture>> getTotalSupplyChartData() { + return CompletableFuture.supplyAsync(() -> toChartData(dataModel.getTotalSupplyByInterval())); + } + CompletableFuture>> getTotalIssuedChartData() { return CompletableFuture.supplyAsync(() -> toChartData(dataModel.getTotalIssuedByInterval())); }