Merge branch 'master' into 02-cli-console-formatting-api

This commit is contained in:
ghubstan 2021-11-09 09:20:06 -03:00
commit 20cc085dc8
No known key found for this signature in database
GPG key ID: E35592D6800A861E
66 changed files with 1145 additions and 742 deletions

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
package bisq.apitest.method.trade;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;
import bisq.apitest.method.offer.AbstractOfferTest;
// @Disabled
@Slf4j
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class BsqSwapTradeTestLoop extends AbstractOfferTest {
@BeforeAll
public static void setUp() {
AbstractOfferTest.setUp();
createBsqSwapBsqPaymentAccounts();
}
@Test
@Order(1)
public void testGetBalancesBeforeTrade() {
BsqSwapTradeTest test = new BsqSwapTradeTest();
runTradeLoop(test);
}
private void runTradeLoop(BsqSwapTradeTest test) {
// TODO Fix wallet inconsistency bugs after 2nd trades.
for (int tradeCount = 1; tradeCount <= 2; tradeCount++) {
log.warn("================================ Trade # {} ================================", tradeCount);
test.testGetBalancesBeforeTrade();
test.testAliceCreateBsqSwapBuyOffer();
genBtcBlocksThenWait(1, 8000);
test.testBobTakesBsqSwapOffer();
genBtcBlocksThenWait(1, 8000);
test.testGetBalancesAfterTrade();
}
}
}

View file

@ -501,8 +501,6 @@ public class PersistenceManager<T extends PersistableEnvelope> {
if (completeHandler != null) {
UserThread.execute(completeHandler);
}
GcUtil.maybeReleaseMemory();
}
}

View file

@ -18,6 +18,7 @@
package bisq.common.util;
import bisq.common.UserThread;
import bisq.common.app.DevEnv;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
@ -47,18 +48,31 @@ public class GcUtil {
* @param trigger Threshold for free memory in MB when we invoke the garbage collector
*/
private static void autoReleaseMemory(long trigger) {
UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 60);
UserThread.runPeriodically(() -> maybeReleaseMemory(trigger), 120);
}
/**
* @param trigger Threshold for free memory in MB when we invoke the garbage collector
*/
private static void maybeReleaseMemory(long trigger) {
long totalMemory = Runtime.getRuntime().totalMemory();
if (totalMemory > trigger * 1024 * 1024) {
log.info("Invoke garbage collector. Total memory: {} {} {}", Utilities.readableFileSize(totalMemory), totalMemory, trigger * 1024 * 1024);
long ts = System.currentTimeMillis();
long preGcMemory = Runtime.getRuntime().totalMemory();
if (preGcMemory > trigger * 1024 * 1024) {
System.gc();
log.info("Total memory after gc() call: {}", Utilities.readableFileSize(Runtime.getRuntime().totalMemory()));
long postGcMemory = Runtime.getRuntime().totalMemory();
log.info("GC reduced memory by {}. Total memory before/after: {}/{}. Took {} ms.",
Utilities.readableFileSize(preGcMemory - postGcMemory),
Utilities.readableFileSize(preGcMemory),
Utilities.readableFileSize(postGcMemory),
System.currentTimeMillis() - ts);
if (DevEnv.isDevMode()) {
try {
// To see from where we got called
throw new RuntimeException("Dummy Exception for print stacktrace at maybeReleaseMemory");
} catch (Throwable t) {
t.printStackTrace();
}
}
}
}
}

View file

