From c70df863d6d239ed4be8da6e74391112aea7feb7 Mon Sep 17 00:00:00 2001 From: Manfred Karrer Date: Wed, 3 Feb 2016 15:43:55 +0100 Subject: [PATCH] Improve tx and withdrawal screens, make multiple address selection possible at withdrawal --- .../bitsquare/common/crypto/PubKeyRing.java | 4 +- .../io/bitsquare/arbitration/Dispute.java | 10 + .../bitsquare/arbitration/DisputeManager.java | 4 +- .../btc/AddressBasedCoinSelector.java | 39 +- .../java/io/bitsquare/btc/WalletService.java | 95 +++- .../main/java/io/bitsquare/trade/Trade.java | 29 +- .../trade/protocol/trade/ProcessModel.java | 11 - .../tasks/taker/BroadcastTakeOfferFeeTx.java | 2 +- .../tasks/taker/CreateTakeOfferFeeTx.java | 2 +- .../tasks/taker/SendPayDepositRequest.java | 9 +- .../tasks/taker/VerifyAndSignContract.java | 4 +- .../main/java/io/bitsquare/gui/bitsquare.css | 25 + .../AddressWithIconAndDirection.java | 72 +++ .../gui/components/HyperlinkWithIcon.java | 41 ++ gui/src/main/java/io/bitsquare/gui/images.css | 4 + .../{trader => }/DisputeSummaryPopup.java | 2 +- .../arbitrator/ArbitratorDisputeView.java | 2 +- .../disputes/trader/TraderDisputeView.java | 1 + .../main/funds/reserved/ReservedListItem.java | 5 + .../gui/main/funds/reserved/ReservedView.fxml | 6 + .../gui/main/funds/reserved/ReservedView.java | 25 +- .../transactions/TransactionsListItem.java | 113 +++-- .../funds/transactions/TransactionsView.fxml | 15 +- .../funds/transactions/TransactionsView.java | 209 ++++++++- .../funds/withdrawal/WithdrawalListItem.java | 84 +--- .../main/funds/withdrawal/WithdrawalView.fxml | 12 +- .../main/funds/withdrawal/WithdrawalView.java | 443 +++++++++++------- .../createoffer/CreateOfferDataModel.java | 1 - .../gui/popups/TradeDetailsPopup.java | 15 +- gui/src/main/resources/images/link.png | Bin 0 -> 338 bytes gui/src/main/resources/images/link@2x.png | Bin 0 -> 587 bytes 31 files changed, 930 insertions(+), 354 deletions(-) create mode 100644 gui/src/main/java/io/bitsquare/gui/components/AddressWithIconAndDirection.java create mode 100644 gui/src/main/java/io/bitsquare/gui/components/HyperlinkWithIcon.java rename gui/src/main/java/io/bitsquare/gui/main/disputes/{trader => }/DisputeSummaryPopup.java (99%) create mode 100644 gui/src/main/resources/images/link.png create mode 100644 gui/src/main/resources/images/link@2x.png diff --git a/common/src/main/java/io/bitsquare/common/crypto/PubKeyRing.java b/common/src/main/java/io/bitsquare/common/crypto/PubKeyRing.java index a51f12d3d6..cce1ea47ad 100644 --- a/common/src/main/java/io/bitsquare/common/crypto/PubKeyRing.java +++ b/common/src/main/java/io/bitsquare/common/crypto/PubKeyRing.java @@ -99,8 +99,8 @@ public class PubKeyRing implements Serializable { @Override public String toString() { return "PubKeyRing{" + - "signaturePubKey.hashCode()=\n" + signaturePubKey.hashCode() + - "encryptionPubKey.hashCode()=\n" + encryptionPubKey.hashCode() + + "signaturePubKey.hashCode()=" + signaturePubKey.hashCode() + + ", encryptionPubKey.hashCode()=" + encryptionPubKey.hashCode() + '}'; } diff --git a/core/src/main/java/io/bitsquare/arbitration/Dispute.java b/core/src/main/java/io/bitsquare/arbitration/Dispute.java index 528e804203..39d9ce848e 100644 --- a/core/src/main/java/io/bitsquare/arbitration/Dispute.java +++ b/core/src/main/java/io/bitsquare/arbitration/Dispute.java @@ -25,6 +25,7 @@ import io.bitsquare.trade.Contract; import javafx.beans.property.*; import javafx.collections.FXCollections; import javafx.collections.ObservableList; +import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -73,6 +74,7 @@ public class Dispute implements Serializable { private boolean isClosed; private DisputeResult disputeResult; + private Transaction disputePayoutTx; transient private Storage> storage; transient private ObservableList disputeDirectMessagesAsObservableList = FXCollections.observableArrayList(disputeDirectMessages); @@ -166,6 +168,11 @@ public class Dispute implements Serializable { storage.queueUpForSave(); } + public void setDisputePayoutTx(Transaction disputePayoutTx) { + this.disputePayoutTx = disputePayoutTx; + } + + /////////////////////////////////////////////////////////////////////////////////////////// // Getters /////////////////////////////////////////////////////////////////////////////////////////// @@ -266,6 +273,9 @@ public class Dispute implements Serializable { return new Date(tradeDate); } + public Transaction getDisputePayoutTx() { + return disputePayoutTx; + } @Override public boolean equals(Object o) { diff --git a/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java b/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java index b21bbe5ebd..950d721ef4 100644 --- a/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java +++ b/core/src/main/java/io/bitsquare/arbitration/DisputeManager.java @@ -510,6 +510,7 @@ public class DisputeManager { // after successful publish we send peer the tx + dispute.setDisputePayoutTx(transaction); sendPeerPublishedPayoutTxMessage(transaction, dispute, contract); } @@ -546,7 +547,8 @@ public class DisputeManager { // losing trader or in case of 50/50 the seller gets the tx sent from the winner or buyer private void onDisputedPayoutTxMessage(PeerPublishedPayoutTxMessage peerPublishedPayoutTxMessage) { - tradeWalletService.addTransactionToWallet(peerPublishedPayoutTxMessage.transaction); + Transaction transaction = tradeWalletService.addTransactionToWallet(peerPublishedPayoutTxMessage.transaction); + findOwnDispute(peerPublishedPayoutTxMessage.tradeId).ifPresent(dispute -> dispute.setDisputePayoutTx(transaction)); } diff --git a/core/src/main/java/io/bitsquare/btc/AddressBasedCoinSelector.java b/core/src/main/java/io/bitsquare/btc/AddressBasedCoinSelector.java index 30f49ec79e..22f61487d2 100644 --- a/core/src/main/java/io/bitsquare/btc/AddressBasedCoinSelector.java +++ b/core/src/main/java/io/bitsquare/btc/AddressBasedCoinSelector.java @@ -24,11 +24,9 @@ import org.bitcoinj.wallet.CoinSelector; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import javax.annotation.Nullable; import java.math.BigInteger; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; +import java.util.*; import static com.google.common.base.Preconditions.checkNotNull; @@ -39,7 +37,10 @@ import static com.google.common.base.Preconditions.checkNotNull; class AddressBasedCoinSelector implements CoinSelector { private static final Logger log = LoggerFactory.getLogger(AddressBasedCoinSelector.class); private final NetworkParameters params; - private final AddressEntry addressEntry; + @Nullable + private Set addressEntries; + @Nullable + private AddressEntry addressEntry; /////////////////////////////////////////////////////////////////////////////////////////// // Constructor @@ -50,6 +51,11 @@ class AddressBasedCoinSelector implements CoinSelector { this.addressEntry = addressEntry; } + public AddressBasedCoinSelector(NetworkParameters params, Set addressEntries) { + this.params = params; + this.addressEntries = addressEntries; + } + @VisibleForTesting static void sortOutputs(ArrayList outputs) { Collections.sort(outputs, (a, b) -> { @@ -94,15 +100,26 @@ class AddressBasedCoinSelector implements CoinSelector { if (transactionOutput.getScriptPubKey().isSentToAddress() || transactionOutput.getScriptPubKey().isPayToScriptHash ()) { Address addressOutput = transactionOutput.getScriptPubKey().getToAddress(params); - log.trace("matchesRequiredAddress?"); + log.trace("matchesRequiredAddress(es)?"); log.trace(addressOutput.toString()); - log.trace(addressEntry.getAddress().toString()); + if (addressEntry != null && addressEntry.getAddress() != null) { + log.trace(addressEntry.getAddress().toString()); + if (addressOutput.equals(addressEntry.getAddress())) + return true; + else { + log.trace("No match found at matchesRequiredAddress addressOutput / addressEntry " + addressOutput.toString + () + " / " + addressEntry.getAddress().toString()); + } + } else if (addressEntries != null) { + log.trace(addressEntries.toString()); + for (AddressEntry entry : addressEntries) { + if (addressOutput.equals(entry.getAddress())) + return true; + } - if (addressOutput.equals(addressEntry.getAddress())) { - return true; + log.trace("No match found at matchesRequiredAddress addressOutput / addressEntries " + addressOutput.toString + () + " / " + addressEntries.toString()); } - log.trace("No match found at matchesRequiredAddress addressOutput / addressEntry " + addressOutput.toString - () + " / " + addressEntry.getAddress().toString()); } return false; } diff --git a/core/src/main/java/io/bitsquare/btc/WalletService.java b/core/src/main/java/io/bitsquare/btc/WalletService.java index a265759f0c..e23dc50be9 100644 --- a/core/src/main/java/io/bitsquare/btc/WalletService.java +++ b/core/src/main/java/io/bitsquare/btc/WalletService.java @@ -56,6 +56,8 @@ import java.util.concurrent.TimeoutException; import java.util.function.Consumer; import java.util.stream.Collectors; +import static com.google.common.base.Preconditions.checkNotNull; + /** * WalletService handles all non trade specific wallet and bitcoin related services. * It startup the wallet app kit and initialized the wallet. @@ -325,7 +327,9 @@ public class WalletService { } public AddressEntry getAddressEntryByOfferId(String offerId) { - Optional addressEntry = getAddressEntryList().stream().filter(e -> offerId.equals(e.getOfferId())).findFirst(); + Optional addressEntry = getAddressEntryList().stream() + .filter(e -> offerId.equals(e.getOfferId())) + .findAny(); if (addressEntry.isPresent()) return addressEntry.get(); else @@ -333,7 +337,9 @@ public class WalletService { } private Optional getAddressEntryByAddress(String address) { - return getAddressEntryList().stream().filter(e -> address.equals(e.getAddressString())).findFirst(); + return getAddressEntryList().stream() + .filter(e -> e.getAddressString() != null && e.getAddressString().equals(address)) + .findAny(); } @@ -450,7 +456,8 @@ public class WalletService { public Coin getRequiredFee(String fromAddress, String toAddress, Coin amount, - KeyParameter aesKey) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException { + @Nullable KeyParameter aesKey) throws AddressFormatException, IllegalArgumentException, + InsufficientMoneyException { Coin fee; try { wallet.completeTx(getSendRequest(fromAddress, toAddress, amount, aesKey)); @@ -463,10 +470,28 @@ public class WalletService { return fee; } + public Coin getRequiredFeeForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin amount, + @Nullable KeyParameter aesKey) throws AddressFormatException, + IllegalArgumentException, InsufficientMoneyException { + Coin fee; + try { + wallet.completeTx(getSendRequestForMultipleAddresses(fromAddresses, toAddress, amount, null, aesKey)); + fee = Coin.ZERO; + } catch (InsufficientMoneyException e) { + log.info("The amount to be transferred is not enough to pay the transaction fees of {}. " + + "We subtract that fee from the receivers amount to make the transaction possible."); + fee = e.missing; + } + return fee; + } + public Wallet.SendRequest getSendRequest(String fromAddress, String toAddress, Coin amount, - @Nullable KeyParameter aesKey) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException { + @Nullable KeyParameter aesKey) throws AddressFormatException, + IllegalArgumentException, InsufficientMoneyException { Transaction tx = new Transaction(params); Preconditions.checkArgument(Restrictions.isAboveDust(amount), "You cannot send an amount which are smaller than 546 satoshis."); @@ -485,11 +510,53 @@ public class WalletService { return sendRequest; } + public Wallet.SendRequest getSendRequestForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin amount, + @Nullable String changeAddress, + @Nullable KeyParameter aesKey) throws + AddressFormatException, IllegalArgumentException, InsufficientMoneyException { + Transaction tx = new Transaction(params); + Preconditions.checkArgument(Restrictions.isAboveDust(amount), + "You cannot send an amount which are smaller than 546 satoshis."); + tx.addOutput(amount, new Address(params, toAddress)); + + Wallet.SendRequest sendRequest = Wallet.SendRequest.forTx(tx); + sendRequest.aesKey = aesKey; + sendRequest.shuffleOutputs = false; + Set addressEntries = fromAddresses.stream() + .map(e -> getAddressEntryByAddress(e)) + .filter(e -> e.isPresent()) + .map(e -> e.get()).collect(Collectors.toSet()); + if (addressEntries.isEmpty()) + throw new IllegalArgumentException("No withdrawFromAddresses not found in our wallets.\n\t" + + "fromAddresses=" + fromAddresses); + + sendRequest.coinSelector = new AddressBasedCoinSelector(params, addressEntries); + Optional addressEntryOptional = Optional.empty(); + AddressEntry changeAddressAddressEntry = null; + if (changeAddress != null) + addressEntryOptional = getAddressEntryByAddress(changeAddress); + + if (addressEntryOptional.isPresent()) { + changeAddressAddressEntry = addressEntryOptional.get(); + } else { + ArrayList list = new ArrayList<>(addressEntries); + if (!list.isEmpty()) + changeAddressAddressEntry = list.get(0); + } + checkNotNull(changeAddressAddressEntry, "change address must not be null"); + sendRequest.changeAddress = changeAddressAddressEntry.getAddress(); + sendRequest.feePerKb = FeePolicy.getFeePerKb(); + return sendRequest; + } + public String sendFunds(String fromAddress, String toAddress, Coin amount, - KeyParameter aesKey, - FutureCallback callback) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException { + @Nullable KeyParameter aesKey, + FutureCallback callback) throws AddressFormatException, + IllegalArgumentException, InsufficientMoneyException { Coin fee = getRequiredFee(fromAddress, toAddress, amount, aesKey); Wallet.SendResult sendResult = wallet.sendCoins(getSendRequest(fromAddress, toAddress, amount.subtract(fee), aesKey)); Futures.addCallback(sendResult.broadcastComplete, callback); @@ -498,6 +565,22 @@ public class WalletService { return sendResult.tx.getHashAsString(); } + public String sendFundsForMultipleAddresses(Set fromAddresses, + String toAddress, + Coin amount, + @Nullable String changeAddress, + @Nullable KeyParameter aesKey, + FutureCallback callback) throws AddressFormatException, + IllegalArgumentException, InsufficientMoneyException { + Coin fee = getRequiredFeeForMultipleAddresses(fromAddresses, toAddress, amount, aesKey); + Wallet.SendResult sendResult = wallet.sendCoins(getSendRequestForMultipleAddresses(fromAddresses, toAddress, + amount.subtract(fee), changeAddress, aesKey)); + Futures.addCallback(sendResult.broadcastComplete, callback); + + printTxWithInputs("sendFunds", sendResult.tx); + return sendResult.tx.getHashAsString(); + } + public void emptyWallet(String toAddress, KeyParameter aesKey, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) throws InsufficientMoneyException, AddressFormatException { Wallet.SendRequest sendRequest = Wallet.SendRequest.emptyWallet(new Address(params, toAddress)); diff --git a/core/src/main/java/io/bitsquare/trade/Trade.java b/core/src/main/java/io/bitsquare/trade/Trade.java index 0b87982fa7..99635d4ba4 100644 --- a/core/src/main/java/io/bitsquare/trade/Trade.java +++ b/core/src/main/java/io/bitsquare/trade/Trade.java @@ -21,6 +21,7 @@ import com.google.common.base.Throwables; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; +import io.bitsquare.app.Log; import io.bitsquare.app.Version; import io.bitsquare.arbitration.ArbitratorManager; import io.bitsquare.btc.FeePolicy; @@ -125,12 +126,6 @@ abstract public class Trade implements Tradable, Model, Serializable { TRADE_PERIOD_OVER } - // Mutable - private Coin tradeAmount; - private NodeAddress tradingPeerNodeAddress; - transient private ObjectProperty tradeAmountProperty; - transient private ObjectProperty tradeVolumeProperty; - /////////////////////////////////////////////////////////////////////////////////////////// // Fields @@ -152,7 +147,8 @@ abstract public class Trade implements Tradable, Model, Serializable { // Mutable private DecryptedMsgWithPubKey decryptedMsgWithPubKey; private Date takeOfferDate = new Date(0); // in some error cases the date is not set and cause null pointers, so we set a default - + private Coin tradeAmount; + private NodeAddress tradingPeerNodeAddress; protected State state; private DisputeState disputeState = DisputeState.NONE; private TradePeriodState tradePeriodState = TradePeriodState.NORMAL; @@ -172,6 +168,10 @@ abstract public class Trade implements Tradable, Model, Serializable { private boolean tradePeriodOverWarningDisplayed; private String errorMessage; transient private StringProperty errorMessageProperty; + transient private ObjectProperty tradeAmountProperty; + transient private ObjectProperty tradeVolumeProperty; + @Nullable + private Transaction takeOfferFeeTx; /////////////////////////////////////////////////////////////////////////////////////////// @@ -306,6 +306,7 @@ abstract public class Trade implements Tradable, Model, Serializable { } public void setDisputeState(DisputeState disputeState) { + Log.traceCall("disputeState=" + disputeState + "\n\ttrade=" + this); this.disputeState = disputeState; disputeStateProperty.set(disputeState); persist(); @@ -561,6 +562,13 @@ abstract public class Trade implements Tradable, Model, Serializable { return contractHash; } + public void setTakeOfferFeeTx(Transaction takeOfferFeeTx) { + this.takeOfferFeeTx = takeOfferFeeTx; + } + + public Transaction getTakeOfferFeeTx() { + return takeOfferFeeTx; + } /////////////////////////////////////////////////////////////////////////////////////////// // Private @@ -620,11 +628,10 @@ abstract public class Trade implements Tradable, Model, Serializable { "\n\tdisputeState=" + disputeState + "\n\ttradePeriodState=" + tradePeriodState + "\n\tdepositTx=" + depositTx + + "\n\ttakeOfferFeeTx.getHashAsString()=" + (takeOfferFeeTx != null ? takeOfferFeeTx.getHashAsString() : "") + "\n\tcontract=" + contract + - /* "\n\tcontractAsJson='" + contractAsJson + '\'' +*/ - /* "\n\tcontractHash=" + Arrays.toString(contractHash) +*/ - "\n\ttakerContractSignature.hashCode()='" + takerContractSignature.hashCode() + '\'' + - "\n\toffererContractSignature.hashCode()='" + offererContractSignature.hashCode() + '\'' + + "\n\ttakerContractSignature.hashCode()='" + (takerContractSignature != null ? takerContractSignature.hashCode() : "") + '\'' + + "\n\toffererContractSignature.hashCode()='" + (offererContractSignature != null ? offererContractSignature.hashCode() : "") + '\'' + "\n\tpayoutTx=" + payoutTx + "\n\tlockTimeAsBlockHeight=" + lockTimeAsBlockHeight + "\n\topenDisputeTimeAsBlockHeight=" + openDisputeTimeAsBlockHeight + diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java index 3033479414..e6fe12d32d 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/ProcessModel.java @@ -36,7 +36,6 @@ import io.bitsquare.trade.offer.Offer; import io.bitsquare.trade.offer.OpenOfferManager; import io.bitsquare.trade.protocol.trade.messages.TradeMessage; import io.bitsquare.user.User; -import org.bitcoinj.core.Transaction; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -68,7 +67,6 @@ public class ProcessModel implements Model, Serializable { transient private TradeMessage tradeMessage; private String takeOfferFeeTxId; private byte[] payoutTxSignature; - private Transaction takeOfferFeeTx; private List takerAcceptedArbitratorNodeAddresses; @@ -168,15 +166,6 @@ public class ProcessModel implements Model, Serializable { return tradeMessage; } - @Nullable - public Transaction getTakeOfferFeeTx() { - return takeOfferFeeTx; - } - - public void setTakeOfferFeeTx(Transaction takeOfferFeeTx) { - this.takeOfferFeeTx = takeOfferFeeTx; - } - public PaymentAccountContractData getPaymentAccountContractData(Trade trade) { if (trade instanceof OffererTrade) return user.getPaymentAccount(offer.getOffererPaymentAccountId()).getContractData(); diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/BroadcastTakeOfferFeeTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/BroadcastTakeOfferFeeTx.java index 821cf17813..247aebc5cd 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/BroadcastTakeOfferFeeTx.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/BroadcastTakeOfferFeeTx.java @@ -37,7 +37,7 @@ public class BroadcastTakeOfferFeeTx extends TradeTask { protected void run() { try { runInterceptHook(); - processModel.getTradeWalletService().broadcastTx(processModel.getTakeOfferFeeTx(), + processModel.getTradeWalletService().broadcastTx(trade.getTakeOfferFeeTx(), new FutureCallback() { @Override public void onSuccess(Transaction transaction) { diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java index 9f3eec61da..cae2540630 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/CreateTakeOfferFeeTx.java @@ -50,7 +50,7 @@ public class CreateTakeOfferFeeTx extends TradeTask { FeePolicy.getTakeOfferFee(), selectedArbitrator.getBtcAddress()); - processModel.setTakeOfferFeeTx(createTakeOfferFeeTx); + trade.setTakeOfferFeeTx(createTakeOfferFeeTx); // TODO check if needed as we have stored tx already at setTakeOfferFeeTx processModel.setTakeOfferFeeTxId(createTakeOfferFeeTx.getHashAsString()); diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java index e74d1862b2..3558a6495b 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/SendPayDepositRequest.java @@ -25,6 +25,8 @@ import io.bitsquare.trade.protocol.trade.tasks.TradeTask; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import static com.google.common.base.Preconditions.checkNotNull; + public class SendPayDepositRequest extends TradeTask { private static final Logger log = LoggerFactory.getLogger(SendPayDepositRequest.class); @@ -36,7 +38,8 @@ public class SendPayDepositRequest extends TradeTask { protected void run() { try { runInterceptHook(); - if (processModel.getTakeOfferFeeTx() != null) { + if (trade.getTakeOfferFeeTx() != null) { + checkNotNull(trade.getTradeAmount()); PayDepositRequest payDepositRequest = new PayDepositRequest( processModel.getMyAddress(), processModel.getId(), @@ -49,7 +52,7 @@ public class SendPayDepositRequest extends TradeTask { processModel.getPubKeyRing(), processModel.getPaymentAccountContractData(trade), processModel.getAccountId(), - processModel.getTakeOfferFeeTx().getHashAsString(), + trade.getTakeOfferFeeTx().getHashAsString(), processModel.getUser().getAcceptedArbitratorAddresses(), trade.getArbitratorNodeAddress() ); @@ -79,7 +82,7 @@ public class SendPayDepositRequest extends TradeTask { } ); } else { - log.error("processModel.getTakeOfferFeeTx() = " + processModel.getTakeOfferFeeTx()); + log.error("trade.getTakeOfferFeeTx() = " + trade.getTakeOfferFeeTx()); failed("TakeOfferFeeTx is null"); } } catch (Throwable t) { diff --git a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java index 56e0d5cc11..0060e6f27a 100644 --- a/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java +++ b/core/src/main/java/io/bitsquare/trade/protocol/trade/tasks/taker/VerifyAndSignContract.java @@ -42,7 +42,7 @@ public class VerifyAndSignContract extends TradeTask { try { runInterceptHook(); - if (processModel.getTakeOfferFeeTx() != null) { + if (trade.getTakeOfferFeeTx() != null) { TradingPeer offerer = processModel.tradingPeer; PaymentAccountContractData offererPaymentAccountContractData = offerer.getPaymentAccountContractData(); PaymentAccountContractData takerPaymentAccountContractData = processModel.getPaymentAccountContractData(trade); @@ -57,7 +57,7 @@ public class VerifyAndSignContract extends TradeTask { Contract contract = new Contract( processModel.getOffer(), trade.getTradeAmount(), - processModel.getTakeOfferFeeTx().getHashAsString(), + trade.getTakeOfferFeeTx().getHashAsString(), buyerNodeAddress, sellerNodeAddress, trade.getArbitratorNodeAddress(), diff --git a/gui/src/main/java/io/bitsquare/gui/bitsquare.css b/gui/src/main/java/io/bitsquare/gui/bitsquare.css index 01034ec8a7..6d844fe3e5 100644 --- a/gui/src/main/java/io/bitsquare/gui/bitsquare.css +++ b/gui/src/main/java/io/bitsquare/gui/bitsquare.css @@ -140,6 +140,31 @@ bg color of non edit textFields: fafafa -fx-text-fill: black; } +.external-link-icon { + -fx-text-fill: -fx-accent; + -fx-cursor: hand; +} + +.received-funds-icon { + -fx-text-fill: -bs-green; + -fx-cursor: hand; +} + +.received-funds-icon:hover { + -fx-text-fill: black; + -fx-cursor: hand; +} + +.sent-funds-icon { + -fx-text-fill: -bs-error-red; + -fx-cursor: hand; +} + +.sent-funds-icon:hover { + -fx-text-fill: black; + -fx-cursor: hand; +} + /******************************************************************************* * * * Tooltip * diff --git a/gui/src/main/java/io/bitsquare/gui/components/AddressWithIconAndDirection.java b/gui/src/main/java/io/bitsquare/gui/components/AddressWithIconAndDirection.java new file mode 100644 index 0000000000..9a3974bf17 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/AddressWithIconAndDirection.java @@ -0,0 +1,72 @@ +package io.bitsquare.gui.components; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.geometry.Insets; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class AddressWithIconAndDirection extends AnchorPane { + private static final Logger log = LoggerFactory.getLogger(AddressWithIconAndDirection.class); + private final Hyperlink hyperlink; + private final Label openLinkIcon; + private final Label directionIcon; + private final Label label; + + public AddressWithIconAndDirection(String text, String address, AwesomeIcon awesomeIcon, boolean received) { + directionIcon = new Label(); + directionIcon.setLayoutY(3); + directionIcon.getStyleClass().add(received ? "received-funds-icon" : "sent-funds-icon"); + AwesomeDude.setIcon(directionIcon, received ? AwesomeIcon.SIGNIN : AwesomeIcon.SIGNOUT); + directionIcon.setOpacity(0.7); + directionIcon.setMouseTransparent(true); + + HBox hBox = new HBox(); + hBox.setSpacing(-1); + label = new Label(text); + label.setMouseTransparent(true); + HBox.setMargin(label, new Insets(4, 0, 0, 0)); + HBox.setHgrow(label, Priority.ALWAYS); + + hyperlink = new Hyperlink(address); + HBox.setHgrow(hyperlink, Priority.SOMETIMES); + // You need to set max width to Double.MAX_VALUE to make HBox.setHgrow working like expected! + // also pref width needs to be not default (-1) + hyperlink.setMaxWidth(Double.MAX_VALUE); + hyperlink.setPrefWidth(0); + + hBox.getChildren().addAll(label, hyperlink); + + openLinkIcon = new Label(); + openLinkIcon.setLayoutY(3); + openLinkIcon.getStyleClass().add("external-link-icon"); + AwesomeDude.setIcon(openLinkIcon, awesomeIcon); + + AnchorPane.setLeftAnchor(directionIcon, 3.0); + AnchorPane.setTopAnchor(directionIcon, 2.0); + AnchorPane.setLeftAnchor(hBox, 20.0); + AnchorPane.setRightAnchor(hBox, 15.0); + AnchorPane.setRightAnchor(openLinkIcon, 4.0); + AnchorPane.setTopAnchor(openLinkIcon, 3.0); + + getChildren().addAll(directionIcon, hBox, openLinkIcon); + } + + public void setOnAction(EventHandler handler) { + hyperlink.setOnAction(handler); + openLinkIcon.setOnMouseClicked(e -> handler.handle(null)); + } + + public void setTooltip(Tooltip tooltip) { + hyperlink.setTooltip(tooltip); + openLinkIcon.setTooltip(tooltip); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/components/HyperlinkWithIcon.java b/gui/src/main/java/io/bitsquare/gui/components/HyperlinkWithIcon.java new file mode 100644 index 0000000000..4f7d345ef8 --- /dev/null +++ b/gui/src/main/java/io/bitsquare/gui/components/HyperlinkWithIcon.java @@ -0,0 +1,41 @@ +package io.bitsquare.gui.components; + +import de.jensd.fx.fontawesome.AwesomeDude; +import de.jensd.fx.fontawesome.AwesomeIcon; +import javafx.event.ActionEvent; +import javafx.event.EventHandler; +import javafx.scene.control.Hyperlink; +import javafx.scene.control.Label; +import javafx.scene.control.Tooltip; +import javafx.scene.layout.AnchorPane; + +public class HyperlinkWithIcon extends AnchorPane { + private final Hyperlink hyperlink; + private final Label openLinkIcon; + + public HyperlinkWithIcon(String text, AwesomeIcon awesomeIcon) { + hyperlink = new Hyperlink(text); + + openLinkIcon = new Label(); + openLinkIcon.setLayoutY(3); + openLinkIcon.getStyleClass().add("external-link-icon"); + AwesomeDude.setIcon(openLinkIcon, awesomeIcon); + + AnchorPane.setLeftAnchor(hyperlink, 0.0); + AnchorPane.setRightAnchor(hyperlink, 15.0); + AnchorPane.setRightAnchor(openLinkIcon, 4.0); + AnchorPane.setTopAnchor(openLinkIcon, 3.0); + + getChildren().addAll(hyperlink, openLinkIcon); + } + + public void setOnAction(EventHandler handler) { + hyperlink.setOnAction(handler); + openLinkIcon.setOnMouseClicked(e -> handler.handle(null)); + } + + public void setTooltip(Tooltip tooltip) { + hyperlink.setTooltip(tooltip); + openLinkIcon.setTooltip(tooltip); + } +} diff --git a/gui/src/main/java/io/bitsquare/gui/images.css b/gui/src/main/java/io/bitsquare/gui/images.css index 6ececd48b8..710e2f8fed 100644 --- a/gui/src/main/java/io/bitsquare/gui/images.css +++ b/gui/src/main/java/io/bitsquare/gui/images.css @@ -200,3 +200,7 @@ -fx-image: url("../../../images/bubble_arrow_blue_right.png"); } +#link { + -fx-image: url("../../../images/link.png"); +} + diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputeSummaryPopup.java similarity index 99% rename from gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java rename to gui/src/main/java/io/bitsquare/gui/main/disputes/DisputeSummaryPopup.java index 055bcd880a..b78135eb29 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/DisputeSummaryPopup.java +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/DisputeSummaryPopup.java @@ -15,7 +15,7 @@ * along with Bitsquare. If not, see . */ -package io.bitsquare.gui.main.disputes.trader; +package io.bitsquare.gui.main.disputes; import io.bitsquare.arbitration.Dispute; import io.bitsquare.arbitration.DisputeManager; diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java index b22d7c42a5..aac3ff1025 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/arbitrator/ArbitratorDisputeView.java @@ -21,7 +21,7 @@ import io.bitsquare.arbitration.Dispute; import io.bitsquare.arbitration.DisputeManager; import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.gui.common.view.FxmlView; -import io.bitsquare.gui.main.disputes.trader.DisputeSummaryPopup; +import io.bitsquare.gui.main.disputes.DisputeSummaryPopup; import io.bitsquare.gui.main.disputes.trader.TraderDisputeView; import io.bitsquare.gui.popups.ContractPopup; import io.bitsquare.gui.popups.TradeDetailsPopup; diff --git a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java index 208771781a..8b1b02ad0d 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/disputes/trader/TraderDisputeView.java @@ -28,6 +28,7 @@ import io.bitsquare.common.crypto.KeyRing; import io.bitsquare.gui.common.view.ActivatableView; import io.bitsquare.gui.common.view.FxmlView; import io.bitsquare.gui.components.TableGroupHeadline; +import io.bitsquare.gui.main.disputes.DisputeSummaryPopup; import io.bitsquare.gui.popups.ContractPopup; import io.bitsquare.gui.popups.Popup; import io.bitsquare.gui.popups.TradeDetailsPopup; diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedListItem.java b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedListItem.java index 026e8a6a29..a18d634715 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedListItem.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedListItem.java @@ -26,6 +26,8 @@ import io.bitsquare.gui.components.confidence.ConfidenceProgressIndicator; import io.bitsquare.gui.util.BSFormatter; import io.bitsquare.trade.Tradable; import io.bitsquare.trade.Trade; +import javafx.beans.property.SimpleStringProperty; +import javafx.beans.property.StringProperty; import javafx.scene.control.Label; import javafx.scene.control.Tooltip; import org.bitcoinj.core.Address; @@ -37,6 +39,7 @@ import org.slf4j.LoggerFactory; public class ReservedListItem { private final Logger log = LoggerFactory.getLogger(this.getClass()); + private final StringProperty date = new SimpleStringProperty(); private final BalanceListener balanceListener; private final Label balanceLabel; @@ -78,6 +81,8 @@ public class ReservedListItem { updateConfidence(walletService.getConfidenceForAddress(getAddress())); + //date.set(formatter.formatDateTime(transaction.getUpdateTime())); + // balance balanceLabel = new Label(); diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.fxml b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.fxml index b912234187..75b766f6a8 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.fxml @@ -18,6 +18,7 @@ --> + + + + + + diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.java b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.java index 09ab13bff8..01bb8d8a1f 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/reserved/ReservedView.java @@ -50,7 +50,7 @@ public class ReservedView extends ActivatableView { @FXML TableView table; @FXML - TableColumn detailsColumn, addressColumn, balanceColumn, confidenceColumn; + TableColumn dateColumn, detailsColumn, addressColumn, balanceColumn, confidenceColumn; private final WalletService walletService; private final TradeManager tradeManager; @@ -61,6 +61,11 @@ public class ReservedView extends ActivatableView { private final TradeDetailsPopup tradeDetailsPopup; private final ObservableList addressList = FXCollections.observableArrayList(); + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + @Inject private ReservedView(WalletService walletService, TradeManager tradeManager, OpenOfferManager openOfferManager, Preferences preferences, BSFormatter formatter, OfferDetailsPopup offerDetailsPopup, TradeDetailsPopup tradeDetailsPopup) { @@ -87,13 +92,13 @@ public class ReservedView extends ActivatableView { @Override protected void activate() { - fillList(); + updateList(); table.setItems(addressList); walletService.addBalanceListener(new BalanceListener() { @Override public void onBalanceChanged(Coin balance) { - fillList(); + updateList(); } }); } @@ -104,10 +109,13 @@ public class ReservedView extends ActivatableView { } - private void fillList() { + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateList() { addressList.forEach(ReservedListItem::cleanup); - addressList.clear(); - addressList.addAll(Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream()) + addressList.setAll(Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream()) .map(tradable -> new ReservedListItem(tradable, walletService.getAddressEntryByOfferId(tradable.getOffer().getId()), walletService, formatter)) .collect(Collectors.toList())); } @@ -122,6 +130,11 @@ public class ReservedView extends ActivatableView { } } + + /////////////////////////////////////////////////////////////////////////////////////////// + // ColumnCellFactories + /////////////////////////////////////////////////////////////////////////////////////////// + private void setLabelColumnCellFactory() { detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); detailsColumn.setCellFactory(new Callback, diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsListItem.java b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsListItem.java index c7188d81c6..28eec18ba8 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsListItem.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsListItem.java @@ -18,30 +18,40 @@ package io.bitsquare.gui.main.funds.transactions; import io.bitsquare.btc.WalletService; -import io.bitsquare.btc.listeners.AddressConfidenceListener; +import io.bitsquare.btc.listeners.TxConfidenceListener; import io.bitsquare.gui.components.confidence.ConfidenceProgressIndicator; import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.trade.Tradable; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.offer.OpenOffer; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.scene.control.Tooltip; import org.bitcoinj.core.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Optional; public class TransactionsListItem { - + private final Logger log = LoggerFactory.getLogger(this.getClass()); private final StringProperty date = new SimpleStringProperty(); private final StringProperty amount = new SimpleStringProperty(); - private final StringProperty type = new SimpleStringProperty(); - + private final String txId; private final WalletService walletService; - private final ConfidenceProgressIndicator progressIndicator; - private final Tooltip tooltip; + private Tradable tradable; + private String details; private String addressString; - private AddressConfidenceListener confidenceListener; + private String direction; + private TxConfidenceListener txConfidenceListener; + private boolean received; + private boolean detailsAvailable; - public TransactionsListItem(Transaction transaction, WalletService walletService, BSFormatter formatter) { + public TransactionsListItem(Transaction transaction, WalletService walletService, Optional tradableOptional, BSFormatter formatter) { + txId = transaction.getHashAsString(); this.walletService = walletService; Coin valueSentToMe = transaction.getValueSentToMe(walletService.getWallet()); @@ -52,8 +62,8 @@ public class TransactionsListItem { for (TransactionOutput transactionOutput : transaction.getOutputs()) { if (!transactionOutput.isMine(walletService.getWallet())) { - type.set("Sent to"); - + direction = "Sent to:"; + received = false; if (transactionOutput.getScriptPubKey().isSentToAddress() || transactionOutput.getScriptPubKey().isPayToScriptHash()) { address = transactionOutput.getScriptPubKey().getToAddress(walletService.getWallet().getParams()); @@ -63,7 +73,8 @@ public class TransactionsListItem { } } else if (valueSentFromMe.isZero()) { amount.set(formatter.formatCoin(valueSentToMe)); - type.set("Received with"); + direction = "Received with:"; + received = true; for (TransactionOutput transactionOutput : transaction.getOutputs()) { if (transactionOutput.isMine(walletService.getWallet())) { @@ -80,8 +91,8 @@ public class TransactionsListItem { for (TransactionOutput transactionOutput : transaction.getOutputs()) { if (!transactionOutput.isMine(walletService.getWallet())) { outgoing = true; - if (transactionOutput.getScriptPubKey().isSentToAddress() || transactionOutput.getScriptPubKey() - .isPayToScriptHash()) { + if (transactionOutput.getScriptPubKey().isSentToAddress() || + transactionOutput.getScriptPubKey().isPayToScriptHash()) { address = transactionOutput.getScriptPubKey().getToAddress(walletService.getWallet().getParams()); addressString = address.toString(); } @@ -89,13 +100,41 @@ public class TransactionsListItem { } if (outgoing) { - type.set("Sent to"); - } else { - type.set("Internal (TX Fee)"); - //addressString = "Internal swap between addresses."; + direction = "Sent to:"; + received = false; } } + + if (tradableOptional.isPresent()) { + tradable = tradableOptional.get(); + detailsAvailable = true; + if (tradable instanceof OpenOffer) { + details = "Create offer fee: " + tradable.getShortId(); + } else if (tradable instanceof Trade) { + Trade trade = (Trade) tradable; + if (trade.getTakeOfferFeeTx() != null && trade.getTakeOfferFeeTx().getHashAsString().equals(txId)) { + details = "Take offer fee: " + tradable.getShortId(); + } else if (trade.getOffer() != null && + trade.getOffer().getOfferFeePaymentTxID() != null && + trade.getOffer().getOfferFeePaymentTxID().equals(txId)) { + details = "Create offer fee: " + tradable.getShortId(); + } else if (trade.getDepositTx() != null && + trade.getDepositTx().getHashAsString().equals(txId)) { + details = "MultiSig deposit: " + tradable.getShortId(); + } else if (trade.getPayoutTx() != null && + trade.getPayoutTx().getHashAsString().equals(txId)) { + details = "MultiSig payout: " + tradable.getShortId(); + } else if (trade.getDisputeState() == Trade.DisputeState.DISPUTE_CLOSED) { + details = "Payout after dispute: " + tradable.getShortId(); + } else { + details = "Unknown reason: " + tradable.getShortId(); + } + } + } else { + details = received ? "Funded to wallet" : "Withdrawn from wallet"; + } + date.set(formatter.formatDateTime(transaction.getUpdateTime())); // confidence @@ -108,26 +147,24 @@ public class TransactionsListItem { Tooltip.install(progressIndicator, tooltip); if (address != null) { - confidenceListener = walletService.addAddressConfidenceListener(new AddressConfidenceListener(address) { + txConfidenceListener = walletService.addTxConfidenceListener(new TxConfidenceListener(txId) { @Override public void onTransactionConfidenceChanged(TransactionConfidence confidence) { updateConfidence(confidence); } }); - updateConfidence(walletService.getConfidenceForAddress(address)); + updateConfidence(transaction.getConfidence()); } } public void cleanup() { - walletService.removeAddressConfidenceListener(confidenceListener); + walletService.removeTxConfidenceListener(txConfidenceListener); } private void updateConfidence(TransactionConfidence confidence) { if (confidence != null) { - //log.debug("Type numBroadcastPeers getDepthInBlocks " + confidence.getConfidenceType() + " / " + - // confidence.numBroadcastPeers() + " / " + confidence.getDepthInBlocks()); switch (confidence.getConfidenceType()) { case UNKNOWN: tooltip.setText("Unknown transaction status"); @@ -151,32 +188,44 @@ public class TransactionsListItem { } } - public ConfidenceProgressIndicator getProgressIndicator() { return progressIndicator; } - public final StringProperty dateProperty() { return this.date; } - public final StringProperty amountProperty() { return this.amount; } - - public final StringProperty typeProperty() { - return this.type; - } - public String getAddressString() { return addressString; } - public boolean isNotAnAddress() { - return addressString == null; + public String getDirection() { + return direction; + } + + public String getTxId() { + return txId; + } + + public boolean getReceived() { + return received; + } + + public String getDetails() { + return details; + } + + public boolean getDetailsAvailable() { + return detailsAvailable; + } + + public Tradable getTradable() { + return tradable; } } diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.fxml b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.fxml index b504d12f2a..a1d53ec0db 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.fxml @@ -28,24 +28,21 @@ - + - - - - - - - + + + + - + diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.java b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.java index e29eabbdf6..5956cd7331 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/transactions/TransactionsView.java @@ -17,12 +17,25 @@ package io.bitsquare.gui.main.funds.transactions; +import de.jensd.fx.fontawesome.AwesomeIcon; +import io.bitsquare.arbitration.DisputeManager; import io.bitsquare.btc.WalletService; import io.bitsquare.common.util.Utilities; import io.bitsquare.gui.common.view.ActivatableView; import io.bitsquare.gui.common.view.FxmlView; +import io.bitsquare.gui.components.AddressWithIconAndDirection; +import io.bitsquare.gui.components.HyperlinkWithIcon; +import io.bitsquare.gui.popups.OfferDetailsPopup; import io.bitsquare.gui.popups.Popup; +import io.bitsquare.gui.popups.TradeDetailsPopup; import io.bitsquare.gui.util.BSFormatter; +import io.bitsquare.trade.Tradable; +import io.bitsquare.trade.Trade; +import io.bitsquare.trade.TradeManager; +import io.bitsquare.trade.closed.ClosedTradableManager; +import io.bitsquare.trade.failed.FailedTradesManager; +import io.bitsquare.trade.offer.OpenOffer; +import io.bitsquare.trade.offer.OpenOfferManager; import io.bitsquare.user.Preferences; import javafx.beans.property.ReadOnlyObjectWrapper; import javafx.collections.FXCollections; @@ -36,7 +49,10 @@ import org.bitcoinj.script.Script; import javax.inject.Inject; import java.util.List; +import java.util.Optional; +import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; @FxmlView public class TransactionsView extends ActivatableView { @@ -44,31 +60,56 @@ public class TransactionsView extends ActivatableView { @FXML TableView table; @FXML - TableColumn dateColumn, addressColumn, amountColumn, typeColumn, + TableColumn dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, typeColumn, confidenceColumn; private final ObservableList transactionsListItems = FXCollections.observableArrayList(); private final WalletService walletService; + private final TradeManager tradeManager; + private final OpenOfferManager openOfferManager; + private final ClosedTradableManager closedTradableManager; + private final FailedTradesManager failedTradesManager; private final BSFormatter formatter; private final Preferences preferences; + private final TradeDetailsPopup tradeDetailsPopup; + private DisputeManager disputeManager; + private final OfferDetailsPopup offerDetailsPopup; private WalletEventListener walletEventListener; + + /////////////////////////////////////////////////////////////////////////////////////////// + // Constructor, lifecycle + /////////////////////////////////////////////////////////////////////////////////////////// + @Inject - private TransactionsView(WalletService walletService, BSFormatter formatter, Preferences preferences) { + private TransactionsView(WalletService walletService, TradeManager tradeManager, OpenOfferManager openOfferManager, + ClosedTradableManager closedTradableManager, FailedTradesManager failedTradesManager, + BSFormatter formatter, Preferences preferences, TradeDetailsPopup tradeDetailsPopup, + DisputeManager disputeManager, + OfferDetailsPopup offerDetailsPopup) { this.walletService = walletService; + this.tradeManager = tradeManager; + this.openOfferManager = openOfferManager; + this.closedTradableManager = closedTradableManager; + this.failedTradesManager = failedTradesManager; this.formatter = formatter; this.preferences = preferences; + this.tradeDetailsPopup = tradeDetailsPopup; + this.disputeManager = disputeManager; + this.offerDetailsPopup = offerDetailsPopup; } @Override public void initialize() { table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); table.setPlaceholder(new Label("No transactions available")); - + setDetailsColumnCellFactory(); setAddressColumnCellFactory(); + setTransactionColumnCellFactory(); setConfidenceColumnCellFactory(); - + table.getSortOrder().add(dateColumn); + walletEventListener = new WalletEventListener() { @Override public void onCoinsReceived(Wallet wallet, Transaction tx, Coin prevBalance, Coin newBalance) { @@ -106,18 +147,10 @@ public class TransactionsView extends ActivatableView { }; } - private void updateList() { - List transactions = walletService.getWallet().getRecentTransactions(10000, true); - transactionsListItems.clear(); - transactionsListItems.addAll(transactions.stream().map(transaction -> - new TransactionsListItem(transaction, walletService, formatter)).collect(Collectors.toList())); - - table.setItems(transactionsListItems); - } - @Override protected void activate() { updateList(); + table.setItems(transactionsListItems); walletService.getWallet().addEventListener(walletEventListener); } @@ -127,8 +160,55 @@ public class TransactionsView extends ActivatableView { walletService.getWallet().removeEventListener(walletEventListener); } - private void openTxDetails(TransactionsListItem item) { - if (!item.isNotAnAddress()) { + + /////////////////////////////////////////////////////////////////////////////////////////// + // Private + /////////////////////////////////////////////////////////////////////////////////////////// + + private void updateList() { + Stream concat1 = Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream()); + Stream concat2 = Stream.concat(concat1, closedTradableManager.getClosedTrades().stream()); + Stream concat3 = Stream.concat(concat2, failedTradesManager.getFailedTrades().stream()); + Set allTradables = concat3.collect(Collectors.toSet()); + + List listItems = walletService.getWallet().getRecentTransactions(1000, true).stream() + .map(transaction -> { + Optional tradableOptional = allTradables.stream() + .filter(e -> { + String txId = transaction.getHashAsString(); + if (e instanceof OpenOffer) + return e.getOffer().getOfferFeePaymentTxID().equals(txId); + else if (e instanceof Trade) { + Trade trade = (Trade) e; + return (trade.getTakeOfferFeeTx() != null && + trade.getTakeOfferFeeTx().getHashAsString().equals(txId)) || + (trade.getOffer() != null && + trade.getOffer().getOfferFeePaymentTxID() != null && + trade.getOffer().getOfferFeePaymentTxID().equals(txId)) || + (trade.getDepositTx() != null && + trade.getDepositTx().getHashAsString().equals(txId)) || + (trade.getPayoutTx() != null && + trade.getPayoutTx().getHashAsString().equals(txId)) || + (disputeManager.getDisputesAsObservableList().stream() + .filter(dispute -> dispute.getDisputePayoutTx() != null && + dispute.getDisputePayoutTx().getHashAsString().equals(txId)) + .findAny() + .isPresent()); + } else + return false; + }) + .findAny(); + return new TransactionsListItem(transaction, walletService, tradableOptional, formatter); + }) + .collect(Collectors.toList()); + + // are sorted by getRecentTransactions + transactionsListItems.forEach(TransactionsListItem::cleanup); + transactionsListItems.setAll(listItems); + } + + private void openBlockExplorer(TransactionsListItem item) { + if (item.getAddressString() != null) { try { Utilities.openWebPage(preferences.getBlockChainExplorer().addressUrl + item.getAddressString()); } catch (Exception e) { @@ -139,6 +219,55 @@ public class TransactionsView extends ActivatableView { } } + private void openDetailPopup(TransactionsListItem item) { + if (item.getTradable() instanceof OpenOffer) + offerDetailsPopup.show(item.getTradable().getOffer()); + else if (item.getTradable() instanceof Trade) + tradeDetailsPopup.show((Trade) item.getTradable()); + } + + + /////////////////////////////////////////////////////////////////////////////////////////// + // ColumnCellFactories + /////////////////////////////////////////////////////////////////////////////////////////// + + private void setDetailsColumnCellFactory() { + detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + detailsColumn.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + + private HyperlinkWithIcon field; + + @Override + public void updateItem(final TransactionsListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + if (item.getDetailsAvailable()) { + field = new HyperlinkWithIcon(item.getDetails(), AwesomeIcon.INFO_SIGN); + field.setOnAction(event -> openDetailPopup(item)); + field.setTooltip(new Tooltip("Open popup for details")); + setGraphic(field); + } else { + setGraphic(new Label(item.getDetails())); + } + } else { + setGraphic(null); + if (field != null) + field.setOnAction(null); + } + } + }; + } + }); + } + private void setAddressColumnCellFactory() { addressColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); addressColumn.setCellFactory( @@ -149,19 +278,59 @@ public class TransactionsView extends ActivatableView { public TableCell call(TableColumn column) { return new TableCell() { - private Hyperlink hyperlink; + + private AddressWithIconAndDirection field; @Override public void updateItem(final TransactionsListItem item, boolean empty) { super.updateItem(item, empty); if (item != null && !empty) { - hyperlink = new Hyperlink(item.getAddressString()); - hyperlink.setOnAction(event -> openTxDetails(item)); - setGraphic(hyperlink); + String addressString = item.getAddressString(); + field = new AddressWithIconAndDirection(item.getDirection(), addressString, + AwesomeIcon.EXTERNAL_LINK, item.getReceived()); + field.setOnAction(event -> openBlockExplorer(item)); + field.setTooltip(new Tooltip("Open external blockchain explorer for " + + "address: " + addressString)); + setGraphic(field); } else { setGraphic(null); - setId(null); + if (field != null) + field.setOnAction(null); + } + } + }; + } + }); + } + + private void setTransactionColumnCellFactory() { + transactionColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue())); + transactionColumn.setCellFactory( + new Callback, TableCell>() { + + @Override + public TableCell call(TableColumn column) { + return new TableCell() { + private HyperlinkWithIcon hyperlinkWithIcon; + + @Override + public void updateItem(final TransactionsListItem item, boolean empty) { + super.updateItem(item, empty); + + if (item != null && !empty) { + String transactionId = item.getTxId(); + hyperlinkWithIcon = new HyperlinkWithIcon(transactionId, AwesomeIcon.EXTERNAL_LINK); + hyperlinkWithIcon.setOnAction(event -> openBlockExplorer(item)); + hyperlinkWithIcon.setTooltip(new Tooltip("Open external blockchain explorer for " + + "transaction: " + transactionId)); + setGraphic(hyperlinkWithIcon); + } else { + setGraphic(null); + if (hyperlinkWithIcon != null) + hyperlinkWithIcon.setOnAction(null); } } }; diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalListItem.java b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalListItem.java index b3b9cb864b..12da09f175 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalListItem.java +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalListItem.java @@ -19,31 +19,18 @@ package io.bitsquare.gui.main.funds.withdrawal; import io.bitsquare.btc.AddressEntry; import io.bitsquare.btc.WalletService; -import io.bitsquare.btc.listeners.AddressConfidenceListener; import io.bitsquare.btc.listeners.BalanceListener; -import io.bitsquare.gui.components.confidence.ConfidenceProgressIndicator; import io.bitsquare.gui.util.BSFormatter; import javafx.scene.control.Label; -import javafx.scene.control.Tooltip; import org.bitcoinj.core.Address; import org.bitcoinj.core.Coin; -import org.bitcoinj.core.TransactionConfidence; public class WithdrawalListItem { private final BalanceListener balanceListener; - private final Label balanceLabel; - private final AddressEntry addressEntry; - private final WalletService walletService; private final BSFormatter formatter; - private final AddressConfidenceListener confidenceListener; - - private final ConfidenceProgressIndicator progressIndicator; - - private final Tooltip tooltip; - private Coin balance; private final String addressString; @@ -53,24 +40,6 @@ public class WithdrawalListItem { this.formatter = formatter; addressString = addressEntry.getAddressString(); - // confidence - progressIndicator = new ConfidenceProgressIndicator(); - progressIndicator.setId("funds-confidence"); - tooltip = new Tooltip("Not used yet"); - progressIndicator.setProgress(0); - progressIndicator.setPrefSize(24, 24); - Tooltip.install(progressIndicator, tooltip); - - confidenceListener = walletService.addAddressConfidenceListener(new AddressConfidenceListener(getAddress()) { - @Override - public void onTransactionConfidenceChanged(TransactionConfidence confidence) { - updateConfidence(confidence); - } - }); - - updateConfidence(walletService.getConfidenceForAddress(getAddress())); - - // balance balanceLabel = new Label(); balanceListener = walletService.addBalanceListener(new BalanceListener(getAddress()) { @@ -84,7 +53,6 @@ public class WithdrawalListItem { } public void cleanup() { - walletService.removeAddressConfidenceListener(confidenceListener); walletService.removeBalanceListener(balanceListener); } @@ -95,62 +63,44 @@ public class WithdrawalListItem { } } - private void updateConfidence(TransactionConfidence confidence) { - if (confidence != null) { - //log.debug("Type numBroadcastPeers getDepthInBlocks " + confidence.getConfidenceType() + " / " + - // confidence.numBroadcastPeers() + " / " + confidence.getDepthInBlocks()); - switch (confidence.getConfidenceType()) { - case UNKNOWN: - tooltip.setText("Unknown transaction status"); - progressIndicator.setProgress(0); - break; - case PENDING: - tooltip.setText("Seen by " + confidence.numBroadcastPeers() + " peer(s) / 0 confirmations"); - progressIndicator.setProgress(-1.0); - break; - case BUILDING: - tooltip.setText("Confirmed in " + confidence.getDepthInBlocks() + " block(s)"); - progressIndicator.setProgress(Math.min(1, (double) confidence.getDepthInBlocks() / 6.0)); - break; - case DEAD: - tooltip.setText("Transaction is invalid."); - progressIndicator.setProgress(0); - break; - } - } - } - - public final String getLabel() { switch (addressEntry.getContext()) { case TRADE: - return "Offer ID: " + addressEntry.getShortOfferId(); + return addressEntry.getShortOfferId(); case ARBITRATOR: return "Arbitration fee"; } return ""; } + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof WithdrawalListItem)) return false; + + WithdrawalListItem that = (WithdrawalListItem) o; + + return !(addressEntry != null ? !addressEntry.equals(that.addressEntry) : that.addressEntry != null); + + } + + @Override + public int hashCode() { + return addressEntry != null ? addressEntry.hashCode() : 0; + } + private Address getAddress() { return addressEntry.getAddress(); } - public AddressEntry getAddressEntry() { return addressEntry; } - - public ConfidenceProgressIndicator getProgressIndicator() { - return progressIndicator; - } - - public Label getBalanceLabel() { return balanceLabel; } - public Coin getBalance() { return balance; } diff --git a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.fxml b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.fxml index 04d70df55c..1330b24f17 100644 --- a/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.fxml +++ b/gui/src/main/java/io/bitsquare/gui/main/funds/withdrawal/WithdrawalView.fxml @@ -29,15 +29,15 @@ - - + + + - - - + + @@ -50,7 +50,7 @@