From 790563e99746189e77e778342be158d9c1c8e7f3 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Mon, 12 Dec 2016 21:51:23 +0100 Subject: [PATCH] Add create proposal view and fee payment tx. Add tabs for other dao subviews --- .../btc/provider/fee/FeeService.java | 5 + .../wallet/BtcProposalFeeCoinSelector.java | 128 ++++++++++++ .../btc/wallet/BtcWalletService.java | 124 ++++++++++- .../btc/wallet/ChangeBelowDustException.java | 37 ++++ .../btc/wallet/SquFeeCoinSelector.java | 132 ++++++++++++ .../bitsquare/btc/wallet/SquUTXOProvider.java | 43 +--- .../btc/wallet/SquWalletService.java | 57 ++++- .../bitsquare/btc/wallet/WalletService.java | 20 +- .../io/bitsquare/dao/proposals/Proposal.java | 132 ++++++++++++ .../io/bitsquare/gui/main/dao/DaoView.java | 7 +- .../gui/main/dao/proposals/ProposalsView.fxml | 25 +-- .../gui/main/dao/proposals/ProposalsView.java | 159 ++++++++++++-- .../proposals/active/ActiveProposalsView.fxml | 31 +++ .../proposals/active/ActiveProposalsView.java | 50 +++++ .../proposals/create/CreateProposalView.fxml | 32 +++ .../proposals/create/CreateProposalView.java | 195 ++++++++++++++++++ .../dao/proposals/past/PastProposalsView.fxml | 31 +++ .../dao/proposals/past/PastProposalsView.java | 50 +++++ .../gui/main/dao/voting/VotingView.fxml | 25 +-- .../gui/main/dao/voting/VotingView.java | 159 ++++++++++++-- .../voting/dashboard/VotingDashboardView.fxml | 32 +++ .../voting/dashboard/VotingDashboardView.java | 50 +++++ .../dao/voting/history/VotingHistoryView.fxml | 31 +++ .../dao/voting/history/VotingHistoryView.java | 50 +++++ .../gui/main/dao/voting/vote/VoteView.fxml | 31 +++ .../gui/main/dao/voting/vote/VoteView.java | 50 +++++ .../gui/main/dao/wallet/TokenWalletView.java | 2 +- 27 files changed, 1565 insertions(+), 123 deletions(-) create mode 100644 core/src/main/java/io/bitsquare/btc/wallet/BtcProposalFeeCoinSelector.java create mode 100644 core/src/main/java/io/bitsquare/btc/wallet/ChangeBelowDustException.java create mode 100644 core/src/main/java/io/bitsquare/btc/wallet/SquFeeCoinSelector.java create mode 100644 core/src/main/java/io/bitsquare/dao/proposals/Proposal.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.java create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.fxml create mode 100644 gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.java diff --git a/core/src/main/java/io/bitsquare/btc/provider/fee/FeeService.java b/core/src/main/java/io/bitsquare/btc/provider/fee/FeeService.java index 51723cc0a2..6cc7114a8d 100644 --- a/core/src/main/java/io/bitsquare/btc/provider/fee/FeeService.java +++ b/core/src/main/java/io/bitsquare/btc/provider/fee/FeeService.java @@ -116,4 +116,9 @@ public class FeeService { return Coin.valueOf(feeData.takeOfferFee); } + public Coin getCreateProposalFee() { + //TODO + return Coin.valueOf(1000); + } + } diff --git a/core/src/main/java/io/bitsquare/btc/wallet/BtcProposalFeeCoinSelector.java b/core/src/main/java/io/bitsquare/btc/wallet/BtcProposalFeeCoinSelector.java new file mode 100644 index 0000000000..eae02d8eb6 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/wallet/BtcProposalFeeCoinSelector.java @@ -0,0 +1,128 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.btc.wallet; + +import com.google.common.annotations.VisibleForTesting; +import org.bitcoinj.core.*; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.wallet.CoinSelection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. + * We lookup for spendable outputs which matches our address of our address. + */ +class BtcProposalFeeCoinSelector { + private static final Logger log = LoggerFactory.getLogger(BtcProposalFeeCoinSelector.class); + + private Wallet wallet; + + public BtcProposalFeeCoinSelector(Wallet wallet) { + this.wallet = wallet; + } + + public CoinSelection getCoinSelection(Coin target) throws InsufficientMoneyException, ChangeBelowDustException { + return select(target, getCandidates()); + } + + private List getCandidates() { + return wallet.calculateAllSpendCandidates(true, true); + } + + private CoinSelection select(Coin target, List candidates) throws InsufficientMoneyException, ChangeBelowDustException { + ArrayList selected = new ArrayList<>(); + // Sort the inputs by value so we get the lowest values first. + ArrayList sortedOutputs = new ArrayList<>(candidates); + // When calculating the wallet balance, we may be asked to select all possible coins, if so, avoid sorting + // them in order to improve performance. + if (!target.equals(NetworkParameters.MAX_MONEY)) + sortOutputsByValue(sortedOutputs); + + // Now iterate over the sorted outputs until we have got as close to the target as possible or a little + // bit over (excessive value will be change). + long total = 0; + long dust = Transaction.MIN_NONDUST_OUTPUT.value; + long targetValue = target.value; + for (TransactionOutput output : sortedOutputs) { + if (total >= targetValue) { + long change = total - targetValue; + // If we get a change it must not be smaller than dust + if (change >= dust || change == 0) + break; + } + + // Only pick chain-included transactions, or transactions that are ours and pending. + if (!isSelectable(output.getParentTransaction())) + continue; + + selected.add(output); + total += output.getValue().value; + } + + long missing = targetValue - total; + if (missing > 0) + throw new InsufficientMoneyException(Coin.valueOf(missing)); + + long change = total - targetValue; + if (change > 0 && change < dust) + throw new ChangeBelowDustException(Coin.valueOf(change)); + + return new CoinSelection(Coin.valueOf(total), selected); + } + + @VisibleForTesting + void sortOutputsByValue(ArrayList outputs) { + Collections.sort(outputs, (a, b) -> { + int c2 = a.getValue().compareTo(b.getValue()); + if (c2 != 0) return c2; + // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. + Sha256Hash parentTransactionHash = a.getParentTransactionHash(); + Sha256Hash parentTransactionHash1 = b.getParentTransactionHash(); + if (parentTransactionHash != null && parentTransactionHash1 != null) { + BigInteger aHash = parentTransactionHash.toBigInteger(); + BigInteger bHash = parentTransactionHash1.toBigInteger(); + return aHash.compareTo(bHash); + } else { + return 0; + } + }); + } + + public boolean isSelectable(Transaction tx) { + if (tx != null) { + // Only pick chain-included transactions, or transactions that are ours and pending. + TransactionConfidence confidence = tx.getConfidence(); + TransactionConfidence.ConfidenceType type = confidence.getConfidenceType(); + return type.equals(TransactionConfidence.ConfidenceType.BUILDING) || + type.equals(TransactionConfidence.ConfidenceType.PENDING) && + confidence.getSource().equals(TransactionConfidence.Source.SELF) && + // In regtest mode we expect to have only one peer, so we won't see transactions propagate. + // TODO: The value 1 below dates from a time when transactions we broadcast *to* were counted, set to 0 + (confidence.numBroadcastPeers() > 1 || tx.getParams() == RegTestParams.get()); + } else { + return true; + } + } + +} diff --git a/core/src/main/java/io/bitsquare/btc/wallet/BtcWalletService.java b/core/src/main/java/io/bitsquare/btc/wallet/BtcWalletService.java index e75bc1768c..27d652a2be 100644 --- a/core/src/main/java/io/bitsquare/btc/wallet/BtcWalletService.java +++ b/core/src/main/java/io/bitsquare/btc/wallet/BtcWalletService.java @@ -17,7 +17,6 @@ package io.bitsquare.btc.wallet; -import com.google.common.base.Preconditions; import com.google.common.collect.ImmutableList; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; @@ -30,6 +29,7 @@ import io.bitsquare.user.Preferences; import org.bitcoinj.core.*; import org.bitcoinj.crypto.DeterministicKey; import org.bitcoinj.crypto.KeyCrypterScrypt; +import org.bitcoinj.script.ScriptBuilder; import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -43,6 +43,7 @@ import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; /** @@ -160,8 +161,10 @@ public class BtcWalletService extends WalletService { wallet.completeTx(sendRequest); resultTx = sendRequest.tx; + //TODO add estimated signature size + txSize = resultTx.bitcoinSerialize().length; } - while (counter < 10 && Math.abs(resultTx.getFee().value - txFeePerByte.multiply(resultTx.bitcoinSerialize().length).value) > 1000); + while (counter < 10 && Math.abs(resultTx.getFee().value - txFeePerByte.multiply(txSize).value) > 1000); if (counter == 10) { log.error("Could not calculate the fee. Tx=" + resultTx); } @@ -185,6 +188,119 @@ public class BtcWalletService extends WalletService { } } + /////////////////////////////////////////////////////////////////////////////////////////// + // Add mining fee input and mandatory change output for prepared proposal fee tx + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction addInputAndOutputToPreparedSquProposalFeeTx(Transaction preparedSquTx, byte[] hash) throws + TransactionVerificationException, WalletException, InsufficientFundsException { + + // preparedSquTx has following structure: + // inputs [1-n] SQU inputs + // outputs [0-1] SQU change output + // mining fee: SQU proposal fee + + try { + int counter = 0; + final int sigSizePerInput = 106; + int txSize = 203; // typical size for a tx with 2 unsigned inputs and 3 outputs + final Coin txFeePerByte = feeService.getTxFeePerByte(); + Coin valueToForceChange = Coin.ZERO; + Address changeAddress = getOrCreateAddressEntry(AddressEntry.Context.AVAILABLE).getAddress(); + checkNotNull(changeAddress, "changeAddress must not be null"); + final BtcCoinSelector coinSelector = new BtcCoinSelector(params, changeAddress); + + final List preparedSquTxInputs = preparedSquTx.getInputs(); + final List preparedSquTxOutputs = preparedSquTx.getOutputs(); + Transaction resultTx = null; + int numInputs = preparedSquTxInputs.size() + 1; + boolean validTx; + do { + counter++; + if (counter >= 10) { + checkNotNull(resultTx, "resultTx must not be null"); + log.error("Could not calculate the fee. Tx=" + resultTx); + break; + } + + Transaction tx = new Transaction(params); + preparedSquTxInputs.stream().forEach(tx::addInput); + + if (valueToForceChange.isZero()) { + preparedSquTxOutputs.stream().forEach(tx::addOutput); + } else { + //TODO test that case + // We have only 1 output at preparedSquTxOutputs otherwise the valueToForceChange must be 0 + checkArgument(preparedSquTxOutputs.size() == 1); + tx.addOutput(valueToForceChange, changeAddress); + tx.addOutput(preparedSquTxOutputs.get(0)); + } + + Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx); + sendRequest.shuffleOutputs = false; + sendRequest.aesKey = aesKey; + // Need to be false as it would try to sign all inputs (SQU inputs are not in this wallet) + sendRequest.signInputs = false; + sendRequest.ensureMinRequiredFee = false; + sendRequest.feePerKb = Coin.ZERO; + Coin fee = txFeePerByte.multiply(txSize + sigSizePerInput * numInputs); + sendRequest.fee = fee; + sendRequest.coinSelector = coinSelector; + sendRequest.changeAddress = changeAddress; + wallet.completeTx(sendRequest); + + Transaction txWithBtcInputs = sendRequest.tx; + // add OP_RETURN output + txWithBtcInputs.addOutput(new TransactionOutput(params, txWithBtcInputs, Coin.ZERO, ScriptBuilder.createOpReturnScript(hash).getProgram())); + + + // As we cannot remove an outut we recreate the tx + resultTx = new Transaction(params); + txWithBtcInputs.getInputs().stream().forEach(resultTx::addInput); + txWithBtcInputs.getOutputs().stream().forEach(resultTx::addOutput); + + numInputs = resultTx.getInputs().size(); + + printTx("resultTx", resultTx); + txSize = resultTx.bitcoinSerialize().length + sigSizePerInput * numInputs; + log.error("txSize " + txSize); + // We need min. 1 output beside op_return + // calculated fee must be inside of a tolerance range with tx fee + final int tolerance = 1000; + final long txFeeAsLong = resultTx.getFee().value; + final long calculatedFeeAsLong = txFeePerByte.multiply(txSize + sigSizePerInput * numInputs).value; + final boolean correctOutputs = resultTx.getOutputs().size() > 1; + validTx = correctOutputs && Math.abs(txFeeAsLong - calculatedFeeAsLong) < tolerance; + + if (resultTx.getOutputs().size() == 1) { + // We have the rare case that both inputs equalled the required fees, so both did not render + // a change output. + // We need to add artificially a change output as the OP_RETURN is not allowed as only output + valueToForceChange = Transaction.MIN_NONDUST_OUTPUT; + } + } + while (!validTx); + + // Sign all BTC inputs + int startIndex = preparedSquTxInputs.size(); + int endIndex = resultTx.getInputs().size(); + for (int i = startIndex; i < endIndex; i++) { + TransactionInput txIn = resultTx.getInputs().get(i); + signTransactionInput(resultTx, txIn, i); + checkScriptSig(resultTx, txIn, i); + } + + checkWalletConsistency(); + verifyTransaction(resultTx); + + printTx("Signed resultTx", resultTx); + return resultTx; + } catch (InsufficientMoneyException e) { + throw new InsufficientFundsException("The fees for that preparedSquTx exceed the available funds " + + "or the resulting output value is below the min. dust value:\n" + + "Missing " + (e.missing != null ? e.missing.toFriendlyString() : "null")); + } + } /////////////////////////////////////////////////////////////////////////////////////////// // AddressEntry @@ -571,7 +687,7 @@ public class BtcWalletService extends WalletService { AddressEntry.Context context) throws AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); - Preconditions.checkArgument(Restrictions.isAboveDust(amount, fee), + checkArgument(Restrictions.isAboveDust(amount, fee), "The amount is too low (dust limit)."); tx.addOutput(amount.subtract(fee), new Address(params, toAddress)); @@ -599,7 +715,7 @@ public class BtcWalletService extends WalletService { @Nullable KeyParameter aesKey) throws AddressFormatException, AddressEntryException { Transaction tx = new Transaction(params); - Preconditions.checkArgument(Restrictions.isAboveDust(amount), + checkArgument(Restrictions.isAboveDust(amount), "The amount is too low (dust limit)."); tx.addOutput(amount.subtract(fee), new Address(params, toAddress)); diff --git a/core/src/main/java/io/bitsquare/btc/wallet/ChangeBelowDustException.java b/core/src/main/java/io/bitsquare/btc/wallet/ChangeBelowDustException.java new file mode 100644 index 0000000000..5281ee72a9 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/wallet/ChangeBelowDustException.java @@ -0,0 +1,37 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.btc.wallet; + +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class ChangeBelowDustException extends Exception { + private static final Logger log = LoggerFactory.getLogger(ChangeBelowDustException.class); + private final Coin change; + + public ChangeBelowDustException(Coin change) { + this(change, "Change value would be below min. dust. Change: " + change + " satoshis"); + } + + public ChangeBelowDustException(Coin change, String message) { + super(message); + this.change = change; + } + +} diff --git a/core/src/main/java/io/bitsquare/btc/wallet/SquFeeCoinSelector.java b/core/src/main/java/io/bitsquare/btc/wallet/SquFeeCoinSelector.java new file mode 100644 index 0000000000..a83c890fd7 --- /dev/null +++ b/core/src/main/java/io/bitsquare/btc/wallet/SquFeeCoinSelector.java @@ -0,0 +1,132 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.btc.wallet; + +import com.google.common.annotations.VisibleForTesting; +import org.bitcoinj.core.*; +import org.bitcoinj.params.RegTestParams; +import org.bitcoinj.wallet.CoinSelection; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * We use a specialized version of the CoinSelector based on the DefaultCoinSelector implementation. + * We lookup for spendable outputs which matches our address of our address. + */ +class SquFeeCoinSelector { + private static final Logger log = LoggerFactory.getLogger(SquFeeCoinSelector.class); + + private Wallet wallet; + private final boolean allowUnconfirmedSpend; + + public SquFeeCoinSelector(Wallet wallet, boolean allowUnconfirmedSpend) { + this.wallet = wallet; + this.allowUnconfirmedSpend = allowUnconfirmedSpend; + } + + public CoinSelection getCoinSelection(Coin target) throws InsufficientMoneyException, ChangeBelowDustException { + return select(target, getCandidates()); + } + + private List getCandidates() { + //TODO + wallet.setUTXOProvider(null); + return wallet.calculateAllSpendCandidates(true, true); + } + + private CoinSelection select(Coin target, List candidates) throws InsufficientMoneyException, ChangeBelowDustException { + ArrayList selected = new ArrayList<>(); + // Sort the inputs by value so we get the lowest values first. + ArrayList sortedOutputs = new ArrayList<>(candidates); + // When calculating the wallet balance, we may be asked to select all possible coins, if so, avoid sorting + // them in order to improve performance. + if (!target.equals(NetworkParameters.MAX_MONEY)) + sortOutputsByValue(sortedOutputs); + + // Now iterate over the sorted outputs until we have got as close to the target as possible or a little + // bit over (excessive value will be change). + long total = 0; + long dust = Transaction.MIN_NONDUST_OUTPUT.value; + long targetValue = target.value; + for (TransactionOutput output : sortedOutputs) { + if (total >= targetValue) { + long change = total - targetValue; + // If we get a change it must not be smaller than dust + if (change >= dust || change == 0) + break; + } + + // Only pick chain-included transactions, or transactions that are ours and pending. + if (!isSelectable(output.getParentTransaction())) + continue; + + selected.add(output); + total += output.getValue().value; + } + + long missing = targetValue - total; + if (missing > 0) + throw new InsufficientMoneyException(Coin.valueOf(missing)); + + long change = total - targetValue; + if (change > 0 && change < dust) + throw new ChangeBelowDustException(Coin.valueOf(change)); + + return new CoinSelection(Coin.valueOf(total), selected); + } + + @VisibleForTesting + void sortOutputsByValue(ArrayList outputs) { + Collections.sort(outputs, (a, b) -> { + int c2 = a.getValue().compareTo(b.getValue()); + if (c2 != 0) return c2; + // They are entirely equivalent (possibly pending) so sort by hash to ensure a total ordering. + Sha256Hash parentTransactionHash = a.getParentTransactionHash(); + Sha256Hash parentTransactionHash1 = b.getParentTransactionHash(); + if (parentTransactionHash != null && parentTransactionHash1 != null) { + BigInteger aHash = parentTransactionHash.toBigInteger(); + BigInteger bHash = parentTransactionHash1.toBigInteger(); + return aHash.compareTo(bHash); + } else { + return 0; + } + }); + } + + public boolean isSelectable(Transaction tx) { + if (tx != null) { + // Only pick chain-included transactions, or transactions that are ours and pending. + TransactionConfidence confidence = tx.getConfidence(); + TransactionConfidence.ConfidenceType type = confidence.getConfidenceType(); + boolean isMine = confidence.getSource().equals(TransactionConfidence.Source.SELF) && + // In regtest mode we expect to have only one peer, so we won't see transactions propagate. + // TODO: The value 1 below dates from a time when transactions we broadcast *to* were counted, set to 0 + (confidence.numBroadcastPeers() > 1 || tx.getParams() == RegTestParams.get()); + return type.equals(TransactionConfidence.ConfidenceType.BUILDING) || + (type.equals(TransactionConfidence.ConfidenceType.PENDING) && (allowUnconfirmedSpend || isMine)); + } else { + return true; + } + } + +} diff --git a/core/src/main/java/io/bitsquare/btc/wallet/SquUTXOProvider.java b/core/src/main/java/io/bitsquare/btc/wallet/SquUTXOProvider.java index 556d41e8e9..1117d4459e 100644 --- a/core/src/main/java/io/bitsquare/btc/wallet/SquUTXOProvider.java +++ b/core/src/main/java/io/bitsquare/btc/wallet/SquUTXOProvider.java @@ -23,15 +23,16 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.inject.Inject; -import java.util.*; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; public class SquUTXOProvider implements UTXOProvider { private static final Logger log = LoggerFactory.getLogger(SquUTXOProvider.class); private Map utxoByAddressAsStringMap = new HashMap<>(); - private Set addressesAsStringSet = new HashSet<>(); - private Set transactionOutputSet = new HashSet<>(); private NetworkParameters params; @Inject @@ -45,22 +46,7 @@ public class SquUTXOProvider implements UTXOProvider { @Override public List getOpenTransactionOutputs(List
addresses) throws UTXOProviderException { - // addressesAsStringSet.clear(); - // addressesAsStringSet.addAll(addresses.stream().map(Address::toString).collect(Collectors.toSet())); - List foundOutputs = addresses.stream().map(e -> utxoByAddressAsStringMap.get(e.toString())).filter(e -> e != null).collect(Collectors.toList()); - - // List foundOutputs = transactionOutputSet.stream().filter(e -> addressesAsStringSet.contains(e.getAddress())).collect(Collectors.toList()); - /* - List foundOutputs = new ArrayList(); - Collection outputsList = transactionOutputMap.values(); - for (UTXO output : outputsList) { - for (Address address : addresses) { - if (output.getAddress().equals(address.toString())) { - foundOutputs.add(output); - } - } - }*/ - return foundOutputs; + return addresses.stream().map(e -> utxoByAddressAsStringMap.get(e.toString())).filter(e -> e != null).collect(Collectors.toList()); } @Override @@ -73,23 +59,4 @@ public class SquUTXOProvider implements UTXOProvider { public NetworkParameters getParams() { return params; } -/* - private String getScriptAddress(@Nullable Script script) { - String address = ""; - try { - if (script != null) { - address = script.getToAddress(params, true).toString(); - } - } catch (Exception e) { - } - return address; - } - - private Script getScript(byte[] scriptBytes) { - try { - return new Script(scriptBytes); - } catch (Exception e) { - return new Script(new byte[0]); - } - }*/ } diff --git a/core/src/main/java/io/bitsquare/btc/wallet/SquWalletService.java b/core/src/main/java/io/bitsquare/btc/wallet/SquWalletService.java index 989d85e328..7fad14d9cf 100644 --- a/core/src/main/java/io/bitsquare/btc/wallet/SquWalletService.java +++ b/core/src/main/java/io/bitsquare/btc/wallet/SquWalletService.java @@ -17,7 +17,6 @@ package io.bitsquare.btc.wallet; -import com.google.common.base.Preconditions; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import io.bitsquare.btc.Restrictions; @@ -30,6 +29,8 @@ import io.bitsquare.common.handlers.ResultHandler; import io.bitsquare.user.Preferences; import org.bitcoinj.core.*; import org.bitcoinj.script.Script; +import org.bitcoinj.wallet.CoinSelection; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,6 +39,8 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import static com.google.common.base.Preconditions.checkArgument; + /** * WalletService handles all non trade specific wallet and bitcoin related services. * It startup the wallet app kit and initialized the wallet. @@ -139,16 +142,20 @@ public class SquWalletService extends WalletService { return wallet != null ? wallet.getBalance(Wallet.BalanceType.AVAILABLE) : Coin.ZERO; } - public void requestSquUtxo(ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) { + public void requestSquUtxo(@Nullable ResultHandler resultHandler, @Nullable ErrorMessageHandler errorMessageHandler) { Set utxoSet = squUtxoFeedService.getUtxoSet(); if (utxoSet != null) squUTXOProvider.setUtxoSet(utxoSet); squUtxoFeedService.requestSquUtxo(utxos -> { squUTXOProvider.setUtxoSet(utxos); - resultHandler.handleResult(); + if (resultHandler != null) + resultHandler.handleResult(); }, - (errorMessage, throwable) -> errorMessageHandler.handleErrorMessage(errorMessage)); + (errorMessage, throwable) -> { + if (errorMessageHandler != null) + errorMessageHandler.handleErrorMessage(errorMessage); + }); } /////////////////////////////////////////////////////////////////////////////////////////// @@ -161,7 +168,7 @@ public class SquWalletService extends WalletService { InsufficientMoneyException, WalletException, TransactionVerificationException { Transaction tx = new Transaction(params); - Preconditions.checkArgument(Restrictions.isAboveDust(receiverAmount), + checkArgument(Restrictions.isAboveDust(receiverAmount), "The amount is too low (dust limit)."); tx.addOutput(receiverAmount, new Address(params, receiverAddress)); @@ -203,4 +210,44 @@ public class SquWalletService extends WalletService { return tx; } + + /////////////////////////////////////////////////////////////////////////////////////////// + // Burn SQU for proposal fee + /////////////////////////////////////////////////////////////////////////////////////////// + + public Transaction getPreparedProposalFeeTx(Coin createProposalFee) throws WalletException, TransactionVerificationException, InsufficientMoneyException, ChangeBelowDustException { + Transaction tx = new Transaction(params); + + SquFeeCoinSelector squFeeCoinSelector = new SquFeeCoinSelector(wallet, true); + CoinSelection coinSelection = squFeeCoinSelector.getCoinSelection(createProposalFee); + coinSelection.gathered.stream().forEach(tx::addInput); + Coin change = coinSelection.valueGathered.subtract(createProposalFee); + if (change.isPositive()) + tx.addOutput(change, wallet.freshReceiveAddress()); + + printTx("preparedProposalFeeTx", tx); + return tx; + } + + public void signAndBroadcastProposalFeeTx(Transaction tx, FutureCallback callback) throws WalletException, TransactionVerificationException { + // Sign all SQU inputs + int endIndex = tx.getInputs().size(); + for (int i = 0; i < endIndex; i++) { + TransactionInput txIn = tx.getInputs().get(i); + TransactionOutput connectedOutput = txIn.getConnectedOutput(); + if (connectedOutput != null && connectedOutput.isMine(wallet)) { + signTransactionInput(tx, txIn, i); + checkScriptSig(tx, txIn, i); + } + } + + checkWalletConsistency(); + verifyTransaction(tx); + printTx("signAndBroadcastProposalFeeTx", tx); + + wallet.commitTx(tx); + checkWalletConsistency(); + Futures.addCallback(walletsSetup.getPeerGroup().broadcastTransaction(tx).future(), callback); + printTx("signAndBroadcastProposalFeeTx", tx); + } } diff --git a/core/src/main/java/io/bitsquare/btc/wallet/WalletService.java b/core/src/main/java/io/bitsquare/btc/wallet/WalletService.java index 90ae33c4e0..4ad62266ad 100644 --- a/core/src/main/java/io/bitsquare/btc/wallet/WalletService.java +++ b/core/src/main/java/io/bitsquare/btc/wallet/WalletService.java @@ -59,13 +59,13 @@ import static com.google.common.base.Preconditions.checkState; public abstract class WalletService { private static final Logger log = LoggerFactory.getLogger(WalletService.class); - private final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); - private final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>(); - private final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); + protected final CopyOnWriteArraySet addressConfidenceListeners = new CopyOnWriteArraySet<>(); + protected final CopyOnWriteArraySet txConfidenceListeners = new CopyOnWriteArraySet<>(); + protected final CopyOnWriteArraySet balanceListeners = new CopyOnWriteArraySet<>(); protected final WalletsSetup walletsSetup; - private final Preferences preferences; - private final FeeService feeService; + protected final Preferences preferences; + protected final FeeService feeService; protected final WalletEventListener walletEventListener = new BitsquareWalletEventListener(); protected final NetworkParameters params; @@ -324,7 +324,7 @@ public abstract class WalletService { return null; } - private TransactionConfidence getTransactionConfidence(Transaction tx, Address address) { + protected TransactionConfidence getTransactionConfidence(Transaction tx, Address address) { List mergedOutputs = getOutputsWithConnectedOutputs(tx); List transactionConfidenceList = new ArrayList<>(); @@ -339,7 +339,7 @@ public abstract class WalletService { } - private List getOutputsWithConnectedOutputs(Transaction tx) { + protected List getOutputsWithConnectedOutputs(Transaction tx) { List transactionOutputs = tx.getOutputs(); List connectedOutputs = new ArrayList<>(); @@ -359,7 +359,7 @@ public abstract class WalletService { } - private TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) { + protected TransactionConfidence getMostRecentConfidence(List transactionConfidenceList) { TransactionConfidence transactionConfidence = null; for (TransactionConfidence confidence : transactionConfidenceList) { if (confidence != null) { @@ -390,7 +390,7 @@ public abstract class WalletService { return wallet != null ? getBalance(wallet.calculateAllSpendCandidates(), address) : Coin.ZERO; } - private Coin getBalance(List transactionOutputs, Address address) { + protected Coin getBalance(List transactionOutputs, Address address) { Coin balance = Coin.ZERO; for (TransactionOutput transactionOutput : transactionOutputs) { if (transactionOutput.getScriptPubKey().isSentToAddress() || transactionOutput.getScriptPubKey().isPayToScriptHash()) { @@ -478,7 +478,7 @@ public abstract class WalletService { StringBuilder sb = new StringBuilder(); sb.append("\n").append(tracePrefix).append(":").append("\n").append(tx.toString()).append("\n"); sb.append("Size: ").append(tx.bitcoinSerialize().length); - log.info(sb.toString()); + log.error(sb.toString()); } diff --git a/core/src/main/java/io/bitsquare/dao/proposals/Proposal.java b/core/src/main/java/io/bitsquare/dao/proposals/Proposal.java new file mode 100644 index 0000000000..ac01894fed --- /dev/null +++ b/core/src/main/java/io/bitsquare/dao/proposals/Proposal.java @@ -0,0 +1,132 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.dao.proposals; + +import io.bitsquare.p2p.NodeAddress; +import io.bitsquare.p2p.storage.payload.LazyProcessedStoragePayload; +import io.bitsquare.p2p.storage.payload.PersistedStoragePayload; +import org.bitcoinj.core.Coin; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.PublicKey; +import java.util.Arrays; +import java.util.Date; +import java.util.concurrent.TimeUnit; + +public class Proposal implements LazyProcessedStoragePayload, PersistedStoragePayload { + private static final Logger log = LoggerFactory.getLogger(Proposal.class); + + public static final long TTL = TimeUnit.DAYS.toMillis(30); + + public final String uid; + public final String text; + public final String title; + public final String category; + public final String description; + public final String link; + public final Date startDate; + public final Date endDate; + public final Coin requestedBtc; + public final String btcAddress; + public final NodeAddress nodeAddress; + private PublicKey p2pStorageSignaturePubKey; + public final byte[] squPubKey; + + // Signature of proposal data without signature, hash and feeTxId + public byte[] signature; + // Sha256Hash of proposal data including the signature but without feeTxId and hash + public byte[] hash; + + // Set after we signed and set the hash. The hash is used in the OP_RETURN of the fee tx + public String feeTxId; + + public Proposal(String uid, + String text, + String title, + String category, + String description, + String link, + Date startDate, + Date endDate, + Coin requestedBtc, + String btcAddress, + NodeAddress nodeAddress, + PublicKey p2pStorageSignaturePubKey, + byte[] squPubKey) { + + this.uid = uid; + this.text = text; + this.title = title; + this.category = category; + this.description = description; + this.link = link; + this.startDate = startDate; + this.endDate = endDate; + this.requestedBtc = requestedBtc; + this.btcAddress = btcAddress; + this.nodeAddress = nodeAddress; + this.p2pStorageSignaturePubKey = p2pStorageSignaturePubKey; + this.squPubKey = squPubKey; + } + + @Override + public long getTTL() { + return TTL; + } + + @Override + public PublicKey getOwnerPubKey() { + return p2pStorageSignaturePubKey; + } + + public void setSignature(byte[] signature) { + this.signature = signature; + } + + public void setHash(byte[] hash) { + this.hash = hash; + } + + // Called after sig and hash + public void setFeeTxId(String feeTxId) { + this.feeTxId = feeTxId; + } + + @Override + public String toString() { + return "Proposal{" + + "uid='" + uid + '\'' + + ", text='" + text + '\'' + + ", title='" + title + '\'' + + ", category='" + category + '\'' + + ", description='" + description + '\'' + + ", link='" + link + '\'' + + ", startDate=" + startDate + + ", endDate=" + endDate + + ", requestedBtc=" + requestedBtc + + ", btcAddress='" + btcAddress + '\'' + + ", nodeAddress=" + nodeAddress + + ", pubKey=" + Arrays.toString(squPubKey) + + ", signature=" + signature + + ", hash=" + hash + + ", feeTxId=" + feeTxId + + '}'; + } + +} diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/DaoView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/DaoView.java index 123afcdc52..151d99649f 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/DaoView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/DaoView.java @@ -17,6 +17,7 @@ package io.bitsquare.gui.main.dao; +import io.bitsquare.btc.wallet.SquWalletService; import io.bitsquare.gui.Navigation; import io.bitsquare.gui.common.model.Activatable; import io.bitsquare.gui.common.view.*; @@ -45,15 +46,17 @@ public class DaoView extends ActivatableViewAndModel { private final ViewLoader viewLoader; private final Navigation navigation; private Preferences preferences; + private SquWalletService squWalletService; private Tab selectedTab; private TokenWalletView tokenWalletView; @Inject - private DaoView(CachingViewLoader viewLoader, Navigation navigation, Preferences preferences) { + private DaoView(CachingViewLoader viewLoader, Navigation navigation, Preferences preferences, SquWalletService squWalletService) { this.viewLoader = viewLoader; this.navigation = navigation; this.preferences = preferences; + this.squWalletService = squWalletService; } @Override @@ -87,6 +90,8 @@ public class DaoView extends ActivatableViewAndModel { navigation.addListener(navigationListener); root.getSelectionModel().selectedItemProperty().addListener(tabChangeListener); + squWalletService.requestSquUtxo(null, null); + if (navigation.getCurrentPath().size() == 2 && navigation.getCurrentPath().get(1) == DaoView.class) { Tab selectedItem = root.getSelectionModel().getSelectedItem(); if (selectedItem == tokenWalletTab) diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.fxml index 6bb3cc0758..f8e6aed488 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.fxml @@ -18,21 +18,14 @@ --> - - - - - + + + + + - - - - - - - \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.java index 87b390651a..1d1745894e 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/ProposalsView.java @@ -17,44 +17,171 @@ package io.bitsquare.gui.main.dao.proposals; -import io.bitsquare.gui.common.model.Activatable; -import io.bitsquare.gui.common.view.ActivatableView; -import io.bitsquare.gui.common.view.FxmlView; -import io.bitsquare.gui.util.BSFormatter; -import javafx.scene.layout.GridPane; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.view.*; +import io.bitsquare.gui.main.MainView; +import io.bitsquare.gui.main.dao.DaoView; +import io.bitsquare.gui.main.dao.proposals.active.ActiveProposalsView; +import io.bitsquare.gui.main.dao.proposals.create.CreateProposalView; +import io.bitsquare.gui.main.dao.proposals.past.PastProposalsView; +import io.bitsquare.gui.util.Colors; +import javafx.beans.value.ChangeListener; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Paint; import javax.inject.Inject; @FxmlView -public class ProposalsView extends ActivatableView { +public class ProposalsView extends ActivatableViewAndModel { + private final ViewLoader viewLoader; + private final Navigation navigation; - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, initialisation - /////////////////////////////////////////////////////////////////////////////////////////// + private MenuItem create, active, past; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; @Inject - public ProposalsView(BSFormatter formatter) { - super(); + private ProposalsView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; } @Override public void initialize() { - } + listener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(ProposalsView.class) != 2) + return; + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + ToggleGroup toggleGroup = new ToggleGroup(); + create = new MenuItem(navigation, toggleGroup, "Create Proposal", CreateProposalView.class, AwesomeIcon.EDIT); + active = new MenuItem(navigation, toggleGroup, "Active proposals", ActiveProposalsView.class, AwesomeIcon.ARROW_RIGHT); + past = new MenuItem(navigation, toggleGroup, "Past proposals", PastProposalsView.class, AwesomeIcon.LIST); + leftVBox.getChildren().addAll(create, active, past); + } @Override protected void activate() { + create.activate(); + active.activate(); + past.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(ProposalsView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = CreateProposalView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(ProposalsView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } } @Override protected void deactivate() { + navigation.removeListener(listener); + + create.deactivate(); + active.deactivate(); + past.deactivate(); } + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); - /////////////////////////////////////////////////////////////////////////////////////////// - // Initialize - /////////////////////////////////////////////////////////////////////////////////////////// - + if (view instanceof CreateProposalView) create.setSelected(true); + else if (view instanceof ActiveProposalsView) active.setSelected(true); + else if (view instanceof PastProposalsView) past.setSelected(true); + } + public Class getSelectedViewClass() { + return selectedViewClass; + } } + + +class MenuItem extends ToggleButton { + + private final ChangeListener selectedPropertyChangeListener; + private final ChangeListener disablePropertyChangeListener; + private final Navigation navigation; + private final Class viewClass; + + MenuItem(Navigation navigation, ToggleGroup toggleGroup, String title, Class viewClass, AwesomeIcon awesomeIcon) { + this.navigation = navigation; + this.viewClass = viewClass; + + setToggleGroup(toggleGroup); + setText(title); + setId("account-settings-item-background-active"); + setPrefHeight(40); + setPrefWidth(240); + setAlignment(Pos.CENTER_LEFT); + + Label icon = new Label(); + AwesomeDude.setIcon(icon, awesomeIcon); + icon.setTextFill(Paint.valueOf("#333")); + icon.setPadding(new Insets(0, 5, 0, 0)); + icon.setAlignment(Pos.CENTER); + icon.setMinWidth(25); + icon.setMaxWidth(25); + setGraphic(icon); + + selectedPropertyChangeListener = (ov, oldValue, newValue) -> { + if (newValue) { + setId("account-settings-item-background-selected"); + icon.setTextFill(Colors.BLUE); + } else { + setId("account-settings-item-background-active"); + icon.setTextFill(Paint.valueOf("#333")); + } + }; + + disablePropertyChangeListener = (ov, oldValue, newValue) -> { + if (newValue) { + setId("account-settings-item-background-disabled"); + icon.setTextFill(Paint.valueOf("#ccc")); + } else { + setId("account-settings-item-background-active"); + icon.setTextFill(Paint.valueOf("#333")); + } + }; + } + + public void activate() { + setOnAction((event) -> navigation.navigateTo(MainView.class, DaoView.class, ProposalsView.class, viewClass)); + selectedProperty().addListener(selectedPropertyChangeListener); + disableProperty().addListener(disablePropertyChangeListener); + } + + public void deactivate() { + setOnAction(null); + selectedProperty().removeListener(selectedPropertyChangeListener); + disableProperty().removeListener(disablePropertyChangeListener); + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.fxml new file mode 100644 index 0000000000..5e11e28aa3 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.java new file mode 100644 index 0000000000..60f7571a7d --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/active/ActiveProposalsView.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.proposals.active; + +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import javafx.scene.layout.GridPane; + +import javax.inject.Inject; + +@FxmlView +public class ActiveProposalsView extends ActivatableView { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private ActiveProposalsView() { + } + + @Override + public void initialize() { + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } + +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.fxml new file mode 100644 index 0000000000..09ddd5369c --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.java new file mode 100644 index 0000000000..770fdc8045 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/create/CreateProposalView.java @@ -0,0 +1,195 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.proposals.create; + +import com.google.common.util.concurrent.FutureCallback; +import io.bitsquare.btc.InsufficientFundsException; +import io.bitsquare.btc.exceptions.TransactionVerificationException; +import io.bitsquare.btc.exceptions.WalletException; +import io.bitsquare.btc.provider.fee.FeeService; +import io.bitsquare.btc.wallet.BtcWalletService; +import io.bitsquare.btc.wallet.ChangeBelowDustException; +import io.bitsquare.btc.wallet.SquWalletService; +import io.bitsquare.common.ByteArrayUtils; +import io.bitsquare.common.crypto.KeyRing; +import io.bitsquare.dao.proposals.Proposal; +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.components.InputTextField; +import io.bitsquare.gui.main.overlays.popups.Popup; +import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.gui.util.Layout; +import io.bitsquare.p2p.NodeAddress; +import io.bitsquare.p2p.P2PService; +import javafx.scene.control.Button; +import javafx.scene.layout.GridPane; +import org.bitcoinj.core.Coin; +import org.bitcoinj.core.InsufficientMoneyException; +import org.bitcoinj.core.Sha256Hash; +import org.bitcoinj.core.Transaction; +import org.bitcoinj.crypto.DeterministicKey; +import org.bitcoinj.wallet.KeyChain; +import org.jetbrains.annotations.NotNull; + +import javax.annotation.Nullable; +import javax.inject.Inject; +import java.security.PublicKey; +import java.util.Date; +import java.util.UUID; + +import static com.google.common.base.Preconditions.checkNotNull; +import static io.bitsquare.gui.util.FormBuilder.*; + +@FxmlView +public class CreateProposalView extends ActivatableView { + + private final NodeAddress nodeAddress; + private final PublicKey p2pStorageSignaturePubKey; + private InputTextField nameTextField, titleTextField, categoryTextField, descriptionTextField, linkTextField, + startDateTextField, endDateTextField, requestedBTCTextField, btcAddressTextField; + private Button createButton; + + private final SquWalletService squWalletService; + private final BtcWalletService btcWalletService; + private FeeService feeService; + private final P2PService p2PService; + private final BSFormatter btcFormatter; + private int gridRow = 0; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private CreateProposalView(SquWalletService squWalletService, BtcWalletService btcWalletService, FeeService feeService, P2PService p2PService, KeyRing keyRing, BSFormatter btcFormatter) { + this.squWalletService = squWalletService; + this.btcWalletService = btcWalletService; + this.feeService = feeService; + this.p2PService = p2PService; + this.btcFormatter = btcFormatter; + + nodeAddress = p2PService.getAddress(); + p2pStorageSignaturePubKey = keyRing.getPubKeyRing().getSignaturePubKey(); + } + + + @Override + public void initialize() { + addTitledGroupBg(root, gridRow, 9, "Create new funding proposal"); + nameTextField = addLabelInputTextField(root, gridRow, "Name/nickname:", Layout.FIRST_ROW_DISTANCE).second; + titleTextField = addLabelInputTextField(root, ++gridRow, "Title:").second; + categoryTextField = addLabelInputTextField(root, ++gridRow, "Category:").second; + descriptionTextField = addLabelInputTextField(root, ++gridRow, "Description:").second; + linkTextField = addLabelInputTextField(root, ++gridRow, "Link to detail info:").second; + startDateTextField = addLabelInputTextField(root, ++gridRow, "Start date:").second; + endDateTextField = addLabelInputTextField(root, ++gridRow, "Delivery date:").second; + requestedBTCTextField = addLabelInputTextField(root, ++gridRow, "Requested funds in BTC:").second; + btcAddressTextField = addLabelInputTextField(root, ++gridRow, "Bitcoin address:").second; + createButton = addButtonAfterGroup(root, ++gridRow, "Create proposal"); + + //TODO + nameTextField.setText("Mock name"); + titleTextField.setText("Mock Title"); + categoryTextField.setText("Mock Category"); + descriptionTextField.setText("Mock Description"); + linkTextField.setText("Mock Link"); + startDateTextField.setText("Mock Start date"); + endDateTextField.setText("Mock Delivery date"); + requestedBTCTextField.setText("Mock Requested funds"); + btcAddressTextField.setText("Mock Bitcoin address"); + } + + @Override + protected void activate() { + createButton.setOnAction(event -> { + DeterministicKey squKeyPair = squWalletService.getWallet().freshKey(KeyChain.KeyPurpose.AUTHENTICATION); + checkNotNull(squKeyPair, "squKeyPair must not be null"); + + //TODO + Date startDate = new Date(); + Date endDate = new Date(); + + Proposal proposal = new Proposal(UUID.randomUUID().toString(), + nameTextField.getText(), + titleTextField.getText(), + categoryTextField.getText(), + descriptionTextField.getText(), + linkTextField.getText(), + startDate, + endDate, + btcFormatter.parseToCoin(requestedBTCTextField.getText()), + btcAddressTextField.getText(), + nodeAddress, + p2pStorageSignaturePubKey, + squKeyPair.getPubKey() + ); + Sha256Hash hash = Sha256Hash.of(ByteArrayUtils.objectToByteArray(proposal)); + proposal.setSignature(squKeyPair.sign(hash).encodeToDER()); + hash = Sha256Hash.of(ByteArrayUtils.objectToByteArray(proposal)); + proposal.setHash(hash.getBytes()); + + try { + Coin createProposalFee = feeService.getCreateProposalFee(); + Transaction preparedSendTx = squWalletService.getPreparedProposalFeeTx(createProposalFee); + Transaction txWithBtcFee = btcWalletService.addInputAndOutputToPreparedSquProposalFeeTx(preparedSendTx, hash.getBytes()); + squWalletService.signAndBroadcastProposalFeeTx(txWithBtcFee, new FutureCallback() { + @Override + public void onSuccess(@Nullable Transaction result) { + checkNotNull(result, "Transaction must not be null at signAndBroadcastProposalFeeTx callback."); + publishToP2PNetwork(proposal); + clearForm(); + new Popup<>().confirmation("Your proposal has been successfully published.").show(); + } + + @Override + public void onFailure(@NotNull Throwable t) { + log.error(t.toString()); + new Popup<>().warning(t.toString()).show(); + } + }); + } catch (InsufficientFundsException | + TransactionVerificationException | WalletException | InsufficientMoneyException | ChangeBelowDustException e) { + log.error(e.toString()); + e.printStackTrace(); + new Popup<>().warning(e.toString()).show(); + } + }); + } + + private void clearForm() { + nameTextField.setText(""); + titleTextField.setText(""); + categoryTextField.setText(""); + descriptionTextField.setText(""); + linkTextField.setText(""); + startDateTextField.setText(""); + endDateTextField.setText(""); + requestedBTCTextField.setText(""); + btcAddressTextField.setText(""); + } + + private void publishToP2PNetwork(Proposal proposal) { + p2PService.addData(proposal, true); + } + + @Override + protected void deactivate() { + createButton.setOnAction(null); + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.fxml new file mode 100644 index 0000000000..14b3d354d4 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.java new file mode 100644 index 0000000000..634cc6fbca --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/proposals/past/PastProposalsView.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.proposals.past; + +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import javafx.scene.layout.GridPane; + +import javax.inject.Inject; + +@FxmlView +public class PastProposalsView extends ActivatableView { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private PastProposalsView() { + } + + @Override + public void initialize() { + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.fxml index 120e65aa78..0c6b56973b 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.fxml @@ -18,21 +18,14 @@ --> - - - - - + + + + + - - - - - - - \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.java index 6ae5716c19..d5fafd3dcf 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/VotingView.java @@ -17,44 +17,171 @@ package io.bitsquare.gui.main.dao.voting; -import io.bitsquare.gui.common.model.Activatable; -import io.bitsquare.gui.common.view.ActivatableView; -import io.bitsquare.gui.common.view.FxmlView; -import io.bitsquare.gui.util.BSFormatter; -import javafx.scene.layout.GridPane; +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bitsquare.gui.Navigation; +import io.bitsquare.gui.common.view.*; +import io.bitsquare.gui.main.MainView; +import io.bitsquare.gui.main.dao.DaoView; +import io.bitsquare.gui.main.dao.voting.dashboard.VotingDashboardView; +import io.bitsquare.gui.main.dao.voting.history.VotingHistoryView; +import io.bitsquare.gui.main.dao.voting.vote.VoteView; +import io.bitsquare.gui.util.Colors; +import javafx.beans.value.ChangeListener; +import javafx.fxml.FXML; +import javafx.geometry.Insets; +import javafx.geometry.Pos; +import javafx.scene.control.Label; +import javafx.scene.control.ToggleButton; +import javafx.scene.control.ToggleGroup; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.VBox; +import javafx.scene.paint.Paint; import javax.inject.Inject; @FxmlView -public class VotingView extends ActivatableView { +public class VotingView extends ActivatableViewAndModel { + private final ViewLoader viewLoader; + private final Navigation navigation; - /////////////////////////////////////////////////////////////////////////////////////////// - // Constructor, initialisation - /////////////////////////////////////////////////////////////////////////////////////////// + private MenuItem dashboard, vote, history; + private Navigation.Listener listener; + + @FXML + private VBox leftVBox; + @FXML + private AnchorPane content; + + private Class selectedViewClass; @Inject - public VotingView(BSFormatter formatter) { - super(); + private VotingView(CachingViewLoader viewLoader, Navigation navigation) { + this.viewLoader = viewLoader; + this.navigation = navigation; } @Override public void initialize() { - } + listener = viewPath -> { + if (viewPath.size() != 4 || viewPath.indexOf(VotingView.class) != 2) + return; + selectedViewClass = viewPath.tip(); + loadView(selectedViewClass); + }; + + ToggleGroup toggleGroup = new ToggleGroup(); + dashboard = new MenuItem(navigation, toggleGroup, "Dashboard", VotingDashboardView.class, AwesomeIcon.DASHBOARD); + vote = new MenuItem(navigation, toggleGroup, "Vote", VoteView.class, AwesomeIcon.EDIT); + history = new MenuItem(navigation, toggleGroup, "History", VotingHistoryView.class, AwesomeIcon.TABLE); + leftVBox.getChildren().addAll(dashboard, vote, history); + } @Override protected void activate() { + dashboard.activate(); + vote.activate(); + history.activate(); + + navigation.addListener(listener); + ViewPath viewPath = navigation.getCurrentPath(); + if (viewPath.size() == 3 && viewPath.indexOf(VotingView.class) == 2 || + viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { + if (selectedViewClass == null) + selectedViewClass = VotingDashboardView.class; + + loadView(selectedViewClass); + + } else if (viewPath.size() == 4 && viewPath.indexOf(VotingView.class) == 2) { + selectedViewClass = viewPath.get(3); + loadView(selectedViewClass); + } } @Override protected void deactivate() { + navigation.removeListener(listener); + + dashboard.deactivate(); + vote.deactivate(); + history.deactivate(); } + private void loadView(Class viewClass) { + View view = viewLoader.load(viewClass); + content.getChildren().setAll(view.getRoot()); - /////////////////////////////////////////////////////////////////////////////////////////// - // Initialize - /////////////////////////////////////////////////////////////////////////////////////////// - + if (view instanceof VotingDashboardView) dashboard.setSelected(true); + else if (view instanceof VoteView) vote.setSelected(true); + else if (view instanceof VotingHistoryView) history.setSelected(true); + } + public Class getSelectedViewClass() { + return selectedViewClass; + } } + + +class MenuItem extends ToggleButton { + + private final ChangeListener selectedPropertyChangeListener; + private final ChangeListener disablePropertyChangeListener; + private final Navigation navigation; + private final Class viewClass; + + MenuItem(Navigation navigation, ToggleGroup toggleGroup, String title, Class viewClass, AwesomeIcon awesomeIcon) { + this.navigation = navigation; + this.viewClass = viewClass; + + setToggleGroup(toggleGroup); + setText(title); + setId("account-settings-item-background-active"); + setPrefHeight(40); + setPrefWidth(240); + setAlignment(Pos.CENTER_LEFT); + + Label icon = new Label(); + AwesomeDude.setIcon(icon, awesomeIcon); + icon.setTextFill(Paint.valueOf("#333")); + icon.setPadding(new Insets(0, 5, 0, 0)); + icon.setAlignment(Pos.CENTER); + icon.setMinWidth(25); + icon.setMaxWidth(25); + setGraphic(icon); + + selectedPropertyChangeListener = (ov, oldValue, newValue) -> { + if (newValue) { + setId("account-settings-item-background-selected"); + icon.setTextFill(Colors.BLUE); + } else { + setId("account-settings-item-background-active"); + icon.setTextFill(Paint.valueOf("#333")); + } + }; + + disablePropertyChangeListener = (ov, oldValue, newValue) -> { + if (newValue) { + setId("account-settings-item-background-disabled"); + icon.setTextFill(Paint.valueOf("#ccc")); + } else { + setId("account-settings-item-background-active"); + icon.setTextFill(Paint.valueOf("#333")); + } + }; + } + + public void activate() { + setOnAction((event) -> navigation.navigateTo(MainView.class, DaoView.class, VotingView.class, viewClass)); + selectedProperty().addListener(selectedPropertyChangeListener); + disableProperty().addListener(disablePropertyChangeListener); + } + + public void deactivate() { + setOnAction(null); + selectedProperty().removeListener(selectedPropertyChangeListener); + disableProperty().removeListener(disablePropertyChangeListener); + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.fxml new file mode 100644 index 0000000000..daaaf1d8ed --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.fxml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.java new file mode 100644 index 0000000000..ef72fc8aaa --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/dashboard/VotingDashboardView.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.voting.dashboard; + +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import javafx.scene.layout.GridPane; + +import javax.inject.Inject; + +@FxmlView +public class VotingDashboardView extends ActivatableView { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private VotingDashboardView() { + } + + @Override + public void initialize() { + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.fxml new file mode 100644 index 0000000000..33aa591992 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.java new file mode 100644 index 0000000000..28a6b38a34 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/history/VotingHistoryView.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.voting.history; + +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import javafx.scene.layout.GridPane; + +import javax.inject.Inject; + +@FxmlView +public class VotingHistoryView extends ActivatableView { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private VotingHistoryView() { + } + + @Override + public void initialize() { + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.fxml b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.fxml new file mode 100644 index 0000000000..266aa58f77 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.fxml @@ -0,0 +1,31 @@ + + + + + + + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.java new file mode 100644 index 0000000000..8108ad10b6 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/voting/vote/VoteView.java @@ -0,0 +1,50 @@ +/* + * This file is part of Bitsquare. + * + * Bitsquare 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. + * + * Bitsquare 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 Bitsquare. If not, see . + */ + +package io.bitsquare.gui.main.dao.voting.vote; + +import io.bitsquare.gui.common.view.ActivatableView; +import io.bitsquare.gui.common.view.FxmlView; +import javafx.scene.layout.GridPane; + +import javax.inject.Inject; + +@FxmlView +public class VoteView extends ActivatableView { + + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + + @Inject + private VoteView() { + } + + @Override + public void initialize() { + } + + @Override + protected void activate() { + } + + @Override + protected void deactivate() { + } +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/dao/wallet/TokenWalletView.java b/gui/src/main/java/io/bitsquare/gui/main/dao/wallet/TokenWalletView.java index 9417db26a7..bb42aaf12f 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/dao/wallet/TokenWalletView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/dao/wallet/TokenWalletView.java @@ -93,7 +93,7 @@ public class TokenWalletView extends ActivatableViewAndModel { if (viewPath.size() == 3 && viewPath.indexOf(TokenWalletView.class) == 2 || viewPath.size() == 2 && viewPath.indexOf(DaoView.class) == 1) { if (selectedViewClass == null) - selectedViewClass = TokenSendView.class; + selectedViewClass = TokenDashboardView.class; loadView(selectedViewClass);