@ -28,6 +28,7 @@ import bisq.core.dao.governance.proofofburn.MyProofOfBurnListService;
import bisq.core.dao.governance.proposal.MyProposalListService;
import bisq.core.filter.FilterManager;
import bisq.core.trade.statistics.TradeStatisticsManager;
import bisq.core.user.Preferences;
import bisq.network.p2p.P2PService;
import bisq.network.p2p.peers.PeerManager;
@ -42,6 +43,7 @@ import lombok.extern.slf4j.Slf4j;
@Slf4j
public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
private final DaoSetup daoSetup;
private final Preferences preferences;
@Inject
public AppSetupWithP2PAndDAO(P2PService p2PService,
@ -58,6 +60,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
MyProposalListService myProposalListService,
MyReputationListService myReputationListService,
MyProofOfBurnListService myProofOfBurnListService,
Preferences preferences,
Config config) {
super(p2PService,
p2PDataStorage,
@ -69,6 +72,7 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
config);
this.daoSetup = daoSetup;
this.preferences = preferences;
// TODO Should be refactored/removed. In the meantime keep in sync with CorePersistedDataHost
if (config.daoActivated) {
@ -86,5 +90,8 @@ public class AppSetupWithP2PAndDAO extends AppSetupWithP2P {
super.onBasicServicesInitialized();
daoSetup.onAllServicesInitialized(log::error, log::warn);
// For seed nodes we need to set default value to true
preferences.setUseFullModeDaoMonitor(true);
}
}

View file

@ -1,69 +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 <http://www.gnu.org/licenses/>.
*/
package bisq.core.dao;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.state.DaoStateListener;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import bisq.core.dao.state.model.blockchain.Block;
import javax.inject.Inject;
public class DaoEventCoordinator implements DaoSetupService, DaoStateListener {
private final DaoStateService daoStateService;
private final DaoStateSnapshotService daoStateSnapshotService;
private final DaoStateMonitoringService daoStateMonitoringService;
@Inject
public DaoEventCoordinator(DaoStateService daoStateService,
DaoStateSnapshotService daoStateSnapshotService,
DaoStateMonitoringService daoStateMonitoringService) {
this.daoStateService = daoStateService;
this.daoStateSnapshotService = daoStateSnapshotService;
this.daoStateMonitoringService = daoStateMonitoringService;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
this.daoStateService.addDaoStateListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing.
// We need to listen during batch processing as well to write snapshots during that process.
@Override
public void onDaoStateChanged(Block block) {
// We need to execute first the daoStateMonitoringService
daoStateMonitoringService.createHashFromBlock(block);
daoStateSnapshotService.maybeCreateSnapshot(block);
}
}

View file

@ -102,7 +102,6 @@ public class DaoModule extends AppModule {
protected void configure() {
bind(DaoSetup.class).in(Singleton.class);
bind(DaoFacade.class).in(Singleton.class);
bind(DaoEventCoordinator.class).in(Singleton.class);
bind(DaoKillSwitch.class).in(Singleton.class);
// Node, parser

View file

@ -39,6 +39,7 @@ import bisq.core.dao.node.BsqNode;
import bisq.core.dao.node.BsqNodeProvider;
import bisq.core.dao.node.explorer.ExportJsonFilesService;
import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.DaoStateSnapshotService;
import com.google.inject.Inject;
@ -78,16 +79,11 @@ public class DaoSetup {
DaoStateMonitoringService daoStateMonitoringService,
ProposalStateMonitoringService proposalStateMonitoringService,
BlindVoteStateMonitoringService blindVoteStateMonitoringService,
DaoEventCoordinator daoEventCoordinator) {
DaoStateSnapshotService daoStateSnapshotService) {
bsqNode = bsqNodeProvider.getBsqNode();
// We need to take care of order of execution.
// For order critical event flow we use the daoEventCoordinator to delegate the calls from anonymous listeners
// to concrete clients.
daoSetupServices.add(daoEventCoordinator);
daoSetupServices.add(daoStateService);
daoSetupServices.add(cycleService);
daoSetupServices.add(ballotListService);
@ -110,6 +106,7 @@ public class DaoSetup {
daoSetupServices.add(daoStateMonitoringService);
daoSetupServices.add(proposalStateMonitoringService);
daoSetupServices.add(blindVoteStateMonitoringService);
daoSetupServices.add(daoStateSnapshotService);
daoSetupServices.add(bsqNodeProvider.getBsqNode());
}

View file

@ -90,8 +90,7 @@ public class CycleService implements DaoStateListener, DaoSetupService {
}
public int getCycleIndex(Cycle cycle) {
Optional<Cycle> previousCycle = getCycle(cycle.getHeightOfFirstBlock() - 1, daoStateService.getCycles());
return previousCycle.map(cycle1 -> getCycleIndex(cycle1) + 1).orElse(0);
return daoStateService.getCycles().indexOf(cycle);
}
public boolean isTxInCycle(Cycle cycle, String txId) {

View file

@ -274,7 +274,7 @@ public class BlindVoteStateMonitoringService implements DaoSetupService, DaoStat
byte[] combined = ArrayUtils.addAll(prevHash, serializedBlindVotes);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, prevHash, blindVotes.size());
BlindVoteStateHash myBlindVoteStateHash = new BlindVoteStateHash(blockHeight, hash, blindVotes.size());
BlindVoteStateBlock blindVoteStateBlock = new BlindVoteStateBlock(myBlindVoteStateHash);
blindVoteStateBlockChain.add(blindVoteStateBlock);
blindVoteStateHashChain.add(myBlindVoteStateHash);

View file

@ -31,6 +31,7 @@ import bisq.core.dao.state.GenesisTxInfo;
import bisq.core.dao.state.model.blockchain.BaseTxOutput;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.model.governance.IssuanceType;
import bisq.core.user.Preferences;
import bisq.network.p2p.NodeAddress;
import bisq.network.p2p.network.Connection;
@ -40,7 +41,6 @@ import bisq.common.UserThread;
import bisq.common.config.Config;
import bisq.common.crypto.Hash;
import bisq.common.file.FileUtil;
import bisq.common.util.GcUtil;
import bisq.common.util.Utilities;
import javax.inject.Inject;
@ -54,19 +54,20 @@ import javafx.collections.ObservableList;
import java.io.File;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.stream.Collectors;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import static com.google.common.base.Preconditions.checkArgument;
import javax.annotation.Nullable;
/**
* Monitors the DaoState by using a hash for the complete daoState and make it accessible to the network
@ -88,7 +89,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
DaoStateNetworkService.Listener<NewDaoStateHashMessage, GetDaoStateHashesRequest, DaoStateHash> {
public interface Listener {
void onChangeAfterBatchProcessing();
void onDaoStateHashesChanged();
void onCheckpointFail();
}
@ -98,7 +99,6 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
private final GenesisTxInfo genesisTxInfo;
private final Set<String> seedNodeAddresses;
@Getter
private final LinkedList<DaoStateBlock> daoStateBlockChain = new LinkedList<>();
@Getter
@ -110,6 +110,8 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Getter
private boolean isInConflictWithSeedNode;
@Getter
private boolean daoStateBlockChainNotConnecting;
@Getter
private final ObservableList<UtxoMismatch> utxoMismatches = FXCollections.observableArrayList();
private final List<Checkpoint> checkpoints = Arrays.asList(
@ -120,7 +122,13 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
private int numCalls;
private long accumulatedDuration;
private final Preferences preferences;
private final File storageDir;
@Nullable
private Runnable createSnapshotHandler;
// Lookup map
private Map<Integer, DaoStateBlock> daoStateBlockByHeight = new HashMap<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -131,11 +139,13 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
DaoStateNetworkService daoStateNetworkService,
GenesisTxInfo genesisTxInfo,
SeedNodeRepository seedNodeRepository,
Preferences preferences,
@Named(Config.STORAGE_DIR) File storageDir,
@Named(Config.IGNORE_DEV_MSG) boolean ignoreDevMsg) {
this.daoStateService = daoStateService;
this.daoStateNetworkService = daoStateNetworkService;
this.genesisTxInfo = genesisTxInfo;
this.preferences = preferences;
this.storageDir = storageDir;
this.ignoreDevMsg = ignoreDevMsg;
seedNodeAddresses = seedNodeRepository.getSeedNodeAddresses().stream()
@ -163,16 +173,19 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We do not use onDaoStateChanged but let the DaoEventCoordinator call createHashFromBlock to ensure the
// correct order of execution.
@Override
public void onParseBlockChainComplete() {
parseBlockChainComplete = true;
daoStateService.getLastBlock().ifPresent(this::checkUtxos);
daoStateNetworkService.addListeners();
// We wait for processing messages until we have completed batch processing
int fromHeight = daoStateService.getChainHeight() - 10;
// We take either the height of the previous hashBlock we have or 10 blocks below the chain tip.
int nextBlockHeight = daoStateBlockChain.isEmpty() ?
genesisTxInfo.getGenesisBlockHeight() :
daoStateBlockChain.getLast().getHeight() + 1;
int past10 = daoStateService.getChainHeight() - 10;
int fromHeight = Math.min(nextBlockHeight, past10);
daoStateNetworkService.requestHashesFromAllConnectedSeedNodes(fromHeight);
if (!ignoreDevMsg) {
@ -188,16 +201,9 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Override
public void onDaoStateChanged(Block block) {
long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value;
long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION);
long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT);
long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq();
// confiscated funds are still in the utxo set
long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq;
if (sumBsq != sumUtxo) {
utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq));
// During syncing we do not call checkUtxos as its a bit slow (about 4 ms)
if (parseBlockChainComplete) {
checkUtxos(block);
}
}
@ -208,44 +214,47 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
@Override
public void onNewStateHashMessage(NewDaoStateHashMessage newStateHashMessage, Connection connection) {
if (newStateHashMessage.getStateHash().getHeight() <= daoStateService.getChainHeight()) {
processPeersDaoStateHash(newStateHashMessage.getStateHash(), connection.getPeersNodeAddressOptional(), true);
// Called when receiving NewDaoStateHashMessages from peers after a new block
DaoStateHash peersDaoStateHash = newStateHashMessage.getStateHash();
if (peersDaoStateHash.getHeight() <= daoStateService.getChainHeight()) {
putInPeersMapAndCheckForConflicts(getPeersAddress(connection.getPeersNodeAddressOptional()), peersDaoStateHash);
listeners.forEach(Listener::onDaoStateHashesChanged);
}
}
@Override
public void onPeersStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
// Called when receiving GetDaoStateHashesResponse from seed nodes
processPeersDaoStateHashes(stateHashes, peersNodeAddress);
listeners.forEach(Listener::onDaoStateHashesChanged);
if (createSnapshotHandler != null) {
createSnapshotHandler.run();
// As we get called multiple times from hashes of diff. seed nodes we want to avoid to
// call our handler multiple times.
createSnapshotHandler = null;
}
}
@Override
public void onGetStateHashRequest(Connection connection, GetDaoStateHashesRequest getStateHashRequest) {
int fromHeight = getStateHashRequest.getHeight();
List<DaoStateHash> daoStateHashes = daoStateBlockChain.stream()
List<DaoStateHash> daoStateHashes = daoStateHashChain.stream()
.filter(e -> e.getHeight() >= fromHeight)
.map(DaoStateBlock::getMyStateHash)
.collect(Collectors.toList());
daoStateNetworkService.sendGetStateHashesResponse(connection, getStateHashRequest.getNonce(), daoStateHashes);
}
@Override
public void onPeersStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
AtomicBoolean hasChanged = new AtomicBoolean(false);
stateHashes.forEach(daoStateHash -> {
boolean changed = processPeersDaoStateHash(daoStateHash, peersNodeAddress, false);
if (changed) {
hasChanged.set(true);
}
});
if (hasChanged.get()) {
listeners.forEach(Listener::onChangeAfterBatchProcessing);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void createHashFromBlock(Block block) {
updateHashChain(block);
createDaoStateBlock(block);
if (parseBlockChainComplete) {
// We notify listeners only after batch processing to avoid performance issues at UI code
listeners.forEach(Listener::onDaoStateHashesChanged);
}
}
public void requestHashesFromGenesisBlockHeight(String peersAddress) {
@ -256,6 +265,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// We could get a reset from a reorg, so we clear all and start over from the genesis block.
daoStateHashChain.clear();
daoStateBlockChain.clear();
daoStateBlockByHeight.clear();
daoStateNetworkService.reset();
if (!persistedDaoStateHashChain.isEmpty()) {
@ -263,7 +273,15 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
persistedDaoStateHashChain.size(), persistedDaoStateHashChain.getLast());
}
daoStateHashChain.addAll(persistedDaoStateHashChain);
daoStateHashChain.forEach(e -> daoStateBlockChain.add(new DaoStateBlock(e)));
daoStateHashChain.forEach(daoStateHash -> {
DaoStateBlock daoStateBlock = new DaoStateBlock(daoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(daoStateHash.getHeight(), daoStateBlock);
});
}
public void setCreateSnapshotHandler(Runnable handler) {
createSnapshotHandler = handler;
}
@ -284,7 +302,7 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateHashChain(Block block) {
private Optional<DaoStateBlock> createDaoStateBlock(Block block) {
long ts = System.currentTimeMillis();
byte[] prevHash;
int height = block.getHeight();
@ -295,34 +313,45 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
} else {
log.warn("DaoStateBlockchain is empty but we received the block which was not the genesis block. " +
"We stop execution here.");
return;
daoStateBlockChainNotConnecting = true;
listeners.forEach(Listener::onDaoStateHashesChanged);
return Optional.empty();
}
} else {
checkArgument(height == daoStateBlockChain.getLast().getHeight() + 1,
"New block must be 1 block above previous block. height={}, " +
"daoStateBlockChain.getLast().getHeight()={}",
height, daoStateBlockChain.getLast().getHeight());
prevHash = daoStateBlockChain.getLast().getHash();
DaoStateBlock last = daoStateBlockChain.getLast();
int heightOfLastBlock = last.getHeight();
if (height == heightOfLastBlock + 1) {
prevHash = last.getHash();
} else {
log.warn("New block must be 1 block above previous block. height={}, " +
"daoStateBlockChain.getLast().getHeight()={}",
height, heightOfLastBlock);
daoStateBlockChainNotConnecting = true;
listeners.forEach(Listener::onDaoStateHashesChanged);
return Optional.empty();
}
}
byte[] stateAsBytes = daoStateService.getSerializedStateForHashChain();
// We include the prev. hash in our new hash so we can be sure that if one hash is matching all the past would
// match as well.
byte[] combined = ArrayUtils.addAll(prevHash, stateAsBytes);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, prevHash);
DaoStateHash myDaoStateHash = new DaoStateHash(height, hash, true);
DaoStateBlock daoStateBlock = new DaoStateBlock(myDaoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(height, daoStateBlock);
daoStateHashChain.add(myDaoStateHash);
// We only broadcast after parsing of blockchain is complete
if (parseBlockChainComplete) {
// We notify listeners only after batch processing to avoid performance issues at UI code
listeners.forEach(Listener::onChangeAfterBatchProcessing);
// We delay broadcast to give peers enough time to have received the block.
// Otherwise they would ignore our data if received block is in future to their local blockchain.
int delayInSec = 5 + new Random().nextInt(10);
if (Config.baseCurrencyNetwork().isRegtest()) {
delayInSec = 1;
}
UserThread.runAfter(() -> daoStateNetworkService.broadcastMyStateHash(myDaoStateHash), delayInSec);
}
long duration = System.currentTimeMillis() - ts;
@ -332,59 +361,93 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
duration);
accumulatedDuration += duration;
numCalls++;
return Optional.of(daoStateBlock);
}
private boolean processPeersDaoStateHash(DaoStateHash daoStateHash,
Optional<NodeAddress> peersNodeAddress,
boolean notifyListeners) {
GcUtil.maybeReleaseMemory();
private void processPeersDaoStateHashes(List<DaoStateHash> stateHashes, Optional<NodeAddress> peersNodeAddress) {
boolean useDaoMonitor = preferences.isUseFullModeDaoMonitor();
stateHashes.forEach(peersHash -> {
Optional<DaoStateBlock> optionalDaoStateBlock;
// If we do not add own hashes during initial parsing we fill the missing hashes from the peer and create
// at the last block our own hash.
int height = peersHash.getHeight();
if (!useDaoMonitor &&
!findDaoStateBlock(height).isPresent()) {
if (daoStateService.getChainHeight() == height) {
// At the most recent block we create our own hash
optionalDaoStateBlock = daoStateService.getLastBlock()
.map(this::createDaoStateBlock)
.orElse(findDaoStateBlock(height));
} else {
// Otherwise we create a block from the peers daoStateHash
DaoStateHash daoStateHash = new DaoStateHash(height, peersHash.getHash(), false);
DaoStateBlock daoStateBlock = new DaoStateBlock(daoStateHash);
daoStateBlockChain.add(daoStateBlock);
daoStateBlockByHeight.put(height, daoStateBlock);
daoStateHashChain.add(daoStateHash);
optionalDaoStateBlock = Optional.of(daoStateBlock);
}
} else {
optionalDaoStateBlock = findDaoStateBlock(height);
}
AtomicBoolean changed = new AtomicBoolean(false);
AtomicBoolean inConflictWithNonSeedNode = new AtomicBoolean(this.isInConflictWithNonSeedNode);
AtomicBoolean inConflictWithSeedNode = new AtomicBoolean(this.isInConflictWithSeedNode);
StringBuilder sb = new StringBuilder();
daoStateBlockChain.stream()
.filter(e -> e.getHeight() == daoStateHash.getHeight()).findAny()
.ifPresent(daoStateBlock -> {
String peersNodeAddressAsString = peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
daoStateBlock.putInPeersMap(peersNodeAddressAsString, daoStateHash);
if (!daoStateBlock.getMyStateHash().hasEqualHash(daoStateHash)) {
daoStateBlock.putInConflictMap(peersNodeAddressAsString, daoStateHash);
if (seedNodeAddresses.contains(peersNodeAddressAsString)) {
inConflictWithSeedNode.set(true);
} else {
inConflictWithNonSeedNode.set(true);
}
sb.append("We received a block hash from peer ")
.append(peersNodeAddressAsString)
.append(" which conflicts with our block hash.\n")
.append("my daoStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers daoStateHash=")
.append(daoStateHash);
}
changed.set(true);
});
// In any case we add the peer to our peersMap and check for conflicts on the relevant daoStateBlock
putInPeersMapAndCheckForConflicts(optionalDaoStateBlock, getPeersAddress(peersNodeAddress), peersHash);
});
}
this.isInConflictWithNonSeedNode = inConflictWithNonSeedNode.get();
this.isInConflictWithSeedNode = inConflictWithSeedNode.get();
private void putInPeersMapAndCheckForConflicts(String peersAddress, DaoStateHash peersHash) {
putInPeersMapAndCheckForConflicts(findDaoStateBlock(peersHash.getHeight()), peersAddress, peersHash);
}
String conflictMsg = sb.toString();
if (!conflictMsg.isEmpty()) {
if (this.isInConflictWithSeedNode)
log.warn("Conflict with seed nodes: {}", conflictMsg);
else if (this.isInConflictWithNonSeedNode)
log.debug("Conflict with non-seed nodes: {}", conflictMsg);
private void putInPeersMapAndCheckForConflicts(Optional<DaoStateBlock> optionalDaoStateBlock,
String peersAddress,
DaoStateHash peersHash) {
optionalDaoStateBlock.ifPresent(daoStateBlock -> {
daoStateBlock.putInPeersMap(peersAddress, peersHash);
checkForHashConflicts(peersHash, peersAddress, daoStateBlock);
});
}
private void checkForHashConflicts(DaoStateHash peersDaoStateHash,
String peersNodeAddress,
DaoStateBlock daoStateBlock) {
if (daoStateBlock.getMyStateHash().hasEqualHash(peersDaoStateHash)) {
return;
}
if (notifyListeners && changed.get()) {
listeners.forEach(Listener::onChangeAfterBatchProcessing);
daoStateBlock.putInConflictMap(peersNodeAddress, peersDaoStateHash);
StringBuilder stringBuilder = new StringBuilder();
stringBuilder.append("We received a block hash from peer ")
.append(peersNodeAddress)
.append(" which conflicts with our block hash.\n")
.append("my peersDaoStateHash=")
.append(daoStateBlock.getMyStateHash())
.append("\npeers peersDaoStateHash=")
.append(peersDaoStateHash);
String conflictMsg = stringBuilder.toString();
if (isSeedNode(peersNodeAddress)) {
isInConflictWithSeedNode = true;
log.warn("Conflict with seed nodes: {}", conflictMsg);
} else {
isInConflictWithNonSeedNode = true;
log.debug("Conflict with non-seed nodes: {}", conflictMsg);
}
}
GcUtil.maybeReleaseMemory();
private void checkUtxos(Block block) {
long genesisTotalSupply = daoStateService.getGenesisTotalSupply().value;
long compensationIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.COMPENSATION);
long reimbursementIssuance = daoStateService.getTotalIssuedAmount(IssuanceType.REIMBURSEMENT);
long totalAmountOfBurntBsq = daoStateService.getTotalAmountOfBurntBsq();
// confiscated funds are still in the utxo set
long sumUtxo = daoStateService.getUnspentTxOutputMap().values().stream().mapToLong(BaseTxOutput::getValue).sum();
long sumBsq = genesisTotalSupply + compensationIssuance + reimbursementIssuance - totalAmountOfBurntBsq;
return changed.get();
if (sumBsq != sumUtxo) {
utxoMismatches.add(new UtxoMismatch(block.getHeight(), sumUtxo, sumBsq));
}
}
private void verifyCheckpoints() {
@ -431,4 +494,17 @@ public class DaoStateMonitoringService implements DaoSetupService, DaoStateListe
log.error(t.toString());
}
}
private boolean isSeedNode(String peersNodeAddress) {
return seedNodeAddresses.contains(peersNodeAddress);
}
private String getPeersAddress(Optional<NodeAddress> peersNodeAddress) {
return peersNodeAddress.map(NodeAddress::getFullAddress)
.orElseGet(() -> "Unknown peer " + new Random().nextInt(10000));
}
private Optional<DaoStateBlock> findDaoStateBlock(int height) {
return Optional.ofNullable(daoStateBlockByHeight.get(height));
}
}

View file

@ -275,7 +275,7 @@ public class ProposalStateMonitoringService implements DaoSetupService, DaoState
}
byte[] combined = ArrayUtils.addAll(prevHash, serializedProposals);
byte[] hash = Hash.getSha256Ripemd160hash(combined);
ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, prevHash, proposals.size());
ProposalStateHash myProposalStateHash = new ProposalStateHash(blockHeight, hash, proposals.size());
ProposalStateBlock proposalStateBlock = new ProposalStateBlock(myProposalStateHash);
proposalStateBlockChain.add(proposalStateBlock);
proposalStateHashChain.add(myProposalStateHash);

View file

@ -29,8 +29,8 @@ public final class BlindVoteStateHash extends StateHash {
@Getter
private final int numBlindVotes;
public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numBlindVotes) {
super(cycleStartBlockHeight, hash, prevHash);
public BlindVoteStateHash(int cycleStartBlockHeight, byte[] hash, int numBlindVotes) {
super(cycleStartBlockHeight, hash);
this.numBlindVotes = numBlindVotes;
}
@ -43,14 +43,12 @@ public final class BlindVoteStateHash extends StateHash {
return protobuf.BlindVoteStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumBlindVotes(numBlindVotes).build();
}
public static BlindVoteStateHash fromProto(protobuf.BlindVoteStateHash proto) {
return new BlindVoteStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumBlindVotes());
}

View file

@ -18,12 +18,14 @@
package bisq.core.dao.monitoring.model;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public class DaoStateBlock extends StateBlock<DaoStateHash> {
public DaoStateBlock(DaoStateHash myDaoStateHash) {
super(myDaoStateHash);
}
public boolean isSelfCreated() {
return myStateHash.isSelfCreated();
}
}

View file

@ -21,11 +21,17 @@ package bisq.core.dao.monitoring.model;
import com.google.protobuf.ByteString;
import lombok.EqualsAndHashCode;
import lombok.Getter;
@Getter
@EqualsAndHashCode(callSuper = true)
public final class DaoStateHash extends StateHash {
public DaoStateHash(int height, byte[] hash, byte[] prevHash) {
super(height, hash, prevHash);
// If we have built the hash by ourself opposed to that we got delivered the hash from seed nodes or resources
private final boolean isSelfCreated;
public DaoStateHash(int height, byte[] hash, boolean isSelfCreated) {
super(height, hash);
this.isSelfCreated = isSelfCreated;
}
@ -38,12 +44,18 @@ public final class DaoStateHash extends StateHash {
return protobuf.DaoStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash)).build();
.setIsSelfCreated(isSelfCreated)
.build();
}
public static DaoStateHash fromProto(protobuf.DaoStateHash proto) {
return new DaoStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray());
return new DaoStateHash(proto.getHeight(), proto.getHash().toByteArray(), proto.getIsSelfCreated());
}
@Override
public String toString() {
return "DaoStateHash{" +
"\r\n isSelfCreated=" + isSelfCreated +
"\r\n} " + super.toString();
}
}

View file

@ -29,8 +29,8 @@ public final class ProposalStateHash extends StateHash {
@Getter
private final int numProposals;
public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, byte[] prevHash, int numProposals) {
super(cycleStartBlockHeight, hash, prevHash);
public ProposalStateHash(int cycleStartBlockHeight, byte[] hash, int numProposals) {
super(cycleStartBlockHeight, hash);
this.numProposals = numProposals;
}
@ -43,14 +43,12 @@ public final class ProposalStateHash extends StateHash {
return protobuf.ProposalStateHash.newBuilder()
.setHeight(height)
.setHash(ByteString.copyFrom(hash))
.setPrevHash(ByteString.copyFrom(prevHash))
.setNumProposals(numProposals).build();
}
public static ProposalStateHash fromProto(protobuf.ProposalStateHash proto) {
return new ProposalStateHash(proto.getHeight(),
proto.getHash().toByteArray(),
proto.getPrevHash().toByteArray(),
proto.getNumProposals());
}

View file

@ -56,10 +56,6 @@ public abstract class StateBlock<T extends StateHash> {
return myStateHash.getHash();
}
public byte[] getPrevHash() {
return myStateHash.getPrevHash();
}
@Override
public String toString() {
return "StateBlock{" +

View file

@ -40,13 +40,10 @@ import lombok.extern.slf4j.Slf4j;
public abstract class StateHash implements PersistablePayload, NetworkPayload {
protected final int height;
protected final byte[] hash;
// For first block the prevHash is an empty byte array
protected final byte[] prevHash;
StateHash(int height, byte[] hash, byte[] prevHash) {
StateHash(int height, byte[] hash) {
this.height = height;
this.hash = hash;
this.prevHash = prevHash;
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -67,7 +64,6 @@ public abstract class StateHash implements PersistablePayload, NetworkPayload {
return "StateHash{" +
"\n height=" + height +
",\n hash=" + Utilities.bytesAsHexString(hash) +
",\n prevHash=" + Utilities.bytesAsHexString(prevHash) +
"\n}";
}
}

View file

@ -102,7 +102,8 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
protected abstract Msg getNewStateHashMessage(StH myStateHash);
protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress, RequestStateHashesHandler.Listener<Res> listener);
protected abstract Han getRequestStateHashesHandler(NodeAddress nodeAddress,
RequestStateHashesHandler.Listener<Res> listener);
///////////////////////////////////////////////////////////////////////////////////////////
@ -155,8 +156,7 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
}
public void broadcastMyStateHash(StH myStateHash) {
NewStateHashMessage newStateHashMessage = getNewStateHashMessage(myStateHash);
broadcaster.broadcast(newStateHashMessage, networkNode.getNodeAddress());
broadcaster.broadcast(getNewStateHashMessage(myStateHash), networkNode.getNodeAddress());
}
public void requestHashes(int fromHeight, String peersAddress) {
@ -167,6 +167,10 @@ public abstract class StateNetworkService<Msg extends NewStateHashMessage,
requestStateHashHandlerMap.clear();
}
public boolean isSeedNode(NodeAddress nodeAddress) {
return peerManager.isSeedNode(nodeAddress);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Listeners

View file

@ -135,10 +135,11 @@ public class FullNode extends BsqNode {
protected void onParseBlockChainComplete() {
super.onParseBlockChainComplete();
if (p2pNetworkReady)
if (p2pNetworkReady) {
addBlockHandler();
else
} else {
log.info("onParseBlockChainComplete but P2P network is not ready yet.");
}
}

View file

@ -222,7 +222,7 @@ public class LiteNode extends BsqNode {
runDelayedBatchProcessing(new ArrayList<>(blockList),
() -> {
log.info("runDelayedBatchProcessing Parsing {} blocks took {} seconds.", blockList.size(),
log.info("Parsing {} blocks took {} seconds.", blockList.size(),
(System.currentTimeMillis() - ts) / 1000d);
// We only request again if wallet is synced, otherwise we would get repeated calls we want to avoid.
// We deal with that case at the setupWalletBestBlockListener method above.

View file

@ -200,6 +200,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
@Override
public void onAllConnectionsLost() {
log.info("onAllConnectionsLost");
closeAllHandlers();
stopRetryTimer();
stopped = true;
@ -208,6 +209,7 @@ public class LiteNodeNetworkService implements MessageListener, ConnectionListen
@Override
public void onNewConnectionAfterAllConnectionsLost() {
log.info("onNewConnectionAfterAllConnectionsLost");
closeAllHandlers();
stopped = false;
tryWithNewSeedNode(lastRequestedBlockHeight);

View file

@ -24,7 +24,6 @@ import bisq.core.dao.state.DaoStateService;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.common.app.DevEnv;
import bisq.common.util.GcUtil;
import org.bitcoinj.core.Coin;
@ -113,10 +112,12 @@ public class BlockParser {
.ifPresent(tx -> daoStateService.onNewTxForLastBlock(block, tx)));
daoStateService.onParseBlockComplete(block);
log.info("Parsing {} transactions at block height {} took {} ms", rawBlock.getRawTxs().size(),
blockHeight, System.currentTimeMillis() - startTs);
long duration = System.currentTimeMillis() - startTs;
if (duration > 10) {
log.info("Parsing {} transactions at block height {} took {} ms", rawBlock.getRawTxs().size(),
blockHeight, duration);
}
GcUtil.maybeReleaseMemory();
return block;
}

View file

@ -17,11 +17,13 @@
package bisq.core.dao.state;
import bisq.core.dao.DaoSetupService;
import bisq.core.dao.monitoring.DaoStateMonitoringService;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.dao.state.model.DaoState;
import bisq.core.dao.state.model.blockchain.Block;
import bisq.core.dao.state.storage.DaoStateStorageService;
import bisq.core.user.Preferences;
import bisq.common.config.Config;
import bisq.common.util.GcUtil;
@ -49,13 +51,14 @@ import javax.annotation.Nullable;
* SNAPSHOT_GRID old not less than 2 times the SNAPSHOT_GRID old.
*/
@Slf4j
public class DaoStateSnapshotService {
public class DaoStateSnapshotService implements DaoSetupService, DaoStateListener {
private static final int SNAPSHOT_GRID = 20;
private final DaoStateService daoStateService;
private final GenesisTxInfo genesisTxInfo;
private final DaoStateStorageService daoStateStorageService;
private final DaoStateMonitoringService daoStateMonitoringService;
private final Preferences preferences;
private final File storageDir;
private DaoState daoStateSnapshotCandidate;
@ -64,7 +67,8 @@ public class DaoStateSnapshotService {
@Setter
@Nullable
private Runnable daoRequiresRestartHandler;
private boolean requestPersistenceCalled;
private boolean readyForPersisting = true;
private boolean isParseBlockChainComplete;
///////////////////////////////////////////////////////////////////////////////////////////
@ -76,21 +80,86 @@ public class DaoStateSnapshotService {
GenesisTxInfo genesisTxInfo,
DaoStateStorageService daoStateStorageService,
DaoStateMonitoringService daoStateMonitoringService,
Preferences preferences,
@Named(Config.STORAGE_DIR) File storageDir) {
this.daoStateService = daoStateService;
this.genesisTxInfo = genesisTxInfo;
this.daoStateStorageService = daoStateStorageService;
this.daoStateMonitoringService = daoStateMonitoringService;
this.preferences = preferences;
this.storageDir = storageDir;
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoSetupService
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void addListeners() {
daoStateService.addDaoStateListener(this);
}
@Override
public void start() {
}
///////////////////////////////////////////////////////////////////////////////////////////
// DaoStateListener
///////////////////////////////////////////////////////////////////////////////////////////
// We listen onDaoStateChanged to ensure the dao state has been processed from listener clients after parsing.
// We need to listen during batch processing as well to write snapshots during that process.
@Override
public void onDaoStateChanged(Block block) {
// If we have isUseDaoMonitor activated we apply the hash and snapshots at each new block during initial parsing.
// Otherwise we do it only after the initial blockchain parsing is completed to not delay the parsing.
// In that case we get the missing hashes from the seed nodes. At any new block we do the hash calculation
// ourself and therefore get back confidence that our DAO state is in sync with the network.
if (preferences.isUseFullModeDaoMonitor() || isParseBlockChainComplete) {
// We need to execute first the daoStateMonitoringService.createHashFromBlock to get the hash created
daoStateMonitoringService.createHashFromBlock(block);
maybeCreateSnapshot(block);
}
}
@Override
public void onParseBlockChainComplete() {
isParseBlockChainComplete = true;
// In case we have dao monitoring deactivated we create the snapshot after we are completed with parsing
// and we got called back from daoStateMonitoringService once the hashes are created from peers data.
if (!preferences.isUseFullModeDaoMonitor()) {
// We register a callback handler once the daoStateMonitoringService has received the missing hashes from
// the seed node and applied the latest hash. After that we are ready to make a snapshot and persist it.
daoStateMonitoringService.setCreateSnapshotHandler(() -> {
// As we did not have created any snapshots during initial parsing we create it now. We cannot use the past
// snapshot height as we have not cloned a candidate (that would cause quite some delay during parsing).
// The next snapshots will be created again according to the snapshot height grid (each 20 blocks).
// This also comes with the improvement that the user does not need to load the past blocks back to the last
// snapshot height. Though it comes also with the small risk that in case of re-orgs the user need to do
// a resync in case the dao state would have been affected by that reorg.
long ts = System.currentTimeMillis();
// We do not keep a copy of the clone as we use it immediately for persistence.
GcUtil.maybeReleaseMemory();
log.info("Create snapshot at height {}", daoStateService.getChainHeight());
daoStateStorageService.requestPersistence(daoStateService.getClone(),
new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain()),
() -> {
log.info("Persisted daoState after parsing completed at height {}. Took {} ms",
daoStateService.getChainHeight(), System.currentTimeMillis() - ts);
});
GcUtil.maybeReleaseMemory();
});
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
// We do not use DaoStateListener.onDaoStateChanged but let the DaoEventCoordinator call maybeCreateSnapshot to ensure the
// correct order of execution.
// We need to process during batch processing as well to write snapshots during that process.
public void maybeCreateSnapshot(Block block) {
int chainHeight = block.getHeight();
@ -107,42 +176,68 @@ public class DaoStateSnapshotService {
// We protect to get called while we are not completed with persisting the daoState. This can take about
// 20 seconds and it is not expected that we get triggered another snapshot event in that period, but this
// check guards that we would skip such calls..
if (requestPersistenceCalled) {
log.warn("We try to persist a daoState but the previous call has not completed yet. " +
"We ignore that call and skip that snapshot. " +
"Snapshot will be created at next snapshot height again. This is not to be expected with live " +
"blockchain data.");
if (!readyForPersisting) {
if (preferences.isUseFullModeDaoMonitor()) {
// In case we dont use isUseFullModeDaoMonitor we might called here too often as the parsing is much
// faster than the persistence and we likely create only 1 snapshot during initial parsing, so
// we log only if isUseFullModeDaoMonitor is true as then parsing is likely slower and we would
// expect that we do a snapshot at each trigger block.
log.info("We try to persist a daoState but the previous call has not completed yet. " +
"We ignore that call and skip that snapshot. " +
"Snapshot will be created at next snapshot height again. This is not to be expected with live " +
"blockchain data.");
}
return;
}
GcUtil.maybeReleaseMemory();
// At trigger event we store the latest snapshotCandidate to disc
long ts = System.currentTimeMillis();
requestPersistenceCalled = true;
daoStateStorageService.requestPersistence(daoStateSnapshotCandidate,
daoStateHashChainSnapshotCandidate,
() -> {
log.info("Serializing snapshotCandidate for writing to Disc with height {} at height {} took {} ms",
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
chainHeight,
System.currentTimeMillis() - ts);
long ts2 = System.currentTimeMillis();
GcUtil.maybeReleaseMemory();
// Now we clone and keep it in memory for the next trigger event
daoStateSnapshotCandidate = daoStateService.getClone();
daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain());
log.info("Cloned new snapshotCandidate at height {} took {} ms", chainHeight, System.currentTimeMillis() - ts2);
requestPersistenceCalled = false;
GcUtil.maybeReleaseMemory();
});
if (daoStateSnapshotCandidate != null) {
persist();
} else {
createClones();
}
}
}
private void persist() {
long ts = System.currentTimeMillis();
readyForPersisting = false;
daoStateStorageService.requestPersistence(daoStateSnapshotCandidate,
daoStateHashChainSnapshotCandidate,
() -> {
log.info("Serializing snapshotCandidate for writing to Disc at chainHeight {} took {} ms.\n" +
"daoStateSnapshotCandidate.height={};\n" +
"daoStateHashChainSnapshotCandidate.height={}",
daoStateService.getChainHeight(),
System.currentTimeMillis() - ts,
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
daoStateHashChainSnapshotCandidate != null && !daoStateHashChainSnapshotCandidate.isEmpty() ?
daoStateHashChainSnapshotCandidate.getLast().getHeight() : "N/A");
createClones();
readyForPersisting = true;
});
}
private void createClones() {
long ts = System.currentTimeMillis();
// Now we clone and keep it in memory for the next trigger event
// We do not fit into the target grid of 20 blocks as we get called here once persistence is
// done from the write thread (mapped back to user thread).
// As we want to prevent to maintain 2 clones we prefer that strategy. If we would do the clone
// after the persist call we would keep an additional copy in memory.
daoStateSnapshotCandidate = daoStateService.getClone();
daoStateHashChainSnapshotCandidate = new LinkedList<>(daoStateMonitoringService.getDaoStateHashChain());
GcUtil.maybeReleaseMemory();
log.info("Cloned new snapshotCandidate at chainHeight {} took {} ms.\n" +
"daoStateSnapshotCandidate.height={};\n" +
"daoStateHashChainSnapshotCandidate.height={}",
daoStateService.getChainHeight(), System.currentTimeMillis() - ts,
daoStateSnapshotCandidate != null ? daoStateSnapshotCandidate.getChainHeight() : "N/A",
daoStateHashChainSnapshotCandidate != null && !daoStateHashChainSnapshotCandidate.isEmpty() ?
daoStateHashChainSnapshotCandidate.getLast().getHeight() : "N/A");
}
public void applySnapshot(boolean fromReorg) {
DaoState persistedBsqState = daoStateStorageService.getPersistedBsqState();
LinkedList<DaoStateHash> persistedDaoStateHashChain = daoStateStorageService.getPersistedDaoStateHashChain();
@ -156,6 +251,7 @@ public class DaoStateSnapshotService {
chainHeightOfLastApplySnapshot = chainHeightOfPersisted;
daoStateService.applySnapshot(persistedBsqState);
daoStateMonitoringService.applySnapshot(persistedDaoStateHashChain);
daoStateStorageService.pruneStore();
} else {
// The reorg might have been caused by the previous parsing which might contains a range of
// blocks.

View file

@ -27,6 +27,7 @@ import bisq.network.p2p.storage.persistence.StoreService;
import bisq.common.config.Config;
import bisq.common.file.FileUtil;
import bisq.common.persistence.PersistenceManager;
import bisq.common.util.GcUtil;
import javax.inject.Inject;
import javax.inject.Named;
@ -92,15 +93,20 @@ public class DaoStateStorageService extends StoreService<DaoStateStore> {
new Thread(() -> {
Thread.currentThread().setName("Serialize and write DaoState");
persistenceManager.persistNow(() -> {
// After we have written to disk we remove the the daoState in the store to avoid that it stays in
// After we have written to disk we remove the daoState in the store to avoid that it stays in
// memory there until the next persist call.
store.setDaoState(null);
pruneStore();
completeHandler.run();
});
}).start();
}
public void pruneStore() {
store.setDaoState(null);
store.setDaoStateHashChain(null);
GcUtil.maybeReleaseMemory();
}
public DaoState getPersistedBsqState() {
return store.getDaoState();
}

View file

@ -82,7 +82,6 @@ public class TradeStatisticsManager {
this.storageDir = storageDir;
this.dumpStatistics = dumpStatistics;
appendOnlyDataStoreService.addService(tradeStatistics3StorageService);
}

View file

@ -138,7 +138,7 @@ class XmrTxProofRequestsPerTrade implements AssetTxProofRequestsPerTrade {
// We set serviceAddresses at request time. If user changes AutoConfirmSettings after request has started
// it will have no impact on serviceAddresses and numRequiredSuccessResults.
// Thought numRequiredConfirmations can be changed during request process and will be read from
// Though numRequiredConfirmations can be changed during request process and will be read from
// autoConfirmSettings at result parsing.
List<String> serviceAddresses = autoConfirmSettings.getServiceAddresses();
numRequiredSuccessResults = serviceAddresses.size();

View file

@ -800,6 +800,11 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
requestPersistence();
}
public void setUseFullModeDaoMonitor(boolean value) {
prefPayload.setUseFullModeDaoMonitor(value);
requestPersistence();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@ -1115,5 +1120,7 @@ public final class Preferences implements PersistedDataHost, BridgeAddressProvid
void setDenyApiTaker(boolean value);
void setNotifyOnPreRelease(boolean value);
void setUseFullModeDaoMonitor(boolean value);
}
}

View file

@ -134,6 +134,7 @@ public final class PreferencesPayload implements PersistableEnvelope {
private boolean showOffersMatchingMyAccounts;
private boolean denyApiTaker;
private boolean notifyOnPreRelease;
private boolean useFullModeDaoMonitor;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -201,7 +202,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
.setHideNonAccountPaymentMethods(hideNonAccountPaymentMethods)
.setShowOffersMatchingMyAccounts(showOffersMatchingMyAccounts)
.setDenyApiTaker(denyApiTaker)
.setNotifyOnPreRelease(notifyOnPreRelease);
.setNotifyOnPreRelease(notifyOnPreRelease)
.setUseFullModeDaoMonitor(useFullModeDaoMonitor);
Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory);
Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((protobuf.TradeCurrency) e.toProtoMessage()));
@ -299,7 +301,8 @@ public final class PreferencesPayload implements PersistableEnvelope {
proto.getHideNonAccountPaymentMethods(),
proto.getShowOffersMatchingMyAccounts(),
proto.getDenyApiTaker(),
proto.getNotifyOnPreRelease()
proto.getNotifyOnPreRelease(),
proto.getUseFullModeDaoMonitor()
);
}
}

View file

@ -336,6 +336,7 @@ market.trades.showVolumeInUSD=Show volume in USD
offerbook.createOffer=Create offer
offerbook.takeOffer=Take offer
offerbook.takeOffer.createAccount=Create account and take offer
offerbook.takeOfferToBuy=Take offer to buy {0}
offerbook.takeOfferToSell=Take offer to sell {0}
offerbook.trader=Trader
@ -388,6 +389,7 @@ offerbook.createOfferToBuy.withFiat=Create new offer to buy {0} with {1}
offerbook.createOfferToSell.forFiat=Create new offer to sell {0} for {1}
offerbook.createOfferToBuy.withCrypto=Create new offer to sell {0} (buy {1})
offerbook.createOfferToSell.forCrypto=Create new offer to buy {0} (sell {1})
offerbook.createOfferDisabled.tooltip=You can only create one offer at a time
offerbook.takeOfferButton.tooltip=Take offer for {0}
offerbook.yesCreateOffer=Yes, create offer
@ -402,6 +404,7 @@ offerbook.warning.noTradingAccountForCurrency.headline=No payment account for se
offerbook.warning.noTradingAccountForCurrency.msg=You don't have a payment account set up for the selected currency.\n\nWould you like to create an offer for another currency instead?
offerbook.warning.noMatchingAccount.headline=No matching payment account.
offerbook.warning.noMatchingAccount.msg=This offer uses a payment method you haven't set up yet. \n\nWould you like to set up a new payment account now?
offerbook.warning.noMatchingBsqAccount.msg=This offer uses BSQ as a payment method you haven't set up yet. \n\nWould you like to automatically create an account now?
offerbook.warning.counterpartyTradeRestrictions=This offer cannot be taken due to counterparty trade restrictions
@ -440,6 +443,13 @@ offerbook.info.buyAtFixedPrice=You will buy at this fixed price.
offerbook.info.sellAtFixedPrice=You will sell at this fixed price.
offerbook.info.noArbitrationInUserLanguage=In case of a dispute, please note that arbitration for this offer will be handled in {0}. Language is currently set to {1}.
offerbook.info.roundedFiatVolume=The amount was rounded to increase the privacy of your trade.
offerbook.info.accountCreated.headline=Congratulations
offerbook.info.accountCreated.message=You''ve just successfully created a BSQ payment account.\n\
Your account can be found under Account > Altcoins Accounts > {0} and your BSQ wallet under DAO > BSQ Wallet.\n\n
offerbook.info.accountCreated.tradeInstant=You've chosen to take a BSQ instant offer, so a BSQ instant payment account was created for you. \
Be aware that instant trades are meant to be completed within 1 hour, \
so you should be online and available for the next 1 hour.\n\n
offerbook.info.accountCreated.takeOffer=You can now proceed to take this offer after closing this popup.
offerbook.bsqSwap.createOffer=Create Bsq swap offer
@ -481,6 +491,11 @@ createOffer.triggerPrice.tooltip=As protection against drastic price movements y
createOffer.triggerPrice.invalid.tooLow=Value must be higher than {0}
createOffer.triggerPrice.invalid.tooHigh=Value must be lower than {0}
createOffer.buyBsq.popupMessage=Trading fees are paid to fund the Bisq DAO. Fees can be paid in BSQ or BTC.\n\n\
BSQ fees directly help fund Bisq's development, so Bisq encourages traders to use BSQ by offering a 50% discount on trading fees. \
This discount varies as the BSQ/BTC rate fluctuates. To maintain the 50% discount target, trading fees are updated every cycle as necessary.\n\n\
For more about fees, see [HYPERLINK:https://bisq.wiki/Trading_fees]. For more about trading BSQ, see [HYPERLINK:https://bisq.wiki/Trading_BSQ]. For more about the Bisq DAO, see [HYPERLINK:https://bisq.wiki/Introduction_to_the_DAO#The_Bisq_DAO].
# new entries
createOffer.placeOfferButton=Review: Place offer to {0} bitcoin
createOffer.createOfferFundWalletInfo.headline=Fund your offer
@ -860,6 +875,7 @@ portfolio.pending.step3_seller.xmrTxHash=Transaction ID
portfolio.pending.step3_seller.xmrTxKey=Transaction key
portfolio.pending.step3_seller.buyersAccount=Buyers account data
portfolio.pending.step3_seller.confirmReceipt=Confirm payment receipt
portfolio.pending.step3_seller.showBsqWallet=Show payment in BSQ wallet
portfolio.pending.step3_seller.buyerStartedPayment=The BTC buyer has started the {0} payment.\n{1}
portfolio.pending.step3_seller.buyerStartedPayment.altcoin=Check for blockchain confirmations at your altcoin wallet or block explorer and confirm the payment when you have sufficient blockchain confirmations.
portfolio.pending.step3_seller.buyerStartedPayment.fiat=Check at your trading account (e.g. bank account) and confirm when you have received the payment.
@ -1335,6 +1351,18 @@ setting.preferences.dao.resyncFromGenesis.resync=Resync from genesis and shutdow
setting.preferences.dao.isDaoFullNode=Run Bisq as DAO full node
setting.preferences.dao.activated=DAO activated
setting.preferences.dao.activated.popup=The change will be applied after a restart
setting.preferences.dao.fullModeDaoMonitor=Full-mode DAO state monitoring
setting.preferences.dao.fullModeDaoMonitor.popup=If full-mode DAO state monitoring is activated the DAO state \
hashes are created during parsing the BSQ blocks. This comes with considerable performance costs at the initial DAO sync.\n\n\
For users who are regularily using Bisq this should not be an issue as there are not many blocks pending for parsing, though for \
users who only use Bisq casually creating the DAO state hashes for 100s or 1000s of blocks degrades heavily the user experience.\n\n\
In case full-mode is deactivated (default) the missing DAO state hashes are requested from network nodes and \
the DAO state hash based on the most recent block will be created by the user. As all hashes are connected by \
reference to the previous hash a correct hash at the chain tip means that all past hashes are correct as well. The \
main functionality of the DAO state monitoring - to detect if the local DAO state is out of sync with the rest of the network - \
is therefore still fulfilled.
setting.preferences.dao.rpcUser=RPC username
setting.preferences.dao.rpcPw=RPC password
setting.preferences.dao.blockNotifyPort=Block notify port
@ -2483,6 +2511,9 @@ dao.monitor.proposals=Proposals state
dao.monitor.blindVotes=Blind votes state
dao.monitor.table.peers=Peers
dao.monitor.table.hashCreator=Hash creator
dao.monitor.table.hashCreator.self=Self
dao.monitor.table.hashCreator.peer=Peer
dao.monitor.table.conflicts=Conflicts
dao.monitor.state=Status
dao.monitor.requestAlHashes=Request all hashes
@ -2516,6 +2547,8 @@ dao.monitor.isInConflictWithSeedNode=Your local data is not in consensus with at
Please resync the DAO state.
dao.monitor.isInConflictWithNonSeedNode=One of your peers is not in consensus with the network but your node \
is in sync with the seed nodes.
dao.monitor.isDaoStateBlockChainNotConnecting=Your DAO state chain is not connecting with the new data. \
Please resync the DAO state.
dao.monitor.daoStateInSync=Your local node is in consensus with the network
dao.monitor.blindVote.headline=Blind votes state

View file

@ -38,6 +38,7 @@ public class DaoStateSnapshotServiceTest {
mock(GenesisTxInfo.class),
mock(DaoStateStorageService.class),
mock(DaoStateMonitoringService.class),
null,
null);
}

View file

@ -175,6 +175,14 @@
-fx-padding: 0 10 0 10;
}
.tiny-button,
.action-button.tiny-button {
-fx-font-size: 0.769em;
-fx-pref-height: 20;
-fx-padding: 3 8 3 8;
-fx-border-radius: 5;
}
.text-button {
-fx-background-color: transparent;
-fx-underline: true;

View file

@ -41,11 +41,15 @@ public class HyperlinkWithIcon extends Hyperlink {
this(text, AwesomeIcon.INFO_SIGN);
}
public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) {
public HyperlinkWithIcon(String text, String fontSize) {
this(text, AwesomeIcon.INFO_SIGN, fontSize);
}
public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon, String fontSize) {
super(text);
Label icon = new Label();
AwesomeDude.setIcon(icon, awesomeIcon);
AwesomeDude.setIcon(icon, awesomeIcon, fontSize);
icon.setMinWidth(20);
icon.setOpacity(0.7);
icon.getStyleClass().addAll("hyperlink", "no-underline");
@ -55,6 +59,10 @@ public class HyperlinkWithIcon extends Hyperlink {
setIcon(icon);
}
public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) {
this(text, awesomeIcon, "1.231em");
}
public HyperlinkWithIcon(String text, GlyphIcons icon) {
this(text, icon, null);
}

View file

@ -34,8 +34,6 @@ import bisq.core.util.validation.InputValidator;
import bisq.common.util.Tuple2;
import org.apache.commons.lang3.StringUtils;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
@ -62,8 +60,13 @@ public class AdvancedCashForm extends PaymentMethodForm {
return gridRow;
}
public AdvancedCashForm(PaymentAccount paymentAccount, AccountAgeWitnessService accountAgeWitnessService, AdvancedCashValidator advancedCashValidator,
InputValidator inputValidator, GridPane gridPane, int gridRow, CoinFormatter formatter) {
public AdvancedCashForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
AdvancedCashValidator advancedCashValidator,
InputValidator inputValidator,
GridPane gridPane,
int gridRow,
CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.advancedCashAccount = (AdvancedCashAccount) paymentAccount;
this.advancedCashValidator = advancedCashValidator;
@ -101,12 +104,7 @@ public class AdvancedCashForm extends PaymentMethodForm {
@Override
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
String accountNr = accountNrInputTextField.getText();
accountNr = StringUtils.abbreviate(accountNr, 9);
String method = Res.get(paymentAccount.getPaymentMethod().getId());
accountNameTextField.setText(method.concat(": ").concat(accountNr));
}
setAccountNameWithString(accountNrInputTextField.getText());
}
@Override

View file

@ -41,8 +41,6 @@ import bisq.core.util.validation.InputValidator;
import bisq.common.UserThread;
import bisq.common.util.Tuple3;
import org.apache.commons.lang3.StringUtils;
import javafx.scene.control.CheckBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
@ -53,6 +51,7 @@ import javafx.geometry.Insets;
import javafx.util.StringConverter;
import static bisq.desktop.util.DisplayUtils.createAssetsAccountName;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextField;
import static bisq.desktop.util.FormBuilder.addCompactTopLabelTextFieldWithCopyIcon;
import static bisq.desktop.util.FormBuilder.addLabelCheckBox;
@ -167,12 +166,7 @@ public class AssetsForm extends PaymentMethodForm {
@Override
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : "";
if (currency != null) {
String address = addressInputTextField.getText();
address = StringUtils.abbreviate(address, 9);
accountNameTextField.setText(currency.concat(": ").concat(address));
}
accountNameTextField.setText(createAssetsAccountName(paymentAccount, addressInputTextField.getText()));
}
}

View file

@ -34,8 +34,6 @@ import bisq.core.util.validation.InputValidator;
import bisq.common.util.Tuple2;
import org.apache.commons.lang3.StringUtils;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
@ -100,12 +98,7 @@ public class CapitualForm extends PaymentMethodForm {
@Override
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
String accountNr = accountNrInputTextField.getText();
accountNr = StringUtils.abbreviate(accountNr, 9);
String method = Res.get(paymentAccount.getPaymentMethod().getId());
accountNameTextField.setText(method.concat(": ").concat(accountNr));
}
setAccountNameWithString(accountNrInputTextField.getText());
}
@Override

View file

@ -25,7 +25,6 @@ import bisq.desktop.util.validation.JapanBankAccountNameValidator;
import bisq.desktop.util.validation.JapanBankAccountNumberValidator;
import bisq.desktop.util.validation.JapanBankBranchCodeValidator;
import bisq.desktop.util.validation.JapanBankBranchNameValidator;
import bisq.desktop.util.validation.JapanBankTransferValidator;
import bisq.desktop.util.validation.LengthValidator;
import bisq.core.account.witness.AccountAgeWitnessService;
@ -57,24 +56,17 @@ import javafx.util.StringConverter;
import static bisq.desktop.util.FormBuilder.*;
import static bisq.desktop.util.GUIUtil.getComboBoxButtonCell;
public class JapanBankTransferForm extends PaymentMethodForm
{
public class JapanBankTransferForm extends PaymentMethodForm {
private final JapanBankAccount japanBankAccount;
protected ComboBox<String> bankComboBox, bankAccountTypeComboBox;
private InputTextField bankAccountNumberInputTextField;
protected ComboBox<String> bankComboBox;
private JapanBankTransferValidator japanBankTransferValidator;
private JapanBankBranchNameValidator japanBankBranchNameValidator;
private JapanBankBranchCodeValidator japanBankBranchCodeValidator;
private JapanBankAccountNameValidator japanBankAccountNameValidator;
private JapanBankAccountNumberValidator japanBankAccountNumberValidator;
private final JapanBankBranchNameValidator japanBankBranchNameValidator;
private final JapanBankBranchCodeValidator japanBankBranchCodeValidator;
private final JapanBankAccountNameValidator japanBankAccountNameValidator;
private final JapanBankAccountNumberValidator japanBankAccountNumberValidator;
private LengthValidator lengthValidator;
private RegexValidator regexValidator;
public static int addFormForBuyer(GridPane gridPane, int gridRow, // {{{
PaymentAccountPayload paymentAccountPayload)
{
public static int addFormForBuyer(GridPane gridPane, int gridRow,
PaymentAccountPayload paymentAccountPayload) {
JapanBankAccountPayload japanBankAccount = ((JapanBankAccountPayload) paymentAccountPayload);
String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName();
@ -90,30 +82,26 @@ public class JapanBankTransferForm extends PaymentMethodForm
addCompactTopLabelTextFieldWithCopyIcon(gridPane, gridRow, 1, Res.get("payment.japan.recipient"), accountNameText);
return gridRow;
} // }}}
}
public JapanBankTransferForm(PaymentAccount paymentAccount,
AccountAgeWitnessService accountAgeWitnessService,
JapanBankTransferValidator japanBankTransferValidator,
InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter)
{
AccountAgeWitnessService accountAgeWitnessService,
InputValidator inputValidator, GridPane gridPane,
int gridRow, CoinFormatter formatter) {
super(paymentAccount, accountAgeWitnessService, inputValidator, gridPane, gridRow, formatter);
this.japanBankAccount = (JapanBankAccount) paymentAccount;
this.japanBankTransferValidator = japanBankTransferValidator;
this.japanBankBranchCodeValidator = new JapanBankBranchCodeValidator();
this.japanBankAccountNumberValidator = new JapanBankAccountNumberValidator();
this.lengthValidator = new LengthValidator();
this.regexValidator = new RegexValidator();
LengthValidator lengthValidator = new LengthValidator();
RegexValidator regexValidator = new RegexValidator();
this.japanBankBranchNameValidator = new JapanBankBranchNameValidator(lengthValidator, regexValidator);
this.japanBankAccountNameValidator = new JapanBankAccountNameValidator(lengthValidator, regexValidator);
}
@Override
public void addFormForDisplayAccount() // {{{
{
public void addFormForDisplayAccount() {
gridRowFrom = gridRow;
addTopLabelTextField(gridPane, ++gridRow, Res.get("payment.account.name"),
@ -128,37 +116,36 @@ public class JapanBankTransferForm extends PaymentMethodForm
addBankAccountTypeDisplay();
addLimitations(true);
} // }}}
private void addBankDisplay() // {{{
{
}
private void addBankDisplay() {
String bankText = japanBankAccount.getBankCode() + " " + japanBankAccount.getBankName();
TextField bankTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("bank"), bankText).second;
bankTextField.setEditable(false);
} // }}}
private void addBankBranchDisplay() // {{{
{
}
private void addBankBranchDisplay() {
String branchText = japanBankAccount.getBankBranchCode() + " " + japanBankAccount.getBankBranchName();
TextField branchTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("branch"), branchText).second;
branchTextField.setEditable(false);
} // }}}
private void addBankAccountDisplay() // {{{
{
}
private void addBankAccountDisplay() {
String accountText = japanBankAccount.getBankAccountNumber() + " " + japanBankAccount.getBankAccountName();
TextField accountTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account"), accountText).second;
accountTextField.setEditable(false);
} // }}}
private void addBankAccountTypeDisplay() // {{{
{
}
private void addBankAccountTypeDisplay() {
TradeCurrency singleTradeCurrency = japanBankAccount.getSingleTradeCurrency();
String currency = singleTradeCurrency != null ? singleTradeCurrency.getNameAndCode() : "null";
String accountTypeText = currency + " " + japanBankAccount.getBankAccountType();
TextField accountTypeTextField = addCompactTopLabelTextField(gridPane, ++gridRow, JapanBankData.getString("account.type"), accountTypeText).second;
accountTypeTextField.setEditable(false);
} // }}}
}
@Override
public void addFormForAddAccount() // {{{
{
public void addFormForAddAccount() {
gridRowFrom = gridRow;
addBankInput();
@ -168,9 +155,9 @@ public class JapanBankTransferForm extends PaymentMethodForm
addLimitations(false);
addAccountNameTextFieldWithAutoFillToggleButton();
} // }}}
private void addBankInput() // {{{
{
}
private void addBankInput() {
gridRow++;
Tuple4<Label, TextField, Label, ComboBox<String>> tuple4 = addTopLabelTextFieldAutocompleteComboBox(gridPane, gridRow, JapanBankData.getString("bank.code"), JapanBankData.getString("bank.name"), 10);
@ -185,14 +172,13 @@ public class JapanBankTransferForm extends PaymentMethodForm
bankComboBox = tuple4.fourth;
bankComboBox.setPromptText(JapanBankData.getString("bank.select"));
bankComboBox.setButtonCell(getComboBoxButtonCell(JapanBankData.getString("bank.name"), bankComboBox));
bankComboBox.getEditor().focusedProperty().addListener(observable -> {
bankComboBox.setPromptText("");
});
bankComboBox.setConverter(new StringConverter<String>() {
bankComboBox.getEditor().focusedProperty().addListener(observable -> bankComboBox.setPromptText(""));
bankComboBox.setConverter(new StringConverter<>() {
@Override
public String toString(String bank) {
return bank != null ? bank : "";
}
public String fromString(String s) {
return s != null ? s : "";
}
@ -208,8 +194,7 @@ public class JapanBankTransferForm extends PaymentMethodForm
// parse first 4 characters as bank code
String bankCode = StringUtils.substring(bank, 0, 4);
if (bankCode != null)
{
if (bankCode != null) {
// set bank code field to this value
bankCodeField.setText(bankCode);
// save to payload
@ -217,26 +202,20 @@ public class JapanBankTransferForm extends PaymentMethodForm
// parse remainder as bank name
String bankNameFull = StringUtils.substringAfter(bank, JapanBankData.SPACE);
if (bankNameFull != null)
{
// parse beginning as Japanese bank name
String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE);
if (bankNameJa != null)
{
// set bank name field to this value
bankComboBox.getEditor().setText(bankNameJa);
// save to payload
japanBankAccount.setBankName(bankNameJa);
}
}
// parse beginning as Japanese bank name
String bankNameJa = StringUtils.substringBefore(bankNameFull, JapanBankData.SPACE);
// set bank name field to this value
bankComboBox.getEditor().setText(bankNameJa);
// save to payload
japanBankAccount.setBankName(bankNameJa);
}
updateFromInputs();
});
} // }}}
private void addBankBranchInput() // {{{
{
}
private void addBankBranchInput() {
gridRow++;
Tuple2<InputTextField, InputTextField> tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("branch.code"), JapanBankData.getString("branch.name"));
@ -259,14 +238,14 @@ public class JapanBankTransferForm extends PaymentMethodForm
japanBankAccount.setBankBranchName(newValue);
updateFromInputs();
});
} // }}}
private void addBankAccountInput() // {{{
{
}
private void addBankAccountInput() {
gridRow++;
Tuple2<InputTextField, InputTextField> tuple2 = addInputTextFieldInputTextField(gridPane, gridRow, JapanBankData.getString("account.number"), JapanBankData.getString("account.name"));
// account number
bankAccountNumberInputTextField = tuple2.first;
InputTextField bankAccountNumberInputTextField = tuple2.first;
bankAccountNumberInputTextField.setValidator(japanBankAccountNumberValidator);
bankAccountNumberInputTextField.setPrefWidth(200);
bankAccountNumberInputTextField.setMaxWidth(200);
@ -284,9 +263,9 @@ public class JapanBankTransferForm extends PaymentMethodForm
japanBankAccount.setBankAccountName(newValue);
updateFromInputs();
});
} // }}}
private void addBankAccountTypeInput() // {{{
{
}
private void addBankAccountTypeInput() {
// account currency
gridRow++;
@ -299,13 +278,13 @@ public class JapanBankTransferForm extends PaymentMethodForm
ToggleGroup toggleGroup = new ToggleGroup();
Tuple3<Label, RadioButton, RadioButton> tuple3 =
addTopLabelRadioButtonRadioButton(
gridPane, gridRow, toggleGroup,
JapanBankData.getString("account.type.select"),
JapanBankData.getString("account.type.futsu"),
JapanBankData.getString("account.type.touza"),
0
);
addTopLabelRadioButtonRadioButton(
gridPane, gridRow, toggleGroup,
JapanBankData.getString("account.type.select"),
JapanBankData.getString("account.type.futsu"),
JapanBankData.getString("account.type.touza"),
0
);
toggleGroup.getToggles().get(0).setSelected(true);
japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja"));
@ -314,66 +293,60 @@ public class JapanBankTransferForm extends PaymentMethodForm
RadioButton touza = tuple3.third;
toggleGroup.selectedToggleProperty().addListener
(
(ov, oldValue, newValue) ->
{
if (futsu.isSelected())
japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja"));
if (touza.isSelected())
japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja"));
}
);
} // }}}
(
(ov, oldValue, newValue) ->
{
if (futsu.isSelected())
japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.futsu.ja"));
if (touza.isSelected())
japanBankAccount.setBankAccountType(JapanBankData.getString("account.type.touza.ja"));
}
);
}
@Override
public void updateFromInputs() // {{{
{
public void updateFromInputs() {
System.out.println("JapanBankTransferForm: updateFromInputs()");
System.out.println("bankName: "+japanBankAccount.getBankName());
System.out.println("bankCode: "+japanBankAccount.getBankCode());
System.out.println("bankBranchName: "+japanBankAccount.getBankBranchName());
System.out.println("bankBranchCode: "+japanBankAccount.getBankBranchCode());
System.out.println("bankAccountType: "+japanBankAccount.getBankAccountType());
System.out.println("bankAccountName: "+japanBankAccount.getBankAccountName());
System.out.println("bankAccountNumber: "+japanBankAccount.getBankAccountNumber());
System.out.println("bankName: " + japanBankAccount.getBankName());
System.out.println("bankCode: " + japanBankAccount.getBankCode());
System.out.println("bankBranchName: " + japanBankAccount.getBankBranchName());
System.out.println("bankBranchCode: " + japanBankAccount.getBankBranchCode());
System.out.println("bankAccountType: " + japanBankAccount.getBankAccountType());
System.out.println("bankAccountName: " + japanBankAccount.getBankAccountName());
System.out.println("bankAccountNumber: " + japanBankAccount.getBankAccountNumber());
super.updateFromInputs();
} // }}}
}
@Override
protected void autoFillNameTextField() // {{{
{
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected())
{
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
accountNameTextField.setText(
Res.get(paymentAccount.getPaymentMethod().getId())
.concat(": ")
.concat(japanBankAccount.getBankName())
.concat(" ")
.concat(japanBankAccount.getBankBranchName())
.concat(" ")
.concat(japanBankAccount.getBankAccountNumber())
.concat(" ")
.concat(japanBankAccount.getBankAccountName())
.concat(": ")
.concat(japanBankAccount.getBankName())
.concat(" ")
.concat(japanBankAccount.getBankBranchName())
.concat(" ")
.concat(japanBankAccount.getBankAccountNumber())
.concat(" ")
.concat(japanBankAccount.getBankAccountName())
);
}
} // }}}
}
@Override
public void updateAllInputsValid() // {{{
{
public void updateAllInputsValid() {
boolean result =
(
isAccountNameValid() &&
inputValidator.validate(japanBankAccount.getBankCode()).isValid &&
inputValidator.validate(japanBankAccount.getBankName()).isValid &&
japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid &&
japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid &&
japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid &&
japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid &&
inputValidator.validate(japanBankAccount.getBankAccountType()).isValid
);
(
isAccountNameValid() &&
inputValidator.validate(japanBankAccount.getBankCode()).isValid &&
inputValidator.validate(japanBankAccount.getBankName()).isValid &&
japanBankBranchCodeValidator.validate(japanBankAccount.getBankBranchCode()).isValid &&
japanBankBranchNameValidator.validate(japanBankAccount.getBankBranchName()).isValid &&
japanBankAccountNumberValidator.validate(japanBankAccount.getBankAccountNumber()).isValid &&
japanBankAccountNameValidator.validate(japanBankAccount.getBankAccountName()).isValid &&
inputValidator.validate(japanBankAccount.getBankAccountType()).isValid
);
allInputsValid.set(result);
} // }}}
}
}
// vim:ts=4:sw=4:expandtab:foldmethod=marker:nowrap:

