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..af0f3c01229 100644 --- a/common/src/main/proto/pb.proto +++ b/common/src/main/proto/pb.proto @@ -941,9 +941,9 @@ 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; + MyProofOfBurnList my_proof_of_burn_list = 26; } } @@ -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 { @@ -1531,14 +1535,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; @@ -1555,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/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..b6b0c931842 100644 --- a/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java +++ b/core/src/main/java/bisq/core/app/misc/AppSetupWithP2PAndDAO.java @@ -19,11 +19,11 @@ 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; 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; @@ -56,7 +56,7 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, MyBlindVoteListService myBlindVoteListService, MyProposalListService myProposalListService, MyReputationListService myReputationListService, - AssetService assetService, + MyProofOfBurnListService myProofOfBurnListService, @Named(DaoOptionKeys.DAO_ACTIVATED) boolean daoActivated) { super(encryptionService, keyRing, @@ -74,7 +74,7 @@ public AppSetupWithP2PAndDAO(EncryptionService encryptionService, persistedDataHosts.add(myBlindVoteListService); persistedDataHosts.add(myProposalListService); persistedDataHosts.add(myReputationListService); - persistedDataHosts.add(assetService); + persistedDataHosts.add(myProofOfBurnListService); } } 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/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 b418d1d535e..e4c15275eae 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; @@ -25,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; @@ -64,6 +66,8 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, BondedRolesRepository bondedRolesRepository, MyReputationListService myReputationListService, MyBondedReputationRepository myBondedReputationRepository, + AssetService assetService, + ProofOfBurnService proofOfBurnService, DaoFacade daoFacade, ExportJsonFilesService exportJsonFilesService) { @@ -83,6 +87,8 @@ public DaoSetup(BsqNodeProvider bsqNodeProvider, daoSetupServices.add(bondedRolesRepository); 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/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..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 @@ -17,25 +17,82 @@ 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.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.proto.persistable.PersistedDataHost; -import bisq.common.storage.Storage; +import bisq.common.Timer; +import bisq.common.UserThread; +import bisq.common.app.DevEnv; +import bisq.common.handlers.ErrorMessageHandler; +import bisq.common.handlers.ResultHandler; + +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Transaction; import javax.inject.Inject; +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 { - 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 IntegerProperty updateFlag = new SimpleIntegerProperty(0); @Getter - private final RemovedAssetsList removedAssetsList = new RemovedAssetsList(); + private final List statefulAssets = new ArrayList<>(); + private Map> tradeStatsByTickerSymbol; + private long bsqFeePerDay; + private long minVolumeInBtc; + private Timer timer; /////////////////////////////////////////////////////////////////////////////////////////// @@ -43,73 +100,252 @@ public class AssetService implements PersistedDataHost { /////////////////////////////////////////////////////////////////////////////////////////// @Inject - public AssetService(Storage storage) { - this.storage = storage; + public AssetService(BsqWalletService bsqWalletService, + BtcWalletService btcWalletService, + WalletsManager walletsManager, + TradeStatisticsManager tradeStatisticsManager, + DaoStateService daoStateService) { + this.bsqWalletService = bsqWalletService; + this.btcWalletService = btcWalletService; + this.walletsManager = walletsManager; + this.tradeStatisticsManager = tradeStatisticsManager; + this.daoStateService = daoStateService; } + 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); + }); + } /////////////////////////////////////////////////////////////////////////////////////////// - // PersistedDataHost + // DaoSetupService /////////////////////////////////////////////////////////////////////////////////////////// @Override - public void readPersisted() { - if (BisqEnvironment.isDAOActivatedAndBaseCurrencySupportingBsq()) { - RemovedAssetsList persisted = storage.initAndGetPersisted(removedAssetsList, 100); - if (persisted != null) { - removedAssetsList.clear(); - removedAssetsList.addAll(persisted.getList()); + public void addListeners() { + daoStateService.addBsqStateListener(this); + } + + @Override + public void start() { + statefulAssets.clear(); + statefulAssets.addAll(CurrencyUtil.getSortedAssetStream() + .filter(asset -> !asset.getTickerSymbol().equals("BSQ")) + .map(StatefulAsset::new) + .collect(Collectors.toList())); + } + + public void updateList() { + 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 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 FeePayment(txId, burntFee); + }) + .collect(Collectors.toList()); + } + + private List getFeeTxs(StatefulAsset statefulAsset) { + 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) + .sorted(Comparator.comparing(BaseTx::getTime)) + .collect(Collectors.toList()); + } + /////////////////////////////////////////////////////////////////////////////////////////// - // API + // DaoStateListener /////////////////////////////////////////////////////////////////////////////////////////// - public void addToRemovedAssetsListByVoting(String tickerSymbol) { - log.info("Asset '{}' was removed by DAO voting", CurrencyUtil.getNameAndCode(tickerSymbol)); - removedAssetsList.add(new RemovedAsset(tickerSymbol, RemoveReason.VOTING)); - persist(); + @Override + public void onNewBlockHeight(int blockHeight) { } - public boolean hasPaidBSQFee(String tickerSymbol) { - //TODO - return false; + @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 + public void onParseBlockChainComplete() { } - public boolean isAssetRemoved(String tickerSymbol) { - boolean isRemoved = removedAssetsList.getList().stream() - .anyMatch(removedAsset -> removedAsset.getTickerSymbol().equals(tickerSymbol)); + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + 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 persist() { - storage.queueUpForSave(20); + + public Transaction payFee(StatefulAsset statefulAsset, long listingFee) throws InsufficientMoneyException, TxException { + 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 { + // 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()); + } + + public void publishTransaction(Transaction transaction, 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()); + } + }); } } diff --git a/core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java similarity index 77% rename from core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java rename to core/src/main/java/bisq/core/dao/governance/asset/AssetState.java index 81635722b10..24fed67670a 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/RemoveReason.java +++ b/core/src/main/java/bisq/core/dao/governance/asset/AssetState.java @@ -17,7 +17,13 @@ package bisq.core.dao.governance.asset; -public enum RemoveReason { - VOTING, - INACTIVITY +/** + * Maintain translation stings ("dao.assetState.*") + */ +public enum AssetState { + 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/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/StatefulAsset.java b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java new file mode 100644 index 00000000000..dca2a1650b2 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/asset/StatefulAsset.java @@ -0,0 +1,107 @@ +/* + * 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 java.util.Optional; + +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Getter +public class StatefulAsset { + private final Asset asset; + @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; + } + + /////////////////////////////////////////////////////////////////////////////////////////// + // API + /////////////////////////////////////////////////////////////////////////////////////////// + + public String getNameAndCode() { + return CurrencyUtil.getNameAndCode(getTickerSymbol()); + } + + public String getTickerSymbol() { + return asset.getTickerSymbol(); + } + + public void setFeePayments(List feePayments) { + this.feePayments = feePayments; + } + + public Optional getLastFeePayment() { + return feePayments.isEmpty() ? Optional.empty() : Optional.of(feePayments.get(feePayments.size() - 1)); + } + + public long getTotalFeesPaid() { + 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 boolean isActive() { + return !wasRemovedByVoting() && !isDeListed(); + } + + public boolean wasRemovedByVoting() { + return assetState == AssetState.REMOVED_BY_VOTING; + } + + 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/param/Param.java b/core/src/main/java/bisq/core/dao/governance/param/Param.java index 550ebd2e6e9..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,10 @@ public enum Param { "2N4mVTpUZAnhm9phnxB7VrHB4aBhnWrcUrV", // testnet ParamType.ADDRESS), - //TODO add asset listing params (nr. of trades, volume, time, fee which defines listing state) + // 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... // Period phase ("11 blocks atm) 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/asset/RemovedAssetsList.java b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java similarity index 60% rename from core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java rename to core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java index 9c655886cea..a5d6c0ce997 100644 --- a/core/src/main/java/bisq/core/dao/governance/asset/RemovedAssetsList.java +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/MyProofOfBurnList.java @@ -15,9 +15,7 @@ * along with Bisq. If not, see . */ -package bisq.core.dao.governance.asset; - -import bisq.core.dao.governance.ConsensusCritical; +package bisq.core.dao.governance.proofofburn; import bisq.common.proto.persistable.PersistableList; @@ -30,16 +28,16 @@ import lombok.EqualsAndHashCode; /** - * PersistableEnvelope wrapper for list of removedAssets. + * PersistableEnvelope wrapper for list of MyProofOfBurn objects. */ @EqualsAndHashCode(callSuper = true) -public class RemovedAssetsList extends PersistableList implements ConsensusCritical { +public class MyProofOfBurnList extends PersistableList { - public RemovedAssetsList(List list) { + private MyProofOfBurnList(List list) { super(list); } - public RemovedAssetsList() { + MyProofOfBurnList() { super(); } @@ -50,26 +48,27 @@ public RemovedAssetsList() { @Override public PB.PersistableEnvelope toProtoMessage() { - return PB.PersistableEnvelope.newBuilder().setRemovedAssetList(getBuilder()).build(); + return PB.PersistableEnvelope.newBuilder().setMyProofOfBurnList(getBuilder()).build(); } - public PB.RemovedAssetList.Builder getBuilder() { - return PB.RemovedAssetList.newBuilder() - .addAllRemovedAsset(getList().stream() - .map(RemovedAsset::toProtoMessage) + private PB.MyProofOfBurnList.Builder getBuilder() { + return PB.MyProofOfBurnList.newBuilder() + .addAllMyProofOfBurn(getList().stream() + .map(MyProofOfBurn::toProtoMessage) .collect(Collectors.toList())); } - public static RemovedAssetsList fromProto(PB.RemovedAssetList proto) { - return new RemovedAssetsList(new ArrayList<>(proto.getRemovedAssetList().stream() - .map(RemovedAsset::fromProto) + 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 tickerSymbols in RemovedAssetList: " + getList().stream() - .map(RemovedAsset::getTickerSymbol) + 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 new file mode 100644 index 00000000000..490a2cefc87 --- /dev/null +++ b/core/src/main/java/bisq/core/dao/governance/proofofburn/ProofOfBurnConsensus.java @@ -0,0 +1,59 @@ +/* + * 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 java.util.Arrays; + +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; + } + + 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..d6fff0cb00c --- /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 burn amount + 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/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/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/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..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,6 +115,9 @@ 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 (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); } else { @@ -170,6 +173,17 @@ private void handleUnlockBondTx(TempTxOutput txOutput) { bsqOutputFound = true; } + 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 + // 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; @@ -240,6 +254,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..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 @@ -418,6 +424,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 +471,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 +896,23 @@ public List getDecryptedBallotsWithMeritsList() { } + /////////////////////////////////////////////////////////////////////////////////////////// + // Asset listing fee + /////////////////////////////////////////////////////////////////////////////////////////// + + 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/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 a235ab973f5..959e28d2cdd 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; @@ -32,20 +38,12 @@ 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; 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 { @@ -53,7 +51,6 @@ public static void setup() { setBaseCurrencyCode(BisqEnvironment.getBaseCurrencyNetwork().getCurrencyCode()); } - @Getter private static final AssetRegistry assetRegistry = new AssetRegistry(); private static String baseCurrencyCode = "BTC"; @@ -110,24 +107,17 @@ 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())) + return getSortedAssetStream() .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; + public static Stream getSortedAssetStream() { + return assetRegistry.stream() + .filter(CurrencyUtil::assetIsNotBaseCurrency) + .filter(asset -> isNotBsqOrBsqTradingActivated(asset, BisqEnvironment.getBaseCurrencyNetwork(), DevEnv.isDaoTradingActivated())) + .filter(asset -> assetMatchesNetworkIfMainnet(asset, BisqEnvironment.getBaseCurrencyNetwork())) + .sorted(Comparator.comparing(Asset::getName)); } public static List getMainCryptoCurrencies() { @@ -488,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..a7cb5ae8a51 100644 --- a/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java +++ b/core/src/main/java/bisq/core/proto/persistable/CorePersistenceProtoResolver.java @@ -20,11 +20,11 @@ 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; 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; @@ -129,12 +129,12 @@ 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: 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 a48b1f44547..ee7e6f42112 100644 --- a/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java +++ b/core/src/main/java/bisq/core/setup/CorePersistedDataHost.java @@ -20,11 +20,11 @@ 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; 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; @@ -68,7 +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(AssetService.class)); + persistedDataHosts.add(injector.getInstance(MyProofOfBurnListService.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/java/bisq/core/trade/statistics/TradeStatisticsManager.java b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java index a1632ab249d..f736ce8c7a6 100644 --- a/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java +++ b/core/src/main/java/bisq/core/trade/statistics/TradeStatisticsManager.java @@ -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"); diff --git a/core/src/main/resources/i18n/displayStrings.properties b/core/src/main/resources/i18n/displayStrings.properties index 0319281f35c..d5031f34b4f 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 listing fee/Proof of burn dao.paidWithBsq=paid with BSQ dao.availableBsqBalance=Available @@ -1273,6 +1274,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 @@ -1411,6 +1417,53 @@ dao.bond.bondedRoleType.MEDIATOR=Mediator # suppress inspection "UnusedProperty" dao.bond.bondedRoleType.ARBITRATOR=Arbitrator +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 +dao.burnBsq.fee=Fee +dao.burnBsq.trialPeriod=Trial period +dao.burnBsq.payFee=Pay fee +dao.burnBsq.allAssets=All assets +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.UNDEFINED=Undefined +# suppress inspection "UnusedProperty" +dao.assetState.IN_TRIAL_PERIOD=In trial period +# suppress inspection "UnusedProperty" +dao.assetState.ACTIVELY_TRADED=Actively traded +# suppress inspection "UnusedProperty" +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 @@ -1610,6 +1663,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\ @@ -2434,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/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/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..925eaeb2061 --- /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.proofofburn.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..402e044f220 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetFeeView.java @@ -0,0 +1,417 @@ +/* + * 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.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.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.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.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 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 { + private 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 AssetService assetService; + private BSFormatter btcFormatter; + + private final ObservableList observableList = FXCollections.observableArrayList(); + private final SortedList sortedList = new SortedList<>(observableList); + + private int gridRow = 0; + + private ChangeListener amountFocusOutListener; + private ChangeListener amountInputTextFieldListener; + @Nullable + private StatefulAsset selectedAsset; + private InvalidationListener updateListener; + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private AssetFeeView(BsqFormatter bsqFormatter, + BsqWalletService bsqWalletService, + BsqValidator bsqValidator, + AssetService assetService, + BSFormatter btcFormatter) { + this.bsqFormatter = bsqFormatter; + this.bsqWalletService = bsqWalletService; + this.bsqValidator = bsqValidator; + this.assetService = assetService; + this.btcFormatter = btcFormatter; + } + + @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.setOnAction(e -> { + selectedAsset = assetComboBox.getSelectionModel().getSelectedItem(); + }); + + feeAmountInputTextField.textProperty().addListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().addListener(amountFocusOutListener); + + sortedList.comparatorProperty().bind(tableView.comparatorProperty()); + + assetService.getUpdateFlag().addListener(updateListener); + bsqWalletService.addBsqBalanceListener(this); + + payFeeButton.setOnAction((event) -> { + Coin listingFee = getListingFee(); + 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"), () -> doPublishFeeTx(transaction)); + } else { + doPublishFeeTx(transaction); + } + } catch (InsufficientMoneyException | TxException e) { + e.printStackTrace(); + new Popup<>().error(e.toString()).show(); + } + }); + + updateList(); + updateButtonState(); + + feeAmountInputTextField.resetValidation(); + } + + @Override + protected void deactivate() { + assetComboBox.setOnAction(null); + + feeAmountInputTextField.textProperty().removeListener(amountInputTextFieldListener); + feeAmountInputTextField.focusedProperty().removeListener(amountFocusOutListener); + + assetService.getUpdateFlag().removeListener(updateListener); + 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) -> { + long days = getListingFee().value / assetService.getFeePerDay().value; + trialPeriodTextField.setText(Res.get("dao.burnBsq.assets.days", days)); + updateButtonState(); + }; + + 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))*/ + .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); + } + + private Coin getListingFee() { + return bsqFormatter.parseToCoin(feeAmountInputTextField.getText()); + } + + private void doPublishFeeTx(Transaction transaction) { + assetService.publishTransaction(transaction, + () -> { + 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 + /////////////////////////////////////////////////////////////////////////////////////////// + + private void createColumns() { + TableColumn column; + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.nameAndCode")); + 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.getNameAndCode()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getNameAndCode)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.state")); + 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.getAssetStateString()); + } else + setText(""); + } + }; + } + }); + tableView.getColumns().add(column); + column.setComparator(Comparator.comparing(AssetListItem::getAssetStateString)); + + column = new AutoTooltipTableColumn<>(Res.get("dao.burnBsq.assets.tradeVolume")); + 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.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 new file mode 100644 index 00000000000..4e3a4063645 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/assetfee/AssetListItem.java @@ -0,0 +1,57 @@ +/* + * 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.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 int trialPeriodInBlocks; + private final String nameAndCode; + 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 = statefulAsset.getNameAndCode(); + assetStateString = Res.get("dao.assetState." + statefulAsset.getAssetState()); + 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/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/proofofburn/ProofOfBurnView.fxml b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml new file mode 100644 index 00000000000..498a89f2a68 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.fxml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + diff --git a/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java new file mode 100644 index 00000000000..11ee32151c8 --- /dev/null +++ b/desktop/src/main/java/bisq/desktop/main/dao/burnbsq/proofofburn/ProofOfBurnView.java @@ -0,0 +1,638 @@ +/* + * 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.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/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/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"; 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/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") 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/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(); 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())); }