From 2bd819a463501f5d4116dc88a7df70941fe41a00 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 7 Nov 2018 19:30:02 -0500 Subject: [PATCH 01/11] Remove commented out print code --- .../java/bisq/core/locale/CurrencyUtil.java | 24 +++++-------------- 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/core/src/main/java/bisq/core/locale/CurrencyUtil.java b/core/src/main/java/bisq/core/locale/CurrencyUtil.java index d9468d634fc..ef6af9d184a 100644 --- a/core/src/main/java/bisq/core/locale/CurrencyUtil.java +++ b/core/src/main/java/bisq/core/locale/CurrencyUtil.java @@ -21,6 +21,12 @@ import bisq.core.btc.BaseCurrencyNetwork; import bisq.core.dao.governance.asset.AssetService; +import bisq.asset.Asset; +import bisq.asset.AssetRegistry; +import bisq.asset.Coin; +import bisq.asset.Token; +import bisq.asset.coins.BSQ; + import bisq.common.app.DevEnv; import java.util.ArrayList; @@ -39,14 +45,6 @@ import static com.google.common.base.Preconditions.checkArgument; - - -import bisq.asset.Asset; -import bisq.asset.AssetRegistry; -import bisq.asset.Coin; -import bisq.asset.Token; -import bisq.asset.coins.BSQ; - @Slf4j public class CurrencyUtil { @@ -119,16 +117,6 @@ private static List createAllSortedCryptoCurrenciesList() { .map(CurrencyUtil::assetToCryptoCurrency) .sorted(TradeCurrency::compareTo) .collect(Collectors.toList()); - - // Util for printing all altcoins for adding to FAQ page - /* StringBuilder sb = new StringBuilder(); - result.stream().forEach(e -> sb.append("
  • “") - .append(e.getCode()) - .append("”, “") - .append(e.getName()) - .append("”
  • ") - .append("\n")); - log.info(sb.toString());*/ return result; } From 53e127f556c55e985cfe8f0b73f256b7f5a62e83 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 7 Nov 2018 20:56:22 -0500 Subject: [PATCH 02/11] Add asset listing params --- core/src/main/java/bisq/core/dao/governance/param/Param.java | 3 ++- core/src/main/resources/i18n/displayStrings.properties | 5 +++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/governance/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java index 550ebd2e6e9..0c600632eaa 100644 --- a/core/src/main/java/bisq/core/dao/governance/param/Param.java +++ b/core/src/main/java/bisq/core/dao/governance/param/Param.java @@ -105,7 +105,8 @@ public enum Param { "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV", // testnet ParamType.ADDRESS), - //TODO add asset listing params (nr. of trades, volume, time, fee which defines listing state) + ASSET_LISTING_FEE("1", ParamType.BSQ, 10, 10), + ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10), // TODO for dev testing we use short periods... // Period phase ("11 blocks atm) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 86863b8ef77..dfa0246f4f1 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1273,6 +1273,11 @@ dao.param.THRESHOLD_ROLE=Required threshold in % for bonded role requests # suppress inspection "UnusedProperty" dao.param.RECIPIENT_BTC_ADDRESS=Recipient BTC address +# suppress inspection "UnusedProperty" +dao.param.ASSET_LISTING_FEE=Asset listing fee +# suppress inspection "UnusedProperty" +dao.param.ASSET_MIN_VOLUME=Min. trade volume + dao.param.currentValue=Current value: {0} dao.param.blocks={0} blocks From f0c435a6bb73541973dc1160b55db03fac0e293f Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 7 Nov 2018 20:56:35 -0500 Subject: [PATCH 03/11] Merge --- .../java/bisq/desktop/util/GUIUtilTest.java | 47 ------------------- 1 file changed, 47 deletions(-) diff --git a/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java b/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java index fcc4d68b188..681b82028b0 100644 --- a/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java +++ b/desktop/src/test/java/bisq/desktop/util/GUIUtilTest.java @@ -18,27 +18,10 @@ package bisq.desktop.util; import bisq.core.locale.GlobalSettings; -import bisq.core.locale.Res; -import bisq.core.locale.TradeCurrency; -import javafx.util.StringConverter; - -import java.util.HashMap; import java.util.Locale; -import java.util.Map; import org.junit.Before; -import org.junit.Test; - -import static bisq.desktop.maker.CurrencyListItemMakers.bitcoinItem; -import static bisq.desktop.maker.CurrencyListItemMakers.euroItem; -import static bisq.desktop.maker.CurrencyListItemMakers.numberOfTrades; -import static bisq.desktop.maker.PreferenceMakers.empty; -import static bisq.desktop.maker.TradeCurrencyMakers.bitcoin; -import static bisq.desktop.maker.TradeCurrencyMakers.euro; -import static com.natpryce.makeiteasy.MakeItEasy.make; -import static com.natpryce.makeiteasy.MakeItEasy.with; -import static org.junit.Assert.assertEquals; public class GUIUtilTest { @@ -47,34 +30,4 @@ public void setup() { Locale.setDefault(new Locale("en", "US")); GlobalSettings.setLocale(new Locale("en", "US")); } - - @Test - public void testTradeCurrencyConverter() { - Map offerCounts = new HashMap() {{ - put("BTC", 11); - put("EUR", 10); - }}; - StringConverter tradeCurrencyConverter = GUIUtil.getTradeCurrencyConverter( - Res.get("shared.oneOffer"), - Res.get("shared.multipleOffers"), - offerCounts - ); - - assertEquals("✦ Bitcoin (BTC) - 11 offers", tradeCurrencyConverter.toString(bitcoin)); - assertEquals("★ Euro (EUR) - 10 offers", tradeCurrencyConverter.toString(euro)); - } - - @Test - public void testCurrencyListWithOffersConverter() { - Res.setBaseCurrencyCode("BTC"); - Res.setBaseCurrencyName("Bitcoin"); - StringConverter currencyListItemConverter = GUIUtil.getCurrencyListItemConverter(Res.get("shared.oneOffer"), - Res.get("shared.multipleOffers"), - empty); - - assertEquals("✦ Bitcoin (BTC) - 10 offers", currencyListItemConverter.toString(make(bitcoinItem.but(with(numberOfTrades, 10))))); - assertEquals("★ Euro (EUR) - 0 offers", currencyListItemConverter.toString(make(euroItem))); - assertEquals("★ Euro (EUR) - 1 offer", currencyListItemConverter.toString(make(euroItem.but(with(numberOfTrades, 1))))); - - } } From cfb88ebf29e2891945c416b1508d23685bfa6bf7 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 7 Nov 2018 20:56:42 -0500 Subject: [PATCH 04/11] cleanup --- .../trade/statistics/TradeStatisticsManager.java | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java index 87e54b956e3..f736ce8c7a6 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -143,7 +143,11 @@ public void publishTradeStatistics(List trades) { } } - public void addToMap(TradeStatistics2 tradeStatistics, boolean storeLocally) { + public ObservableSet getObservableTradeStatisticsSet() { + return observableTradeStatisticsSet; + } + + private void addToMap(TradeStatistics2 tradeStatistics, boolean storeLocally) { if (!observableTradeStatisticsSet.contains(tradeStatistics)) { boolean itemAlreadyAdded = observableTradeStatisticsSet.stream() .anyMatch(e -> (e.getOfferId().equals(tradeStatistics.getOfferId()))); @@ -159,10 +163,6 @@ public void addToMap(TradeStatistics2 tradeStatistics, boolean storeLocally) { } } - public ObservableSet getObservableTradeStatisticsSet() { - return observableTradeStatisticsSet; - } - private void addToMap(TradeStatistics2 tradeStatistics, Map map) { TradeStatistics2 prevValue = map.putIfAbsent(tradeStatistics.getOfferId(), tradeStatistics); if (prevValue != null) @@ -177,8 +177,9 @@ private void dump() { // Need a more scalable solution later when we get more volume. // The flag will only be activated by dedicated nodes, so it should not be too critical for the moment, but needs to // get improved. Maybe a LevelDB like DB...? Could be impl. in a headless version only. - List list = observableTradeStatisticsSet.stream().map(TradeStatisticsForJson::new).collect(Collectors.toList()); - list.sort((o1, o2) -> (o1.tradeDate < o2.tradeDate ? 1 : (o1.tradeDate == o2.tradeDate ? 0 : -1))); + List list = observableTradeStatisticsSet.stream().map(TradeStatisticsForJson::new) + .sorted((o1, o2) -> (Long.compare(o2.tradeDate, o1.tradeDate))) + .collect(Collectors.toList()); TradeStatisticsForJson[] array = new TradeStatisticsForJson[list.size()]; list.toArray(array); jsonFileManager.writeToDisc(Utilities.objectToJson(array), "trade_statistics"); From ca4a4a47ce165eb71d4f7a4dc82174565d6ebcb7 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Thu, 8 Nov 2018 16:44:58 -0500 Subject: [PATCH 05/11] Add asset listing fee and proof of burn domain and views (WIP) --- .../main/java/bisq/common/app/Version.java | 2 + common/src/main/proto/pb.proto | 12 +- .../core/btc/wallet/BtcWalletService.java | 10 +- .../main/java/bisq/core/dao/DaoFacade.java | 8 +- .../src/main/java/bisq/core/dao/DaoSetup.java | 3 + .../dao/governance/asset/AssetConsensus.java | 65 ++++ .../dao/governance/asset/AssetService.java | 170 ++++++++- .../core/dao/governance/asset/AssetState.java | 28 ++ .../dao/governance/asset/StatefulAsset.java | 110 ++++++ .../bisq/core/dao/governance/param/Param.java | 4 +- .../proofofburn/ProofOfBurnConsensus.java | 53 +++ .../proposal/BaseProposalFactory.java | 2 +- .../dao/node/explorer/JsonTxOutputType.java | 2 + .../core/dao/node/explorer/JsonTxType.java | 4 +- .../core/dao/node/parser/OpReturnParser.java | 12 + .../core/dao/node/parser/TxInputParser.java | 2 + .../core/dao/node/parser/TxOutputParser.java | 4 + .../bisq/core/dao/node/parser/TxParser.java | 6 + .../bisq/core/dao/state/DaoStateService.java | 14 + .../state/model/blockchain/OpReturnType.java | 4 +- .../state/model/blockchain/TxOutputType.java | 2 + .../dao/state/model/blockchain/TxType.java | 4 +- .../java/bisq/core/locale/CurrencyUtil.java | 13 +- .../resources/i18n/displayStrings.properties | 26 ++ .../java/bisq/desktop/main/dao/DaoView.java | 19 +- .../desktop/main/dao/burnbsq/BurnBsqView.fxml | 37 ++ .../desktop/main/dao/burnbsq/BurnBsqView.java | 125 +++++++ .../dao/burnbsq/assetfee/AssetFeeView.fxml | 33 ++ .../dao/burnbsq/assetfee/AssetFeeView.java | 346 ++++++++++++++++++ .../dao/burnbsq/assetfee/AssetListItem.java | 50 +++ .../reputation/ProofOfBurnListItem.java | 29 ++ .../burnbsq/reputation/ProofOfBurnView.fxml | 33 ++ .../burnbsq/reputation/ProofOfBurnView.java | 101 +++++ .../dao/governance/make/MakeProposalView.java | 1 - .../desktop/main/dao/wallet/tx/BsqTxView.java | 8 + 35 files changed, 1311 insertions(+), 31 deletions(-) create mode 100644 core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java create mode 100644 core/src/main/java/bisq/core/dao/governance/asset/AssetState.java create mode 100644 core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java create mode 100644 core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java diff --git a/common/src/main/java/bisq/common/app/Version.java b/common/src/main/java/bisq/common/app/Version.java index 22fbaa81db9..b005b0ee7c9 100644 --- a/common/src/main/java/bisq/common/app/Version.java +++ b/common/src/main/java/bisq/common/app/Version.java @@ -125,4 +125,6 @@ public static void printVersion() { public static final byte BLIND_VOTE = (byte) 0x01; public static final byte VOTE_REVEAL = (byte) 0x01; public static final byte LOCKUP = (byte) 0x01; + public static final byte ASSET_LISTING_FEE = (byte) 0x01; + public static final byte PROOF_OF_BURN = (byte) 0x01; } diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 798dfc51ee9..8030685de0a 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -1370,6 +1370,8 @@ enum TxType { VOTE_REVEAL = 11; LOCKUP = 12; UNLOCK = 13; + ASSET_LISTING_FEE = 14; + PROOF_OF_BURN = 15; } message TxInput { @@ -1416,10 +1418,12 @@ enum TxOutputType { BLIND_VOTE_OP_RETURN_OUTPUT = 11; VOTE_REVEAL_UNLOCK_STAKE_OUTPUT = 12; VOTE_REVEAL_OP_RETURN_OUTPUT = 13; - LOCKUP_OUTPUT = 14; - LOCKUP_OP_RETURN_OUTPUT = 15; - UNLOCK_OUTPUT = 16; - INVALID_OUTPUT = 17; + ASSET_LISTING_FEE_OP_RETURN_OUTPUT = 14; + PROOF_OF_BURN_OP_RETURN_OUTPUT = 15; + LOCKUP_OUTPUT = 16; + LOCKUP_OP_RETURN_OUTPUT = 17; + UNLOCK_OUTPUT = 18; + INVALID_OUTPUT = 19; } message SpentInfo { diff --git a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java index cff029e41cd..25c8be245fe 100644 --- a/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/bisq/core/btc/wallet/BtcWalletService.java @@ -144,17 +144,19 @@ String getWalletAsString(boolean includePrivKeys) { // Public Methods /////////////////////////////////////////////////////////////////////////////////////////// - /////////////////////////////////////////////////////////////////////////////////////////// - // Proposal txs + // Burn BSQ txs (some proposal txs, asset listing fee tx, proof of burn tx) /////////////////////////////////////////////////////////////////////////////////////////// - - public Transaction completePreparedProposalTx(Transaction preparedBurnFeeTx, byte[] opReturnData) + public Transaction completePreparedBurnBsqTx(Transaction preparedBurnFeeTx, byte[] opReturnData) throws WalletException, InsufficientMoneyException, TransactionVerificationException { return completePreparedProposalTx(preparedBurnFeeTx, opReturnData, null, null); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Proposal txs + /////////////////////////////////////////////////////////////////////////////////////////// + public Transaction completePreparedReimbursementRequestTx(Coin issuanceAmount, Address issuanceAddress, Transaction feeTx, byte[] opReturnData) throws TransactionVerificationException, WalletException, InsufficientMoneyException { return completePreparedProposalTx(feeTx, opReturnData, issuanceAmount, issuanceAddress); diff --git a/core/src/main/java/bisq/core/dao/DaoFacade.java b/core/src/main/java/bisq/core/dao/DaoFacade.java index 5ea20900cfc..7d3b4fef8ca 100644 --- a/core/src/main/java/bisq/core/dao/DaoFacade.java +++ b/core/src/main/java/bisq/core/dao/DaoFacade.java @@ -182,10 +182,6 @@ public DaoFacade(MyProposalListService myProposalListService, @Override public void addListeners() { - } - - @Override - public void start() { daoStateService.addBsqStateListener(new DaoStateListener() { @Override public void onNewBlockHeight(int blockHeight) { @@ -203,6 +199,10 @@ public void onParseBlockChainComplete() { }); } + @Override + public void start() { + } + public void addBsqStateListener(DaoStateListener listener) { daoStateService.addBsqStateListener(listener); diff --git a/core/src/main/java/bisq/core/dao/DaoSetup.java b/core/src/main/java/bisq/core/dao/DaoSetup.java index b418d1d535e..74bf5e1dd83 100644 --- a/core/src/main/java/bisq/core/dao/DaoSetup.java +++ b/core/src/main/java/bisq/core/dao/DaoSetup.java @@ -17,6 +17,7 @@ package bisq.core.dao; +import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.BlindVoteListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; @@ -64,6 +65,7 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, BondedRolesRepository bondedRolesRepository, MyReputationListService myReputationListService, MyBondedReputationRepository myBondedReputationRepository, + AssetService assetService, DaoFacade daoFacade, ExportJsonFilesService exportJsonFilesService) { @@ -83,6 +85,7 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, daoSetupServices.add(bondedRolesRepository); daoSetupServices.add(myReputationListService); daoSetupServices.add(myBondedReputationRepository); + daoSetupServices.add(assetService); daoSetupServices.add(daoFacade); daoSetupServices.add(exportJsonFilesService); daoSetupServices.add(bsqNodeProvider.getBsqNode()); diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java new file mode 100644 index 00000000000..f144f00eb3e --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetConsensus.java @@ -0,0 +1,65 @@ +/* + * 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.governance.asset; + +import bisq.core.dao.governance.param.Param; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.crypto.Hash; + +import org.bitcoinj.core.Coin; + +import com.google.common.base.Charsets; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class AssetConsensus { + public static Coin getFeePerDay(DaoStateService daoStateService, int chainHeight) { + return daoStateService.getParamValueAsCoin(Param.ASSET_LISTING_FEE_PER_DAY, chainHeight); + } + + public static byte[] getHash(StatefulAsset statefulAsset) { + String stringInput = "AssetListingFee-" + statefulAsset.getTickerSymbol(); + final byte[] bytes = stringInput.getBytes(Charsets.UTF_8); + return Hash.getSha256Ripemd160hash(bytes); + } + + public static byte[] getOpReturnData(byte[] hash) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.ASSET_LISTING_FEE.getType()); + outputStream.write(Version.ASSET_LISTING_FEE); + outputStream.write(hash); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + return new byte[0]; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java index 1ac69f94c1a..b7e61f157d9 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -18,24 +18,60 @@ package bisq.core.dao.governance.asset; import bisq.core.app.BisqEnvironment; +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.proposal.TxException; +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.Tx; +import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.locale.CurrencyUtil; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; import bisq.common.proto.persistable.PersistedDataHost; import bisq.common.storage.Storage; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + import javax.inject.Inject; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; + +import java.util.Arrays; import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import static com.google.common.base.Preconditions.checkArgument; + @Slf4j -public class AssetService implements PersistedDataHost { +public class AssetService implements PersistedDataHost, DaoSetupService, DaoStateListener { private final Storage storage; + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + private final DaoStateService daoStateService; + @Getter private final RemovedAssetsList removedAssetsList = new RemovedAssetsList(); + @Getter + private final ObservableList statefulAssets = FXCollections.observableArrayList(); /////////////////////////////////////////////////////////////////////////////////////////// @@ -43,8 +79,16 @@ public class AssetService implements PersistedDataHost { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public AssetService(Storage storage) { + public AssetService(Storage storage, + BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + DaoStateService daoStateService) { this.storage = storage; + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.daoStateService = daoStateService; } @@ -54,6 +98,7 @@ public AssetService(Storage storage) { @Override public void readPersisted() { + //TODO why not use dao state? if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { RemovedAssetsList persisted = storage.initAndGetPersisted(removedAssetsList, 100); if (persisted != null) { @@ -64,6 +109,76 @@ public void readPersisted() { } + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addBsqStateListener(this); + } + + @Override + public void start() { + updateList(); + } + + public void updateList() { + statefulAssets.setAll(CurrencyUtil.getAssetStream() + .map(StatefulAsset::new) + .peek(statefulAsset -> { + terminateAssetIfRemovedByVoting(statefulAsset); + findFeePayment(statefulAsset).ifPresent(statefulAsset::addFeePayment); + }) + .sorted(StatefulAsset::compareTo) + .collect(Collectors.toList())); + } + + private Optional findFeePayment(StatefulAsset statefulAsset) { + return findFeeTx(statefulAsset) + .map(tx -> { + String txId = tx.getId(); + long burntFee = tx.getBurntFee(); + return new StatefulAsset.FeePayment(txId, burntFee); + }); + } + + private Optional findFeeTx(StatefulAsset statefulAsset) { + Set res = daoStateService.getAssetListingFeeOpReturnTxOutputs(); + Set res1 = daoStateService.getTxOutputStream().collect(Collectors.toSet()); + + if (res1.size() > 0) { + } + return daoStateService.getAssetListingFeeOpReturnTxOutputs().stream() + .filter(txOutput -> { + byte[] hash = AssetConsensus.getHash(statefulAsset); + byte[] opReturnData = AssetConsensus.getOpReturnData(hash); + return Arrays.equals(opReturnData, txOutput.getOpReturnData()); + }) + .map(txOutput -> daoStateService.getTx(txOutput.getTxId()).orElse(null)) + .filter(Objects::nonNull) + .findAny(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + updateList(); + } + + @Override + public void onParseBlockChainComplete() { + } + + /////////////////////////////////////////////////////////////////////////////////////////// // API /////////////////////////////////////////////////////////////////////////////////////////// @@ -72,6 +187,9 @@ public void addToRemovedAssetsListByVoting(String tickerSymbol) { log.info("Asset '{}' was removed by DAO voting", CurrencyUtil.getNameAndCode(tickerSymbol)); removedAssetsList.add(new RemovedAsset(tickerSymbol, RemoveReason.VOTING)); persist(); + + statefulAssets.stream().filter(statefulAsset -> statefulAsset.getTickerSymbol().equals(tickerSymbol)) + .findAny().ifPresent(this::terminateAssetIfRemovedByVoting); } public boolean hasPaidBSQFee(String tickerSymbol) { @@ -109,7 +227,55 @@ private List getRemovedAssetsByRemoveReason(RemoveReason removeRea .collect(Collectors.toList()); } + private void terminateAssetIfRemovedByVoting(StatefulAsset statefulAsset) { + if (isAssetRemoved(statefulAsset.getTickerSymbol())) + statefulAsset.terminate(); + } + private void persist() { storage.queueUpForSave(20); } + + public Transaction payFee(StatefulAsset statefulAsset, long listingFee) throws InsufficientMoneyException, TxException { + checkArgument(!statefulAsset.wasTerminated(), "Asset must not have been removed"); + checkArgument(listingFee >= getFeePerDay().value, "Fee must not be less then listing fee for 1 day."); + checkArgument(listingFee % 100 == 0, "Fee must be a multiple of 1 BSQ (100 satoshi)."); + try { + // We create a prepared Bsq Tx for the listing fee. + final Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTx(Coin.valueOf(listingFee)); + byte[] hash = AssetConsensus.getHash(statefulAsset); + byte[] opReturnData = AssetConsensus.getOpReturnData(hash); + // We add the BTC inputs for the miner fee. + final Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); + // We sign the BSQ inputs of the final tx. + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Asset listing fee tx: " + transaction); + return transaction; + } catch (WalletException | TransactionVerificationException e) { + throw new TxException(e); + } + } + + public Coin getFeePerDay() { + return AssetConsensus.getFeePerDay(daoStateService, daoStateService.getChainHeight()); + } + + // Broadcast tx and publish proposal to P2P network + public void publishTransaction(StatefulAsset statefulAsset, Transaction transaction, long listingFee, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + walletsManager.publishAndCommitBsqTx(transaction, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("Asset listing fee tx has been published. TxId={}", transaction.getHashAsString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } + }); + + statefulAsset.addFeePayment(new StatefulAsset.FeePayment(transaction.getHashAsString(), listingFee)); + } } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java new file mode 100644 index 00000000000..15d1f9bc4ca --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.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.core.dao.governance.asset; + +/** + * Maintain translation stings ("dao.assetState.*") + */ +public enum AssetState { + NOT_ACTIVATED, // Added to code base but no listing fee paid yet + ENABLED_BY_FEE_PAYMENT, + DE_LISTED_BY_INACTIVITY, + TERMINATED // Was removed by voting +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java new file mode 100644 index 00000000000..c52c7588316 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java @@ -0,0 +1,110 @@ +/* + * 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.governance.asset; + +import bisq.core.locale.CurrencyUtil; + +import bisq.asset.Asset; + +import java.util.ArrayList; +import java.util.List; + +import lombok.Getter; +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import org.jetbrains.annotations.NotNull; + +import static com.google.common.base.Preconditions.checkArgument; + +@Slf4j +@Getter +public class StatefulAsset implements Comparable { + @Value + public static class FeePayment { + private String txId; + private long fee; + + @Override + public String toString() { + return "FeePayment{" + + "\n txId='" + txId + '\'' + + ",\n fee=" + fee + + "\n}"; + } + } + + private final Asset asset; + // keep access private to ensure we cannot change state once remove by voting + private AssetState assetState = AssetState.NOT_ACTIVATED; + private List feePayments = new ArrayList<>(); + + public StatefulAsset(Asset asset) { + this.asset = asset; + } + + public boolean wasTerminated() { + return assetState == AssetState.TERMINATED; + } + + public String getTickerSymbol() { + return asset.getTickerSymbol(); + } + + public void terminate() { + assetState = AssetState.TERMINATED; + } + + public void enableByFeePayment(FeePayment feePayment) { + checkArgument(!wasTerminated(), "Cannot pay a fee for a removed asset"); + feePayments.add(feePayment); + assetState = AssetState.ENABLED_BY_FEE_PAYMENT; + } + + public void deListByInactivity() { + checkArgument(!wasTerminated(), "Cannot pay a fee for a removed asset"); + assetState = AssetState.DE_LISTED_BY_INACTIVITY; + } + + public long getActiveFee() { + return feePayments.stream().mapToLong(FeePayment::getFee).sum(); + } + + public void addFeePayment(FeePayment feePayment) { + log.error(feePayment.toString()); + feePayments.add(feePayment); + } + + @Override + public int compareTo(@NotNull StatefulAsset other) { + return getNameAndCode().compareTo(other.getNameAndCode()); + } + + public String getNameAndCode() { + return CurrencyUtil.getNameAndCode(getTickerSymbol()); + } + + @Override + public String toString() { + return "StatefulAsset{" + + "\n asset=" + asset + + ",\n assetState=" + assetState + + ",\n feePayments=" + feePayments + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java index 0c600632eaa..4b4952bac14 100644 --- a/core/src/main/java/bisq/core/dao/governance/param/Param.java +++ b/core/src/main/java/bisq/core/dao/governance/param/Param.java @@ -105,7 +105,9 @@ public enum Param { "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV", // testnet ParamType.ADDRESS), - ASSET_LISTING_FEE("1", ParamType.BSQ, 10, 10), + // Fee for activating an asset or re-listing after deactivation due lack of trade activity. Fee per day of trial period without activity checks. + ASSET_LISTING_FEE_PER_DAY("1", ParamType.BSQ, 10, 10), + // Min required trade volume to not get de-listed. Check starts after trial period and use trial period afterwards to look back for trade activity. ASSET_MIN_VOLUME("0.01", ParamType.BTC, 10, 10), // TODO for dev testing we use short periods... diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java new file mode 100644 index 00000000000..1870f419fab --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java @@ -0,0 +1,53 @@ +/* + * 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.governance.proofofburn; + +import bisq.core.dao.state.model.blockchain.OpReturnType; + +import bisq.common.app.Version; +import bisq.common.crypto.Hash; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; + +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class ProofOfBurnConsensus { + public static byte[] getHash(byte[] bytes) { + return Hash.getSha256Ripemd160hash(bytes); + } + + public static byte[] getOpReturnData(byte[] hash) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + outputStream.write(OpReturnType.PROOF_OF_BURN.getType()); + outputStream.write(Version.PROOF_OF_BURN); + outputStream.write(hash); + return outputStream.toByteArray(); + } catch (IOException e) { + // Not expected to happen ever + e.printStackTrace(); + log.error(e.toString()); + return new byte[0]; + } + } + + public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { + return opReturnData.length == 22; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java b/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java index dbecf46283e..26b187bd288 100644 --- a/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java +++ b/core/src/main/java/bisq/core/dao/governance/proposal/BaseProposalFactory.java @@ -112,6 +112,6 @@ protected byte[] getOpReturnData(byte[] hashOfPayload) { protected Transaction completeTx(Transaction preparedBurnFeeTx, byte[] opReturnData, Proposal proposal) throws WalletException, InsufficientMoneyException, TransactionVerificationException { - return btcWalletService.completePreparedProposalTx(preparedBurnFeeTx, opReturnData); + return btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); } } diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java index 15bfab0f2c4..5b21bd5bcb3 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxOutputType.java @@ -34,6 +34,8 @@ enum JsonTxOutputType { 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"), diff --git a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java index f0a47960b83..b58c2b6723e 100644 --- a/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java +++ b/core/src/main/java/bisq/core/dao/node/explorer/JsonTxType.java @@ -33,7 +33,9 @@ enum JsonTxType { BLIND_VOTE("Blind vote"), VOTE_REVEAL("Vote reveal"), LOCKUP("Lockup"), - UNLOCK("Unlock"); + UNLOCK("Unlock"), + ASSET_LISTING_FEE("Asset listing fee"), + PROOF_OF_BURN("Proof of burn"); @Getter private String displayString; diff --git a/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java b/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java index 75355c5fd56..6c56b0672d6 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/OpReturnParser.java @@ -17,9 +17,11 @@ package bisq.core.dao.node.parser; +import bisq.core.dao.governance.asset.AssetConsensus; import bisq.core.dao.governance.blindvote.BlindVoteConsensus; import bisq.core.dao.governance.bond.BondConsensus; import bisq.core.dao.governance.bond.lockup.LockupReason; +import bisq.core.dao.governance.proofofburn.ProofOfBurnConsensus; import bisq.core.dao.governance.proposal.ProposalConsensus; import bisq.core.dao.governance.voteresult.VoteResultConsensus; import bisq.core.dao.node.parser.exceptions.InvalidParsingConditionException; @@ -119,6 +121,16 @@ static TxOutputType getTxOutputType(TempTxOutput tempTxOutput) { } else { break; } + case ASSET_LISTING_FEE: + if (AssetConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.ASSET_LISTING_FEE_OP_RETURN_OUTPUT; + else + break; + case PROOF_OF_BURN: + if (ProofOfBurnConsensus.hasOpReturnDataValidLength(opReturnData)) + return TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT; + else + break; default: throw new InvalidParsingConditionException("We must have a defined opReturnType as it was checked earlier in the caller."); } diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java index c842f9efc39..6f4b111bd9a 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxInputParser.java @@ -100,6 +100,8 @@ void process(TxOutputKey txOutputKey, int blockHeight, String txId, int inputInd case BLIND_VOTE_OP_RETURN_OUTPUT: case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: case VOTE_REVEAL_OP_RETURN_OUTPUT: + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: break; case LOCKUP_OUTPUT: // A LOCKUP BSQ txOutput is spent to a corresponding UNLOCK diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index ed93be47ff2..68bf31d1e56 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -240,6 +240,10 @@ static Optional getMappedOpReturnType(TxOutputType outputType) { return Optional.of(OpReturnType.VOTE_REVEAL); case LOCKUP_OP_RETURN_OUTPUT: return Optional.of(OpReturnType.LOCKUP); + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.ASSET_LISTING_FEE); + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + return Optional.of(OpReturnType.PROOF_OF_BURN); default: return Optional.empty(); } diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java index 39eb276f791..8badd3d168f 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxParser.java @@ -203,6 +203,8 @@ private void applyTxTypeAndTxOutputType(int blockHeight, TempTx tempTx, long bsq processVoteReveal(blockHeight, tempTx); break; case LOCKUP: + case ASSET_LISTING_FEE: + case PROOF_OF_BURN: // do nothing break; } @@ -422,6 +424,10 @@ static TxType evaluateTxTypeFromOpReturnType(TempTx tempTx, OpReturnType opRetur return TxType.VOTE_REVEAL; case LOCKUP: return TxType.LOCKUP; + case ASSET_LISTING_FEE: + return TxType.ASSET_LISTING_FEE; + case PROOF_OF_BURN: + return TxType.PROOF_OF_BURN; default: log.warn("We got a BSQ tx with an unknown OP_RETURN. tx={}, opReturnType={}", tempTx, opReturnType); return TxType.INVALID; 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 acee24eb0cc..fb20bd81a87 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -418,6 +418,9 @@ public boolean isTxOutputSpendable(TxOutputKey key) { case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: case VOTE_REVEAL_OP_RETURN_OUTPUT: return true; + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: + return false; case LOCKUP_OUTPUT: return false; case LOCKUP_OP_RETURN_OUTPUT: @@ -462,6 +465,8 @@ public boolean isBsqTxOutputType(TxOutput txOutput) { case BLIND_VOTE_OP_RETURN_OUTPUT: case VOTE_REVEAL_UNLOCK_STAKE_OUTPUT: case VOTE_REVEAL_OP_RETURN_OUTPUT: + case ASSET_LISTING_FEE_OP_RETURN_OUTPUT: + case PROOF_OF_BURN_OP_RETURN_OUTPUT: case LOCKUP_OUTPUT: case LOCKUP_OP_RETURN_OUTPUT: case UNLOCK_OUTPUT: @@ -885,6 +890,15 @@ public List getDecryptedBallotsWithMeritsList() { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Asset listing fee + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getAssetListingFeeOpReturnTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.ASSET_LISTING_FEE_OP_RETURN_OUTPUT); + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java index d693f3c56bf..2c758b7ef5f 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/OpReturnType.java @@ -37,7 +37,9 @@ public enum OpReturnType implements ImmutableDaoStateModel { REIMBURSEMENT_REQUEST((byte) 0x12), BLIND_VOTE((byte) 0x13), VOTE_REVEAL((byte) 0x14), - LOCKUP((byte) 0x15); + LOCKUP((byte) 0x15), + ASSET_LISTING_FEE((byte) 0x16), + PROOF_OF_BURN((byte) 0x17); @Getter private byte type; diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java index 81532ed0540..6ddb51bea73 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxOutputType.java @@ -40,6 +40,8 @@ public enum TxOutputType implements ImmutableDaoStateModel { BLIND_VOTE_OP_RETURN_OUTPUT, VOTE_REVEAL_UNLOCK_STAKE_OUTPUT, VOTE_REVEAL_OP_RETURN_OUTPUT, + ASSET_LISTING_FEE_OP_RETURN_OUTPUT, + PROOF_OF_BURN_OP_RETURN_OUTPUT, LOCKUP_OUTPUT, LOCKUP_OP_RETURN_OUTPUT, UNLOCK_OUTPUT, diff --git a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java index cb6d7a0e3a4..96d0ef00fbf 100644 --- a/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java +++ b/core/src/main/java/bisq/core/dao/state/model/blockchain/TxType.java @@ -42,7 +42,9 @@ public enum TxType implements ImmutableDaoStateModel { BLIND_VOTE(true, true), VOTE_REVEAL(true, false), LOCKUP(true, false), - UNLOCK(true, false); + UNLOCK(true, false), + ASSET_LISTING_FEE(true, true), + PROOF_OF_BURN(true, true); /////////////////////////////////////////////////////////////////////////////////////////// diff --git a/core/src/main/java/bisq/core/locale/CurrencyUtil.java b/core/src/main/java/bisq/core/locale/CurrencyUtil.java index 58013495548..7f299497ac5 100644 --- a/core/src/main/java/bisq/core/locale/CurrencyUtil.java +++ b/core/src/main/java/bisq/core/locale/CurrencyUtil.java @@ -38,6 +38,7 @@ import java.util.Objects; import java.util.Optional; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; @@ -108,16 +109,20 @@ public static List getAllSortedCryptoCurrencies() { } private static List createAllSortedCryptoCurrenciesList() { - List result = assetRegistry.stream() - .filter(CurrencyUtil::assetIsNotBaseCurrency) - .filter(asset -> isNotBsqOrBsqTradingActivated(asset, BisqEnvironment.getBaseCurrencyNetwork(), DevEnv.isDaoTradingActivated())) - .filter(asset -> assetMatchesNetworkIfMainnet(asset, BisqEnvironment.getBaseCurrencyNetwork())) + List result = getAssetStream() .map(CurrencyUtil::assetToCryptoCurrency) .sorted(TradeCurrency::compareTo) .collect(Collectors.toList()); return result; } + public static Stream getAssetStream() { + return assetRegistry.stream() + .filter(CurrencyUtil::assetIsNotBaseCurrency) + .filter(asset -> isNotBsqOrBsqTradingActivated(asset, BisqEnvironment.getBaseCurrencyNetwork(), DevEnv.isDaoTradingActivated())) + .filter(asset -> assetMatchesNetworkIfMainnet(asset, BisqEnvironment.getBaseCurrencyNetwork())); + } + public static List getMainCryptoCurrencies() { final List result = new ArrayList<>(); if (DevEnv.isDaoTradingActivated()) diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index dfa0246f4f1..5fe11ef30d7 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1163,6 +1163,7 @@ account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must dao.tab.bsqWallet=BSQ wallet dao.tab.proposals=Governance dao.tab.bonding=Bonding +dao.tab.proofOfBurn=Asset fee/Proof of burn dao.paidWithBsq=paid with BSQ dao.availableBsqBalance=Available @@ -1416,6 +1417,27 @@ dao.bond.bondedRoleType.MEDIATOR=Mediator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.ARBITRATOR=Arbitrator +dao.burnBsq.assetFee=Asset fee +dao.burnBsq.menuItem.assetFee=Asset fee +dao.burnBsq.menuItem.proofOfBurn=Proof of burn +dao.burnBsq.header=Fee for asset listing +dao.burnBsq.selectAsset=Select Asset +dao.burnBsq.fee=Fee +dao.burnBsq.trialPeriod=Trial period +dao.burnBsq.payFee=Pay fee +dao.burnBsq.allAssets=All assets +dao.burnBsq.assets.nameAndCode=Asset +dao.burnBsq.assets.activeFee=Active fee +dao.burnBsq.assets.tradedVolume=Trade volume + +# suppress inspection "UnusedProperty" +dao.assetState.NOT_ACTIVATED=Not activated +# suppress inspection "UnusedProperty" +dao.assetState.ENABLED_BY_FEE_PAYMENT=Activated by fee payment +# suppress inspection "UnusedProperty" +dao.assetState.DE_LISTED_BY_INACTIVITY=Delisted due inactivity +# suppress inspection "UnusedProperty" +dao.assetState.TERMINATED=Removed by voting # suppress inspection "UnusedProperty" dao.phase.UNDEFINED=Undefined @@ -1615,6 +1637,10 @@ dao.tx.type.enum.VOTE_REVEAL=Vote reveal dao.tx.type.enum.LOCKUP=Lock up bond # suppress inspection "UnusedProperty" dao.tx.type.enum.UNLOCK=Unlock bond +# suppress inspection "UnusedProperty" +dao.tx.type.enum.ASSET_LISTING_FEE=Asset listing fee +# suppress inspection "UnusedProperty" +dao.tx.type.enum.PROOF_OF_BURN=Proof of burn dao.tx.issuanceFromCompReq=Compensation request/issuance dao.tx.issuanceFromCompReq.tooltip=Compensation request which led to an issuance of new BSQ.\n\ diff --git a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java index 9cfdf26d737..d19677e11e7 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/DaoView.java @@ -26,6 +26,7 @@ import bisq.desktop.common.view.ViewLoader; import bisq.desktop.main.MainView; import bisq.desktop.main.dao.bonding.BondingView; +import bisq.desktop.main.dao.burnbsq.BurnBsqView; import bisq.desktop.main.dao.governance.GovernanceView; import bisq.desktop.main.dao.wallet.BsqWalletView; import bisq.desktop.main.dao.wallet.dashboard.BsqDashboardView; @@ -48,12 +49,7 @@ public class DaoView extends ActivatableViewAndModel { @FXML - private - Tab bsqWalletTab; - @FXML - private Tab proposalsTab; - @FXML - private Tab bondingTab; + private Tab bsqWalletTab, proposalsTab, bondingTab, burnBsqTab; private Navigation.Listener navigationListener; private ChangeListener tabChangeListener; @@ -74,16 +70,19 @@ public void initialize() { bsqWalletTab = new Tab(Res.get("dao.tab.bsqWallet").toUpperCase()); proposalsTab = new Tab(Res.get("dao.tab.proposals").toUpperCase()); bondingTab = new Tab(Res.get("dao.tab.bonding").toUpperCase()); + burnBsqTab = new Tab(Res.get("dao.tab.proofOfBurn").toUpperCase()); bsqWalletTab.setClosable(false); proposalsTab.setClosable(false); bondingTab.setClosable(false); + burnBsqTab.setClosable(false); - root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab); + root.getTabs().addAll(bsqWalletTab, proposalsTab, bondingTab, burnBsqTab); if (!BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq() || !DevEnv.isDaoPhase2Activated()) { bondingTab.setDisable(true); proposalsTab.setDisable(true); + burnBsqTab.setDisable(true); } navigationListener = viewPath -> { @@ -106,6 +105,8 @@ public void initialize() { navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); } else if (newValue == bondingTab) { navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); + } else if (newValue == burnBsqTab) { + navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); } }; } @@ -123,6 +124,8 @@ else if (selectedItem == proposalsTab) navigation.navigateTo(MainView.class, DaoView.class, GovernanceView.class); else if (selectedItem == bondingTab) navigation.navigateTo(MainView.class, DaoView.class, BondingView.class); + else if (selectedItem == burnBsqTab) + navigation.navigateTo(MainView.class, DaoView.class, BurnBsqView.class); } } @@ -141,6 +144,8 @@ private void loadView(Class viewClass) { selectedTab = proposalsTab; } else if (view instanceof BondingView) { selectedTab = bondingTab; + } else if (view instanceof BurnBsqView) { + selectedTab = burnBsqTab; } selectedTab.setContent(view.getRoot()); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml new file mode 100644 index 00000000000..92b77529244 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.fxml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java new file mode 100644 index 00000000000..380fa4d47f0 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java @@ -0,0 +1,125 @@ +/* + * 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.desktop.main.dao.burnbsq; + +import bisq.desktop.Navigation; +import bisq.desktop.common.view.ActivatableViewAndModel; +import bisq.desktop.common.view.CachingViewLoader; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.common.view.View; +import bisq.desktop.common.view.ViewLoader; +import bisq.desktop.common.view.ViewPath; +import bisq.desktop.components.MenuItem; +import bisq.desktop.main.MainView; +import bisq.desktop.main.dao.DaoView; +import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView; +import bisq.desktop.main.dao.burnbsq.reputation.ProofOfBurnView; + +import bisq.core.locale.Res; + +import javax.inject.Inject; + +import javafx.fxml.FXML; + +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; + +import java.util.Arrays; +import java.util.List; + +@FxmlView +public class BurnBsqView extends ActivatableViewAndModel { + + private final ViewLoader viewLoader; + private final Navigation navigation; + + private MenuItem assetFee, proofOfBurn; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; + private ToggleGroup toggleGroup; + + @Inject + private BurnBsqView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; + } + + @Override + public void initialize() { + listener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(BurnBsqView.class) != 2) + return; + + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + toggleGroup = new ToggleGroup(); + final List> baseNavPath = Arrays.asList(MainView.class, DaoView.class, BurnBsqView.class); + assetFee = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.assetFee"), + AssetFeeView.class, baseNavPath); + proofOfBurn = new MenuItem(navigation, toggleGroup, Res.get("dao.burnBsq.menuItem.proofOfBurn"), + ProofOfBurnView.class, baseNavPath); + + leftVBox.getChildren().addAll(assetFee, proofOfBurn); + } + + @Override + protected void activate() { + assetFee.activate(); + proofOfBurn.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(BurnBsqView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = AssetFeeView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(BurnBsqView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } + } + + @SuppressWarnings("Duplicates") + @Override + protected void deactivate() { + navigation.removeListener(listener); + + assetFee.deactivate(); + proofOfBurn.deactivate(); + } + + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); + + if (view instanceof AssetFeeView) toggleGroup.selectToggle(assetFee); + else if (view instanceof ProofOfBurnView) toggleGroup.selectToggle(proofOfBurn); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml new file mode 100644 index 00000000000..bbb0b8ae4db --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java new file mode 100644 index 00000000000..b302b86879e --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -0,0 +1,346 @@ +/* + * 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.desktop.main.dao.burnbsq.assetfee; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.dao.bonding.BondingViewUtils; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.dao.governance.asset.AssetService; +import bisq.core.dao.governance.asset.StatefulAsset; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.BSFormatter; +import bisq.core.util.BsqFormatter; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.layout.GridPane; + +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ListChangeListener; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; +import javafx.util.StringConverter; + +import java.util.Comparator; +import java.util.stream.Collectors; + +import javax.annotation.Nullable; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; + +@FxmlView +public class AssetFeeView extends ActivatableView implements BsqBalanceListener { + public ComboBox assetComboBox; + private InputTextField feeAmountInputTextField; + private TextField trialPeriodTextField; + private Button payFeeButton; + private TableView tableView; + + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final BsqValidator bsqValidator; + private final DaoFacade daoFacade; + private final AssetService assetService; + private BSFormatter btcFormatter; + private final Preferences preferences; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener; + private ChangeListener amountInputTextFieldListener; + private ListChangeListener statefulAssetsChangeListener; + @Nullable + private StatefulAsset selectedAsset; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private AssetFeeView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BondingViewUtils bondingViewUtils, + BsqValidator bsqValidator, + DaoFacade daoFacade, + AssetService assetService, + BSFormatter btcFormatter, + Preferences preferences) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bsqValidator = bsqValidator; + this.daoFacade = daoFacade; + this.assetService = assetService; + this.btcFormatter = btcFormatter; + this.preferences = preferences; + } + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 3, Res.get("dao.burnBsq.header")); + + assetComboBox = FormBuilder.addComboBox(root, gridRow, + Res.get("dao.burnBsq.selectAsset"), Layout.FIRST_ROW_DISTANCE); + assetComboBox.setConverter(new StringConverter<>() { + @Override + public String toString(StatefulAsset statefulAsset) { + return CurrencyUtil.getNameAndCode(statefulAsset.getAsset().getTickerSymbol()); + } + + @Override + public StatefulAsset fromString(String string) { + return null; + } + }); + + feeAmountInputTextField = addInputTextField(root, ++gridRow, Res.get("dao.burnBsq.fee")); + feeAmountInputTextField.setValidator(bsqValidator); + + trialPeriodTextField = FormBuilder.addTopLabelTextField(root, ++gridRow, Res.get("dao.burnBsq.trialPeriod")).second; + + payFeeButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.burnBsq.payFee")); + + tableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.burnBsq.allAssets"), 20); + createColumns(); + tableView.setItems(sortedList); + + createListeners(); + } + + @Override + protected void activate() { + assetComboBox.setItems(assetService.getStatefulAssets()); + assetComboBox.setOnAction(e -> { + selectedAsset = assetComboBox.getSelectionModel().getSelectedItem(); + }); + + feeAmountInputTextField.textProperty().addListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + + assetService.getStatefulAssets().addListener(statefulAssetsChangeListener); + bsqWalletService.addBsqBalanceListener(this); + + payFeeButton.setOnAction((event) -> { + Coin listingFee = bsqFormatter.parseToCoin(feeAmountInputTextField.getText()); + try { + Transaction transaction = assetService.payFee(selectedAsset, listingFee.value); + Coin miningFee = transaction.getFee(); + int txSize = transaction.bitcoinSerialize().length; + + if (!DevEnv.isDevMode()) { + GUIUtil.showBsqFeeInfoPopup(listingFee, miningFee, txSize, bsqFormatter, btcFormatter, + Res.get("dao.burnBsq.assetFee"), () -> doPublishMyProposal(transaction, listingFee)); + } else { + doPublishMyProposal(transaction, listingFee); + } + } catch (InsufficientMoneyException | TxException e) { + e.printStackTrace(); + new Popup<>().error(e.toString()).show(); + } + + }); + + feeAmountInputTextField.resetValidation(); + + updateList(); + updateButtonState(); + } + + private void doPublishMyProposal(Transaction transaction, Coin listingFee) { + assetService.publishTransaction(selectedAsset, transaction, listingFee.value, + () -> { + assetComboBox.getSelectionModel().clearSelection(); + if (!DevEnv.isDevMode()) + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup<>().warning(errorMessage).show()); + } + + @Override + protected void deactivate() { + assetComboBox.setOnAction(null); + + feeAmountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + assetService.getStatefulAssets().removeListener(statefulAssetsChangeListener); + bsqWalletService.removeBsqBalanceListener(this); + + sortedList.comparatorProperty().unbind(); + + payFeeButton.setOnAction(null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin availableNonBsqBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + bsqValidator.setAvailableBalance(confirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + amountFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + amountInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + + statefulAssetsChangeListener = c -> updateList(); + } + + private void updateList() { + observableList.setAll(assetService.getStatefulAssets().stream() + .map(statefulAsset -> new AssetListItem(statefulAsset, bsqFormatter)) + .sorted(Comparator.comparing(AssetListItem::getNameAndCode)) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 10); + } + + private void updateButtonState() { + boolean isValid = bsqValidator.validate(feeAmountInputTextField.getText()).isValid && + selectedAsset != null; + payFeeButton.setDisable(!isValid); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.nameAndCode")); + column.setMinWidth(120); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getNameAndCode()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.activeFee")); + column.setMinWidth(60); + column.setMaxWidth(column.getMinWidth()); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getActiveFeeAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.tradedVolume")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(120); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getTradedVolumeAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java new file mode 100644 index 00000000000..90537d0ae78 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java @@ -0,0 +1,50 @@ +/* + * 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.desktop.main.dao.burnbsq.assetfee; + +import bisq.core.dao.governance.asset.StatefulAsset; +import bisq.core.locale.CurrencyUtil; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import lombok.Value; + +@Value +class AssetListItem { + private final StatefulAsset statefulAsset; + private final String tickerSymbol; + private final String assetStateString; + private final String activeFeeAsString; + private final int trialPeriodInBlocks; + private final String nameAndCode; + private final long activeFee; + private final String tradedVolumeAsString; + + AssetListItem(StatefulAsset statefulAsset, + BsqFormatter bsqFormatter) { + this.statefulAsset = statefulAsset; + + tickerSymbol = statefulAsset.getTickerSymbol(); + nameAndCode = CurrencyUtil.getNameAndCode(tickerSymbol); + assetStateString = Res.get("dao.assetState." + statefulAsset.getAssetState()); + activeFee = statefulAsset.getActiveFee(); + activeFeeAsString = Long.toString(activeFee); + trialPeriodInBlocks = (int) activeFee * 144; + tradedVolumeAsString = "TODO"; + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java new file mode 100644 index 00000000000..35e6a09a7b5 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java @@ -0,0 +1,29 @@ +/* + * 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.desktop.main.dao.burnbsq.reputation; + +import bisq.core.util.BsqFormatter; + +import lombok.Value; + +@Value +class ProofOfBurnListItem { + + ProofOfBurnListItem(BsqFormatter bsqFormatter) { + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml new file mode 100644 index 00000000000..3f8a173e4e1 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java new file mode 100644 index 00000000000..7a5f7fa8f69 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java @@ -0,0 +1,101 @@ +/* + * 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.desktop.main.dao.burnbsq.reputation; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.DaoFacade; +import bisq.core.user.Preferences; +import bisq.core.util.BsqFormatter; + +import org.bitcoinj.core.Coin; + +import javax.inject.Inject; + +import javafx.scene.layout.GridPane; + +@FxmlView +public class ProofOfBurnView extends ActivatableView implements BsqBalanceListener { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ProofOfBurnView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BsqValidator bsqValidator, + DaoFacade daoFacade, + Preferences preferences) { + } + + @Override + public void initialize() { + + } + + @Override + protected void activate() { + + updateList(); + } + + @Override + protected void deactivate() { + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin availableNonBsqBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + } + + private void updateList() { + } + + private void updateButtonState() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java index 6b984142688..80c68c17aa8 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/make/MakeProposalView.java @@ -124,7 +124,6 @@ private MakeProposalView(DaoFacade daoFacade, this.bsqFormatter = bsqFormatter; } - @Override public void initialize() { gridRow = phasesView.addGroup(root, gridRow); diff --git a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java index 331adc466f0..a16a9f3d3a4 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/wallet/tx/BsqTxView.java @@ -589,6 +589,14 @@ public void updateItem(final BsqTxListItem item, boolean empty) { awesomeIcon = AwesomeIcon.UNLOCK; style = "dao-tx-type-unlock-icon"; break; + case ASSET_LISTING_FEE: + awesomeIcon = AwesomeIcon.FILE_TEXT; + style = "dao-tx-type-proposal-fee-icon"; + break; + case PROOF_OF_BURN: + awesomeIcon = AwesomeIcon.FILE_TEXT; + style = "dao-tx-type-proposal-fee-icon"; + break; default: awesomeIcon = AwesomeIcon.QUESTION_SIGN; style = "dao-tx-type-unverified-icon"; From 33f671ed8b5b169e8394410f7e02ba32394fa559 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Thu, 8 Nov 2018 20:22:40 -0500 Subject: [PATCH 06/11] Cleanup --- .../java/bisq/core/dao/governance/asset/AssetService.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java index b7e61f157d9..cc8e0dba8b8 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -31,7 +31,6 @@ import bisq.core.dao.state.DaoStateService; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Tx; -import bisq.core.dao.state.model.blockchain.TxOutput; import bisq.core.locale.CurrencyUtil; import bisq.common.handlers.ErrorMessageHandler; @@ -52,7 +51,6 @@ import java.util.List; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import lombok.Getter; @@ -144,11 +142,6 @@ private Optional findFeePayment(StatefulAsset stateful } private Optional findFeeTx(StatefulAsset statefulAsset) { - Set res = daoStateService.getAssetListingFeeOpReturnTxOutputs(); - Set res1 = daoStateService.getTxOutputStream().collect(Collectors.toSet()); - - if (res1.size() > 0) { - } return daoStateService.getAssetListingFeeOpReturnTxOutputs().stream() .filter(txOutput -> { byte[] hash = AssetConsensus.getHash(statefulAsset); From d8bae356ff8c61c4335cdc1109b4968cd70210d4 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Fri, 9 Nov 2018 20:24:17 -0500 Subject: [PATCH 07/11] Complete asset listing fee feature --- common/src/main/proto/pb.proto | 13 +- .../main/java/bisq/core/app/BisqSetup.java | 6 + .../core/app/misc/AppSetupWithP2PAndDAO.java | 3 - .../dao/governance/asset/AssetService.java | 244 ++++++++++++------ .../core/dao/governance/asset/AssetState.java | 9 +- .../core/dao/governance/asset/FeePayment.java | 58 +++++ .../dao/governance/asset/RemoveReason.java | 23 -- .../dao/governance/asset/RemovedAsset.java | 72 ------ .../governance/asset/RemovedAssetsList.java | 75 ------ .../dao/governance/asset/StatefulAsset.java | 83 +++--- .../voteresult/VoteResultService.java | 10 - .../java/bisq/core/locale/CurrencyUtil.java | 15 +- .../payment/AccountAgeWitnessService.java | 2 +- .../CorePersistenceProtoResolver.java | 3 - .../core/setup/CorePersistedDataHost.java | 2 - .../statistics/AssetTradeActivityCheck.java | 12 +- .../resources/i18n/displayStrings.properties | 27 +- .../paymentmethods/CryptoCurrencyForm.java | 2 +- .../dao/burnbsq/assetfee/AssetFeeView.java | 185 +++++++++---- .../dao/burnbsq/assetfee/AssetListItem.java | 23 +- .../main/dao/governance/ProposalDisplay.java | 4 +- .../settings/preferences/PreferencesView.java | 2 +- 22 files changed, 448 insertions(+), 425 deletions(-) create mode 100644 core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java delete mode 100644 core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java delete mode 100644 core/src/main/java/bisq/core/dao/governance/asset/RemovedAsset.java delete mode 100644 core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index 8030685de0a..ae720d65591 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -941,9 +941,8 @@ message PersistableEnvelope { MyVoteList my_vote_list = 21; MyBlindVoteList my_blind_vote_list = 22; MeritList merit_list = 23; - RemovedAssetList removed_asset_list = 24; - DaoStateStore dao_state_store = 25; - MyReputationList my_reputation_list = 26; + DaoStateStore dao_state_store = 24; + MyReputationList my_reputation_list = 25; } } @@ -1535,14 +1534,6 @@ message RemoveAssetProposal { string ticker_symbol = 1; } -message RemovedAsset { - string ticker_symbol = 1; - string remove_reason = 2; -} -message RemovedAssetList { - repeated RemovedAsset removed_asset = 1; -} - message Role { string uid = 1; string name = 2; diff --git a/core/src/main/java/bisq/core/app/BisqSetup.java b/core/src/main/java/bisq/core/app/BisqSetup.java index 08ae08ac16c..a279939a33e 100644 --- a/core/src/main/java/bisq/core/app/BisqSetup.java +++ b/core/src/main/java/bisq/core/app/BisqSetup.java @@ -30,6 +30,7 @@ import bisq.core.btc.wallet.BtcWalletService; import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.DaoSetup; +import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.voteresult.VoteResultException; import bisq.core.dao.governance.voteresult.VoteResultService; import bisq.core.filter.FilterManager; @@ -152,6 +153,7 @@ public interface BisqSetupCompleteListener { private final MarketAlerts marketAlerts; private final VoteResultService voteResultService; private final AssetTradeActivityCheck tradeActivityCheck; + private final AssetService assetService; private final BSFormatter formatter; @Setter @Nullable @@ -226,6 +228,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, MarketAlerts marketAlerts, VoteResultService voteResultService, AssetTradeActivityCheck tradeActivityCheck, + AssetService assetService, BSFormatter formatter) { @@ -263,6 +266,7 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup, this.marketAlerts = marketAlerts; this.voteResultService = voteResultService; this.tradeActivityCheck = tradeActivityCheck; + this.assetService = assetService; this.formatter = formatter; } @@ -630,6 +634,8 @@ public void onBalanceChanged(Coin balance, Transaction tx) { tradeStatisticsManager.onAllServicesInitialized(); tradeActivityCheck.onAllServicesInitialized(); + assetService.onAllServicesInitialized(); + accountAgeWitnessService.onAllServicesInitialized(); priceFeedService.setCurrencyCodeOnInit(); diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java index 5195ab90e84..82cd737f018 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -19,7 +19,6 @@ import bisq.core.dao.DaoOptionKeys; import bisq.core.dao.DaoSetup; -import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; import bisq.core.dao.governance.bond.reputation.MyReputationListService; @@ -56,7 +55,6 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, MyBlindVoteListService myBlindVoteListService, MyProposalListService myProposalListService, MyReputationListService myReputationListService, - AssetService assetService, @Named(DaoOptionKeys.DAO_ACTIVATED) boolean daoActivated) { super(encryptionService, keyRing, @@ -74,7 +72,6 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, persistedDataHosts.add(myBlindVoteListService); persistedDataHosts.add(myProposalListService); persistedDataHosts.add(myReputationListService); - persistedDataHosts.add(assetService); } } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java index cc8e0dba8b8..41653405a87 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -17,7 +17,6 @@ package bisq.core.dao.governance.asset; -import bisq.core.app.BisqEnvironment; import bisq.core.btc.exceptions.TransactionVerificationException; import bisq.core.btc.exceptions.TxBroadcastException; import bisq.core.btc.exceptions.WalletException; @@ -26,17 +25,23 @@ import bisq.core.btc.wallet.TxBroadcaster; import bisq.core.btc.wallet.WalletsManager; import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.param.Param; import bisq.core.dao.governance.proposal.TxException; import bisq.core.dao.state.DaoStateListener; import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; import bisq.core.dao.state.model.blockchain.Block; import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.dao.state.model.governance.RemoveAssetProposal; import bisq.core.locale.CurrencyUtil; +import bisq.core.trade.statistics.TradeStatistics2; +import bisq.core.trade.statistics.TradeStatisticsManager; +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; import bisq.common.handlers.ErrorMessageHandler; import bisq.common.handlers.ResultHandler; -import bisq.common.proto.persistable.PersistedDataHost; -import bisq.common.storage.Storage; import org.bitcoinj.core.Coin; import org.bitcoinj.core.InsufficientMoneyException; @@ -44,32 +49,50 @@ import javax.inject.Inject; -import javafx.collections.FXCollections; -import javafx.collections.ObservableList; +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.collections.SetChangeListener; + +import java.util.ArrayList; import java.util.Arrays; +import java.util.Comparator; +import java.util.Date; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicLong; import java.util.stream.Collectors; +import java.util.stream.Stream; import lombok.Getter; import lombok.extern.slf4j.Slf4j; +import org.jetbrains.annotations.NotNull; + import static com.google.common.base.Preconditions.checkArgument; @Slf4j -public class AssetService implements PersistedDataHost, DaoSetupService, DaoStateListener { - private final Storage storage; +public class AssetService implements DaoSetupService, DaoStateListener { + private static final long DEFAULT_LOOK_BACK_PERIOD = 120; // 120 days + private final BsqWalletService bsqWalletService; private final BtcWalletService btcWalletService; private final WalletsManager walletsManager; + private final TradeStatisticsManager tradeStatisticsManager; private final DaoStateService daoStateService; @Getter - private final RemovedAssetsList removedAssetsList = new RemovedAssetsList(); + private IntegerProperty updateFlag = new SimpleIntegerProperty(0); @Getter - private final ObservableList statefulAssets = FXCollections.observableArrayList(); + private final List statefulAssets = new ArrayList<>(); + private Map> tradeStatsByTickerSymbol; + private long bsqFeePerDay; + private long minVolumeInBtc; + private Timer timer; /////////////////////////////////////////////////////////////////////////////////////////// @@ -77,36 +100,34 @@ public class AssetService implements PersistedDataHost, DaoSetupService, DaoStat /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public AssetService(Storage storage, - BsqWalletService bsqWalletService, + public AssetService(BsqWalletService bsqWalletService, BtcWalletService btcWalletService, WalletsManager walletsManager, + TradeStatisticsManager tradeStatisticsManager, DaoStateService daoStateService) { - this.storage = storage; this.bsqWalletService = bsqWalletService; this.btcWalletService = btcWalletService; this.walletsManager = walletsManager; + this.tradeStatisticsManager = tradeStatisticsManager; this.daoStateService = daoStateService; } - - /////////////////////////////////////////////////////////////////////////////////////////// - // PersistedDataHost - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void readPersisted() { - //TODO why not use dao state? - if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { - RemovedAssetsList persisted = storage.initAndGetPersisted(removedAssetsList, 100); - if (persisted != null) { - removedAssetsList.clear(); - removedAssetsList.addAll(persisted.getList()); - } - } + public void onAllServicesInitialized() { + tradeStatsByTickerSymbol = getTradeStatsByTickerSymbol(); + tradeStatisticsManager.getObservableTradeStatisticsSet().addListener((SetChangeListener) change -> { + // At startup if a user has downloaded the app long after the release he might receive a lots of trade statistic + // objects from the seed node. We don't want to trigger the expensive getTradeStatsByTickerSymbol call in + // between so we delay 20 sec. to be sure to call it after the data has been processed. + // To use a listener would be better but that requires bigger effort at the p2p lib side. + if (timer == null) + timer = UserThread.runAfter(() -> { + tradeStatsByTickerSymbol = getTradeStatsByTickerSymbol(); + updateList(); + timer = null; + }, 20); + }); } - /////////////////////////////////////////////////////////////////////////////////////////// // DaoSetupService /////////////////////////////////////////////////////////////////////////////////////////// @@ -118,30 +139,109 @@ public void addListeners() { @Override public void start() { - updateList(); + statefulAssets.clear(); + statefulAssets.addAll(CurrencyUtil.getSortedAssetStream() + .filter(asset -> !asset.getTickerSymbol().equals("BSQ")) + .map(StatefulAsset::new) + .collect(Collectors.toList())); } public void updateList() { - statefulAssets.setAll(CurrencyUtil.getAssetStream() - .map(StatefulAsset::new) - .peek(statefulAsset -> { - terminateAssetIfRemovedByVoting(statefulAsset); - findFeePayment(statefulAsset).ifPresent(statefulAsset::addFeePayment); - }) - .sorted(StatefulAsset::compareTo) - .collect(Collectors.toList())); + if (tradeStatsByTickerSymbol == null) + return; + + statefulAssets.forEach(statefulAsset -> { + AssetState assetState; + if (wasAssetRemovedByVoting(statefulAsset.getTickerSymbol())) { + assetState = AssetState.REMOVED_BY_VOTING; + } else { + statefulAsset.setFeePayments(getFeePayments(statefulAsset)); + + long lookBackPeriodInDays = getLookBackPeriodInDays(statefulAsset); + statefulAsset.setLookBackPeriodInDays(lookBackPeriodInDays); + long tradeVolume = getTradeVolume(statefulAsset, lookBackPeriodInDays); + statefulAsset.setTradeVolume(tradeVolume); + if (isInTrialPeriod(statefulAsset)) { + assetState = AssetState.IN_TRIAL_PERIOD; + } else if (tradeVolume >= minVolumeInBtc) { + assetState = AssetState.ACTIVELY_TRADED; + } else { + assetState = AssetState.DE_LISTED; + } + } + statefulAsset.setAssetState(assetState); + }); + + updateFlag.set(updateFlag.get() + 1); } - private Optional findFeePayment(StatefulAsset statefulAsset) { - return findFeeTx(statefulAsset) + private Map> getTradeStatsByTickerSymbol() { + Map> map = new HashMap<>(); + tradeStatisticsManager.getObservableTradeStatisticsSet().stream() + .filter(e -> CurrencyUtil.isCryptoCurrency(e.getBaseCurrency())) + .forEach(e -> { + map.putIfAbsent(e.getBaseCurrency(), new ArrayList<>()); + map.get(e.getBaseCurrency()).add(e); + }); + return map; + } + + private boolean isInTrialPeriod(StatefulAsset statefulAsset) { + for (FeePayment feePayment : statefulAsset.getFeePayments()) { + Optional passedDays = feePayment.getPassedDays(daoStateService); + if (passedDays.isPresent()) { + long daysCoveredByFee = feePayment.daysCoveredByFee(bsqFeePerDay); + if (daysCoveredByFee >= passedDays.get()) { + return true; + } + } + } + return false; + } + + private long getTradeVolume(StatefulAsset statefulAsset, long lookBackPeriodInDays) { + String tickerSymbol = statefulAsset.getTickerSymbol(); + if (tradeStatsByTickerSymbol.containsKey(tickerSymbol)) { + List tradeStatisticsForAsset = tradeStatsByTickerSymbol.get(tickerSymbol); + return getTradeVolume(tradeStatisticsForAsset, lookBackPeriodInDays); + } else { + return 0; + } + } + + @NotNull + private Long getLookBackPeriodInDays(StatefulAsset statefulAsset) { + return statefulAsset.getLastFeePayment() + .map(feePayment -> feePayment.daysCoveredByFee(bsqFeePerDay)) + .orElse(DEFAULT_LOOK_BACK_PERIOD); + } + + private long getTradeVolume(List tradeStatisticsForAsset, long lookBackPeriodInDays) { + // We cannot use blocks as the block height is not in the TradeStatistics2 object and the lookup for all the + // deposit txs would be too expensive. + long lookBackPeriodInMs = TimeUnit.DAYS.toMillis(lookBackPeriodInDays); + AtomicLong accumulatedTradeAmount = new AtomicLong(0); + long now = new Date().getTime(); + tradeStatisticsForAsset.forEach(stat -> { + long timePassed = now - stat.getTradeDate().getTime(); + if (timePassed < lookBackPeriodInMs) { + accumulatedTradeAmount.addAndGet(stat.getTradeAmount().value); + } + }); + return accumulatedTradeAmount.get(); + } + + private List getFeePayments(StatefulAsset statefulAsset) { + return getFeeTxs(statefulAsset).stream() .map(tx -> { String txId = tx.getId(); long burntFee = tx.getBurntFee(); - return new StatefulAsset.FeePayment(txId, burntFee); - }); + return new FeePayment(txId, burntFee); + }) + .collect(Collectors.toList()); } - private Optional findFeeTx(StatefulAsset statefulAsset) { + private List getFeeTxs(StatefulAsset statefulAsset) { return daoStateService.getAssetListingFeeOpReturnTxOutputs().stream() .filter(txOutput -> { byte[] hash = AssetConsensus.getHash(statefulAsset); @@ -150,7 +250,8 @@ private Optional findFeeTx(StatefulAsset statefulAsset) { }) .map(txOutput -> daoStateService.getTx(txOutput.getTxId()).orElse(null)) .filter(Objects::nonNull) - .findAny(); + .sorted(Comparator.comparing(BaseTx::getTime)) + .collect(Collectors.toList()); } @@ -164,7 +265,11 @@ public void onNewBlockHeight(int blockHeight) { @Override public void onParseTxsComplete(Block block) { + int chainHeight = daoStateService.getChainHeight(); + bsqFeePerDay = daoStateService.getParamValueAsCoin(Param.ASSET_LISTING_FEE_PER_DAY, chainHeight).value; + minVolumeInBtc = daoStateService.getParamValueAsCoin(Param.ASSET_MIN_VOLUME, chainHeight).value; updateList(); + } @Override @@ -176,61 +281,36 @@ public void onParseBlockChainComplete() { // API /////////////////////////////////////////////////////////////////////////////////////////// - public void addToRemovedAssetsListByVoting(String tickerSymbol) { - log.info("Asset '{}' was removed by DAO voting", CurrencyUtil.getNameAndCode(tickerSymbol)); - removedAssetsList.add(new RemovedAsset(tickerSymbol, RemoveReason.VOTING)); - persist(); - - statefulAssets.stream().filter(statefulAsset -> statefulAsset.getTickerSymbol().equals(tickerSymbol)) - .findAny().ifPresent(this::terminateAssetIfRemovedByVoting); - } - - public boolean hasPaidBSQFee(String tickerSymbol) { - //TODO - return false; - } - - - public boolean isAssetRemoved(String tickerSymbol) { - boolean isRemoved = removedAssetsList.getList().stream() - .anyMatch(removedAsset -> removedAsset.getTickerSymbol().equals(tickerSymbol)); + public boolean wasAssetRemovedByVoting(String tickerSymbol) { + boolean isRemoved = getRemoveAssetProposalStream() + .anyMatch(proposal -> proposal.getTickerSymbol().equals(tickerSymbol)); if (isRemoved) log.info("Asset '{}' was removed", CurrencyUtil.getNameAndCode(tickerSymbol)); return isRemoved; } - public boolean isAssetRemovedByVoting1(String tickerSymbol) { - boolean isRemoved = getRemovedAssetsByRemoveReason(RemoveReason.VOTING).stream() - .anyMatch(removedAsset -> removedAsset.getTickerSymbol().equals(tickerSymbol)); - if (isRemoved) - log.info("Asset '{}' was removed by DAO voting", CurrencyUtil.getNameAndCode(tickerSymbol)); - - return isRemoved; + public boolean isActive(String tickerSymbol) { + return DevEnv.isDaoActivated() ? findAsset(tickerSymbol).map(StatefulAsset::isActive).orElse(false) : true; } + private Optional findAsset(String tickerSymbol) { + return statefulAssets.stream().filter(e -> e.getTickerSymbol().equals(tickerSymbol)).findAny(); + } /////////////////////////////////////////////////////////////////////////////////////////// // Private /////////////////////////////////////////////////////////////////////////////////////////// - private List getRemovedAssetsByRemoveReason(RemoveReason removeReason) { - return removedAssetsList.getList().stream() - .filter(e -> e.getRemoveReason() == removeReason) - .collect(Collectors.toList()); + private Stream getRemoveAssetProposalStream() { + return daoStateService.getEvaluatedProposalList().stream() + .filter(evaluatedProposal -> evaluatedProposal.getProposal() instanceof RemoveAssetProposal) + .map(e -> ((RemoveAssetProposal) e.getProposal())); } - private void terminateAssetIfRemovedByVoting(StatefulAsset statefulAsset) { - if (isAssetRemoved(statefulAsset.getTickerSymbol())) - statefulAsset.terminate(); - } - - private void persist() { - storage.queueUpForSave(20); - } public Transaction payFee(StatefulAsset statefulAsset, long listingFee) throws InsufficientMoneyException, TxException { - checkArgument(!statefulAsset.wasTerminated(), "Asset must not have been removed"); + checkArgument(!statefulAsset.wasRemovedByVoting(), "Asset must not have been removed"); checkArgument(listingFee >= getFeePerDay().value, "Fee must not be less then listing fee for 1 day."); checkArgument(listingFee % 100 == 0, "Fee must be a multiple of 1 BSQ (100 satoshi)."); try { @@ -268,7 +348,5 @@ public void onFailure(TxBroadcastException exception) { errorMessageHandler.handleErrorMessage(exception.getMessage()); } }); - - statefulAsset.addFeePayment(new StatefulAsset.FeePayment(transaction.getHashAsString(), listingFee)); } } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java index 15d1f9bc4ca..24fed67670a 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java @@ -21,8 +21,9 @@ * Maintain translation stings ("dao.assetState.*") */ public enum AssetState { - NOT_ACTIVATED, // Added to code base but no listing fee paid yet - ENABLED_BY_FEE_PAYMENT, - DE_LISTED_BY_INACTIVITY, - TERMINATED // Was removed by voting + UNDEFINED, + IN_TRIAL_PERIOD, + ACTIVELY_TRADED, + DE_LISTED, + REMOVED_BY_VOTING // Was removed by voting } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java b/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java new file mode 100644 index 00000000000..9bfa435d577 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/FeePayment.java @@ -0,0 +1,58 @@ +/* + * 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.governance.asset; + +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.Tx; + +import java.util.Optional; + +import lombok.Value; + +@Value +public class FeePayment { + private final String txId; + private final long fee; + + FeePayment(String txId, long fee) { + this.txId = txId; + this.fee = fee; + } + + public long daysCoveredByFee(long bsqFeePerDay) { + return fee / bsqFeePerDay; + } + + public Optional getPassedDays(DaoStateService daoStateService) { + Optional optionalTx = daoStateService.getTx(txId); + if (optionalTx.isPresent()) { + int passedBlocks = daoStateService.getChainHeight() - optionalTx.get().getBlockHeight(); + return Optional.of(passedBlocks / 144); + } else { + return Optional.empty(); + } + } + + @Override + public String toString() { + return "FeePayment{" + + "\n txId='" + txId + '\'' + + ",\n fee=" + fee + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java b/core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java deleted file mode 100644 index 81635722b10..00000000000 --- a/core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java +++ /dev/null @@ -1,23 +0,0 @@ -/* - * 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.governance.asset; - -public enum RemoveReason { - VOTING, - INACTIVITY -} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/RemovedAsset.java b/core/src/main/java/bisq/core/dao/governance/asset/RemovedAsset.java deleted file mode 100644 index 122693c2b59..00000000000 --- a/core/src/main/java/bisq/core/dao/governance/asset/RemovedAsset.java +++ /dev/null @@ -1,72 +0,0 @@ -/* - * 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.governance.asset; - -import bisq.common.proto.ProtoUtil; -import bisq.common.proto.persistable.PersistablePayload; - -import io.bisq.generated.protobuffer.PB; - -import java.util.Objects; - -import lombok.Value; - -@Value -public class RemovedAsset implements PersistablePayload { - private final String tickerSymbol; - private final RemoveReason removeReason; - - RemovedAsset(String tickerSymbol, RemoveReason removeReason) { - this.tickerSymbol = tickerSymbol; - this.removeReason = removeReason; - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public PB.RemovedAsset toProtoMessage() { - PB.RemovedAsset.Builder builder = PB.RemovedAsset.newBuilder() - .setTickerSymbol(tickerSymbol) - .setRemoveReason(removeReason.name()); - return builder.build(); - } - - public static RemovedAsset fromProto(PB.RemovedAsset proto) { - return new RemovedAsset(proto.getTickerSymbol(), - ProtoUtil.enumFromProto(RemoveReason.class, proto.getRemoveReason())); - } - - // 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 RemovedAsset)) return false; - if (!super.equals(o)) return false; - RemovedAsset that = (RemovedAsset) o; - return Objects.equals(tickerSymbol, that.tickerSymbol) && - removeReason.name().equals(that.removeReason.name()); - } - - @Override - public int hashCode() { - return Objects.hash(super.hashCode(), tickerSymbol, removeReason.name()); - } -} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java b/core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java deleted file mode 100644 index 9c655886cea..00000000000 --- a/core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java +++ /dev/null @@ -1,75 +0,0 @@ -/* - * 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.governance.asset; - -import bisq.core.dao.governance.ConsensusCritical; - -import bisq.common.proto.persistable.PersistableList; - -import io.bisq.generated.protobuffer.PB; - -import java.util.ArrayList; -import java.util.List; -import java.util.stream.Collectors; - -import lombok.EqualsAndHashCode; - -/** - * PersistableEnvelope wrapper for list of removedAssets. - */ -@EqualsAndHashCode(callSuper = true) -public class RemovedAssetsList extends PersistableList implements ConsensusCritical { - - public RemovedAssetsList(List list) { - super(list); - } - - public RemovedAssetsList() { - super(); - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // PROTO BUFFER - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public PB.PersistableEnvelope toProtoMessage() { - return PB.PersistableEnvelope.newBuilder().setRemovedAssetList(getBuilder()).build(); - } - - public PB.RemovedAssetList.Builder getBuilder() { - return PB.RemovedAssetList.newBuilder() - .addAllRemovedAsset(getList().stream() - .map(RemovedAsset::toProtoMessage) - .collect(Collectors.toList())); - } - - public static RemovedAssetsList fromProto(PB.RemovedAssetList proto) { - return new RemovedAssetsList(new ArrayList<>(proto.getRemovedAssetList().stream() - .map(RemovedAsset::fromProto) - .collect(Collectors.toList()))); - } - - @Override - public String toString() { - return "List of tickerSymbols in RemovedAssetList: " + getList().stream() - .map(RemovedAsset::getTickerSymbol) - .collect(Collectors.toList()); - } -} diff --git a/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java index c52c7588316..dca2a1650b2 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java @@ -23,88 +23,85 @@ import java.util.ArrayList; import java.util.List; +import java.util.Optional; import lombok.Getter; -import lombok.Value; +import lombok.Setter; import lombok.extern.slf4j.Slf4j; -import org.jetbrains.annotations.NotNull; - -import static com.google.common.base.Preconditions.checkArgument; - @Slf4j @Getter -public class StatefulAsset implements Comparable { - @Value - public static class FeePayment { - private String txId; - private long fee; - - @Override - public String toString() { - return "FeePayment{" + - "\n txId='" + txId + '\'' + - ",\n fee=" + fee + - "\n}"; - } - } - +public class StatefulAsset { private final Asset asset; - // keep access private to ensure we cannot change state once remove by voting - private AssetState assetState = AssetState.NOT_ACTIVATED; + @Setter + private AssetState assetState = AssetState.UNDEFINED; private List feePayments = new ArrayList<>(); + @Setter + private long tradeVolume; + @Setter + private long lookBackPeriodInDays; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// public StatefulAsset(Asset asset) { this.asset = asset; } - public boolean wasTerminated() { - return assetState == AssetState.TERMINATED; + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getNameAndCode() { + return CurrencyUtil.getNameAndCode(getTickerSymbol()); } public String getTickerSymbol() { return asset.getTickerSymbol(); } - public void terminate() { - assetState = AssetState.TERMINATED; + public void setFeePayments(List feePayments) { + this.feePayments = feePayments; } - public void enableByFeePayment(FeePayment feePayment) { - checkArgument(!wasTerminated(), "Cannot pay a fee for a removed asset"); - feePayments.add(feePayment); - assetState = AssetState.ENABLED_BY_FEE_PAYMENT; + public Optional getLastFeePayment() { + return feePayments.isEmpty() ? Optional.empty() : Optional.of(feePayments.get(feePayments.size() - 1)); } - public void deListByInactivity() { - checkArgument(!wasTerminated(), "Cannot pay a fee for a removed asset"); - assetState = AssetState.DE_LISTED_BY_INACTIVITY; + public long getTotalFeesPaid() { + return feePayments.stream().mapToLong(FeePayment::getFee).sum(); } - public long getActiveFee() { - return feePayments.stream().mapToLong(FeePayment::getFee).sum(); + public long getFeeOfTrialPeriod() { + return getLastFeePayment() + .map(FeePayment::getFee) + .filter(e -> assetState == AssetState.IN_TRIAL_PERIOD) + .orElse(0L); } - public void addFeePayment(FeePayment feePayment) { - log.error(feePayment.toString()); - feePayments.add(feePayment); + public boolean isActive() { + return !wasRemovedByVoting() && !isDeListed(); } - @Override - public int compareTo(@NotNull StatefulAsset other) { - return getNameAndCode().compareTo(other.getNameAndCode()); + public boolean wasRemovedByVoting() { + return assetState == AssetState.REMOVED_BY_VOTING; } - public String getNameAndCode() { - return CurrencyUtil.getNameAndCode(getTickerSymbol()); + public boolean isDeListed() { + return assetState == AssetState.DE_LISTED; } + @Override public String toString() { return "StatefulAsset{" + "\n asset=" + asset + ",\n assetState=" + assetState + ",\n feePayments=" + feePayments + + ",\n tradeVolume=" + tradeVolume + + ",\n lookBackPeriodInDays=" + lookBackPeriodInDays + "\n}"; } } diff --git a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java index b0fad9b88ae..f647105f6df 100644 --- a/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java +++ b/core/src/main/java/bisq/core/dao/governance/voteresult/VoteResultService.java @@ -18,14 +18,12 @@ package bisq.core.dao.governance.voteresult; import bisq.core.dao.DaoSetupService; -import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.BlindVote; import bisq.core.dao.governance.blindvote.BlindVoteConsensus; import bisq.core.dao.governance.blindvote.BlindVoteListService; import bisq.core.dao.governance.blindvote.VoteWithProposalTxId; import bisq.core.dao.governance.blindvote.VoteWithProposalTxIdList; -import bisq.core.dao.governance.bond.role.BondedRolesRepository; import bisq.core.dao.governance.merit.MeritConsensus; import bisq.core.dao.governance.period.PeriodService; import bisq.core.dao.governance.proposal.IssuanceProposal; @@ -102,9 +100,7 @@ public class VoteResultService implements DaoStateListener, DaoSetupService { private final PeriodService periodService; private final BallotListService ballotListService; private final BlindVoteListService blindVoteListService; - private final BondedRolesRepository bondedRolesRepository; private final IssuanceService issuanceService; - private final AssetService assetService; private final MissingDataRequestService missingDataRequestService; @Getter private final ObservableList voteResultExceptions = FXCollections.observableArrayList(); @@ -121,9 +117,7 @@ public VoteResultService(VoteRevealService voteRevealService, PeriodService periodService, BallotListService ballotListService, BlindVoteListService blindVoteListService, - BondedRolesRepository bondedRolesRepository, IssuanceService issuanceService, - AssetService assetService, MissingDataRequestService missingDataRequestService) { this.voteRevealService = voteRevealService; this.proposalListPresentation = proposalListPresentation; @@ -131,9 +125,7 @@ public VoteResultService(VoteRevealService voteRevealService, this.periodService = periodService; this.ballotListService = ballotListService; this.blindVoteListService = blindVoteListService; - this.bondedRolesRepository = bondedRolesRepository; this.issuanceService = issuanceService; - this.assetService = assetService; this.missingDataRequestService = missingDataRequestService; } @@ -677,8 +669,6 @@ private void applyRemoveAsset(Set acceptedEvaluatedProposals, if (evaluatedProposal.getProposal() instanceof RemoveAssetProposal) { RemoveAssetProposal removeAssetProposal = (RemoveAssetProposal) evaluatedProposal.getProposal(); String tickerSymbol = removeAssetProposal.getTickerSymbol(); - assetService.addToRemovedAssetsListByVoting(tickerSymbol); - StringBuilder sb = new StringBuilder(); sb.append("\n################################################################################\n"); sb.append("We removed an asset. ProposalTxId=").append(removeAssetProposal.getTxId()) diff --git a/core/src/main/java/bisq/core/locale/CurrencyUtil.java b/core/src/main/java/bisq/core/locale/CurrencyUtil.java index 7f299497ac5..959e28d2cdd 100644 --- a/core/src/main/java/bisq/core/locale/CurrencyUtil.java +++ b/core/src/main/java/bisq/core/locale/CurrencyUtil.java @@ -40,7 +40,6 @@ import java.util.stream.Collectors; import java.util.stream.Stream; -import lombok.Getter; import lombok.extern.slf4j.Slf4j; import static com.google.common.base.Preconditions.checkArgument; @@ -52,7 +51,6 @@ public static void setup() { setBaseCurrencyCode(BisqEnvironment.getBaseCurrencyNetwork().getCurrencyCode()); } - @Getter private static final AssetRegistry assetRegistry = new AssetRegistry(); private static String baseCurrencyCode = "BTC"; @@ -109,18 +107,17 @@ public static List getAllSortedCryptoCurrencies() { } private static List createAllSortedCryptoCurrenciesList() { - List result = getAssetStream() + return getSortedAssetStream() .map(CurrencyUtil::assetToCryptoCurrency) - .sorted(TradeCurrency::compareTo) .collect(Collectors.toList()); - return result; } - public static Stream getAssetStream() { + public static Stream getSortedAssetStream() { return assetRegistry.stream() .filter(CurrencyUtil::assetIsNotBaseCurrency) .filter(asset -> isNotBsqOrBsqTradingActivated(asset, BisqEnvironment.getBaseCurrencyNetwork(), DevEnv.isDaoTradingActivated())) - .filter(asset -> assetMatchesNetworkIfMainnet(asset, BisqEnvironment.getBaseCurrencyNetwork())); + .filter(asset -> assetMatchesNetworkIfMainnet(asset, BisqEnvironment.getBaseCurrencyNetwork())) + .sorted(Comparator.comparing(Asset::getName)); } public static List getMainCryptoCurrencies() { @@ -481,9 +478,9 @@ public static Optional findAsset(String tickerSymbol, BaseCurrencyNetwork } // Excludes all assets which got removed by DAO voting - public static List getWhiteListedSortedCryptoCurrencies(AssetService assetService) { + public static List getActiveSortedCryptoCurrencies(AssetService assetService) { return getAllSortedCryptoCurrencies().stream() - .filter(e -> !assetService.isAssetRemoved(e.getCode())) + .filter(e -> assetService.isActive(e.getCode())) .collect(Collectors.toList()); } } diff --git a/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java b/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java index 38cac92d482..eff237354ae 100644 --- a/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java +++ b/core/src/main/java/bisq/core/payment/AccountAgeWitnessService.java @@ -136,7 +136,7 @@ private void republishAllFiatAccounts() { } private void addToMap(AccountAgeWitness accountAgeWitness) { - log.debug("addToMap hash=" + Utilities.bytesAsHexString(accountAgeWitness.getHash())); + //log.debug("addToMap hash=" + Utilities.bytesAsHexString(accountAgeWitness.getHash())); if (!accountAgeWitnessMap.containsKey(accountAgeWitness.getHashAsByteArray())) accountAgeWitnessMap.put(accountAgeWitness.getHashAsByteArray(), accountAgeWitness); } diff --git a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java index 4f6ed3065e7..b4b1cc3ecf4 100644 --- a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -20,7 +20,6 @@ import bisq.core.arbitration.DisputeList; import bisq.core.btc.model.AddressEntryList; import bisq.core.btc.wallet.BtcWalletService; -import bisq.core.dao.governance.asset.RemovedAssetsList; import bisq.core.dao.governance.blindvote.MyBlindVoteList; import bisq.core.dao.governance.blindvote.storage.BlindVoteStore; import bisq.core.dao.governance.bond.reputation.MyReputationList; @@ -129,8 +128,6 @@ public PersistableEnvelope fromProto(PB.PersistableEnvelope proto) { return MyBlindVoteList.fromProto(proto.getMyBlindVoteList()); case MERIT_LIST: return MeritList.fromProto(proto.getMeritList()); - case REMOVED_ASSET_LIST: - return RemovedAssetsList.fromProto(proto.getRemovedAssetList()); case DAO_STATE_STORE: return DaoStateStore.fromProto(proto.getDaoStateStore()); case MY_REPUTATION_LIST: diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index a48b1f44547..ced7dccd155 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -20,7 +20,6 @@ import bisq.core.arbitration.DisputeManager; import bisq.core.btc.model.AddressEntryList; import bisq.core.dao.DaoOptionKeys; -import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.ballot.BallotListService; import bisq.core.dao.governance.blindvote.MyBlindVoteListService; import bisq.core.dao.governance.bond.reputation.MyReputationListService; @@ -68,7 +67,6 @@ public static List getPersistedDataHosts(Injector injector) { persistedDataHosts.add(injector.getInstance(MyVoteListService.class)); persistedDataHosts.add(injector.getInstance(MyProposalListService.class)); persistedDataHosts.add(injector.getInstance(MyReputationListService.class)); - persistedDataHosts.add(injector.getInstance(AssetService.class)); } return persistedDataHosts; } diff --git a/core/src/main/java/bisq/core/trade/statistics/AssetTradeActivityCheck.java b/core/src/main/java/bisq/core/trade/statistics/AssetTradeActivityCheck.java index 8127260c748..85b35493507 100644 --- a/core/src/main/java/bisq/core/trade/statistics/AssetTradeActivityCheck.java +++ b/core/src/main/java/bisq/core/trade/statistics/AssetTradeActivityCheck.java @@ -44,6 +44,10 @@ import lombok.extern.slf4j.Slf4j; +/** + * This can be removed once the AssetService is activated with the DAO. + * At the moment it is only used for printing out trade statistics. + */ @Slf4j public class AssetTradeActivityCheck { private final AssetService assetService; @@ -75,7 +79,7 @@ public void onAllServicesInitialized() { StringBuilder sufficientlyTraded = new StringBuilder("\nSufficiently traded assets:"); StringBuilder insufficientlyTraded = new StringBuilder("\nInsufficiently traded assets:"); StringBuilder notTraded = new StringBuilder("\nNot traded assets:"); - List whiteListedSortedCryptoCurrencies = CurrencyUtil.getWhiteListedSortedCryptoCurrencies(assetService); + List whiteListedSortedCryptoCurrencies = CurrencyUtil.getActiveSortedCryptoCurrencies(assetService); Set assetsToRemove = new HashSet<>(whiteListedSortedCryptoCurrencies); whiteListedSortedCryptoCurrencies.forEach(e -> { String code = e.getCode(); @@ -99,7 +103,7 @@ public void onAllServicesInitialized() { .append(numTrades); } - if (!isWarmingUp(code) && !hasPaidBSQFee(code)) { + if (!isWarmingUp(code) /*&& !hasPaidBSQFee(code)*/) { if (isInTradeStatMap) { if (tradeAmount >= minTradeAmount || numTrades >= minNumOfTrades) { assetsToRemove.remove(e); @@ -136,10 +140,6 @@ public void onAllServicesInitialized() { log.debug(result); } - private boolean hasPaidBSQFee(String code) { - return assetService.hasPaidBSQFee(code); - } - private boolean isWarmingUp(String code) { Set newlyAdded = new HashSet<>(); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index a5403461851..b9682cae7a3 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1163,7 +1163,7 @@ account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must dao.tab.bsqWallet=BSQ wallet dao.tab.proposals=Governance dao.tab.bonding=Bonding -dao.tab.proofOfBurn=Asset fee/Proof of burn +dao.tab.proofOfBurn=Asset listing fee/Proof of burn dao.paidWithBsq=paid with BSQ dao.availableBsqBalance=Available @@ -1417,8 +1417,8 @@ dao.bond.bondedRoleType.MEDIATOR=Mediator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.ARBITRATOR=Arbitrator -dao.burnBsq.assetFee=Asset fee -dao.burnBsq.menuItem.assetFee=Asset fee +dao.burnBsq.assetFee=Asset listing fee +dao.burnBsq.menuItem.assetFee=Asset listing fee dao.burnBsq.menuItem.proofOfBurn=Proof of burn dao.burnBsq.header=Fee for asset listing dao.burnBsq.selectAsset=Select Asset @@ -1426,18 +1426,25 @@ dao.burnBsq.fee=Fee dao.burnBsq.trialPeriod=Trial period dao.burnBsq.payFee=Pay fee dao.burnBsq.allAssets=All assets -dao.burnBsq.assets.nameAndCode=Asset -dao.burnBsq.assets.activeFee=Active fee -dao.burnBsq.assets.tradedVolume=Trade volume +dao.burnBsq.assets.nameAndCode=Asset name +dao.burnBsq.assets.state=State +dao.burnBsq.assets.tradeVolume=Trade volume +dao.burnBsq.assets.lookBackPeriod=Verification period +dao.burnBsq.assets.trialFee=Fee for trial period +dao.burnBsq.assets.totalFee=Total fees paid +dao.burnBsq.assets.days={0} days # suppress inspection "UnusedProperty" -dao.assetState.NOT_ACTIVATED=Not activated +dao.assetState.UNDEFINED=Undefined # suppress inspection "UnusedProperty" -dao.assetState.ENABLED_BY_FEE_PAYMENT=Activated by fee payment +dao.assetState.IN_TRIAL_PERIOD=In trial period # suppress inspection "UnusedProperty" -dao.assetState.DE_LISTED_BY_INACTIVITY=Delisted due inactivity +dao.assetState.ACTIVELY_TRADED=Actively traded # suppress inspection "UnusedProperty" -dao.assetState.TERMINATED=Removed by voting +dao.assetState.DE_LISTED=De-listed due inactivity +# suppress inspection "UnusedProperty" +dao.assetState.REMOVED_BY_VOTING=Removed by voting + # suppress inspection "UnusedProperty" dao.phase.UNDEFINED=Undefined diff --git a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CryptoCurrencyForm.java b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CryptoCurrencyForm.java index cda09319663..b76b0ca38eb 100644 --- a/desktop/src/main/java/bisq/desktop/components/paymentmethods/CryptoCurrencyForm.java +++ b/desktop/src/main/java/bisq/desktop/components/paymentmethods/CryptoCurrencyForm.java @@ -162,7 +162,7 @@ protected void addTradeCurrencyComboBox() { currencyComboBox.setPromptText(""); }); - currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getWhiteListedSortedCryptoCurrencies(assetService))); + currencyComboBox.setItems(FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(assetService))); currencyComboBox.setVisibleRowCount(Math.min(currencyComboBox.getItems().size(), 15)); currencyComboBox.setConverter(new StringConverter() { @Override diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java index b302b86879e..a387cd42369 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -21,7 +21,6 @@ import bisq.desktop.common.view.FxmlView; import bisq.desktop.components.AutoTooltipTableColumn; import bisq.desktop.components.InputTextField; -import bisq.desktop.main.dao.bonding.BondingViewUtils; import bisq.desktop.main.overlays.popups.Popup; import bisq.desktop.util.FormBuilder; import bisq.desktop.util.GUIUtil; @@ -30,13 +29,11 @@ import bisq.core.btc.listeners.BsqBalanceListener; import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.dao.DaoFacade; import bisq.core.dao.governance.asset.AssetService; import bisq.core.dao.governance.asset.StatefulAsset; import bisq.core.dao.governance.proposal.TxException; import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; -import bisq.core.user.Preferences; import bisq.core.util.BSFormatter; import bisq.core.util.BsqFormatter; @@ -56,11 +53,11 @@ import javafx.scene.control.TextField; import javafx.scene.layout.GridPane; +import javafx.beans.InvalidationListener; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.beans.value.ChangeListener; import javafx.collections.FXCollections; -import javafx.collections.ListChangeListener; import javafx.collections.ObservableList; import javafx.collections.transformation.SortedList; @@ -78,7 +75,7 @@ @FxmlView public class AssetFeeView extends ActivatableView implements BsqBalanceListener { - public ComboBox assetComboBox; + private ComboBox assetComboBox; private InputTextField feeAmountInputTextField; private TextField trialPeriodTextField; private Button payFeeButton; @@ -87,10 +84,8 @@ public class AssetFeeView extends ActivatableView implements Bsq private final BsqFormatter bsqFormatter; private final BsqWalletService bsqWalletService; private final BsqValidator bsqValidator; - private final DaoFacade daoFacade; private final AssetService assetService; private BSFormatter btcFormatter; - private final Preferences preferences; private final ObservableList observableList = FXCollections.observableArrayList(); private final SortedList sortedList = new SortedList<>(observableList); @@ -99,9 +94,9 @@ public class AssetFeeView extends ActivatableView implements Bsq private ChangeListener amountFocusOutListener; private ChangeListener amountInputTextFieldListener; - private ListChangeListener statefulAssetsChangeListener; @Nullable private StatefulAsset selectedAsset; + private InvalidationListener updateListener; /////////////////////////////////////////////////////////////////////////////////////////// @@ -111,19 +106,14 @@ public class AssetFeeView extends ActivatableView implements Bsq @Inject private AssetFeeView(BsqFormatter bsqFormatter, BsqWalletService bsqWalletService, - BondingViewUtils bondingViewUtils, BsqValidator bsqValidator, - DaoFacade daoFacade, AssetService assetService, - BSFormatter btcFormatter, - Preferences preferences) { + BSFormatter btcFormatter) { this.bsqFormatter = bsqFormatter; this.bsqWalletService = bsqWalletService; this.bsqValidator = bsqValidator; - this.daoFacade = daoFacade; this.assetService = assetService; this.btcFormatter = btcFormatter; - this.preferences = preferences; } @Override @@ -160,7 +150,6 @@ public StatefulAsset fromString(String string) { @Override protected void activate() { - assetComboBox.setItems(assetService.getStatefulAssets()); assetComboBox.setOnAction(e -> { selectedAsset = assetComboBox.getSelectionModel().getSelectedItem(); }); @@ -170,11 +159,11 @@ protected void activate() { sortedList.comparatorProperty().bind(tableView.comparatorProperty()); - assetService.getStatefulAssets().addListener(statefulAssetsChangeListener); + assetService.getUpdateFlag().addListener(updateListener); bsqWalletService.addBsqBalanceListener(this); payFeeButton.setOnAction((event) -> { - Coin listingFee = bsqFormatter.parseToCoin(feeAmountInputTextField.getText()); + Coin listingFee = getListingFee(); try { Transaction transaction = assetService.payFee(selectedAsset, listingFee.value); Coin miningFee = transaction.getFee(); @@ -182,31 +171,20 @@ protected void activate() { if (!DevEnv.isDevMode()) { GUIUtil.showBsqFeeInfoPopup(listingFee, miningFee, txSize, bsqFormatter, btcFormatter, - Res.get("dao.burnBsq.assetFee"), () -> doPublishMyProposal(transaction, listingFee)); + Res.get("dao.burnBsq.assetFee"), () -> doPublishFeeTx(transaction, listingFee)); } else { - doPublishMyProposal(transaction, listingFee); + doPublishFeeTx(transaction, listingFee); } } catch (InsufficientMoneyException | TxException e) { e.printStackTrace(); new Popup<>().error(e.toString()).show(); } - }); - feeAmountInputTextField.resetValidation(); - updateList(); updateButtonState(); - } - private void doPublishMyProposal(Transaction transaction, Coin listingFee) { - assetService.publishTransaction(selectedAsset, transaction, listingFee.value, - () -> { - assetComboBox.getSelectionModel().clearSelection(); - if (!DevEnv.isDevMode()) - new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); - }, - errorMessage -> new Popup<>().warning(errorMessage).show()); + feeAmountInputTextField.resetValidation(); } @Override @@ -216,7 +194,7 @@ protected void deactivate() { feeAmountInputTextField.textProperty().removeListener(amountInputTextFieldListener); feeAmountInputTextField.focusedProperty().removeListener(amountFocusOutListener); - assetService.getStatefulAssets().removeListener(statefulAssetsChangeListener); + assetService.getUpdateFlag().removeListener(updateListener); bsqWalletService.removeBsqBalanceListener(this); sortedList.comparatorProperty().unbind(); @@ -250,15 +228,26 @@ private void createListeners() { } }; - amountInputTextFieldListener = (observable, oldValue, newValue) -> updateButtonState(); + amountInputTextFieldListener = (observable, oldValue, newValue) -> { + long days = getListingFee().value / assetService.getFeePerDay().value; + trialPeriodTextField.setText(Res.get("dao.burnBsq.assets.days", days)); + updateButtonState(); + }; - statefulAssetsChangeListener = c -> updateList(); + updateListener = observable -> updateList(); } private void updateList() { + // Here we exclude the assets which have been removed by voting. Paying a fee would not change the state. + ObservableList nonRemovedStatefulAssets = FXCollections.observableArrayList(assetService.getStatefulAssets().stream() + .filter(e -> !e.wasRemovedByVoting()) + .collect(Collectors.toList())); + assetComboBox.setItems(nonRemovedStatefulAssets); + + // In the table we want to show all. observableList.setAll(assetService.getStatefulAssets().stream() .map(statefulAsset -> new AssetListItem(statefulAsset, bsqFormatter)) - .sorted(Comparator.comparing(AssetListItem::getNameAndCode)) + /*.sorted(Comparator.comparing(AssetListItem::getNameAndCode))*/ .collect(Collectors.toList())); GUIUtil.setFitToRowsForTableView(tableView, 41, 28, 2, 10); } @@ -269,6 +258,22 @@ private void updateButtonState() { payFeeButton.setDisable(!isValid); } + private Coin getListingFee() { + return bsqFormatter.parseToCoin(feeAmountInputTextField.getText()); + } + + private void doPublishFeeTx(Transaction transaction, Coin listingFee) { + assetService.publishTransaction(selectedAsset, transaction, listingFee.value, + () -> { + assetComboBox.getSelectionModel().clearSelection(); + if (!DevEnv.isDevMode()) + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup<>().warning(errorMessage).show()); + + feeAmountInputTextField.clear(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Table columns @@ -279,7 +284,6 @@ private void createColumns() { column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.nameAndCode")); column.setMinWidth(120); - column.setMaxWidth(column.getMinWidth()); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory(new Callback<>() { @Override @@ -298,10 +302,10 @@ public void updateItem(final AssetListItem item, boolean empty) { } }); tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getNameAndCode)); - column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.activeFee")); - column.setMinWidth(60); - column.setMaxWidth(column.getMinWidth()); + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.state")); + column.setMinWidth(120); column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); column.setCellFactory(new Callback<>() { @Override @@ -312,7 +316,7 @@ public TableCell call(TableColumn(Res.get("dao.burnBsq.assets.tradedVolume")); - column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.tradeVolume")); column.setMinWidth(120); - column.setCellFactory( - new Callback<>() { + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { @Override - public TableCell call(TableColumn column) { - return new TableCell<>() { - @Override - public void updateItem(AssetListItem item, boolean empty) { - super.updateItem(item, empty); - if (item != null && !empty) { - setText(item.getTradedVolumeAsString()); - } else - setText(""); - } - }; + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getTradedVolumeAsString()); + } else + setText(""); } - }); + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getTradedVolume)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.lookBackPeriod")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getLookBackPeriodInDays()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getLookBackPeriodInDays)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.trialFee")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getFeeOfTrialPeriodAsString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getFeeOfTrialPeriod)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.totalFee")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final AssetListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getTotalFeesPaidAsString()); + } else + setText(""); + } + }; + } + }); tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getTotalFeesPaid)); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java index 90537d0ae78..4e3a4063645 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java @@ -18,7 +18,6 @@ package bisq.desktop.main.dao.burnbsq.assetfee; import bisq.core.dao.governance.asset.StatefulAsset; -import bisq.core.locale.CurrencyUtil; import bisq.core.locale.Res; import bisq.core.util.BsqFormatter; @@ -29,22 +28,30 @@ class AssetListItem { private final StatefulAsset statefulAsset; private final String tickerSymbol; private final String assetStateString; - private final String activeFeeAsString; private final int trialPeriodInBlocks; private final String nameAndCode; - private final long activeFee; + private final long totalFeesPaid; + private final String totalFeesPaidAsString; + private final long feeOfTrialPeriod; + private final String feeOfTrialPeriodAsString; private final String tradedVolumeAsString; + private final String lookBackPeriodInDays; + private final long tradedVolume; AssetListItem(StatefulAsset statefulAsset, BsqFormatter bsqFormatter) { this.statefulAsset = statefulAsset; tickerSymbol = statefulAsset.getTickerSymbol(); - nameAndCode = CurrencyUtil.getNameAndCode(tickerSymbol); + nameAndCode = statefulAsset.getNameAndCode(); assetStateString = Res.get("dao.assetState." + statefulAsset.getAssetState()); - activeFee = statefulAsset.getActiveFee(); - activeFeeAsString = Long.toString(activeFee); - trialPeriodInBlocks = (int) activeFee * 144; - tradedVolumeAsString = "TODO"; + feeOfTrialPeriod = statefulAsset.getFeeOfTrialPeriod(); + feeOfTrialPeriodAsString = bsqFormatter.formatCoinWithCode(feeOfTrialPeriod); + totalFeesPaid = statefulAsset.getTotalFeesPaid(); + totalFeesPaidAsString = bsqFormatter.formatCoinWithCode(totalFeesPaid); + trialPeriodInBlocks = (int) totalFeesPaid * 144; + tradedVolume = statefulAsset.getTradeVolume(); + tradedVolumeAsString = bsqFormatter.formatBTCWithCode(tradedVolume); + lookBackPeriodInDays = Res.get("dao.burnBsq.assets.days", statefulAsset.getLookBackPeriodInDays()); } } diff --git a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java index ad732028f1d..f3b8a9f0c5c 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/governance/ProposalDisplay.java @@ -338,10 +338,8 @@ public Bond fromString(String string) { Res.get("dao.proposal.display.assetComboBox.label")); comboBoxValueTextFieldIndex = gridRow; checkNotNull(assetComboBox, "assetComboBox must not be null"); - List assetList = CurrencyUtil.getAssetRegistry().stream() + List assetList = CurrencyUtil.getSortedAssetStream() .filter(e -> !e.getTickerSymbol().equals("BSQ")) - .filter(e -> !e.getTickerSymbol().equals("BTC")) - .filter(e -> CurrencyUtil.assetMatchesNetwork(e, BaseCurrencyNetwork.BTC_MAINNET)) .collect(Collectors.toList()); assetComboBox.setItems(FXCollections.observableArrayList(assetList)); assetComboBox.setConverter(new StringConverter<>() { diff --git a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java index 146e7b33524..dbe0e5ad9b3 100644 --- a/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java +++ b/desktop/src/main/java/bisq/desktop/main/settings/preferences/PreferencesView.java @@ -183,7 +183,7 @@ public void initialize() { @Override protected void activate() { // We want to have it updated in case an asset got removed - allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getWhiteListedSortedCryptoCurrencies(assetService)); + allCryptoCurrencies = FXCollections.observableArrayList(CurrencyUtil.getActiveSortedCryptoCurrencies(assetService)); allCryptoCurrencies.removeAll(cryptoCurrencies); activateGeneralOptions(); From 87efe896626dd4cf13d37e61b3ef50b0035bddd3 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sun, 11 Nov 2018 12:56:54 -0500 Subject: [PATCH 08/11] Add proof of burn feature to dao --- common/src/main/proto/pb.proto | 10 + .../core/app/misc/AppSetupWithP2PAndDAO.java | 3 + .../main/java/bisq/core/dao/DaoModule.java | 6 + .../src/main/java/bisq/core/dao/DaoSetup.java | 3 + .../dao/governance/asset/AssetService.java | 3 +- .../governance/proofofburn/MyProofOfBurn.java | 98 +++ .../proofofburn/MyProofOfBurnList.java | 74 ++ .../proofofburn/MyProofOfBurnListService.java | 104 +++ .../proofofburn/ProofOfBurnConsensus.java | 6 + .../proofofburn/ProofOfBurnService.java | 245 +++++++ .../core/dao/node/parser/TxOutputParser.java | 13 + .../bisq/core/dao/state/DaoStateService.java | 14 + .../dao/state/DaoStateStorageService.java | 2 +- .../CorePersistenceProtoResolver.java | 3 + .../core/setup/CorePersistedDataHost.java | 2 + .../resources/i18n/displayStrings.properties | 20 +- .../desktop/main/dao/burnbsq/BurnBsqView.java | 2 +- .../dao/burnbsq/assetfee/AssetFeeView.java | 8 +- .../proofofburn/MyProofOfBurnListItem.java | 72 ++ .../proofofburn/ProofOfBurnListItem.java | 51 ++ .../ProofOfBurnSignatureWindow.java | 89 +++ .../ProofOfBurnVerificationWindow.java | 85 +++ .../ProofOfBurnView.fxml | 2 +- .../burnbsq/proofofburn/ProofOfBurnView.java | 638 ++++++++++++++++++ .../reputation/ProofOfBurnListItem.java | 29 - .../burnbsq/reputation/ProofOfBurnView.java | 101 --- .../main/funds/withdrawal/WithdrawalView.java | 3 +- .../bisq/desktop/main/overlays/Overlay.java | 14 +- .../desktop/util/validation/BsqValidator.java | 3 +- .../desktop/util/validation/BtcValidator.java | 3 +- 30 files changed, 1553 insertions(+), 153 deletions(-) create mode 100644 core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java create mode 100644 core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java create mode 100644 core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java create mode 100644 core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java rename desktop/src/main/java/bisq/desktop/main/dao/burnbsq/{reputation => proofofburn}/ProofOfBurnView.fxml (97%) create mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java delete mode 100644 desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java diff --git a/common/src/main/proto/pb.proto b/common/src/main/proto/pb.proto index ae720d65591..af0f3c01229 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -943,6 +943,7 @@ message PersistableEnvelope { MeritList merit_list = 23; DaoStateStore dao_state_store = 24; MyReputationList my_reputation_list = 25; + MyProofOfBurnList my_proof_of_burn_list = 26; } } @@ -1550,6 +1551,15 @@ message MyReputationList { repeated MyReputation my_reputation = 1; } +message MyProofOfBurn { + string tx_id = 1; + string pre_image = 2; +} + +message MyProofOfBurnList { + repeated MyProofOfBurn my_proof_of_burn = 1; +} + message TempProposalPayload { Proposal proposal = 1; bytes owner_pub_key_encoded = 2; diff --git a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java index 82cd737f018..b6b0c931842 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -23,6 +23,7 @@ import bisq.core.dao.governance.blindvote.MyBlindVoteListService; import bisq.core.dao.governance.bond.reputation.MyReputationListService; import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.filter.FilterManager; import bisq.core.payment.AccountAgeWitnessService; @@ -55,6 +56,7 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, MyBlindVoteListService myBlindVoteListService, MyProposalListService myProposalListService, MyReputationListService myReputationListService, + MyProofOfBurnListService myProofOfBurnListService, @Named(DaoOptionKeys.DAO_ACTIVATED) boolean daoActivated) { super(encryptionService, keyRing, @@ -72,6 +74,7 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, persistedDataHosts.add(myBlindVoteListService); persistedDataHosts.add(myProposalListService); persistedDataHosts.add(myReputationListService); + persistedDataHosts.add(myProofOfBurnListService); } } diff --git a/core/src/main/java/bisq/core/dao/DaoModule.java b/core/src/main/java/bisq/core/dao/DaoModule.java index 42416c97142..ae28f9eef30 100644 --- a/core/src/main/java/bisq/core/dao/DaoModule.java +++ b/core/src/main/java/bisq/core/dao/DaoModule.java @@ -35,6 +35,8 @@ import bisq.core.dao.governance.myvote.MyVoteListService; import bisq.core.dao.governance.period.CycleService; import bisq.core.dao.governance.period.PeriodService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.dao.governance.proposal.ProposalListPresentation; import bisq.core.dao.governance.proposal.ProposalService; @@ -196,6 +198,10 @@ protected void configure() { // Asset bind(AssetService.class).in(Singleton.class); + // Proof of burn + bind(ProofOfBurnService.class).in(Singleton.class); + bind(MyProofOfBurnListService.class).in(Singleton.class); + // Options bindConstant().annotatedWith(named(DaoOptionKeys.RPC_USER)).to(environment.getRequiredProperty(DaoOptionKeys.RPC_USER)); bindConstant().annotatedWith(named(DaoOptionKeys.RPC_PASSWORD)).to(environment.getRequiredProperty(DaoOptionKeys.RPC_PASSWORD)); diff --git a/core/src/main/java/bisq/core/dao/DaoSetup.java b/core/src/main/java/bisq/core/dao/DaoSetup.java index 74bf5e1dd83..e4c15275eae 100644 --- a/core/src/main/java/bisq/core/dao/DaoSetup.java +++ b/core/src/main/java/bisq/core/dao/DaoSetup.java @@ -26,6 +26,7 @@ import bisq.core.dao.governance.bond.reputation.MyReputationListService; import bisq.core.dao.governance.bond.role.BondedRolesRepository; import bisq.core.dao.governance.period.CycleService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; import bisq.core.dao.governance.proposal.ProposalService; import bisq.core.dao.governance.voteresult.MissingDataRequestService; import bisq.core.dao.governance.voteresult.VoteResultService; @@ -66,6 +67,7 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, MyReputationListService myReputationListService, MyBondedReputationRepository myBondedReputationRepository, AssetService assetService, + ProofOfBurnService proofOfBurnService, DaoFacade daoFacade, ExportJsonFilesService exportJsonFilesService) { @@ -86,6 +88,7 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, daoSetupServices.add(myReputationListService); daoSetupServices.add(myBondedReputationRepository); daoSetupServices.add(assetService); + daoSetupServices.add(proofOfBurnService); daoSetupServices.add(daoFacade); daoSetupServices.add(exportJsonFilesService); daoSetupServices.add(bsqNodeProvider.getBsqNode()); diff --git a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java index 41653405a87..c0008ec75fb 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetService.java @@ -333,8 +333,7 @@ public Coin getFeePerDay() { return AssetConsensus.getFeePerDay(daoStateService, daoStateService.getChainHeight()); } - // Broadcast tx and publish proposal to P2P network - public void publishTransaction(StatefulAsset statefulAsset, Transaction transaction, long listingFee, ResultHandler resultHandler, + public void publishTransaction(Transaction transaction, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { walletsManager.publishAndCommitBsqTx(transaction, new TxBroadcaster.Callback() { @Override diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java new file mode 100644 index 00000000000..25317d7c8e1 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurn.java @@ -0,0 +1,98 @@ +/* + * 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.governance.proofofburn; + +import bisq.common.crypto.Hash; +import bisq.common.proto.network.NetworkPayload; +import bisq.common.proto.persistable.PersistablePayload; +import bisq.common.util.Utilities; + +import io.bisq.generated.protobuffer.PB; + +import com.google.common.base.Charsets; + +import java.util.Objects; + +import lombok.Value; +import lombok.extern.slf4j.Slf4j; + +import javax.annotation.concurrent.Immutable; + +/** + * MyProofOfBurn is persisted locally and holds the preImage and txId. + */ +@Immutable +@Value +@Slf4j +public final class MyProofOfBurn implements PersistablePayload, NetworkPayload { + private final String txId; + private final String preImage; + private final transient byte[] hash; // Not persisted as it is derived from preImage. Stored for caching purpose only. + + public MyProofOfBurn(String txId, String preImage) { + this.txId = txId; + this.preImage = preImage; + this.hash = Hash.getSha256Ripemd160hash(preImage.getBytes(Charsets.UTF_8)); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.MyProofOfBurn toProtoMessage() { + return PB.MyProofOfBurn.newBuilder() + .setTxId(txId) + .setPreImage(preImage) + .build(); + } + + public static MyProofOfBurn fromProto(PB.MyProofOfBurn proto) { + return new MyProofOfBurn(proto.getTxId(), proto.getPreImage()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof MyProofOfBurn)) return false; + if (!super.equals(o)) return false; + MyProofOfBurn that = (MyProofOfBurn) o; + return Objects.equals(txId, that.txId) && + Objects.equals(preImage, that.preImage); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), txId, preImage); + } + + @Override + public String toString() { + return "MyProofOfBurn{" + + "\n txId='" + txId + '\'' + + ",\n preImage=" + preImage + + ",\n hash=" + Utilities.bytesAsHexString(hash) + + "\n}"; + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java new file mode 100644 index 00000000000..a5d6c0ce997 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java @@ -0,0 +1,74 @@ +/* + * 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.governance.proofofburn; + +import bisq.common.proto.persistable.PersistableList; + +import io.bisq.generated.protobuffer.PB; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Collectors; + +import lombok.EqualsAndHashCode; + +/** + * PersistableEnvelope wrapper for list of MyProofOfBurn objects. + */ +@EqualsAndHashCode(callSuper = true) +public class MyProofOfBurnList extends PersistableList { + + private MyProofOfBurnList(List list) { + super(list); + } + + MyProofOfBurnList() { + super(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PROTO BUFFER + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public PB.PersistableEnvelope toProtoMessage() { + return PB.PersistableEnvelope.newBuilder().setMyProofOfBurnList(getBuilder()).build(); + } + + private PB.MyProofOfBurnList.Builder getBuilder() { + return PB.MyProofOfBurnList.newBuilder() + .addAllMyProofOfBurn(getList().stream() + .map(MyProofOfBurn::toProtoMessage) + .collect(Collectors.toList())); + } + + public static MyProofOfBurnList fromProto(PB.MyProofOfBurnList proto) { + return new MyProofOfBurnList(new ArrayList<>(proto.getMyProofOfBurnList().stream() + .map(MyProofOfBurn::fromProto) + .collect(Collectors.toList()))); + } + + @Override + public String toString() { + return "List of txIds in MyProofOfBurnList: " + getList().stream() + .map(MyProofOfBurn::getTxId) + .collect(Collectors.toList()); + } +} + diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java new file mode 100644 index 00000000000..e82fc36f9e5 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnListService.java @@ -0,0 +1,104 @@ +/* + * 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.governance.proofofburn; + +import bisq.core.app.BisqEnvironment; +import bisq.core.dao.DaoSetupService; + +import bisq.common.proto.persistable.PersistedDataHost; +import bisq.common.storage.Storage; + +import javax.inject.Inject; + +import java.util.List; + +import lombok.extern.slf4j.Slf4j; + +/** + * Manages the persistence of MyProofOfBurn objects. + */ +@Slf4j +public class MyProofOfBurnListService implements PersistedDataHost, DaoSetupService { + + private final Storage storage; + private final MyProofOfBurnList myProofOfBurnList = new MyProofOfBurnList(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public MyProofOfBurnListService(Storage storage) { + this.storage = storage; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // PersistedDataHost + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void readPersisted() { + if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { + MyProofOfBurnList persisted = storage.initAndGetPersisted(myProofOfBurnList, 100); + if (persisted != null) { + myProofOfBurnList.clear(); + myProofOfBurnList.addAll(persisted.getList()); + } + } + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + } + + @Override + public void start() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public void addMyProofOfBurn(MyProofOfBurn myProofOfBurn) { + if (!myProofOfBurnList.contains(myProofOfBurn)) { + myProofOfBurnList.add(myProofOfBurn); + persist(); + } + } + + public List getMyProofOfBurnList() { + return myProofOfBurnList.getList(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void persist() { + storage.queueUpForSave(20); + } +} diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java index 1870f419fab..490a2cefc87 100644 --- a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java @@ -25,6 +25,8 @@ import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.util.Arrays; + import lombok.extern.slf4j.Slf4j; @Slf4j @@ -50,4 +52,8 @@ public static byte[] getOpReturnData(byte[] hash) { public static boolean hasOpReturnDataValidLength(byte[] opReturnData) { return opReturnData.length == 22; } + + public static byte[] getHashFromOpReturnData(byte[] opReturnData) { + return Arrays.copyOfRange(opReturnData, 2, 22); + } } diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java new file mode 100644 index 00000000000..a2b3334f3b6 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java @@ -0,0 +1,245 @@ +/* + * 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.governance.proofofburn; + +import bisq.core.btc.exceptions.TransactionVerificationException; +import bisq.core.btc.exceptions.TxBroadcastException; +import bisq.core.btc.exceptions.WalletException; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.btc.wallet.BtcWalletService; +import bisq.core.btc.wallet.TxBroadcaster; +import bisq.core.btc.wallet.WalletsManager; +import bisq.core.dao.DaoSetupService; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.dao.state.DaoStateListener; +import bisq.core.dao.state.DaoStateService; +import bisq.core.dao.state.model.blockchain.BaseTx; +import bisq.core.dao.state.model.blockchain.Block; +import bisq.core.dao.state.model.blockchain.Tx; + +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.ECKey; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import com.google.common.base.Charsets; + +import javafx.beans.property.IntegerProperty; +import javafx.beans.property.SimpleIntegerProperty; + +import java.security.SignatureException; + +import java.util.ArrayList; +import java.util.Comparator; +import java.util.List; +import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; + +import lombok.Getter; +import lombok.extern.slf4j.Slf4j; + +import static com.google.common.base.Preconditions.checkNotNull; +import static org.bitcoinj.core.Utils.HEX; + +@Slf4j +public class ProofOfBurnService implements DaoSetupService, DaoStateListener { + private final BsqWalletService bsqWalletService; + private final BtcWalletService btcWalletService; + private final WalletsManager walletsManager; + private final MyProofOfBurnListService myProofOfBurnListService; + private final DaoStateService daoStateService; + + @Getter + private IntegerProperty updateFlag = new SimpleIntegerProperty(0); + @Getter + private final List proofOfBurnTxList = new ArrayList<>(); + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + public ProofOfBurnService(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + MyProofOfBurnListService myProofOfBurnListService, + DaoStateService daoStateService) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.myProofOfBurnListService = myProofOfBurnListService; + this.daoStateService = daoStateService; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoSetupService + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void addListeners() { + daoStateService.addBsqStateListener(this); + } + + @Override + public void start() { + } + + public void updateList() { + proofOfBurnTxList.clear(); + proofOfBurnTxList.addAll(getAllProofOfBurnTxs()); + + updateFlag.set(updateFlag.get() + 1); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // DaoStateListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onNewBlockHeight(int blockHeight) { + } + + @Override + public void onParseTxsComplete(Block block) { + updateList(); + } + + @Override + public void onParseBlockChainComplete() { + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction burn(String preImageAsString, long amount) throws InsufficientMoneyException, TxException { + try { + // We create a prepared Bsq Tx for the listing fee. + final Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTx(Coin.valueOf(amount)); + byte[] hash = getHashFromPreImage(preImageAsString); + byte[] opReturnData = ProofOfBurnConsensus.getOpReturnData(hash); + // We add the BTC inputs for the miner fee. + final Transaction txWithBtcFee = btcWalletService.completePreparedBurnBsqTx(preparedBurnFeeTx, opReturnData); + // We sign the BSQ inputs of the final tx. + Transaction transaction = bsqWalletService.signTx(txWithBtcFee); + log.info("Proof of burn tx: " + transaction); + return transaction; + } catch (WalletException | TransactionVerificationException e) { + throw new TxException(e); + } + } + + public void publishTransaction(Transaction transaction, String preImageAsString, ResultHandler resultHandler, + ErrorMessageHandler errorMessageHandler) { + walletsManager.publishAndCommitBsqTx(transaction, new TxBroadcaster.Callback() { + @Override + public void onSuccess(Transaction transaction) { + log.info("Proof of burn tx has been published. TxId={}", transaction.getHashAsString()); + resultHandler.handleResult(); + } + + @Override + public void onFailure(TxBroadcastException exception) { + errorMessageHandler.handleErrorMessage(exception.getMessage()); + } + }); + + MyProofOfBurn myProofOfBurn = new MyProofOfBurn(transaction.getHashAsString(), preImageAsString); + myProofOfBurnListService.addMyProofOfBurn(myProofOfBurn); + } + + public byte[] getHashFromOpReturnData(Tx tx) { + return ProofOfBurnConsensus.getHashFromOpReturnData(tx.getLastTxOutput().getOpReturnData()); + } + + public String getHashAsString(String preImageAsString) { + return Utilities.bytesAsHexString(getHashFromPreImage(preImageAsString)); + } + + public Optional getTx(String txId) { + return daoStateService.getTx(txId); + } + + // Of connected output of first input. Used for signing and verification. + // Proofs ownership of the proof of burn tx. + public byte[] getPubKey(String txId) { + return daoStateService.getTx(txId) + .map(tx -> tx.getTxInputs().get(0)) + .map(e -> Utilities.decodeFromHex(e.getPubKey())) + .orElse(new byte[0]); + } + + public String getPubKeyAsHex(String proofOfBurnTxId) { + return Utilities.bytesAsHexString(getPubKey(proofOfBurnTxId)); + } + + public Optional sign(String proofOfBurnTxId, String message) { + byte[] pubKey = getPubKey(proofOfBurnTxId); + ECKey key = bsqWalletService.findKeyFromPubKey(pubKey); + if (key == null) + return Optional.empty(); + + try { + String signatureBase64 = key.signMessage(message); + return Optional.of(signatureBase64); + } catch (Throwable t) { + log.error(t.toString()); + t.printStackTrace(); + return Optional.empty(); + } + } + + public void verify(String message, String pubKey, String signatureBase64) throws SignatureException { + ECKey key = ECKey.fromPublicOnly(HEX.decode(pubKey)); + checkNotNull(key, "ECKey must not be null"); + key.verifyMessage(message, signatureBase64); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private List getAllProofOfBurnTxs() { + return daoStateService.getProofOfBurnOpReturnTxOutputs().stream() + .map(txOutput -> daoStateService.getTx(txOutput.getTxId()).orElse(null)) + .filter(Objects::nonNull) + .sorted(Comparator.comparing(BaseTx::getTime).reversed()) + .collect(Collectors.toList()); + } + + private byte[] getHashFromPreImage(String preImageAsString) { + byte[] preImage = preImageAsString.getBytes(Charsets.UTF_8); + return ProofOfBurnConsensus.getHash(preImage); + } + + public long getAmount(Tx tx) { + return tx.getBurntFee(); + } +} diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index 68bf31d1e56..e61086009a0 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -115,6 +115,8 @@ void processTxOutput(TempTxOutput tempTxOutput) { // The LOCKUP BSQ is burnt unless the output exactly matches the input, that would cause the // output to not be BSQ output at all handleUnlockBondTx(tempTxOutput); + } else if (isBurnFeeTxOutput(tempTxOutput)) { + handleBtcOutput(tempTxOutput, index); } else if (availableInputValue > 0 && availableInputValue >= txOutputValue) { handleBsqOutput(tempTxOutput, index, txOutputValue); } else { @@ -170,6 +172,17 @@ private void handleUnlockBondTx(TempTxOutput txOutput) { bsqOutputFound = true; } + private boolean isBurnFeeTxOutput(TempTxOutput tempTxOutput) { + // If we get a asset listing or proof of burn tx we have only 1 BSQ output and if the + // burned amount is larger than the miner fee we might have a BTC output for receiving the burned funds. + // If the burned funds are less than the miner fee a BTC input is used for miner fee and a BTC change output for + // the remaining funds. In any case only the first output is BSQ all the others are BTC. + return optionalOpReturnType.isPresent() && + (optionalOpReturnType.get() == OpReturnType.ASSET_LISTING_FEE || + optionalOpReturnType.get() == OpReturnType.PROOF_OF_BURN) && + tempTxOutput.getIndex() >= 1; + } + private void handleBsqOutput(TempTxOutput txOutput, int index, long txOutputValue) { // Update the input balance. availableInputValue -= txOutputValue; 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 fb20bd81a87..7ea792fdd84 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateService.java @@ -361,6 +361,12 @@ public boolean existsTxOutput(TxOutputKey key) { return getTxOutputStream().anyMatch(txOutput -> txOutput.getKey().equals(key)); } + public Optional getTxOutput(TxOutputKey txOutputKey) { + return getTxOutputStream() + .filter(txOutput -> txOutput.getKey().equals(txOutputKey)) + .findAny(); + } + /////////////////////////////////////////////////////////////////////////////////////////// // UnspentTxOutput @@ -898,6 +904,14 @@ public Set getAssetListingFeeOpReturnTxOutputs() { return getTxOutputsByTxOutputType(TxOutputType.ASSET_LISTING_FEE_OP_RETURN_OUTPUT); } + /////////////////////////////////////////////////////////////////////////////////////////// + // Proof of burn + /////////////////////////////////////////////////////////////////////////////////////////// + + public Set getProofOfBurnOpReturnTxOutputs() { + return getTxOutputsByTxOutputType(TxOutputType.PROOF_OF_BURN_OP_RETURN_OUTPUT); + } + /////////////////////////////////////////////////////////////////////////////////////////// // Listeners diff --git a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java index 25ff8e405fb..97b334f7612 100644 --- a/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java +++ b/core/src/main/java/bisq/core/dao/state/DaoStateStorageService.java @@ -84,7 +84,7 @@ public DaoState getPersistedBsqState() { public void resetDaoState(Runnable resultHandler) { persist(new DaoState(), 1); - UserThread.runAfter(resultHandler::run, 300, TimeUnit.MILLISECONDS); + UserThread.runAfter(resultHandler, 300, TimeUnit.MILLISECONDS); } diff --git a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java index b4b1cc3ecf4..a7cb5ae8a51 100644 --- a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -24,6 +24,7 @@ import bisq.core.dao.governance.blindvote.storage.BlindVoteStore; import bisq.core.dao.governance.bond.reputation.MyReputationList; import bisq.core.dao.governance.myvote.MyVoteList; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnList; import bisq.core.dao.governance.proposal.MyProposalList; import bisq.core.dao.governance.proposal.storage.appendonly.ProposalStore; import bisq.core.dao.governance.proposal.storage.temp.TempProposalStore; @@ -132,6 +133,8 @@ public PersistableEnvelope fromProto(PB.PersistableEnvelope proto) { return DaoStateStore.fromProto(proto.getDaoStateStore()); case MY_REPUTATION_LIST: return MyReputationList.fromProto(proto.getMyReputationList()); + case MY_PROOF_OF_BURN_LIST: + return MyProofOfBurnList.fromProto(proto.getMyProofOfBurnList()); default: throw new ProtobufferRuntimeException("Unknown proto message case(PB.PersistableEnvelope). " + diff --git a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java index ced7dccd155..ee7e6f42112 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -24,6 +24,7 @@ import bisq.core.dao.governance.blindvote.MyBlindVoteListService; import bisq.core.dao.governance.bond.reputation.MyReputationListService; import bisq.core.dao.governance.myvote.MyVoteListService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; import bisq.core.dao.governance.proposal.MyProposalListService; import bisq.core.offer.OpenOfferManager; import bisq.core.trade.TradeManager; @@ -67,6 +68,7 @@ public static List getPersistedDataHosts(Injector injector) { persistedDataHosts.add(injector.getInstance(MyVoteListService.class)); persistedDataHosts.add(injector.getInstance(MyProposalListService.class)); persistedDataHosts.add(injector.getInstance(MyReputationListService.class)); + persistedDataHosts.add(injector.getInstance(MyProofOfBurnListService.class)); } return persistedDataHosts; } diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index b9682cae7a3..d5031f34b4f 100644 --- a/core/src/main/resources/i18n/displayStrings.properties +++ b/core/src/main/resources/i18n/displayStrings.properties @@ -1445,6 +1445,25 @@ dao.assetState.DE_LISTED=De-listed due inactivity # suppress inspection "UnusedProperty" dao.assetState.REMOVED_BY_VOTING=Removed by voting +dao.proofOfBurn.header=Proof of burn +dao.proofOfBurn.amount=Amount +dao.proofOfBurn.preImage=Pre-image +dao.proofOfBurn.burn=Burn +dao.proofOfBurn.allTxs=All proof of burn transactions +dao.proofOfBurn.myItems=My proof of burn transactions +dao.proofOfBurn.date=Date +dao.proofOfBurn.hash=Hash +dao.proofOfBurn.txs=Transactions +dao.proofOfBurn.pubKey=Pubkey +dao.proofOfBurn.signature.window.title=Sign a message with key from proof or burn transaction +dao.proofOfBurn.copySig=Copy signature to clipboard +dao.proofOfBurn.sign=Sign +dao.proofOfBurn.message=Message +dao.proofOfBurn.sig=Signature +dao.proofOfBurn.verify=Verify +dao.proofOfBurn.verify.header=Verify message with key from proof or burn transaction +dao.proofOfBurn.verificationResult.ok=Verification succeeded +dao.proofOfBurn.verificationResult.failed=Verification failed # suppress inspection "UnusedProperty" dao.phase.UNDEFINED=Undefined @@ -2472,7 +2491,6 @@ validation.bankIdNumber={0} must consist of {1} numbers. validation.accountNr=Account number must consist of {0} numbers. validation.accountNrChars=Account number must consist of {0} characters. validation.btc.invalidAddress=The address is not correct. Please check the address format. -validation.btc.amountBelowDust=The amount you would like to send is below the dust limit of {0} \nand would be rejected by the Bitcoin network.\nPlease use a higher amount. validation.integerOnly=Please enter integer numbers only. validation.inputError=Your input caused an error:\n{0} validation.bsq.insufficientBalance=Amount exceeds the available balance of {0}. diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java index 380fa4d47f0..925eaeb2061 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/BurnBsqView.java @@ -28,7 +28,7 @@ import bisq.desktop.main.MainView; import bisq.desktop.main.dao.DaoView; import bisq.desktop.main.dao.burnbsq.assetfee.AssetFeeView; -import bisq.desktop.main.dao.burnbsq.reputation.ProofOfBurnView; +import bisq.desktop.main.dao.burnbsq.proofofburn.ProofOfBurnView; import bisq.core.locale.Res; diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java index a387cd42369..402e044f220 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -171,9 +171,9 @@ protected void activate() { if (!DevEnv.isDevMode()) { GUIUtil.showBsqFeeInfoPopup(listingFee, miningFee, txSize, bsqFormatter, btcFormatter, - Res.get("dao.burnBsq.assetFee"), () -> doPublishFeeTx(transaction, listingFee)); + Res.get("dao.burnBsq.assetFee"), () -> doPublishFeeTx(transaction)); } else { - doPublishFeeTx(transaction, listingFee); + doPublishFeeTx(transaction); } } catch (InsufficientMoneyException | TxException e) { e.printStackTrace(); @@ -262,8 +262,8 @@ private Coin getListingFee() { return bsqFormatter.parseToCoin(feeAmountInputTextField.getText()); } - private void doPublishFeeTx(Transaction transaction, Coin listingFee) { - assetService.publishTransaction(selectedAsset, transaction, listingFee.value, + private void doPublishFeeTx(Transaction transaction) { + assetService.publishTransaction(transaction, () -> { assetComboBox.getSelectionModel().clearSelection(); if (!DevEnv.isDevMode()) diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java new file mode 100644 index 00000000000..b1764e4e001 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/MyProofOfBurnListItem.java @@ -0,0 +1,72 @@ +/* + * 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.desktop.main.dao.burnbsq.proofofburn; + +import bisq.core.dao.governance.proofofburn.MyProofOfBurn; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.locale.Res; +import bisq.core.util.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; +import java.util.Optional; + +import lombok.Value; + +@Value +class MyProofOfBurnListItem { + private final MyProofOfBurn myProofOfBurn; + private final long amount; + private final String amountAsString; + private final String txId; + private final String hashAsHex; + private final String preImage; + private final String pubKey; + private final Date date; + private final String dateAsString; + + MyProofOfBurnListItem(MyProofOfBurn myProofOfBurn, ProofOfBurnService proofOfBurnService, BsqFormatter bsqFormatter) { + this.myProofOfBurn = myProofOfBurn; + + preImage = myProofOfBurn.getPreImage(); + Optional optionalTx = proofOfBurnService.getTx(myProofOfBurn.getTxId()); + if (optionalTx.isPresent()) { + Tx tx = optionalTx.get(); + date = new Date(tx.getTime()); + dateAsString = bsqFormatter.formatDateTime(date); + amount = proofOfBurnService.getAmount(tx); + amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); + txId = tx.getId(); + hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); + } else { + amount = 0; + amountAsString = Res.get("shared.na"); + txId = Res.get("shared.na"); + hashAsHex = Res.get("shared.na"); + pubKey = Res.get("shared.na"); + dateAsString = Res.get("shared.na"); + date = new Date(0); + } + } + +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.java new file mode 100644 index 00000000000..2e1254b1376 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnListItem.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.desktop.main.dao.burnbsq.proofofburn; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.state.model.blockchain.Tx; +import bisq.core.util.BsqFormatter; + +import bisq.common.util.Utilities; + +import org.bitcoinj.core.Coin; + +import java.util.Date; + +import lombok.Value; + +@Value +class ProofOfBurnListItem { + private final long amount; + private final String amountAsString; + private final String txId; + private final String hashAsHex; + private final String pubKey; + private final Date date; + private final String dateAsString; + + ProofOfBurnListItem(Tx tx, ProofOfBurnService proofOfBurnService, BsqFormatter bsqFormatter) { + amount = proofOfBurnService.getAmount(tx); + amountAsString = bsqFormatter.formatCoinWithCode(Coin.valueOf(amount)); + txId = tx.getId(); + hashAsHex = Utilities.bytesAsHexString(proofOfBurnService.getHashFromOpReturnData(tx)); + pubKey = Utilities.bytesAsHexString(proofOfBurnService.getPubKey(txId)); + date = new Date(tx.getTime()); + dateAsString = bsqFormatter.formatDateTime(date); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java new file mode 100644 index 00000000000..44e8e86402a --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnSignatureWindow.java @@ -0,0 +1,89 @@ +/* + * 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.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.locale.Res; + +import bisq.common.util.Tuple3; +import bisq.common.util.Utilities; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; + +import java.util.Optional; + +import static bisq.desktop.util.FormBuilder.addInputTextField; + +public class ProofOfBurnSignatureWindow extends Overlay { + private final ProofOfBurnService proofOfBurnService; + private final String proofOfBurnTxId; + private final String pubKey; + + private TextField sigTextField; + private VBox sigTextFieldBox; + + ProofOfBurnSignatureWindow(ProofOfBurnService proofOfBurnService, String proofOfBurnTxId) { + this.proofOfBurnService = proofOfBurnService; + this.proofOfBurnTxId = proofOfBurnTxId; + this.pubKey = proofOfBurnService.getPubKeyAsHex(proofOfBurnTxId); + type = Type.Attention; + } + + public void show() { + if (headLine == null) + headLine = Res.get("dao.proofOfBurn.signature.window.title"); + + width = 800; + createGridPane(); + addHeadLine(); + addContent(); + addCloseButton(); + applyStyles(); + display(); + } + + private void addContent() { + FormBuilder.addTopLabelTextField(gridPane, rowIndex, Res.get("dao.proofOfBurn.pubKey"), pubKey, 40); + + InputTextField messageInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.message")); + + Button signButton = FormBuilder.addButton(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sign"), 10); + signButton.setOnAction(e -> { + proofOfBurnService.sign(proofOfBurnTxId, messageInputTextField.getText()).ifPresent(sig -> { + sigTextFieldBox.setVisible(true); + sigTextField.setText(sig); + }); + }); + Tuple3 tuple = FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + sigTextFieldBox = tuple.third; + sigTextField = tuple.second; + sigTextFieldBox.setVisible(false); + + actionHandlerOptional = Optional.of(() -> { + Utilities.copyToClipboard(sigTextField.getText()); + }); + actionButtonText = Res.get("dao.proofOfBurn.copySig"); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java new file mode 100644 index 00000000000..e8d982d688b --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnVerificationWindow.java @@ -0,0 +1,85 @@ +/* + * 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.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.Overlay; +import bisq.desktop.util.FormBuilder; + +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.locale.Res; + +import bisq.common.util.Tuple3; + +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.layout.VBox; + +import java.security.SignatureException; + +import static bisq.desktop.util.FormBuilder.addInputTextField; + +public class ProofOfBurnVerificationWindow extends Overlay { + private final ProofOfBurnService proofOfBurnService; + private final String pubKey; + + private TextField verificationResultTextField; + private VBox verificationResultBox; + + ProofOfBurnVerificationWindow(ProofOfBurnService proofOfBurnService, String proofOfBurnTxId) { + this.proofOfBurnService = proofOfBurnService; + this.pubKey = proofOfBurnService.getPubKeyAsHex(proofOfBurnTxId); + type = Type.Attention; + } + + public void show() { + if (headLine == null) + headLine = Res.get("dao.proofOfBurn.signature.window.title"); + + width = 800; + createGridPane(); + addHeadLine(); + addContent(); + addCloseButton(); + applyStyles(); + display(); + } + + private void addContent() { + FormBuilder.addTopLabelTextField(gridPane, rowIndex, Res.get("dao.proofOfBurn.pubKey"), pubKey, 40); + InputTextField messageInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.message")); + InputTextField signatureInputTextField = addInputTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + Button signButton = FormBuilder.addButton(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sign"), 10); + + signButton.setOnAction(e -> { + try { + verificationResultBox.setVisible(true); + proofOfBurnService.verify(messageInputTextField.getText(), pubKey, signatureInputTextField.getText()); + verificationResultTextField.setText(Res.get("dao.proofOfBurn.verificationResult.ok")); + } catch (SignatureException e1) { + verificationResultTextField.setText(Res.get("dao.proofOfBurn.verificationResult.failed")); + } + }); + + Tuple3 tuple = FormBuilder.addTopLabelTextField(gridPane, ++rowIndex, Res.get("dao.proofOfBurn.sig")); + verificationResultBox = tuple.third; + verificationResultTextField = tuple.second; + verificationResultBox.setVisible(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml similarity index 97% rename from desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml rename to desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml index 3f8a173e4e1..498a89f2a68 100644 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.fxml +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml @@ -21,7 +21,7 @@ -. + */ + +package bisq.desktop.main.dao.burnbsq.proofofburn; + +import bisq.desktop.common.view.ActivatableView; +import bisq.desktop.common.view.FxmlView; +import bisq.desktop.components.AutoTooltipButton; +import bisq.desktop.components.AutoTooltipTableColumn; +import bisq.desktop.components.HyperlinkWithIcon; +import bisq.desktop.components.InputTextField; +import bisq.desktop.main.overlays.popups.Popup; +import bisq.desktop.util.FormBuilder; +import bisq.desktop.util.GUIUtil; +import bisq.desktop.util.Layout; +import bisq.desktop.util.validation.BsqValidator; + +import bisq.core.btc.listeners.BsqBalanceListener; +import bisq.core.btc.wallet.BsqWalletService; +import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService; +import bisq.core.dao.governance.proofofburn.ProofOfBurnService; +import bisq.core.dao.governance.proposal.TxException; +import bisq.core.locale.Res; +import bisq.core.user.Preferences; +import bisq.core.util.BSFormatter; +import bisq.core.util.BsqFormatter; +import bisq.core.util.validation.InputValidator; + +import bisq.common.app.DevEnv; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; + +import javax.inject.Inject; + +import de.jensd.fx.fontawesome.AwesomeIcon; + +import javafx.scene.control.Button; +import javafx.scene.control.TableCell; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.GridPane; + +import javafx.beans.InvalidationListener; +import javafx.beans.property.ReadOnlyObjectWrapper; +import javafx.beans.value.ChangeListener; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.SortedList; + +import javafx.util.Callback; + +import java.util.Comparator; +import java.util.stream.Collectors; + +import static bisq.desktop.util.FormBuilder.addButtonAfterGroup; +import static bisq.desktop.util.FormBuilder.addInputTextField; +import static bisq.desktop.util.FormBuilder.addTitledGroupBg; +import static bisq.desktop.util.FormBuilder.addTopLabelTextField; + +@FxmlView +public class ProofOfBurnView extends ActivatableView implements BsqBalanceListener { + private final ProofOfBurnService proofOfBurnService; + private final MyProofOfBurnListService myProofOfBurnListService; + private final Preferences preferences; + private final BSFormatter btcFormatter; + private final BsqFormatter bsqFormatter; + private final BsqWalletService bsqWalletService; + private final BsqValidator bsqValidator; + + private InputTextField amountInputTextField, preImageTextField; + private TextField hashTextField; + private Button burnButton; + private TableView myItemsTableView; + private TableView allTxsTableView; + + private final ObservableList myItemsObservableList = FXCollections.observableArrayList(); + private final SortedList myItemsSortedList = new SortedList<>(myItemsObservableList); + + private final ObservableList allItemsObservableList = FXCollections.observableArrayList(); + private final SortedList allItemsSortedList = new SortedList<>(allItemsObservableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener, preImageFocusOutListener; + private ChangeListener amountInputTextFieldListener, preImageInputTextFieldListener; + private InvalidationListener updateListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ProofOfBurnView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BsqValidator bsqValidator, + ProofOfBurnService proofOfBurnService, + MyProofOfBurnListService myProofOfBurnListService, + Preferences preferences, + BSFormatter btcFormatter) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bsqValidator = bsqValidator; + this.proofOfBurnService = proofOfBurnService; + this.myProofOfBurnListService = myProofOfBurnListService; + this.preferences = preferences; + this.btcFormatter = btcFormatter; + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 4, Res.get("dao.proofOfBurn.header")); + amountInputTextField = addInputTextField(root, ++gridRow, Res.get("dao.proofOfBurn.amount"), Layout.FIRST_ROW_DISTANCE); + preImageTextField = addInputTextField(root, ++gridRow, Res.get("dao.proofOfBurn.preImage")); + hashTextField = addTopLabelTextField(root, ++gridRow, Res.get("dao.proofOfBurn.hash")).second; + burnButton = addButtonAfterGroup(root, ++gridRow, Res.get("dao.proofOfBurn.burn")); + + myItemsTableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.proofOfBurn.myItems"), 30); + createColumnsForMyItems(); + myItemsTableView.setItems(myItemsSortedList); + + allTxsTableView = FormBuilder.addTableViewWithHeader(root, ++gridRow, Res.get("dao.proofOfBurn.allTxs"), 30); + createColumnsForAllTxs(); + allTxsTableView.setItems(allItemsSortedList); + + createListeners(); + } + + @Override + protected void activate() { + amountInputTextField.textProperty().addListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + preImageTextField.textProperty().addListener(preImageInputTextFieldListener); + preImageTextField.focusedProperty().addListener(preImageFocusOutListener); + + allItemsSortedList.comparatorProperty().bind(allTxsTableView.comparatorProperty()); + + proofOfBurnService.getUpdateFlag().addListener(updateListener); + bsqWalletService.addBsqBalanceListener(this); + + burnButton.setOnAction((event) -> { + Coin amount = getAmountFee(); + try { + String preImageAsString = preImageTextField.getText(); + Transaction transaction = proofOfBurnService.burn(preImageAsString, amount.value); + Coin miningFee = transaction.getFee(); + int txSize = transaction.bitcoinSerialize().length; + + if (!DevEnv.isDevMode()) { + GUIUtil.showBsqFeeInfoPopup(amount, miningFee, txSize, bsqFormatter, btcFormatter, + Res.get("dao.proofOfBurn.amount"), () -> doPublishFeeTx(transaction, preImageAsString)); + } else { + doPublishFeeTx(transaction, preImageAsString); + } + } catch (InsufficientMoneyException | TxException e) { + e.printStackTrace(); + new Popup<>().error(e.toString()).show(); + } + }); + + amountInputTextField.setValidator(bsqValidator); + preImageTextField.setValidator(new InputValidator()); + + updateList(); + updateButtonState(); + } + + @Override + protected void deactivate() { + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + amountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + amountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + allItemsSortedList.comparatorProperty().unbind(); + + proofOfBurnService.getUpdateFlag().removeListener(updateListener); + bsqWalletService.removeBsqBalanceListener(this); + + burnButton.setOnAction(null); + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // BsqBalanceListener + /////////////////////////////////////////////////////////////////////////////////////////// + + @Override + public void onUpdateBalances(Coin confirmedBalance, + Coin availableNonBsqBalance, + Coin pendingBalance, + Coin lockedForVotingBalance, + Coin lockupBondsBalance, + Coin unlockingBondsBalance) { + bsqValidator.setAvailableBalance(confirmedBalance); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createListeners() { + amountFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + amountInputTextFieldListener = (observable, oldValue, newValue) -> { + updateButtonState(); + }; + preImageFocusOutListener = (observable, oldValue, newValue) -> { + if (!newValue) { + updateButtonState(); + } + }; + + preImageInputTextFieldListener = (observable, oldValue, newValue) -> { + hashTextField.setText(proofOfBurnService.getHashAsString(newValue)); + updateButtonState(); + }; + + updateListener = observable -> updateList(); + } + + private void updateList() { + myItemsObservableList.setAll(myProofOfBurnListService.getMyProofOfBurnList().stream() + .map(myProofOfBurn -> new MyProofOfBurnListItem(myProofOfBurn, proofOfBurnService, bsqFormatter)) + .sorted(Comparator.comparing(MyProofOfBurnListItem::getDate).reversed()) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(myItemsTableView, 41, 28, 2, 4); + + + allItemsObservableList.setAll(proofOfBurnService.getProofOfBurnTxList().stream() + .map(tx -> new ProofOfBurnListItem(tx, proofOfBurnService, bsqFormatter)) + .collect(Collectors.toList())); + GUIUtil.setFitToRowsForTableView(allTxsTableView, 41, 28, 2, 10); + } + + private void updateButtonState() { + boolean isValid = bsqValidator.validate(amountInputTextField.getText()).isValid && + preImageTextField.validate(); + burnButton.setDisable(!isValid); + } + + private Coin getAmountFee() { + return bsqFormatter.parseToCoin(amountInputTextField.getText()); + } + + private void doPublishFeeTx(Transaction transaction, String preImageAsString) { + proofOfBurnService.publishTransaction(transaction, preImageAsString, + () -> { + if (!DevEnv.isDevMode()) + new Popup<>().confirmation(Res.get("dao.tx.published.success")).show(); + }, + errorMessage -> new Popup<>().warning(errorMessage).show()); + + amountInputTextField.clear(); + preImageTextField.clear(); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Table columns + /////////////////////////////////////////////////////////////////////////////////////////// + + + private void createColumnsForMyItems() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.amount")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmountAsString()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getAmount)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.date")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getDateAsString()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getDate)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.preImage")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPreImage()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getPreImage)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.hash")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getHashAsHex()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getHashAsHex)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.txs")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(item.getTxId(), preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getTxId)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.pubKey")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPubKey()); + } else + setText(""); + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(MyProofOfBurnListItem::getPubKey)); + + column = new AutoTooltipTableColumn<>(""); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(60); + column.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final MyProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (button == null) { + button = new AutoTooltipButton(Res.get("dao.proofOfBurn.sign")); + setGraphic(button); + } + button.setOnAction(e -> new ProofOfBurnSignatureWindow(proofOfBurnService, item.getTxId()).show()); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + myItemsTableView.getColumns().add(column); + column.setSortable(false); + } + + private void createColumnsForAllTxs() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.amount")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getAmountAsString()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getAmount)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.date")); + column.setMinWidth(120); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getDateAsString()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getDate)); + + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.hash")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getHashAsHex()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getHashAsHex)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.txs")); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + //noinspection Duplicates + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> GUIUtil.openTxInBsqBlockExplorer(item.getTxId(), preferences)); + hyperlinkWithIcon.setTooltip(new Tooltip(Res.get("tooltip.openBlockchainForTx", transactionId))); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); + } + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getTxId)); + + + column = new AutoTooltipTableColumn<>(Res.get("dao.proofOfBurn.pubKey")); + column.setMinWidth(80); + column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setCellFactory(new Callback<>() { + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + setText(item.getPubKey()); + } else + setText(""); + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setComparator(Comparator.comparing(ProofOfBurnListItem::getPubKey)); + + + column = new AutoTooltipTableColumn<>(""); + column.setCellValueFactory(item -> new ReadOnlyObjectWrapper<>(item.getValue())); + column.setMinWidth(80); + column.setCellFactory( + new Callback<>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell<>() { + Button button; + + @Override + public void updateItem(final ProofOfBurnListItem item, boolean empty) { + super.updateItem(item, empty); + if (item != null && !empty) { + if (button == null) { + button = new AutoTooltipButton(Res.get("dao.proofOfBurn.verify")); + setGraphic(button); + } + button.setOnAction(e -> new ProofOfBurnVerificationWindow(proofOfBurnService, item.getTxId()).show()); + } else { + setGraphic(null); + if (button != null) { + button.setOnAction(null); + button = null; + } + } + } + }; + } + }); + allTxsTableView.getColumns().add(column); + column.setSortable(false); + } +} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java deleted file mode 100644 index 35e6a09a7b5..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnListItem.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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.desktop.main.dao.burnbsq.reputation; - -import bisq.core.util.BsqFormatter; - -import lombok.Value; - -@Value -class ProofOfBurnListItem { - - ProofOfBurnListItem(BsqFormatter bsqFormatter) { - } -} diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java deleted file mode 100644 index 7a5f7fa8f69..00000000000 --- a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/reputation/ProofOfBurnView.java +++ /dev/null @@ -1,101 +0,0 @@ -/* - * 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.desktop.main.dao.burnbsq.reputation; - -import bisq.desktop.common.view.ActivatableView; -import bisq.desktop.common.view.FxmlView; -import bisq.desktop.util.validation.BsqValidator; - -import bisq.core.btc.listeners.BsqBalanceListener; -import bisq.core.btc.wallet.BsqWalletService; -import bisq.core.dao.DaoFacade; -import bisq.core.user.Preferences; -import bisq.core.util.BsqFormatter; - -import org.bitcoinj.core.Coin; - -import javax.inject.Inject; - -import javafx.scene.layout.GridPane; - -@FxmlView -public class ProofOfBurnView extends ActivatableView implements BsqBalanceListener { - - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, lifecycle - /////////////////////////////////////////////////////////////////////////////////////////// - - @Inject - private ProofOfBurnView(BsqFormatter bsqFormatter, - BsqWalletService bsqWalletService, - BsqValidator bsqValidator, - DaoFacade daoFacade, - Preferences preferences) { - } - - @Override - public void initialize() { - - } - - @Override - protected void activate() { - - updateList(); - } - - @Override - protected void deactivate() { - } - - /////////////////////////////////////////////////////////////////////////////////////////// - // BsqBalanceListener - /////////////////////////////////////////////////////////////////////////////////////////// - - @Override - public void onUpdateBalances(Coin confirmedBalance, - Coin availableNonBsqBalance, - Coin pendingBalance, - Coin lockedForVotingBalance, - Coin lockupBondsBalance, - Coin unlockingBondsBalance) { - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Private - /////////////////////////////////////////////////////////////////////////////////////////// - - private void createListeners() { - } - - private void updateList() { - } - - private void updateButtonState() { - } - - - /////////////////////////////////////////////////////////////////////////////////////////// - // Table columns - /////////////////////////////////////////////////////////////////////////////////////////// - - private void createColumns() { - - } -} diff --git a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java index 7823f7cf0f1..32c1fc954b0 100644 --- a/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java +++ b/desktop/src/main/java/bisq/desktop/main/funds/withdrawal/WithdrawalView.java @@ -472,7 +472,8 @@ private void sendFunds(Coin amount, Coin fee, KeyParameter aesKey, FutureCallbac } catch (AddressFormatException e) { new Popup<>().warning(Res.get("validation.btc.invalidAddress")).show(); } catch (Wallet.DustySendRequested e) { - new Popup<>().warning(Res.get("validation.btc.amountBelowDust", formatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))).show(); + new Popup<>().warning(Res.get("validation.amountBelowDust", + formatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))).show(); } catch (AddressEntryException e) { new Popup<>().error(e.getMessage()).show(); } catch (InsufficientMoneyException e) { diff --git a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java index f167a872fd2..0f4fae5293b 100644 --- a/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java +++ b/desktop/src/main/java/bisq/desktop/main/overlays/Overlay.java @@ -455,12 +455,9 @@ protected void createGridPane() { gridPane.setPadding(new Insets(64, 64, 64, 64)); gridPane.setPrefWidth(width); - ColumnConstraints columnConstraints1 = new ColumnConstraints(); - columnConstraints1.setHalignment(HPos.RIGHT); - columnConstraints1.setHgrow(Priority.SOMETIMES); - ColumnConstraints columnConstraints2 = new ColumnConstraints(); - columnConstraints2.setHgrow(Priority.ALWAYS); - gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2); + ColumnConstraints columnConstraints = new ColumnConstraints(); + columnConstraints.setPercentWidth(100); + gridPane.getColumnConstraints().add(columnConstraints); } protected void blurAgain() { @@ -709,13 +706,13 @@ protected void applyStyles() { case Confirmation: case Feedback: case Notification: + case Attention: headLineLabel.getStyleClass().add("popup-headline-information"); headlineIcon.getStyleClass().add("popup-icon-information"); headlineIcon.setManaged(true); headlineIcon.setVisible(true); FormBuilder.getIconForLabel(AwesomeIcon.INFO_SIGN, headlineIcon, "1.5em"); break; - case Attention: case Warning: case Error: headLineLabel.getStyleClass().add("popup-headline-warning"); @@ -786,7 +783,6 @@ private void addReportErrorButtons() { GridPane.setMargin(logButton, new Insets(20, 0, 0, 0)); GridPane.setHalignment(logButton, HPos.RIGHT); GridPane.setRowIndex(logButton, ++rowIndex); - GridPane.setColumnIndex(logButton, 1); gridPane.getChildren().add(logButton); logButton.setOnAction(event -> { try { @@ -802,7 +798,6 @@ private void addReportErrorButtons() { Button gitHubButton = new AutoTooltipButton(Res.get("popup.reportError.gitHub")); GridPane.setHalignment(gitHubButton, HPos.RIGHT); GridPane.setRowIndex(gitHubButton, ++rowIndex); - GridPane.setColumnIndex(gitHubButton, 1); gridPane.getChildren().add(gitHubButton); gitHubButton.setOnAction(event -> { if (message != null) @@ -870,7 +865,6 @@ protected void addCloseButton() { if (!showReportErrorButtons) GridPane.setMargin(closeButton, new Insets(buttonDistance, 0, 0, 0)); GridPane.setRowIndex(closeButton, ++rowIndex); - GridPane.setColumnIndex(closeButton, 1); gridPane.getChildren().add(closeButton); } } diff --git a/desktop/src/main/java/bisq/desktop/util/validation/BsqValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/BsqValidator.java index 6841fbbf620..5d913f26273 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/BsqValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/BsqValidator.java @@ -94,7 +94,8 @@ private ValidationResult validateIfAboveDust(String input) { if (Restrictions.isAboveDust(coin)) return new ValidationResult(true); else - return new ValidationResult(false, Res.get("validation.btc.amountBelowDust", bsqFormatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); + return new ValidationResult(false, Res.get("validation.amountBelowDust", + bsqFormatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); } private ValidationResult validateIfNotFractionalBtcValue(String input) { diff --git a/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java b/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java index 6a09c74e7ef..f7586fa4db1 100644 --- a/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java +++ b/desktop/src/main/java/bisq/desktop/util/validation/BtcValidator.java @@ -80,7 +80,8 @@ protected ValidationResult validateIfAboveDust(String input) { if (Restrictions.isAboveDust(coin)) return new ValidationResult(true); else - return new ValidationResult(false, Res.get("validation.btc.amountBelowDust", formatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); + return new ValidationResult(false, Res.get("validation.amountBelowDust", + formatter.formatCoinWithCode(Restrictions.getMinNonDustOutput()))); } catch (Throwable t) { return new ValidationResult(false, Res.get("validation.invalidInput", t.getMessage())); } From cf46f0b06a8c518ea6bc77c7590e84152a66aa53 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Sun, 11 Nov 2018 17:20:08 -0500 Subject: [PATCH 09/11] Do not show popup in devmode --- .../src/main/java/bisq/desktop/main/offer/MutableOfferView.java | 2 +- .../java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java index 6affde950bc..1ac6e54fff3 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/MutableOfferView.java @@ -250,7 +250,7 @@ protected void doActivate() { balanceTextField.setTargetAmount(model.getDataModel().totalToPayAsCoinProperty().get()); updatePriceToggle(); - if (CurrencyUtil.isFiatCurrency(model.tradeCurrencyCode.get())) { + if (CurrencyUtil.isFiatCurrency(model.tradeCurrencyCode.get()) && !DevEnv.isDevMode()) { new Popup<>().headLine(Res.get("popup.roundedFiatValues.headline")) .information(Res.get("popup.roundedFiatValues.msg", model.tradeCurrencyCode.get())) .dontShowAgainId("FiatValuesRoundedWarning") diff --git a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java index 7fab39b6806..761069f0142 100644 --- a/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java +++ b/desktop/src/main/java/bisq/desktop/main/offer/takeoffer/TakeOfferView.java @@ -296,7 +296,7 @@ protected void activate() { showNextStepAfterAmountIsSet(); } - if (CurrencyUtil.isFiatCurrency(model.getOffer().getCurrencyCode())) { + if (CurrencyUtil.isFiatCurrency(model.getOffer().getCurrencyCode()) && !DevEnv.isDevMode()) { new Popup<>().headLine(Res.get("popup.roundedFiatValues.headline")) .information(Res.get("popup.roundedFiatValues.msg", model.getOffer().getCurrencyCode())) .dontShowAgainId("FiatValuesRoundedWarning") From 7e342990c00b5957a86891a86e1446a88f6d11ae Mon Sep 17 00:00:00 2001 From: sqrrm Date: Mon, 12 Nov 2018 20:10:36 -0500 Subject: [PATCH 10/11] Fix text Co-Authored-By: ManfredKarrer --- .../core/dao/governance/proofofburn/ProofOfBurnService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java index a2b3334f3b6..d6fff0cb00c 100644 --- a/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnService.java @@ -140,7 +140,7 @@ public void onParseBlockChainComplete() { public Transaction burn(String preImageAsString, long amount) throws InsufficientMoneyException, TxException { try { - // We create a prepared Bsq Tx for the listing fee. + // We create a prepared Bsq Tx for the burn amount final Transaction preparedBurnFeeTx = bsqWalletService.getPreparedBurnFeeTx(Coin.valueOf(amount)); byte[] hash = getHashFromPreImage(preImageAsString); byte[] opReturnData = ProofOfBurnConsensus.getOpReturnData(hash); From a65f1df5c86d53545e30aea8cc599818d1738d25 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Mon, 12 Nov 2018 20:16:55 -0500 Subject: [PATCH 11/11] Improve method name, add comment --- .../main/java/bisq/core/dao/node/parser/TxOutputParser.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java index e61086009a0..7a313adae81 100644 --- a/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java +++ b/core/src/main/java/bisq/core/dao/node/parser/TxOutputParser.java @@ -115,7 +115,8 @@ void processTxOutput(TempTxOutput tempTxOutput) { // The LOCKUP BSQ is burnt unless the output exactly matches the input, that would cause the // output to not be BSQ output at all handleUnlockBondTx(tempTxOutput); - } else if (isBurnFeeTxOutput(tempTxOutput)) { + } else if (isBtcOutputOfBurnFeeTx(tempTxOutput)) { + // In case we have the opReturn for a burn fee tx all outputs after 1st output are considered BTC handleBtcOutput(tempTxOutput, index); } else if (availableInputValue > 0 && availableInputValue >= txOutputValue) { handleBsqOutput(tempTxOutput, index, txOutputValue); @@ -172,7 +173,7 @@ private void handleUnlockBondTx(TempTxOutput txOutput) { bsqOutputFound = true; } - private boolean isBurnFeeTxOutput(TempTxOutput tempTxOutput) { + private boolean isBtcOutputOfBurnFeeTx(TempTxOutput tempTxOutput) { // If we get a asset listing or proof of burn tx we have only 1 BSQ output and if the // burned amount is larger than the miner fee we might have a BTC output for receiving the burned funds. // If the burned funds are less than the miner fee a BTC input is used for miner fee and a BTC change output for