View file

@ -37,8 +37,6 @@ import bisq.core.util.validation.InputValidator;
import bisq.common.util.Tuple2;
import org.apache.commons.lang3.StringUtils;
import javafx.scene.control.Label;
import javafx.scene.layout.FlowPane;
import javafx.scene.layout.GridPane;
@ -172,11 +170,7 @@ public class MoneyGramForm extends PaymentMethodForm {
@Override
protected void autoFillNameTextField() {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
accountNameTextField.setText(Res.get(paymentAccount.getPaymentMethod().getId())
.concat(": ")
.concat(StringUtils.abbreviate(holderNameInputTextField.getText(), 9)));
}
setAccountNameWithString(holderNameInputTextField.getText());
}
@Override

View file

@ -73,6 +73,7 @@ import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import static bisq.desktop.util.DisplayUtils.createAccountName;
import static bisq.desktop.util.FormBuilder.*;
@Slf4j
@ -287,10 +288,8 @@ public abstract class PaymentMethodForm {
void setAccountNameWithString(String name) {
if (useCustomAccountNameToggleButton != null && !useCustomAccountNameToggleButton.isSelected()) {
name = name.trim();
name = StringUtils.abbreviate(name, 9);
String method = Res.get(paymentAccount.getPaymentMethod().getId());
accountNameTextField.setText(method.concat(": ").concat(name));
String accountName = createAccountName(paymentAccount.getPaymentMethod().getId(), name);
accountNameTextField.setText(accountName);
}
}

View file

@ -416,7 +416,7 @@ public class MainView extends InitializableView<StackPane, MainViewModel>
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onChangeAfterBatchProcessing() {
public void onDaoStateHashesChanged() {
}
@Override

View file

@ -20,9 +20,7 @@ package bisq.desktop.main.account.content.altcoinaccounts;
import bisq.desktop.common.model.ActivatableDataModel;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.TradeCurrency;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.AssetAccount;
@ -44,7 +42,6 @@ import javafx.collections.SetChangeListener;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
class AltCoinAccountsDataModel extends ActivatableDataModel {
@ -53,7 +50,6 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
private final Preferences preferences;
private final OpenOfferManager openOfferManager;
private final TradeManager tradeManager;
private final AccountAgeWitnessService accountAgeWitnessService;
final ObservableList<PaymentAccount> paymentAccounts = FXCollections.observableArrayList();
private final SetChangeListener<PaymentAccount> setChangeListener;
private final String accountsFileName = "AltcoinPaymentAccounts";
@ -65,14 +61,12 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
Preferences preferences,
OpenOfferManager openOfferManager,
TradeManager tradeManager,
AccountAgeWitnessService accountAgeWitnessService,
PersistenceProtoResolver persistenceProtoResolver,
CorruptedStorageFileHandler corruptedStorageFileHandler) {
this.user = user;
this.preferences = preferences;
this.openOfferManager = openOfferManager;
this.tradeManager = tradeManager;
this.accountAgeWitnessService = accountAgeWitnessService;
this.persistenceProtoResolver = persistenceProtoResolver;
this.corruptedStorageFileHandler = corruptedStorageFileHandler;
setChangeListener = change -> fillAndSortPaymentAccounts();
@ -105,25 +99,8 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
public void onSaveNewAccount(PaymentAccount paymentAccount) {
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
List<TradeCurrency> tradeCurrencies = paymentAccount.getTradeCurrencies();
if (singleTradeCurrency != null) {
if (singleTradeCurrency instanceof FiatCurrency)
preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency);
else
preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency);
} else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) {
tradeCurrencies.forEach(tradeCurrency -> {
if (tradeCurrency instanceof FiatCurrency)
preferences.addFiatCurrency((FiatCurrency) tradeCurrency);
else
preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency);
});
}
preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency);
user.addPaymentAccount(paymentAccount);
if (!(paymentAccount instanceof AssetAccount))
accountAgeWitnessService.publishMyAccountAgeWitness(paymentAccount.getPaymentAccountPayload());
}
public boolean onDeleteAccount(PaymentAccount paymentAccount) {
@ -147,9 +124,9 @@ class AltCoinAccountsDataModel extends ActivatableDataModel {
public void exportAccounts(Stage stage) {
if (user.getPaymentAccounts() != null) {
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
ArrayList<PaymentAccount> accounts = user.getPaymentAccounts().stream()
.filter(paymentAccount -> paymentAccount instanceof AssetAccount)
.collect(Collectors.toList()));
.collect(Collectors.toCollection(ArrayList::new));
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
}
}

View file

@ -21,7 +21,6 @@ import bisq.desktop.common.model.ActivatableDataModel;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.locale.CryptoCurrency;
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.FiatCurrency;
import bisq.core.locale.TradeCurrency;
@ -109,22 +108,14 @@ class FiatAccountsDataModel extends ActivatableDataModel {
TradeCurrency singleTradeCurrency = paymentAccount.getSingleTradeCurrency();
List<TradeCurrency> tradeCurrencies = paymentAccount.getTradeCurrencies();
if (singleTradeCurrency != null) {
if (singleTradeCurrency instanceof FiatCurrency)
preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency);
else
preferences.addCryptoCurrency((CryptoCurrency) singleTradeCurrency);
preferences.addFiatCurrency((FiatCurrency) singleTradeCurrency);
} else if (tradeCurrencies != null && !tradeCurrencies.isEmpty()) {
if (tradeCurrencies.contains(CurrencyUtil.getDefaultTradeCurrency()))
paymentAccount.setSelectedTradeCurrency(CurrencyUtil.getDefaultTradeCurrency());
else
paymentAccount.setSelectedTradeCurrency(tradeCurrencies.get(0));
tradeCurrencies.forEach(tradeCurrency -> {
if (tradeCurrency instanceof FiatCurrency)
preferences.addFiatCurrency((FiatCurrency) tradeCurrency);
else
preferences.addCryptoCurrency((CryptoCurrency) tradeCurrency);
});
tradeCurrencies.forEach(tradeCurrency -> preferences.addFiatCurrency((FiatCurrency) tradeCurrency));
}
user.addPaymentAccount(paymentAccount);
@ -154,9 +145,9 @@ class FiatAccountsDataModel extends ActivatableDataModel {
public void exportAccounts(Stage stage) {
if (user.getPaymentAccounts() != null) {
ArrayList<PaymentAccount> accounts = new ArrayList<>(user.getPaymentAccounts().stream()
ArrayList<PaymentAccount> accounts = user.getPaymentAccounts().stream()
.filter(paymentAccount -> !(paymentAccount instanceof AssetAccount))
.collect(Collectors.toList()));
.collect(Collectors.toCollection(ArrayList::new));
GUIUtil.exportAccounts(accounts, accountsFileName, preferences, stage, persistenceProtoResolver, corruptedStorageFileHandler);
}
}

View file

@ -520,7 +520,7 @@ public class FiatAccountsView extends PaymentAccountsView<GridPane, FiatAccounts
case PaymentMethod.SPECIFIC_BANKS_ID:
return new SpecificBankForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter);
case PaymentMethod.JAPAN_BANK_ID:
return new JapanBankTransferForm(paymentAccount, accountAgeWitnessService, japanBankTransferValidator, inputValidator, root, gridRow, formatter);
return new JapanBankTransferForm(paymentAccount, accountAgeWitnessService, inputValidator, root, gridRow, formatter);
case PaymentMethod.AUSTRALIA_PAYID_ID:
return new AustraliaPayidForm(paymentAccount, accountAgeWitnessService, australiapayidValidator, inputValidator, root, gridRow, formatter);
case PaymentMethod.ALI_PAY_ID:

View file

@ -36,10 +36,9 @@ import lombok.extern.slf4j.Slf4j;
@Getter
@EqualsAndHashCode
public abstract class StateBlockListItem<StH extends StateHash, StB extends StateBlock<StH>> {
private final StateBlock<StH> stateBlock;
protected final StateBlock<StH> stateBlock;
private final Supplier<String> height;
private final String hash;
private final String prevHash;
private final String numNetworkMessages;
private final String numMisMatches;
private final boolean isInSync;
@ -58,7 +57,6 @@ public abstract class StateBlockListItem<StH extends StateHash, StB extends Stat
Res.get("dao.monitor.table.cycleBlockHeight", cycleIndexSupplier.getAsInt() + 1,
String.valueOf(stateBlock.getHeight())))::get;
hash = Utilities.bytesAsHexString(stateBlock.getHash());
prevHash = stateBlock.getPrevHash().length > 0 ? Utilities.bytesAsHexString(stateBlock.getPrevHash()) : "-";
numNetworkMessages = String.valueOf(stateBlock.getPeersMap().size());
int size = stateBlock.getInConflictMap().size();
numMisMatches = String.valueOf(size);

View file

@ -38,7 +38,6 @@ public abstract class StateInConflictListItem<T extends StateHash> {
private final String peerAddressString;
private final String height;
private final String hash;
private final String prevHash;
private final T stateHash;
protected StateInConflictListItem(String peerAddress, T stateHash, int cycleIndex,
@ -52,7 +51,5 @@ public abstract class StateInConflictListItem<T extends StateHash> {
cycleIndex + 1,
String.valueOf(stateHash.getHeight()));
hash = Utilities.bytesAsHexString(stateHash.getHash());
prevHash = stateHash.getPrevHash().length > 0 ?
Utilities.bytesAsHexString(stateHash.getPrevHash()) : "-";
}
}

View file

@ -99,6 +99,7 @@ public abstract class StateMonitorView<StH extends StateHash,
private Subscription selectedItemSubscription;
protected final BooleanProperty isInConflictWithNonSeedNode = new SimpleBooleanProperty();
protected final BooleanProperty isInConflictWithSeedNode = new SimpleBooleanProperty();
protected final BooleanProperty isDaoStateBlockChainNotConnecting = new SimpleBooleanProperty();
///////////////////////////////////////////////////////////////////////////////////////////
@ -134,8 +135,10 @@ public abstract class StateMonitorView<StH extends StateHash,
daoStateService.addDaoStateListener(this);
resyncButton.visibleProperty().bind(isInConflictWithSeedNode);
resyncButton.managedProperty().bind(isInConflictWithSeedNode);
resyncButton.visibleProperty().bind(isInConflictWithSeedNode
.or(isDaoStateBlockChainNotConnecting));
resyncButton.managedProperty().bind(isInConflictWithSeedNode
.or(isDaoStateBlockChainNotConnecting));
resyncButton.setOnAction(ev -> resyncDaoState());
@ -177,8 +180,6 @@ public abstract class StateMonitorView<StH extends StateHash,
protected abstract String getPeersTableHeader();
protected abstract String getPrevHashTableHeader();
protected abstract String getHashTableHeader();
protected abstract String getBlockHeightTableHeader();
@ -272,6 +273,9 @@ public abstract class StateMonitorView<StH extends StateHash,
} else if (isInConflictWithNonSeedNode.get()) {
statusTextField.setText(Res.get("dao.monitor.isInConflictWithNonSeedNode"));
statusTextField.getStyleClass().remove("dao-inConflict");
} else if (isDaoStateBlockChainNotConnecting.get()) {
statusTextField.setText(Res.get("dao.monitor.isDaoStateBlockChainNotConnecting"));
statusTextField.getStyleClass().add("dao-inConflict");
} else {
statusTextField.setText(Res.get("dao.monitor.daoStateInSync"));
statusTextField.getStyleClass().remove("dao-inConflict");
@ -328,7 +332,6 @@ public abstract class StateMonitorView<StH extends StateHash,
tableView.getSortOrder().add(column);
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
@ -353,31 +356,6 @@ public abstract class StateMonitorView<StH extends StateHash,
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPrevHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<BLI, BLI> call(TableColumn<BLI,
BLI> column) {
return new TableCell<>() {
@Override
public void updateItem(final BLI item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getPrevHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(BLI::getPrevHash));
tableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPeersTableHeader());
column.setMinWidth(80);
column.setMaxWidth(column.getMinWidth());
@ -543,30 +521,6 @@ public abstract class StateMonitorView<StH extends StateHash,
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>(getPrevHashTableHeader());
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<CLI, CLI> call(
TableColumn<CLI, CLI> column) {
return new TableCell<>() {
@Override
public void updateItem(final CLI item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.getPrevHash());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(CLI::getPrevHash));
conflictTableView.getColumns().add(column);
column = new AutoTooltipTableColumn<>("");
column.setMinWidth(120);
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));

View file

@ -149,11 +149,6 @@ public class BlindVoteStateMonitorView extends StateMonitorView<BlindVoteStateHa
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.blindVote.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.blindVote.table.hash");
@ -221,7 +216,6 @@ public class BlindVoteStateMonitorView extends StateMonitorView<BlindVoteStateHa
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();

View file

@ -21,6 +21,7 @@ import bisq.desktop.main.dao.monitor.StateBlockListItem;
import bisq.core.dao.monitoring.model.DaoStateBlock;
import bisq.core.dao.monitoring.model.DaoStateHash;
import bisq.core.locale.Res;
import java.util.function.IntSupplier;
@ -35,4 +36,10 @@ class DaoStateBlockListItem extends StateBlockListItem<DaoStateHash, DaoStateBlo
DaoStateBlockListItem(DaoStateBlock stateBlock, IntSupplier cycleIndexSupplier) {
super(stateBlock, cycleIndexSupplier);
}
String hashCreator() {
return ((DaoStateBlock) stateBlock).isSelfCreated() ?
Res.get("dao.monitor.table.hashCreator.self") :
Res.get("dao.monitor.table.hashCreator.peer");
}
}

