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 extends View> 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 extends View> 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 extends View> 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