View file

@ -18,6 +18,7 @@
package bisq.desktop.main.dao.monitor.daostate;
import bisq.desktop.common.view.FxmlView;
import bisq.desktop.components.AutoTooltipTableColumn;
import bisq.desktop.main.dao.monitor.StateMonitorView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.util.FormBuilder;
@ -40,10 +41,18 @@ import bisq.common.util.Utilities;
import javax.inject.Inject;
import javax.inject.Named;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.ListChangeListener;
import javafx.util.Callback;
import java.io.File;
import java.util.Comparator;
import java.util.Map;
import java.util.function.IntSupplier;
import java.util.stream.Collectors;
@ -60,7 +69,6 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private DaoStateMonitorView(DaoStateService daoStateService,
DaoFacade daoFacade,
@ -111,7 +119,7 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public void onChangeAfterBatchProcessing() {
public void onDaoStateHashesChanged() {
if (daoStateService.isParseBlockChainComplete()) {
onDataUpdate();
}
@ -160,11 +168,6 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.daoState.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.daoState.table.hash");
@ -189,6 +192,7 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
protected void onDataUpdate() {
isInConflictWithSeedNode.set(daoStateMonitoringService.isInConflictWithSeedNode());
isInConflictWithNonSeedNode.set(daoStateMonitoringService.isInConflictWithNonSeedNode());
isDaoStateBlockChainNotConnecting.set(daoStateMonitoringService.isDaoStateBlockChainNotConnecting());
listItems.setAll(daoStateMonitoringService.getDaoStateBlockChain().stream()
.map(this::getStateBlockListItem)
@ -202,6 +206,35 @@ public class DaoStateMonitorView extends StateMonitorView<DaoStateHash, DaoState
daoStateMonitoringService.requestHashesFromGenesisBlockHeight(peerAddress);
}
@Override
protected void createColumns() {
super.createColumns();
TableColumn<DaoStateBlockListItem, DaoStateBlockListItem> column = new AutoTooltipTableColumn<>(Res.get("dao.monitor.table.hashCreator"));
column.setMinWidth(90);
column.setMaxWidth(column.getMinWidth());
column.setCellValueFactory((item) -> new ReadOnlyObjectWrapper<>(item.getValue()));
column.setCellFactory(
new Callback<>() {
@Override
public TableCell<DaoStateBlockListItem, DaoStateBlockListItem> call(
TableColumn<DaoStateBlockListItem, DaoStateBlockListItem> column) {
return new TableCell<>() {
@Override
public void updateItem(final DaoStateBlockListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null)
setText(item.hashCreator());
else
setText("");
}
};
}
});
column.setComparator(Comparator.comparing(e -> e.getStateBlock().getPeersMap().size()));
tableView.getColumns().add(2, column);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private

View file

@ -147,11 +147,6 @@ public class ProposalStateMonitorView extends StateMonitorView<ProposalStateHash
return Res.get("dao.monitor.table.peers");
}
@Override
protected String getPrevHashTableHeader() {
return Res.get("dao.monitor.proposal.table.prev");
}
@Override
protected String getHashTableHeader() {
return Res.get("dao.monitor.proposal.table.hash");
@ -219,7 +214,6 @@ public class ProposalStateMonitorView extends StateMonitorView<ProposalStateHash
tableView.getColumns().add(1, column);
}
protected void createConflictColumns() {
super.createConflictColumns();

View file

@ -176,7 +176,9 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener);
root.getTabs().addListener(tabListChangeListener);
navigation.addListener(navigationListener);
navigation.navigateTo(MainView.class, this.getClass(), OfferBookView.class);
if (offerBookView == null) {
navigation.navigateTo(MainView.class, this.getClass(), OfferBookView.class);
}
}
@Override
@ -202,17 +204,21 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
View view;
boolean isBuy = direction == OfferDirection.BUY;
if (viewClass == OfferBookView.class && offerBookView == null) {
view = viewLoader.load(viewClass);
// Offerbook must not be cached by ViewLoader as we use 2 instances for sell and buy screens.
offerBookTab = new Tab(isBuy ? Res.get("shared.buyBitcoin").toUpperCase() : Res.get("shared.sellBitcoin").toUpperCase());
offerBookTab.setClosable(false);
offerBookTab.setContent(view.getRoot());
tabPane.getTabs().add(offerBookTab);
offerBookView = (OfferBookView) view;
offerBookView.onTabSelected(true);
offerBookView.setOfferActionHandler(offerActionHandler);
offerBookView.setDirection(direction);
if (viewClass == OfferBookView.class) {
if (offerBookTab != null && offerBookView != null) {
tabPane.getSelectionModel().select(offerBookTab);
} else {
view = viewLoader.load(viewClass);
// Offerbook must not be cached by ViewLoader as we use 2 instances for sell and buy screens.
offerBookTab = new Tab(isBuy ? Res.get("shared.buyBitcoin").toUpperCase() : Res.get("shared.sellBitcoin").toUpperCase());
offerBookTab.setClosable(false);
offerBookTab.setContent(view.getRoot());
tabPane.getTabs().add(offerBookTab);
offerBookView = (OfferBookView) view;
offerBookView.onTabSelected(true);
offerBookView.setOfferActionHandler(offerActionHandler);
offerBookView.setDirection(direction);
}
} else if (viewClass == CreateOfferView.class && createOfferView == null) {
view = viewLoader.load(viewClass);
// CreateOffer and TakeOffer must not be cached by ViewLoader as we cannot use a view multiple times
@ -247,7 +253,7 @@ public abstract class OfferView extends ActivatableView<TabPane, Void> {
// in different graphs
takeOfferView = (TakeOfferView) view;
takeOfferView.initWithData(offer);
takeOfferPane = ((TakeOfferView) view).getRoot();
takeOfferPane = takeOfferView.getRoot();
takeOfferTab = new Tab(getTakeOfferTabName());
takeOfferTab.setClosable(true);
// close handler from close on take offer action

View file

@ -781,6 +781,12 @@ public abstract class MutableOfferDataModel extends OfferDataModel implements Bs
return offerUtil.isBsqForMakerFeeAvailable(amount.get());
}
boolean isAttemptToBuyBsq() {
// When you buy an asset you actually sell BTC.
// This is why an offer to buy BSQ is actually an offer to sell BTC for BSQ.
return !isBuyOffer() && getTradeCurrency().getCode().equals("BSQ");
}
boolean canPlaceOffer() {
return GUIUtil.isBootstrappedOrShowPopup(p2PService) &&
GUIUtil.canCreateOrTakeOfferOrShowPopup(user, navigation, tradeCurrency);

View file

@ -83,7 +83,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@ -122,6 +121,7 @@ import lombok.Setter;
import org.jetbrains.annotations.NotNull;
import static bisq.core.payment.payload.PaymentMethod.HAL_CASH_ID;
import static bisq.desktop.main.offer.bisq_v1.OfferViewUtil.addPayInfoEntry;
import static bisq.desktop.util.FormBuilder.*;
import static javafx.beans.binding.Bindings.createStringBinding;
@ -156,7 +156,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private VBox currencySelection, fixedPriceBox, percentagePriceBox, currencyTextFieldBox, triggerPriceVBox;
private HBox fundingHBox, firstRowHBox, secondRowHBox, placeOfferBox, amountValueCurrencyBox,
priceAsPercentageValueCurrencyBox, volumeValueCurrencyBox, priceValueCurrencyBox,
minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox;
minAmountValueCurrencyBox, advancedOptionsBox, triggerPriceHBox, buyBsqBox;
private Subscription isWaitingForFundsSubscription, balanceSubscription;
private ChangeListener<Boolean> amountFocusedListener, minAmountFocusedListener, volumeFocusedListener,
@ -270,6 +270,13 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
tradeFeeInBtcToggle.setManaged(false);
tradeFeeInBsqToggle.setVisible(false);
tradeFeeInBsqToggle.setManaged(false);
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
}
if (!model.isShowBuyBsqHint()) {
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
}
Label popOverLabel = OfferViewUtil.createPopOverLabel(Res.get("createOffer.triggerPrice.tooltip"));
@ -336,7 +343,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
showInsufficientBsqFundsForBtcFeePaymentPopup();
}
// called form parent as the view does not get notified when the tab is closed
// called from parent as the view does not get notified when the tab is closed
public void onClose() {
// we use model.placeOfferCompleted to not react on close which was triggered by a successful placeOffer
if (model.getDataModel().getBalance().get().isPositive() && !model.placeOfferCompleted.get()) {
@ -372,6 +379,16 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
}
private void showInsufficientBsqFundsForBtcFeePaymentPopup() {
String message = getMissingBsqForFeePaymentMessage();
if (message != null)
new Popup().warning(message)
.actionButtonTextWithGoTo("navigation.dao.wallet.receive")
.onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, BsqReceiveView.class))
.show();
}
private String getMissingBsqForFeePaymentMessage() {
Coin makerFee = model.getDataModel().getMakerFee(false);
String message = null;
if (makerFee != null) {
@ -381,11 +398,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
} else if (model.getDataModel().getUsableBsqBalance().isZero())
message = Res.get("popup.warning.noBsqFundsForBtcFeePayment");
if (message != null)
new Popup().warning(message)
.actionButtonTextWithGoTo("navigation.dao.wallet.receive")
.onAction(() -> navigation.navigateTo(MainView.class, DaoView.class, BsqWalletView.class, BsqReceiveView.class))
.show();
return message;
}
private void onShowPayFundsScreen() {
@ -400,6 +413,8 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
tradeFeeInBtcToggle.setMouseTransparent(true);
tradeFeeInBsqToggle.setMouseTransparent(true);
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
setDepositTitledGroupBg.setVisible(false);
setDepositTitledGroupBg.setManaged(false);
@ -514,17 +529,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
private void maybeShowAccountWarning(PaymentAccount paymentAccount, boolean isBuyer) {
String msgKey = paymentAccount.getPreTradeMessage(isBuyer);
if (msgKey == null || paymentAccountWarningDisplayed.getOrDefault(msgKey, false)) {
return;
}
paymentAccountWarningDisplayed.put(msgKey, true);
UserThread.runAfter(() -> {
new Popup().information(Res.get(msgKey))
.width(900)
.closeButtonText(Res.get("shared.iConfirm"))
.dontShowAgainId(msgKey)
.show();
}, 500, TimeUnit.MILLISECONDS);
OfferViewUtil.showPaymentAccountWarning(msgKey, paymentAccountWarningDisplayed);
}
protected void onPaymentAccountsComboBoxSelected() {
@ -894,6 +899,10 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcToggle.setVisible(newValue);
tradeFeeInBsqToggle.setVisible(newValue);
if (model.isShowBuyBsqHint()) {
buyBsqBox.setVisible(newValue);
buyBsqBox.setManaged(newValue);
}
}
};
@ -1007,15 +1016,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
///////////////////////////////////////////////////////////////////////////////////////////
private void addScrollPane() {
scrollPane = new ScrollPane();
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
AnchorPane.setLeftAnchor(scrollPane, 0d);
AnchorPane.setTopAnchor(scrollPane, 0d);
AnchorPane.setRightAnchor(scrollPane, 0d);
AnchorPane.setBottomAnchor(scrollPane, 0d);
scrollPane = GUIUtil.createScrollPane();
root.getChildren().add(scrollPane);
}
@ -1025,13 +1026,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
gridPane.setPadding(new Insets(30, 25, -1, 25));
gridPane.setHgap(5);
gridPane.setVgap(5);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.NEVER);
columnConstraints1.setMinWidth(200);
ColumnConstraints columnConstraints2 = new ColumnConstraints();
columnConstraints2.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane);
scrollPane.setContent(gridPane);
}
@ -1105,12 +1100,21 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
advancedOptionsBox.setSpacing(40);
GridPane.setRowIndex(advancedOptionsBox, gridRow);
GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING);
GridPane.setColumnIndex(advancedOptionsBox, 0);
GridPane.setHalignment(advancedOptionsBox, HPos.LEFT);
GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
gridPane.getChildren().add(advancedOptionsBox);
advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), getTradeFeeFieldsBox());
Tuple2<AutoTooltipButton, HBox> buyBsqButtonBox = OfferViewUtil.createBuyBsqButtonBox(
navigation, preferences);
buyBsqBox = buyBsqButtonBox.second;
buyBsqBox.setManaged(false);
buyBsqBox.setVisible(false);
VBox tradeFeeFieldsBox = getTradeFeeFieldsBox();
tradeFeeFieldsBox.setMinWidth(240);
advancedOptionsBox.getChildren().addAll(getBuyerSecurityDepositBox(), tradeFeeFieldsBox, buyBsqBox);
Tuple2<Button, Button> tuple = add2ButtonsAfterGroup(gridPane, ++gridRow,
Res.get("shared.nextStep"), Res.get("shared.cancel"));
@ -1152,15 +1156,7 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
boolean isPreferredFeeCurrencyBtc = model.getDataModel().isPreferredFeeCurrencyBtc();
boolean isBsqForFeeAvailable = model.getDataModel().isBsqForFeeAvailable();
if (!isPreferredFeeCurrencyBtc && !isBsqForFeeAvailable) {
Coin makerFee = model.getDataModel().getMakerFee(false);
String missingBsq = null;
if (makerFee != null) {
missingBsq = Res.get("popup.warning.insufficientBsqFundsForBtcFeePayment",
bsqFormatter.formatCoinWithCode(makerFee.subtract(model.getDataModel().getUsableBsqBalance())));
} else if (model.getDataModel().getUsableBsqBalance().isZero()) {
missingBsq = Res.get("popup.warning.noBsqFundsForBtcFeePayment");
}
String missingBsq = getMissingBsqForFeePaymentMessage();
if (missingBsq != null) {
new Popup().warning(missingBsq)
@ -1542,8 +1538,9 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
infoGridPane.setPadding(new Insets(10, 10, 10, 10));
int i = 0;
if (model.isSellOffer())
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.tradeAmount.get());
if (model.isSellOffer()) {
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.tradeAmount"), model.getTradeAmount());
}
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo());
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("createOffer.fundsBox.offerFee"), model.getTradeFee());
@ -1553,19 +1550,8 @@ public abstract class MutableOfferView<M extends MutableOfferViewModel<?>> exten
separator.getStyleClass().add("offer-separator");
GridPane.setConstraints(separator, 1, i++);
infoGridPane.getChildren().add(separator);
addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"), model.getTotalToPayInfo());
addPayInfoEntry(infoGridPane, i, Res.getWithCol("shared.total"),
model.getTotalToPayInfo());
return infoGridPane;
}
private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new AutoTooltipLabel(labelText);
TextField textField = new TextField(value);
textField.setMinWidth(500);
textField.setEditable(false);
textField.setFocusTraversable(false);
textField.setId("payment-info");
GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER);
GridPane.setConstraints(textField, 1, row);
infoGridPane.getChildren().addAll(label, textField);
}
}

View file

@ -678,11 +678,10 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
updateSpinnerInfo();
}
boolean fundFromSavingsWallet() {
void fundFromSavingsWallet() {
dataModel.fundFromSavingsWallet();
if (dataModel.getIsBtcWalletFunded().get()) {
updateButtonDisableState();
return true;
} else {
new Popup().warning(Res.get("shared.notEnoughFunds",
btcFormatter.formatCoinWithCode(dataModel.totalToPayAsCoinProperty().get()),
@ -690,7 +689,6 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
.actionButtonTextWithGoTo("navigation.funds.depositFunds")
.onAction(() -> navigation.navigateTo(MainView.class, FundsView.class, DepositView.class))
.show();
return false;
}
}
@ -1355,4 +1353,8 @@ public abstract class MutableOfferViewModel<M extends MutableOfferDataModel> ext
}
}
}
public boolean isShowBuyBsqHint() {
return !dataModel.isBsqForFeeAvailable() && !dataModel.isAttemptToBuyBsq();
}
}

View file

@ -17,12 +17,37 @@
package bisq.desktop.main.offer.bisq_v1;
import javafx.scene.control.Label;
import bisq.desktop.Navigation;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.AutoTooltipLabel;
import bisq.desktop.components.HyperlinkWithIcon;
import bisq.desktop.main.MainView;
import bisq.desktop.main.offer.SellOfferView;
import bisq.desktop.main.offer.offerbook.OfferBookView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.core.locale.Res;
import bisq.core.user.Preferences;
import bisq.common.UserThread;
import bisq.common.util.Tuple2;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.geometry.HPos;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.geometry.VPos;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
// Shared utils for Views
public class OfferViewUtil {
public static Label createPopOverLabel(String text) {
final Label label = new Label(text);
label.setPrefWidth(300);
@ -31,4 +56,64 @@ public class OfferViewUtil {
label.setPadding(new Insets(10));
return label;
}
public static void showPaymentAccountWarning(String msgKey,
HashMap<String, Boolean> paymentAccountWarningDisplayed) {
if (msgKey == null || paymentAccountWarningDisplayed.getOrDefault(msgKey, false)) {
return;
}
paymentAccountWarningDisplayed.put(msgKey, true);
UserThread.runAfter(() -> {
new Popup().information(Res.get(msgKey))
.width(900)
.closeButtonText(Res.get("shared.iConfirm"))
.dontShowAgainId(msgKey)
.show();
}, 500, TimeUnit.MILLISECONDS);
}
public static void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new AutoTooltipLabel(labelText);
TextField textField = new TextField(value);
textField.setMinWidth(500);
textField.setEditable(false);
textField.setFocusTraversable(false);
textField.setId("payment-info");
GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER);
GridPane.setConstraints(textField, 1, row);
infoGridPane.getChildren().addAll(label, textField);
}
public static Tuple2<AutoTooltipButton, HBox> createBuyBsqButtonBox(Navigation navigation,
Preferences preferences) {
String buyBsqText = Res.get("shared.buyCurrency", "BSQ");
var buyBsqButton = new AutoTooltipButton(buyBsqText);
buyBsqButton.getStyleClass().add("action-button");
buyBsqButton.getStyleClass().add("tiny-button");
buyBsqButton.setMinWidth(60);
buyBsqButton.setOnAction(e -> openBuyBsqOfferBook(navigation, preferences)
);
var info = new AutoTooltipLabel("BSQ is colored BTC that helps fund Bisq developers.");
var learnMore = new HyperlinkWithIcon("Learn More");
learnMore.setOnAction(e -> new Popup().headLine(buyBsqText)
.information(Res.get("createOffer.buyBsq.popupMessage"))
.actionButtonText(buyBsqText)
.buttonAlignment(HPos.CENTER)
.onAction(() -> openBuyBsqOfferBook(navigation, preferences)).show());
learnMore.setMinWidth(100);
HBox buyBsqBox = new HBox(buyBsqButton, info, learnMore);
buyBsqBox.setAlignment(Pos.BOTTOM_LEFT);
buyBsqBox.setSpacing(10);
buyBsqBox.setPadding(new Insets(0, 0, 4, -20));
return new Tuple2<>(buyBsqButton, buyBsqBox);
}
private static void openBuyBsqOfferBook(Navigation navigation, Preferences preferences) {
preferences.setSellScreenCurrencyCode("BSQ");
navigation.navigateTo(
MainView.class, SellOfferView.class, OfferBookView.class);
}
}

View file

@ -712,4 +712,10 @@ class TakeOfferDataModel extends OfferDataModel {
public boolean isBsqForFeeAvailable() {
return offerUtil.isBsqForTakerFeeAvailable(amount.get());
}
public boolean isAttemptToBuyBsq() {
// When you buy an asset you actually sell BTC.
// This is why an offer to buy BSQ is actually an offer to sell BTC for BSQ.
return !isBuyOffer() && getOffer().getCurrencyCode().equals("BSQ");
}
}

View file

@ -90,7 +90,6 @@ import javafx.scene.control.Tooltip;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
@ -118,6 +117,7 @@ import java.util.concurrent.TimeUnit;
import org.jetbrains.annotations.NotNull;
import static bisq.desktop.main.offer.bisq_v1.OfferViewUtil.addPayInfoEntry;
import static bisq.desktop.util.FormBuilder.*;
import static javafx.beans.binding.Bindings.createStringBinding;
@ -135,7 +135,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private VBox priceAsPercentageInputBox, amountRangeBox;
private HBox fundingHBox, amountValueCurrencyBox, priceValueCurrencyBox, volumeValueCurrencyBox,
priceAsPercentageValueCurrencyBox, minAmountValueCurrencyBox, advancedOptionsBox,
takeOfferBox, buttonBox, firstRowHBox;
takeOfferBox, buttonBox, firstRowHBox, buyBsqBox;
private ComboBox<PaymentAccount> paymentAccountsComboBox;
private Label amountDescriptionLabel,
paymentMethodLabel,
@ -247,6 +247,10 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
if (DevEnv.isDaoActivated()) {
tradeFeeInBtcToggle.setVisible(newValue);
tradeFeeInBsqToggle.setVisible(newValue);
if (model.isShowBuyBsqHint()) {
buyBsqBox.setVisible(newValue);
buyBsqBox.setManaged(newValue);
}
}
};
@ -330,6 +334,13 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
tradeFeeInBtcToggle.setManaged(false);
tradeFeeInBsqToggle.setVisible(false);
tradeFeeInBsqToggle.setManaged(false);
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
}
if (!model.isShowBuyBsqHint()) {
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
}
}
@ -470,6 +481,8 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
tradeFeeInBtcToggle.setMouseTransparent(true);
tradeFeeInBsqToggle.setMouseTransparent(true);
buyBsqBox.setVisible(false);
buyBsqBox.setManaged(false);
int delay = 500;
int diff = 100;
@ -672,7 +685,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
offerDetailsWindow.hide();
UserThread.runAfter(() -> new Popup().warning(newValue + "\n\n" +
Res.get("takeOffer.alreadyPaidInFunds"))
Res.get("takeOffer.alreadyPaidInFunds"))
.actionButtonTextWithGoTo("navigation.funds.availableForWithdrawal")
.onAction(() -> {
errorPopupDisplayed.set(true);
@ -787,15 +800,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
///////////////////////////////////////////////////////////////////////////////////////////
private void addScrollPane() {
scrollPane = new ScrollPane();
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
AnchorPane.setLeftAnchor(scrollPane, 0d);
AnchorPane.setTopAnchor(scrollPane, 0d);
AnchorPane.setRightAnchor(scrollPane, 0d);
AnchorPane.setBottomAnchor(scrollPane, 0d);
scrollPane = GUIUtil.createScrollPane();
root.getChildren().add(scrollPane);
}
@ -805,13 +810,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
gridPane.setPadding(new Insets(15, 15, -1, 15));
gridPane.setHgap(5);
gridPane.setVgap(5);
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.NEVER);
columnConstraints1.setMinWidth(200);
ColumnConstraints columnConstraints2 = new ColumnConstraints();
columnConstraints2.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
GUIUtil.setDefaultTwoColumnConstraintsForGridPane(gridPane);
scrollPane.setContent(gridPane);
}
@ -879,12 +878,21 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
advancedOptionsBox.setSpacing(40);
GridPane.setRowIndex(advancedOptionsBox, gridRow);
GridPane.setColumnSpan(advancedOptionsBox, GridPane.REMAINING);
GridPane.setColumnIndex(advancedOptionsBox, 0);
GridPane.setHalignment(advancedOptionsBox, HPos.LEFT);
GridPane.setMargin(advancedOptionsBox, new Insets(Layout.COMPACT_FIRST_ROW_AND_GROUP_DISTANCE, 0, 0, 0));
gridPane.getChildren().add(advancedOptionsBox);
advancedOptionsBox.getChildren().addAll(getTradeFeeFieldsBox());
Tuple2<AutoTooltipButton, HBox> buyBsqButtonBox = OfferViewUtil.createBuyBsqButtonBox(
navigation, model.dataModel.preferences);
buyBsqBox = buyBsqButtonBox.second;
buyBsqBox.setManaged(false);
buyBsqBox.setVisible(false);
VBox tradeFeeFieldsBox = getTradeFeeFieldsBox();
tradeFeeFieldsBox.setMinWidth(240);
advancedOptionsBox.getChildren().addAll(tradeFeeFieldsBox, buyBsqBox);
}
private void addButtons() {
@ -1279,17 +1287,7 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private void maybeShowAccountWarning(PaymentAccount paymentAccount, boolean isBuyer) {
String msgKey = paymentAccount.getPreTradeMessage(!isBuyer);
if (msgKey == null || paymentAccountWarningDisplayed.getOrDefault(msgKey, false)) {
return;
}
paymentAccountWarningDisplayed.put(msgKey, true);
UserThread.runAfter(() -> {
new Popup().information(Res.get(msgKey))
.width(900)
.closeButtonText(Res.get("shared.iConfirm"))
.dontShowAgainId(msgKey)
.show();
}, 500, TimeUnit.MILLISECONDS);
OfferViewUtil.showPaymentAccountWarning(msgKey, paymentAccountWarningDisplayed);
}
private void maybeShowCashByMailWarning(PaymentAccount paymentAccount, Offer offer) {
@ -1329,8 +1327,9 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
infoGridPane.setPadding(new Insets(10, 10, 10, 10));
int i = 0;
if (model.isSeller())
if (model.isSeller()) {
addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.tradeAmount"), model.getTradeAmount());
}
addPayInfoEntry(infoGridPane, i++, Res.getWithCol("shared.yourSecurityDeposit"), model.getSecurityDepositInfo());
addPayInfoEntry(infoGridPane, i++, Res.get("takeOffer.fundsBox.offerFee"), model.getTradeFee());
@ -1345,17 +1344,5 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
return infoGridPane;
}
private void addPayInfoEntry(GridPane infoGridPane, int row, String labelText, String value) {
Label label = new AutoTooltipLabel(labelText);
TextField textField = new TextField(value);
textField.setMinWidth(500);
textField.setEditable(false);
textField.setFocusTraversable(false);
textField.setId("payment-info");
GridPane.setConstraints(label, 0, row, 1, 1, HPos.RIGHT, VPos.CENTER);
GridPane.setConstraints(textField, 1, row);
infoGridPane.getChildren().addAll(label, textField);
}
}

View file

@ -800,4 +800,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
Res.get("shared.aboveInPercent");
}
}
public boolean isShowBuyBsqHint() {
return !dataModel.isBsqForFeeAvailable() && !dataModel.isAttemptToBuyBsq();
}
}

View file

@ -33,6 +33,7 @@ import bisq.desktop.components.PeerInfoIconTrading;
import bisq.desktop.components.TitledGroupBg;
import bisq.desktop.main.MainView;
import bisq.desktop.main.account.AccountView;
import bisq.desktop.main.account.content.altcoinaccounts.AltCoinAccountsView;
import bisq.desktop.main.account.content.fiataccounts.FiatAccountsView;
import bisq.desktop.main.funds.FundsView;
import bisq.desktop.main.funds.withdrawal.WithdrawalView;
@ -91,6 +92,7 @@ import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
@ -135,17 +137,22 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private AutocompleteComboBox<PaymentMethod> paymentMethodComboBox;
private AutoTooltipButton createOfferButton;
private AutoTooltipSlideToggleButton matchingOffersToggle;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn, volumeColumn, marketColumn,
priceColumn, paymentMethodColumn, depositColumn, signingStateColumn, avatarColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> amountColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> volumeColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> marketColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> priceColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> depositColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> signingStateColumn;
private AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> avatarColumn;
private TableView<OfferBookListItem> tableView;
private OfferView.OfferActionHandler offerActionHandler;
private int gridRow = 0;
private Label nrOfOffersLabel;
private ListChangeListener<OfferBookListItem> offerListListener;
private ChangeListener<Number> priceFeedUpdateCounterListener;
private Subscription currencySelectionSubscriber;
private static final int SHOW_ALL = 0;
private Label disabledCreateOfferButtonTooltip;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
@ -200,13 +207,23 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
matchingOffersToggle.setText(Res.get("offerbook.matchingOffers"));
HBox.setMargin(matchingOffersToggle, new Insets(7, 0, -9, -15));
createOfferButton = new AutoTooltipButton();
createOfferButton.setMinHeight(40);
createOfferButton.setGraphicTextGap(10);
disabledCreateOfferButtonTooltip = new Label("");
disabledCreateOfferButtonTooltip.setMinSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
disabledCreateOfferButtonTooltip.setMaxSize(Region.USE_PREF_SIZE, Region.USE_PREF_SIZE);
disabledCreateOfferButtonTooltip.prefWidthProperty().bind(createOfferButton.widthProperty());
disabledCreateOfferButtonTooltip.prefHeightProperty().bind(createOfferButton.heightProperty());
disabledCreateOfferButtonTooltip.setTooltip(new Tooltip(Res.get("offerbook.createOfferDisabled.tooltip")));
disabledCreateOfferButtonTooltip.setManaged(false);
disabledCreateOfferButtonTooltip.setVisible(false);
var createOfferButtonStack = new StackPane(createOfferButton, disabledCreateOfferButtonTooltip);
offerToolsBox.getChildren().addAll(currencyBoxTuple.first, paymentBoxTuple.first,
matchingOffersToggle, getSpacer(), createOfferButton);
matchingOffersToggle, getSpacer(), createOfferButtonStack);
GridPane.setHgrow(offerToolsBox, Priority.ALWAYS);
GridPane.setRowIndex(offerToolsBox, gridRow);
@ -231,7 +248,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
tableView.getColumns().add(amountColumn);
volumeColumn = getVolumeColumn();
tableView.getColumns().add(volumeColumn);
paymentMethodColumn = getPaymentMethodColumn();
AutoTooltipTableColumn<OfferBookListItem, OfferBookListItem> paymentMethodColumn = getPaymentMethodColumn();
tableView.getColumns().add(paymentMethodColumn);
depositColumn = getDepositColumn();
tableView.getColumns().add(depositColumn);
@ -538,6 +555,16 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
public void enableCreateOfferButton() {
createOfferButton.setDisable(false);
disabledCreateOfferButtonTooltip.setManaged(false);
disabledCreateOfferButtonTooltip.setVisible(false);
}
private void disableCreateOfferButton() {
createOfferButton.setDisable(true);
disabledCreateOfferButtonTooltip.setManaged(true);
disabledCreateOfferButtonTooltip.setVisible(true);
model.onCreateOffer();
}
public void setDirection(OfferDirection direction) {
@ -581,11 +608,16 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
}
public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) {
this.offerActionHandler = offerActionHandler;
model.setOfferActionHandler(offerActionHandler);
}
public void onTabSelected(boolean isSelected) {
model.onTabSelected(isSelected);
if (isSelected) {
updateCurrencyComboBoxFromModel();
root.requestFocus();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -594,16 +626,11 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private void onCreateOffer() {
if (model.canCreateOrTakeOffer()) {
PaymentMethod selectedPaymentMethod = model.selectedPaymentMethod;
TradeCurrency selectedTradeCurrency = model.getSelectedTradeCurrency();
if (!model.hasPaymentAccountForCurrency()) {
new Popup().headLine(Res.get("offerbook.warning.noTradingAccountForCurrency.headline"))
.instruction(Res.get("offerbook.warning.noTradingAccountForCurrency.msg"))
.actionButtonText(Res.get("offerbook.yesCreateOffer"))
.onAction(() -> {
createOfferButton.setDisable(true);
offerActionHandler.onCreateOffer(selectedTradeCurrency, selectedPaymentMethod);
})
.onAction(this::disableCreateOfferButton)
.secondaryActionButtonText(Res.get("offerbook.setupNewAccount"))
.onSecondaryAction(() -> {
navigation.setReturnPath(navigation.getCurrentPath());
@ -614,24 +641,18 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
return;
}
createOfferButton.setDisable(true);
offerActionHandler.onCreateOffer(selectedTradeCurrency, selectedPaymentMethod);
disableCreateOfferButton();
}
}
private void onShowInfo(Offer offer, OfferFilterService.Result result) {
switch (result) {
case VALID:
break;
case API_DISABLED:
DevEnv.logErrorAndThrowIfDevMode("We are in desktop and in the taker position " +
"viewing offers, so it cannot be that we got that result as we are not an API user.");
break;
case HAS_NO_PAYMENT_ACCOUNT_VALID_FOR_OFFER:
openPopupForMissingAccountSetup(Res.get("offerbook.warning.noMatchingAccount.headline"),
Res.get("offerbook.warning.noMatchingAccount.msg"),
FiatAccountsView.class,
"navigation.account");
openPopupForMissingAccountSetup(offer);
break;
case HAS_NOT_SAME_PROTOCOL_VERSION:
new Popup().warning(Res.get("offerbook.warning.wrongTradeProtocol")).show();
@ -675,6 +696,7 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
case HIDE_BSQ_SWAPS_DUE_DAO_DEACTIVATED:
new Popup().warning(Res.get("offerbook.warning.hideBsqSwapsDueDaoDeactivated")).show();
break;
case VALID:
default:
break;
}
@ -686,10 +708,10 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
offer.getPaymentMethod().getId().equals(PaymentMethod.CASH_DEPOSIT.getId())) {
new Popup().confirmation(Res.get("popup.info.cashDepositInfo", offer.getBankId()))
.actionButtonText(Res.get("popup.info.cashDepositInfo.confirm"))
.onAction(() -> offerActionHandler.onTakeOffer(offer))
.onAction(() -> model.onTakeOffer(offer))
.show();
} else {
offerActionHandler.onTakeOffer(offer);
model.onTakeOffer(offer);
}
}
}
@ -728,14 +750,37 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
});
}
private void openPopupForMissingAccountSetup(String headLine, String message, Class target, String targetAsString) {
new Popup().headLine(headLine)
.instruction(message)
.actionButtonTextWithGoTo(targetAsString)
.onAction(() -> {
navigation.setReturnPath(navigation.getCurrentPath());
navigation.navigateTo(MainView.class, AccountView.class, target);
}).show();
private void openPopupForMissingAccountSetup(Offer offer) {
String headline = Res.get("offerbook.warning.noMatchingAccount.headline");
if (offer.getCurrencyCode().equals("BSQ")) {
new Popup().headLine(headline)
.instruction(Res.get("offerbook.warning.noMatchingBsqAccount.msg"))
.actionButtonText(Res.get("offerbook.takeOffer.createAccount"))
.onAction(() -> {
var bsqAccount = model.createBsqAccount(offer);
var message = Res.get("offerbook.info.accountCreated.message", bsqAccount.getAccountName());
if (model.isInstantPaymentMethod(offer)) {
message += Res.get("offerbook.info.accountCreated.tradeInstant");
}
message += Res.get("offerbook.info.accountCreated.takeOffer");
new Popup().headLine(Res.get("offerbook.info.accountCreated.headline"))
.information(message)
.onClose(() -> model.onTakeOffer(offer))
.show();
}).show();
} else {
var accountViewClass = CurrencyUtil.isFiatCurrency(offer.getCurrencyCode()) ? FiatAccountsView.class : AltCoinAccountsView.class;
new Popup().headLine(headline)
.instruction(Res.get("offerbook.warning.noMatchingAccount.msg"))
.actionButtonTextWithGoTo("navigation.account")
.onAction(() -> {
navigation.setReturnPath(navigation.getCurrentPath());
navigation.navigateTo(MainView.class, AccountView.class, accountViewClass);
}).show();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -1068,13 +1113,11 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
tableRow.setOnMousePressed(null);
} else {
button.setDefaultButton(false);
if (!myOffer) {
tableRow.setOnMousePressed(e -> {
// ugly hack to get the icon clickable when deactivated
if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas))
onShowInfo(offer, canTakeOfferResult);
});
}
tableRow.setOnMousePressed(e -> {
// ugly hack to get the icon clickable when deactivated
if (!(e.getTarget() instanceof ImageView || e.getTarget() instanceof Canvas))
onShowInfo(offer, canTakeOfferResult);
});
}
}

View file

@ -20,13 +20,16 @@ package bisq.desktop.main.offer.offerbook;
import bisq.desktop.Navigation;
import bisq.desktop.common.model.ActivatableViewModel;
import bisq.desktop.main.MainView;
import bisq.desktop.main.offer.OfferView;
import bisq.desktop.main.settings.SettingsView;
import bisq.desktop.main.settings.preferences.PreferencesView;
import bisq.desktop.util.DisplayUtils;
import bisq.desktop.util.GUIUtil;
import bisq.core.account.witness.AccountAgeWitnessService;
import bisq.core.api.CoreApi;
import bisq.core.btc.setup.WalletsSetup;
import bisq.core.btc.wallet.BsqWalletService;
import bisq.core.locale.BankUtil;
import bisq.core.locale.CountryUtil;
import bisq.core.locale.CryptoCurrency;
@ -111,6 +114,8 @@ class OfferBookViewModel extends ActivatableViewModel {
private final BsqFormatter bsqFormatter;
private final FilteredList<OfferBookListItem> filteredItems;
private final BsqWalletService bsqWalletService;
private final CoreApi coreApi;
private final SortedList<OfferBookListItem> sortedItems;
private final ListChangeListener<TradeCurrency> tradeCurrencyListChangeListener;
private final ListChangeListener<OfferBookListItem> filterItemsListener;
@ -121,6 +126,8 @@ class OfferBookViewModel extends ActivatableViewModel {
final StringProperty tradeCurrencyCode = new SimpleStringProperty();
private OfferView.OfferActionHandler offerActionHandler;
// If id is empty string we ignore filter (display all methods)
PaymentMethod selectedPaymentMethod = getShowAllEntryForPaymentMethod();
@ -154,7 +161,9 @@ class OfferBookViewModel extends ActivatableViewModel {
PriceUtil priceUtil,
OfferFilterService offerFilterService,
@Named(FormattingUtils.BTC_FORMATTER_KEY) CoinFormatter btcFormatter,
BsqFormatter bsqFormatter) {
BsqFormatter bsqFormatter,
BsqWalletService bsqWalletService,
CoreApi coreApi) {
super();
this.openOfferManager = openOfferManager;
@ -173,6 +182,8 @@ class OfferBookViewModel extends ActivatableViewModel {
this.bsqFormatter = bsqFormatter;
this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems());
this.bsqWalletService = bsqWalletService;
this.coreApi = coreApi;
this.sortedItems = new SortedList<>(filteredItems);
tradeCurrencyListChangeListener = c -> fillAllTradeCurrencies();
@ -207,7 +218,7 @@ class OfferBookViewModel extends ActivatableViewModel {
.filter(o -> o.getOffer().isUseMarketBasedPrice())
.max(Comparator.comparing(o -> new DecimalFormat("#0.00").format(o.getOffer().getMarketPriceMargin() * 100).length()));
highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer(), false).length()));
highestMarketPriceMarginOffer.ifPresent(offerBookListItem -> maxPlacesForMarketPriceMargin.set(formatMarketPriceMargin(offerBookListItem.getOffer()).length()));
};
}
@ -215,16 +226,7 @@ class OfferBookViewModel extends ActivatableViewModel {
protected void activate() {
filteredItems.addListener(filterItemsListener);
String code = direction == OfferDirection.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode();
if (code != null && !code.isEmpty() && !isShowAllEntry(code) &&
CurrencyUtil.getTradeCurrency(code).isPresent()) {
showAllTradeCurrenciesProperty.set(false);
selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get();
} else {
showAllTradeCurrenciesProperty.set(true);
selectedTradeCurrency = GlobalSettings.getDefaultTradeCurrency();
}
tradeCurrencyCode.set(selectedTradeCurrency.getCode());
updateSelectedTradeCurrency();
if (user != null) {
disableMatchToggle.set(user.getPaymentAccounts() == null || user.getPaymentAccounts().isEmpty());
@ -258,6 +260,11 @@ class OfferBookViewModel extends ActivatableViewModel {
void onTabSelected(boolean isSelected) {
this.isTabSelected = isSelected;
setMarketPriceFeedCurrency();
if (isTabSelected) {
updateSelectedTradeCurrency();
filterOffers();
}
}
///////////////////////////////////////////////////////////////////////////////////////////
@ -413,16 +420,12 @@ class OfferBookViewModel extends ActivatableViewModel {
return priceUtil.getMarketBasedPrice(offer, direction);
}
String formatMarketPriceMargin(Offer offer, boolean decimalAligned) {
String formatMarketPriceMargin(Offer offer) {
String postFix = "";
if (offer.isUseMarketBasedPrice()) {
postFix = " (" + FormattingUtils.formatPercentagePrice(offer.getMarketPriceMargin()) + ")";
}
if (decimalAligned) {
postFix = FormattingUtils.fillUpPlacesWithEmptyStrings(postFix, maxPlacesForMarketPriceMargin.get());
}
return postFix;
}
@ -670,4 +673,43 @@ class OfferBookViewModel extends ActivatableViewModel {
PaymentMethod getShowAllEntryForPaymentMethod() {
return PaymentMethod.getDummyPaymentMethod(GUIUtil.SHOW_ALL_FLAG);
}
public boolean isInstantPaymentMethod(Offer offer) {
return offer.getPaymentMethod().equals(PaymentMethod.BLOCK_CHAINS_INSTANT);
}
public PaymentAccount createBsqAccount(Offer offer) {
var unusedBsqAddressAsString = bsqWalletService.getUnusedBsqAddressAsString();
return coreApi.createCryptoCurrencyPaymentAccount(DisplayUtils.createAssetsAccountName("BSQ", unusedBsqAddressAsString),
"BSQ",
unusedBsqAddressAsString,
isInstantPaymentMethod(offer),
false);
}
public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) {
this.offerActionHandler = offerActionHandler;
}
public void onCreateOffer() {
offerActionHandler.onCreateOffer(getSelectedTradeCurrency(), selectedPaymentMethod);
}
public void onTakeOffer(Offer offer) {
offerActionHandler.onTakeOffer(offer);
}
private void updateSelectedTradeCurrency() {
String code = direction == OfferDirection.BUY ? preferences.getBuyScreenCurrencyCode() : preferences.getSellScreenCurrencyCode();
if (code != null && !code.isEmpty() && !isShowAllEntry(code) &&
CurrencyUtil.getTradeCurrency(code).isPresent()) {
showAllTradeCurrenciesProperty.set(false);
selectedTradeCurrency = CurrencyUtil.getTradeCurrency(code).get();
} else {
showAllTradeCurrenciesProperty.set(true);
selectedTradeCurrency = GlobalSettings.getDefaultTradeCurrency();
}
tradeCurrencyCode.set(selectedTradeCurrency.getCode());
}
}

View file

@ -17,10 +17,15 @@
package bisq.desktop.main.portfolio.pendingtrades.steps.seller;
import bisq.desktop.components.AutoTooltipButton;
import bisq.desktop.components.BusyAnimation;
import bisq.desktop.components.InfoTextField;
import bisq.desktop.components.TextFieldWithCopyIcon;
import bisq.desktop.components.indicator.TxConfidenceIndicator;
import bisq.desktop.main.MainView;
import bisq.desktop.main.dao.DaoView;
import bisq.desktop.main.dao.wallet.BsqWalletView;
import bisq.desktop.main.dao.wallet.tx.BsqTxView;
import bisq.desktop.main.overlays.popups.Popup;
import bisq.desktop.main.portfolio.pendingtrades.PendingTradesViewModel;
import bisq.desktop.main.portfolio.pendingtrades.steps.TradeStepView;
@ -82,6 +87,7 @@ import static com.google.common.base.Preconditions.checkNotNull;
public class SellerStep3View extends TradeStepView {
private Button confirmButton;
private AutoTooltipButton showBsqWallet;
private Label statusLabel;
private BusyAnimation busyAnimation;
private Subscription tradeStatePropertySubscription;
@ -296,11 +302,21 @@ public class SellerStep3View extends TradeStepView {
Tuple4<Button, BusyAnimation, Label, HBox> tuple = addButtonBusyAnimationLabelAfterGroup(gridPane, ++gridRow,
Res.get("portfolio.pending.step3_seller.confirmReceipt"));
HBox hBox = tuple.fourth;
GridPane.setColumnSpan(tuple.fourth, 2);
confirmButton = tuple.first;
confirmButton.setOnAction(e -> onPaymentReceived());
busyAnimation = tuple.second;
statusLabel = tuple.third;
if (trade.getOffer().getCurrencyCode().equals("BSQ")) {
showBsqWallet = new AutoTooltipButton(Res.get("portfolio.pending.step3_seller.showBsqWallet"));
hBox.getChildren().add(1, showBsqWallet);
showBsqWallet.setOnAction(e -> {
model.getNavigation().navigateTo(MainView.class, DaoView.class, BsqWalletView.class,
BsqTxView.class);
});
}
}

View file

@ -48,6 +48,7 @@ import bisq.core.locale.TradeCurrency;
import bisq.core.payment.PaymentAccount;
import bisq.core.payment.payload.PaymentMethod;
import bisq.core.provider.fee.FeeService;
import bisq.core.user.DontShowAgainLookup;
import bisq.core.user.Preferences;
import bisq.core.user.User;
import bisq.core.util.FormattingUtils;
@ -122,13 +123,12 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
private ToggleButton showOwnOffersInOfferBook, useAnimations, useDarkMode, sortMarketCurrenciesNumerically,
avoidStandbyMode, useCustomFee, autoConfirmXmrToggle, hideNonAccountPaymentMethodsToggle, denyApiTakerToggle,
notifyOnPreReleaseToggle;
notifyOnPreReleaseToggle, isDaoFullNodeToggleButton, daoActivatedToggleButton, fullModeDaoMonitorToggleButton;
private int gridRow = 0;
private int displayCurrenciesGridRowIndex = 0;
private InputTextField transactionFeeInputTextField, ignoreTradersListInputTextField, ignoreDustThresholdInputTextField,
autoConfRequiredConfirmationsTf, autoConfServiceAddressTf, autoConfTradeLimitTf, /*referralIdInputTextField,*/
autoConfRequiredConfirmationsTf, autoConfServiceAddressTf, autoConfTradeLimitTf,
rpcUserTextField, blockNotifyPortTextField;
private ToggleButton isDaoFullNodeToggleButton, daoActivatedToggleButton;
private PasswordTextField rpcPwTextField;
private TitledGroupBg daoOptionsTitledGroupBg;
@ -623,7 +623,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
}
private void initializeDaoOptions() {
int rowSpan = DevEnv.isDaoActivated() ? 5 : 1;
int rowSpan = DevEnv.isDaoActivated() ? 6 : 1;
daoOptionsTitledGroupBg = addTitledGroupBg(root, ++gridRow, rowSpan,
Res.get("setting.preferences.daoOptions"), Layout.GROUP_DISTANCE);
daoActivatedToggleButton = addSlideToggleButton(root, gridRow,
@ -632,6 +632,9 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton = addSlideToggleButton(root, ++gridRow,
Res.get("setting.preferences.dao.fullModeDaoMonitor"));
resyncDaoFromResourcesButton = addButton(root, ++gridRow, Res.get("setting.preferences.dao.resyncFromResources.label"));
resyncDaoFromResourcesButton.setMaxWidth(Double.MAX_VALUE);
GridPane.setHgrow(resyncDaoFromResourcesButton, Priority.ALWAYS);
@ -1021,6 +1024,21 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton.setSelected(preferences.isUseFullModeDaoMonitor());
fullModeDaoMonitorToggleButton.setOnAction(e -> {
preferences.setUseFullModeDaoMonitor(fullModeDaoMonitorToggleButton.isSelected());
if (fullModeDaoMonitorToggleButton.isSelected()) {
String key = "fullModeDaoMonitor";
if (DontShowAgainLookup.showAgain(key)) {
new Popup().information(Res.get("setting.preferences.dao.fullModeDaoMonitor.popup"))
.width(1000)
.dontShowAgainId(key)
.closeButtonText(Res.get("shared.iUnderstand"))
.show();
}
}
});
boolean daoFullNode = preferences.isDaoFullNode();
isDaoFullNodeToggleButton.setSelected(daoFullNode);
@ -1173,6 +1191,7 @@ public class PreferencesView extends ActivatableViewAndModel<GridPane, Preferenc
return;
}
fullModeDaoMonitorToggleButton.setOnAction(null);
resyncDaoFromResourcesButton.setOnAction(null);
resyncDaoFromGenesisButton.setOnAction(null);
bsqAverageTrimThresholdTextField.textProperty().removeListener(bsqAverageTrimThresholdListener);

View file

@ -6,8 +6,9 @@ import bisq.core.locale.Res;
import bisq.core.monetary.Price;
import bisq.core.monetary.Volume;
import bisq.core.offer.Offer;
import bisq.core.offer.OfferDirection;
import bisq.core.util.FormattingUtils;
import bisq.core.offer.OfferDirection;
import bisq.core.payment.PaymentAccount;
import bisq.core.util.ParsingUtils;
import bisq.core.util.VolumeUtil;
import bisq.core.util.coin.CoinFormatter;
@ -251,4 +252,21 @@ public class DisplayUtils {
public static Coin reduceTo4Decimals(Coin coin, CoinFormatter coinFormatter) {
return ParsingUtils.parseToCoin(coinFormatter.formatCoin(coin), coinFormatter);
}
public static String createAccountName(String paymentMethodId, String name) {
name = name.trim();
name = StringUtils.abbreviate(name, 9);
String method = Res.get(paymentMethodId);
return method.concat(": ").concat(name);
}
public static String createAssetsAccountName(PaymentAccount paymentAccount, String address) {
String currency = paymentAccount.getSingleTradeCurrency() != null ? paymentAccount.getSingleTradeCurrency().getCode() : "";
return createAssetsAccountName(currency, address);
}
public static String createAssetsAccountName(String currency, String address) {
address = StringUtils.abbreviate(address, 9);
return currency.concat(": ").concat(address);
}
}

View file

@ -108,13 +108,17 @@ import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.ScrollBar;
import javafx.scene.control.ScrollPane;
import javafx.scene.control.TableView;
import javafx.scene.control.TextArea;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.ColumnConstraints;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.geometry.HPos;
import javafx.geometry.Orientation;
import javafx.collections.FXCollections;
@ -1218,4 +1222,28 @@ public class GUIUtil {
return result.name();
}
}
public static ScrollPane createScrollPane() {
ScrollPane scrollPane = new ScrollPane();
scrollPane.setHbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
scrollPane.setFitToWidth(true);
scrollPane.setFitToHeight(true);
AnchorPane.setLeftAnchor(scrollPane, 0d);
AnchorPane.setTopAnchor(scrollPane, 0d);
AnchorPane.setRightAnchor(scrollPane, 0d);
AnchorPane.setBottomAnchor(scrollPane, 0d);
return scrollPane;
}
public static void setDefaultTwoColumnConstraintsForGridPane(GridPane gridPane) {
ColumnConstraints columnConstraints1 = new ColumnConstraints();
columnConstraints1.setHalignment(HPos.RIGHT);
columnConstraints1.setHgrow(Priority.NEVER);
columnConstraints1.setMinWidth(200);
ColumnConstraints columnConstraints2 = new ColumnConstraints();
columnConstraints2.setHgrow(Priority.ALWAYS);
gridPane.getColumnConstraints().addAll(columnConstraints1, columnConstraints2);
}
}

View file

@ -41,7 +41,7 @@ public class DaoStateBlockListItemTest {
@Test
public void testEqualsAndHashCode() {
var block = new DaoStateBlock(new DaoStateHash(0, new byte[0], new byte[0]));
var block = new DaoStateBlock(new DaoStateHash(0, new byte[0], true));
var item1 = new DaoStateBlockListItem(block, newSupplier(1));
var item2 = new DaoStateBlockListItem(block, newSupplier(2));
var item3 = new DaoStateBlockListItem(block, newSupplier(1));

View file

@ -239,7 +239,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
assertEquals(0, model.maxPlacesForAmount.intValue());
}
@ -253,7 +253,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(6, model.maxPlacesForAmount.intValue());
@ -271,7 +271,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(15, model.maxPlacesForAmount.intValue());
@ -290,7 +290,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
assertEquals(0, model.maxPlacesForVolume.intValue());
}
@ -304,7 +304,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(5, model.maxPlacesForVolume.intValue());
@ -322,7 +322,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(9, model.maxPlacesForVolume.intValue());
@ -341,7 +341,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
assertEquals(0, model.maxPlacesForPrice.intValue());
}
@ -355,7 +355,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(7, model.maxPlacesForPrice.intValue());
@ -373,7 +373,7 @@ public class OfferBookViewModelTest {
when(offerBook.getOfferBookListItems()).thenReturn(offerBookListItems);
final OfferBookViewModel model = new OfferBookViewModel(null, null, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
assertEquals(0, model.maxPlacesForMarketPriceMargin.intValue());
}
@ -408,7 +408,7 @@ public class OfferBookViewModelTest {
offerBookListItems.addAll(item1, item2);
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, priceFeedService,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
model.activate();
assertEquals(8, model.maxPlacesForMarketPriceMargin.intValue()); //" (1.97%)"
@ -429,7 +429,7 @@ public class OfferBookViewModelTest {
when(priceFeedService.getMarketPrice(anyString())).thenReturn(new MarketPrice("USD", 12684.0450, Instant.now().getEpochSecond(), true));
final OfferBookViewModel model = new OfferBookViewModel(null, openOfferManager, offerBook, empty, null, null, null,
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter());
null, null, null, getPriceUtil(), null, coinFormatter, new BsqFormatter(), null, null);
final OfferBookListItem item = make(btcBuyItem.but(
with(useMarketBasedPrice, true),

View file

@ -1945,6 +1945,7 @@ message PreferencesPayload {
bool show_offers_matching_my_accounts = 59;
bool deny_api_taker = 60;
bool notify_on_pre_release = 61;
bool use_full_mode_dao_monitor = 62;
}
message AutoConfirmSettings {
@ -2381,20 +2382,21 @@ message DaoStateStore {
message DaoStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
bool is_self_created = 4;
}
message ProposalStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
int32 num_proposals = 4;
}
message BlindVoteStateHash {
int32 height = 1;
bytes hash = 2;
bytes prev_hash = 3;
bytes prev_hash = 3 [deprecated = true];
int32 num_blind_votes = 4;
}