Improve tx and withdrawal screens, make multiple address selection possible at withdrawal

This commit is contained in:
Manfred Karrer 2016-02-03 15:43:55 +01:00
parent 85b2cb1d44
commit c70df863d6
31 changed files with 930 additions and 354 deletions

View File

@ -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() +
'}';
}

View File

@ -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<DisputeList<Dispute>> storage;
transient private ObservableList<DisputeDirectMessage> 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) {

View File

@ -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));
}

View File

@ -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<AddressEntry> addressEntries;
@Nullable
private AddressEntry addressEntry;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
@ -50,6 +51,11 @@ class AddressBasedCoinSelector implements CoinSelector {
this.addressEntry = addressEntry;
}
public AddressBasedCoinSelector(NetworkParameters params, Set<AddressEntry> addressEntries) {
this.params = params;
this.addressEntries = addressEntries;
}
@VisibleForTesting
static void sortOutputs(ArrayList<TransactionOutput> 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;
}

View File

@ -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> addressEntry = getAddressEntryList().stream().filter(e -> offerId.equals(e.getOfferId())).findFirst();
Optional<AddressEntry> 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<AddressEntry> 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<String> 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<String> 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<AddressEntry> 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<AddressEntry> addressEntryOptional = Optional.empty();
AddressEntry changeAddressAddressEntry = null;
if (changeAddress != null)
addressEntryOptional = getAddressEntryByAddress(changeAddress);
if (addressEntryOptional.isPresent()) {
changeAddressAddressEntry = addressEntryOptional.get();
} else {
ArrayList<AddressEntry> 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<Transaction> callback) throws AddressFormatException, IllegalArgumentException, InsufficientMoneyException {
@Nullable KeyParameter aesKey,
FutureCallback<Transaction> 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<String> fromAddresses,
String toAddress,
Coin amount,
@Nullable String changeAddress,
@Nullable KeyParameter aesKey,
FutureCallback<Transaction> 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));

View File

@ -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<Coin> tradeAmountProperty;
transient private ObjectProperty<Fiat> 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<Coin> tradeAmountProperty;
transient private ObjectProperty<Fiat> 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 +

View File

@ -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<NodeAddress> 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();

View File

@ -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<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {

View File

@ -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());

View File

@ -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) {

View File

@ -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(),

View File

@ -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 *

View File

@ -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<ActionEvent> handler) {
hyperlink.setOnAction(handler);
openLinkIcon.setOnMouseClicked(e -> handler.handle(null));
}
public void setTooltip(Tooltip tooltip) {
hyperlink.setTooltip(tooltip);
openLinkIcon.setTooltip(tooltip);
}
}

View File

@ -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<ActionEvent> handler) {
hyperlink.setOnAction(handler);
openLinkIcon.setOnMouseClicked(e -> handler.handle(null));
}
public void setTooltip(Tooltip tooltip) {
hyperlink.setTooltip(tooltip);
openLinkIcon.setTooltip(tooltip);
}
}

View File

@ -200,3 +200,7 @@
-fx-image: url("../../../images/bubble_arrow_blue_right.png");
}
#link {
-fx-image: url("../../../images/link.png");
}

View File

@ -15,7 +15,7 @@
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.gui.main.disputes.trader;
package io.bitsquare.gui.main.disputes;
import io.bitsquare.arbitration.Dispute;
import io.bitsquare.arbitration.DisputeManager;

View File

@ -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;

View File

@ -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;

View File

@ -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();

View File

@ -18,6 +18,7 @@
-->
<?import javafx.geometry.Insets?>
<?import javafx.scene.control.cell.PropertyValueFactory?>
<?import javafx.scene.control.*?>
<?import javafx.scene.layout.VBox?>
<VBox fx:id="root" fx:controller="io.bitsquare.gui.main.funds.reserved.ReservedView"
@ -28,6 +29,11 @@
<TableView fx:id="table" VBox.vgrow="ALWAYS">
<columns>
<TableColumn text="Date/Time" fx:id="dateColumn" minWidth="100" sortable="false">
<cellValueFactory>
<PropertyValueFactory property="date"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Details" fx:id="detailsColumn" minWidth="100" sortable="false"/>
<TableColumn text="Address" fx:id="addressColumn" minWidth="240" sortable="false"/>
<TableColumn text="Balance" fx:id="balanceColumn" minWidth="90" sortable="false"/>

View File

@ -50,7 +50,7 @@ public class ReservedView extends ActivatableView<VBox, Void> {
@FXML
TableView<ReservedListItem> table;
@FXML
TableColumn<ReservedListItem, ReservedListItem> detailsColumn, addressColumn, balanceColumn, confidenceColumn;
TableColumn<ReservedListItem, ReservedListItem> dateColumn, detailsColumn, addressColumn, balanceColumn, confidenceColumn;
private final WalletService walletService;
private final TradeManager tradeManager;
@ -61,6 +61,11 @@ public class ReservedView extends ActivatableView<VBox, Void> {
private final TradeDetailsPopup tradeDetailsPopup;
private final ObservableList<ReservedListItem> 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<VBox, Void> {
@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<VBox, Void> {
}
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<VBox, Void> {
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// ColumnCellFactories
///////////////////////////////////////////////////////////////////////////////////////////
private void setLabelColumnCellFactory() {
detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
detailsColumn.setCellFactory(new Callback<TableColumn<ReservedListItem, ReservedListItem>,

View File

@ -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<Tradable> 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;
}
}

View File

@ -28,24 +28,21 @@
</padding>
<TableView fx:id="table" VBox.vgrow="ALWAYS">
<columns>
<TableColumn text="Date/Time" fx:id="dateColumn" minWidth="100" sortable="false">
<TableColumn text="Date/Time" fx:id="dateColumn" minWidth="160" maxWidth="160">
<cellValueFactory>
<PropertyValueFactory property="date"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Type" fx:id="typeColumn" minWidth="70" sortable="false">
<cellValueFactory>
<PropertyValueFactory property="type"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Address" fx:id="addressColumn" minWidth="240" sortable="false"/>
<TableColumn text="Amount" fx:id="amountColumn" minWidth="70" sortable="false">
<TableColumn text="Details" fx:id="detailsColumn" minWidth="210" maxWidth="210"/>
<TableColumn text="Address" fx:id="addressColumn" minWidth="180"/>
<TableColumn text="Transaction" fx:id="transactionColumn" minWidth="100"/>
<TableColumn text="Amount" fx:id="amountColumn" minWidth="110" maxWidth="110">
<cellValueFactory>
<PropertyValueFactory property="amount"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Confirmations" fx:id="confidenceColumn" minWidth="30" sortable="false"/>
<TableColumn text="Confirmations" fx:id="confidenceColumn" minWidth="110" maxWidth="110"/>
</columns>
</TableView>
</VBox>

View File

@ -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<VBox, Void> {
@ -44,31 +60,56 @@ public class TransactionsView extends ActivatableView<VBox, Void> {
@FXML
TableView<TransactionsListItem> table;
@FXML
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, addressColumn, amountColumn, typeColumn,
TableColumn<TransactionsListItem, TransactionsListItem> dateColumn, detailsColumn, addressColumn, transactionColumn, amountColumn, typeColumn,
confidenceColumn;
private final ObservableList<TransactionsListItem> 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<VBox, Void> {
};
}
private void updateList() {
List<Transaction> 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<VBox, Void> {
walletService.getWallet().removeEventListener(walletEventListener);
}
private void openTxDetails(TransactionsListItem item) {
if (!item.isNotAnAddress()) {
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateList() {
Stream<Tradable> concat1 = Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream());
Stream<Tradable> concat2 = Stream.concat(concat1, closedTradableManager.getClosedTrades().stream());
Stream<Tradable> concat3 = Stream.concat(concat2, failedTradesManager.getFailedTrades().stream());
Set<Tradable> allTradables = concat3.collect(Collectors.toSet());
List<TransactionsListItem> listItems = walletService.getWallet().getRecentTransactions(1000, true).stream()
.map(transaction -> {
Optional<Tradable> 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<VBox, Void> {
}
}
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<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
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<VBox, Void> {
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
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<TableColumn<TransactionsListItem, TransactionsListItem>, TableCell<TransactionsListItem,
TransactionsListItem>>() {
@Override
public TableCell<TransactionsListItem, TransactionsListItem> call(TableColumn<TransactionsListItem,
TransactionsListItem> column) {
return new TableCell<TransactionsListItem, TransactionsListItem>() {
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);
}
}
};

View File

@ -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;
}

View File

@ -29,15 +29,15 @@
<TableView fx:id="table" VBox.vgrow="ALWAYS">
<columns>
<TableColumn text="Details" fx:id="detailsColumn" minWidth="100" sortable="false"/>
<TableColumn text="Address" fx:id="addressColumn" minWidth="240" sortable="false">
<TableColumn text="Date/Time" fx:id="dateColumn" minWidth="170" maxWidth="170"/>
<TableColumn text="Details" fx:id="detailsColumn" minWidth="160" maxWidth="160"/>
<TableColumn text="Address" fx:id="addressColumn" minWidth="320" maxWidth="320">
<cellValueFactory>
<PropertyValueFactory property="addressString"/>
</cellValueFactory>
</TableColumn>
<TableColumn text="Balance" fx:id="balanceColumn" minWidth="50" sortable="false"/>
<TableColumn text="Confirmations" fx:id="confidenceColumn" minWidth="30" sortable="false"/>
<TableColumn text="" fx:id="selectColumn" prefWidth="80" sortable="false"/>
<TableColumn text="Balance" fx:id="balanceColumn" minWidth="110"/>
<TableColumn text="Selection" fx:id="selectColumn" minWidth="160" sortable="false"/>
</columns>
</TableView>
@ -50,7 +50,7 @@
<TextField fx:id="amountTextField" GridPane.rowIndex="0" GridPane.columnIndex="1"/>
<Label text="Withdraw from address:" GridPane.rowIndex="1"/>
<TextField fx:id="withdrawFromTextField" promptText="Select a source address from the table"
<TextField fx:id="withdrawFromTextField" editable="false" focusTraversable="false"
GridPane.rowIndex="1" GridPane.columnIndex="1"/>
<Label text="Withdraw to address:" GridPane.rowIndex="2"/>

View File

@ -18,14 +18,17 @@
package io.bitsquare.gui.main.funds.withdrawal;
import com.google.common.util.concurrent.FutureCallback;
import de.jensd.fx.fontawesome.AwesomeIcon;
import io.bitsquare.app.BitsquareApp;
import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.Restrictions;
import io.bitsquare.btc.WalletService;
import io.bitsquare.btc.listeners.BalanceListener;
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.HyperlinkWithIcon;
import io.bitsquare.gui.popups.OfferDetailsPopup;
import io.bitsquare.gui.popups.Popup;
import io.bitsquare.gui.popups.TradeDetailsPopup;
@ -48,6 +51,7 @@ import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.util.Callback;
import org.apache.commons.lang3.StringUtils;
import org.bitcoinj.core.AddressFormatException;
import org.bitcoinj.core.Coin;
import org.bitcoinj.core.InsufficientMoneyException;
@ -56,8 +60,10 @@ import org.jetbrains.annotations.NotNull;
import org.spongycastle.crypto.params.KeyParameter;
import javax.inject.Inject;
import java.util.List;
import java.util.Date;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@ -71,7 +77,7 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
@FXML
TextField withdrawFromTextField, withdrawToTextField, amountTextField;
@FXML
TableColumn<WithdrawalListItem, WithdrawalListItem> detailsColumn, addressColumn, balanceColumn, confidenceColumn, selectColumn;
TableColumn<WithdrawalListItem, WithdrawalListItem> dateColumn, detailsColumn, addressColumn, balanceColumn, selectColumn;
private final WalletService walletService;
private final TradeManager tradeManager;
@ -84,11 +90,19 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
private final WalletPasswordPopup walletPasswordPopup;
private final OfferDetailsPopup offerDetailsPopup;
private final TradeDetailsPopup tradeDetailsPopup;
private final ObservableList<WithdrawalListItem> addressList = FXCollections.observableArrayList();
private final ObservableList<WithdrawalListItem> fundedAddresses = FXCollections.observableArrayList();
private Set<WithdrawalListItem> selectedItems = new HashSet<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private WithdrawalView(WalletService walletService, TradeManager tradeManager, ClosedTradableManager closedTradableManager,
FailedTradesManager failedTradesManager, OpenOfferManager openOfferManager, BSFormatter formatter, Preferences preferences,
private WithdrawalView(WalletService walletService, TradeManager tradeManager,
ClosedTradableManager closedTradableManager,
FailedTradesManager failedTradesManager, OpenOfferManager openOfferManager,
BSFormatter formatter, Preferences preferences,
BtcAddressValidator btcAddressValidator, WalletPasswordPopup walletPasswordPopup,
OfferDetailsPopup offerDetailsPopup, TradeDetailsPopup tradeDetailsPopup) {
this.walletService = walletService;
@ -104,72 +118,45 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
this.tradeDetailsPopup = tradeDetailsPopup;
}
@Override
public void initialize() {
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setPlaceholder(new Label("No funds for withdrawal available"));
setLabelColumnCellFactory();
setDateColumnCellFactory();
setDetailsColumnCellFactory();
setAddressColumnCellFactory();
setBalanceColumnCellFactory();
setConfidenceColumnCellFactory();
setSelectColumnCellFactory();
if (BitsquareApp.DEV_MODE)
withdrawToTextField.setText("mxAkWWaQBqwqcYstKzqLku3kzR6pbu2zHq");
}
private boolean areInputsValid() {
return btcAddressValidator.validate(withdrawFromTextField.getText()).and(
btcAddressValidator.validate(withdrawToTextField.getText())).isValid;
}
private void openTxDetails(WithdrawalListItem item) {
try {
Utilities.openWebPage(preferences.getBlockChainExplorer().addressUrl + item.getAddressString());
} catch (Exception e) {
log.error(e.getMessage());
new Popup().warning("Opening browser failed. Please check your internet " +
"connection.").show();
}
table.getSortOrder().add(dateColumn);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
}
@Override
protected void activate() {
withdrawButton.disableProperty().bind(Bindings.createBooleanBinding(() -> !areInputsValid(),
withdrawFromTextField.textProperty(), amountTextField.textProperty(), withdrawToTextField.textProperty()));
table.getSelectionModel().selectedItemProperty().addListener((observableValue, oldValue, newValue) -> {
if (newValue != null) {
if (Coin.ZERO.compareTo(newValue.getBalance()) <= 0) {
amountTextField.setText(newValue.getBalance().toPlainString());
withdrawFromTextField.setText(newValue.getAddressEntry().getAddressString());
} else {
withdrawFromTextField.setText("");
withdrawFromTextField.setPromptText("No fund to withdrawal on that address.");
amountTextField.setText("");
amountTextField.setPromptText("Invalid amount");
}
}
});
fillList();
table.setItems(addressList);
updateList();
table.setItems(fundedAddresses);
reset();
walletService.addBalanceListener(new BalanceListener() {
@Override
public void onBalanceChanged(Coin balance) {
fillList();
updateList();
}
});
withdrawButton.disableProperty().bind(Bindings.createBooleanBinding(() -> !areInputsValid(),
amountTextField.textProperty(), withdrawToTextField.textProperty()));
}
@Override
protected void deactivate() {
addressList.forEach(WithdrawalListItem::cleanup);
fundedAddresses.forEach(WithdrawalListItem::cleanup);
withdrawButton.disableProperty().unbind();
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI handlers
///////////////////////////////////////////////////////////////////////////////////////////
@FXML
public void onWithdraw() {
Coin senderAmount = formatter.parseToCoin(amountTextField.getText());
@ -189,108 +176,288 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
log.error("onWithdraw onFailure");
}
};
try {
Coin requiredFee = walletService.getRequiredFee(withdrawFromTextField.getText(),
withdrawToTextField.getText(), senderAmount, null);
Coin receiverAmount = senderAmount.subtract(requiredFee);
if (BitsquareApp.DEV_MODE) {
doWithdraw(receiverAmount, callback);
} else {
new Popup().headLine("Confirm your withdrawal request")
.message("Sending: " + formatter.formatCoinWithCode(senderAmount) + "\n" +
"From address: " + withdrawFromTextField.getText() + "\n" +
"To receiving address: " + withdrawToTextField.getText() + ".\n\n" +
"Required transaction fee is: " + formatter.formatCoinWithCode(requiredFee) + "\n" +
"Recipient will receive: " + formatter.formatCoinWithCode(receiverAmount) + "\n\n" +
"Are you sure you want to withdraw that amount?")
.onAction(() -> doWithdraw(receiverAmount, callback))
.show();
// try {
/* Coin requiredFee = walletService.getRequiredFee(withdrawFromTextField.getText(),
withdrawToTextField.getText(), senderAmount, null);*/
// TODO static fee might be not enough when using many inputs, but for now its high enough to get probably into the blockchain
// Use bitcoinJ fee calculation instead....
Coin requiredFee = FeePolicy.getFeePerKb();
Coin receiverAmount = senderAmount.subtract(requiredFee);
if (BitsquareApp.DEV_MODE) {
doWithdraw(receiverAmount, callback);
} else {
new Popup().headLine("Confirm your withdrawal request")
.message("Sending: " + formatter.formatCoinWithCode(senderAmount) + "\n" +
"From address: " + withdrawFromTextField.getText() + "\n" +
"To receiving address: " + withdrawToTextField.getText() + ".\n\n" +
"Required transaction fee is: " + formatter.formatCoinWithCode(requiredFee) + "\n" +
"Recipient will receive: " + formatter.formatCoinWithCode(receiverAmount) + "\n\n" +
"Are you sure you want to withdraw that amount?")
.onAction(() -> doWithdraw(receiverAmount, callback))
.show();
}
} catch (AddressFormatException | InsufficientMoneyException e) {
}
/*} catch (AddressFormatException | InsufficientMoneyException e) {
e.printStackTrace();
log.error(e.getMessage());
}
}*/
} else {
new Popup().warning("The amount to transfer is lower than the transaction fee and the min. possible tx value.").show();
}
}
private void selectForWithdrawal(WithdrawalListItem item, boolean isSelected) {
if (isSelected)
selectedItems.add(item);
else
selectedItems.remove(item);
if (!selectedItems.isEmpty()) {
Coin sum = Coin.valueOf(selectedItems.stream().mapToLong(e -> e.getBalance().getValue()).sum());
if (sum.isPositive()) {
amountTextField.setText(formatter.formatCoin(sum));
} else {
amountTextField.setText("");
withdrawFromTextField.setText("");
}
if (selectedItems.size() == 1) {
withdrawFromTextField.setText(selectedItems.stream().findAny().get().getAddressEntry().getAddressString());
withdrawFromTextField.setTooltip(null);
} else {
//selectedItems.stream().
String tooltipText = "Withdraw from multiple addresses:\n" +
selectedItems.stream()
.map(e -> e.getAddressString())
.collect(Collectors.joining(",\n"));
int abbr = Math.max(10, 66 / selectedItems.size());
String text = "Withdraw from multiple addresses (" +
selectedItems.stream()
.map(e -> StringUtils.abbreviate(e.getAddressString(), abbr))
.collect(Collectors.joining(", ")) +
")";
withdrawFromTextField.setText(text);
withdrawFromTextField.setTooltip(new Tooltip(tooltipText));
}
} else {
reset();
}
}
private void openBlockExplorer(WithdrawalListItem item) {
if (item.getAddressString() != null) {
try {
Utilities.openWebPage(preferences.getBlockChainExplorer().addressUrl + item.getAddressString());
} catch (Exception e) {
log.error(e.getMessage());
new Popup().warning("Opening browser failed. Please check your internet " +
"connection.").show();
}
}
}
private void openDetailPopup(WithdrawalListItem item) {
Optional<Tradable> tradableOptional = getTradable(item);
if (tradableOptional.isPresent()) {
Tradable tradable = tradableOptional.get();
if (tradable instanceof Trade) {
tradeDetailsPopup.show((Trade) tradable);
} else if (tradable instanceof OpenOffer) {
offerDetailsPopup.show(tradable.getOffer());
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
///////////////////////////////////////////////////////////////////////////////////////////
private void updateList() {
Set<String> reservedTrades = Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream())
.map(tradable -> tradable.getOffer().getId())
.collect(Collectors.toSet());
fundedAddresses.forEach(WithdrawalListItem::cleanup);
fundedAddresses.setAll(walletService.getAddressEntryList().stream()
.filter(e -> walletService.getBalanceForAddress(e.getAddress()).isPositive())
.filter(e -> !reservedTrades.contains(e.getOfferId()))
.map(addressEntry -> new WithdrawalListItem(addressEntry, walletService, formatter))
.collect(Collectors.toList()));
fundedAddresses.sort((o1, o2) -> {
Optional<Tradable> tradable1 = getTradable(o1);
Optional<Tradable> tradable2 = getTradable(o2);
// if we dont have a date we set it to now as it is likely a recent funding tx
// TODO get tx date from wallet instead
Date date1 = new Date();
Date date2 = new Date();
if (tradable1.isPresent())
date1 = tradable1.get().getDate();
if (tradable2.isPresent())
date2 = tradable2.get().getDate();
return date2.compareTo(date1);
});
}
private void doWithdraw(Coin amount, FutureCallback<Transaction> callback) {
if (walletService.getWallet().isEncrypted())
walletPasswordPopup.show().onAesKey(aesKey -> sendFunds(amount, aesKey, callback));
else
sendFunds(amount, null, callback);
fillList();
updateList();
}
private void sendFunds(Coin amount, KeyParameter aesKey, FutureCallback<Transaction> callback) {
try {
walletService.sendFunds(withdrawFromTextField.getText(), withdrawToTextField.getText(), amount, aesKey, callback);
Set<String> fromAddresses = selectedItems.stream()
.map(e -> e.getAddressString())
.collect(Collectors.toSet());
if (!fromAddresses.isEmpty()) {
walletService.sendFundsForMultipleAddresses(fromAddresses, withdrawToTextField.getText(), amount, null, aesKey, callback);
reset();
}
} catch (AddressFormatException e) {
new Popup().error("The address is not correct. Please check the address format.").show();
} catch (InsufficientMoneyException e) {
log.warn(e.getMessage());
new Popup().error("You don't have enough fund in your wallet.").show();
}
}
private void reset() {
selectedItems = new HashSet<>();
table.getSelectionModel().clearSelection();
withdrawFromTextField.setText("");
withdrawFromTextField.setPromptText("Select a source address from the table");
withdrawFromTextField.setTooltip(null);
amountTextField.setText("");
amountTextField.setPromptText("");
amountTextField.setPromptText("Set the amount to withdraw");
withdrawToTextField.setText("");
withdrawToTextField.setPromptText("");
withdrawToTextField.setPromptText("Fill in your destination address");
if (BitsquareApp.DEV_MODE)
withdrawToTextField.setText("mxAkWWaQBqwqcYstKzqLku3kzR6pbu2zHq");
}
private void fillList() {
addressList.clear();
List<AddressEntry> addressEntryList = walletService.getAddressEntryList();
List<String> reservedTrades = Stream.concat(openOfferManager.getOpenOffers().stream(), tradeManager.getTrades().stream())
.map(tradable -> tradable.getOffer().getId())
.collect(Collectors.toList());
addressList.addAll(addressEntryList.stream()
.filter(e -> walletService.getBalanceForAddress(e.getAddress()).isPositive())
.filter(e -> !reservedTrades.contains(e.getOfferId()))
.map(anAddressEntryList -> new WithdrawalListItem(anAddressEntryList, walletService, formatter))
.collect(Collectors.toList()));
private Optional<Tradable> getTradable(WithdrawalListItem item) {
String offerId = item.getAddressEntry().getOfferId();
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(offerId);
if (tradableOptional.isPresent()) {
return tradableOptional;
} else if (failedTradesManager.getTradeById(offerId).isPresent()) {
return Optional.of(failedTradesManager.getTradeById(offerId).get());
} else {
return Optional.empty();
}
}
private void setLabelColumnCellFactory() {
detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
detailsColumn.setCellFactory(new Callback<TableColumn<WithdrawalListItem, WithdrawalListItem>,
TableCell<WithdrawalListItem,
WithdrawalListItem>>() {
private boolean isTradableAvailable(WithdrawalListItem item) {
String offerId = item.getAddressEntry().getOfferId();
return closedTradableManager.getTradableById(offerId).isPresent() ||
failedTradesManager.getTradeById(offerId).isPresent();
}
private boolean areInputsValid() {
return btcAddressValidator.validate(withdrawToTextField.getText()).isValid &&
amountTextField.getText().length() > 0 &&
Restrictions.isAboveFixedTxFeeAndDust(formatter.parseToCoin(amountTextField.getText()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// ColumnCellFactories
///////////////////////////////////////////////////////////////////////////////////////////
private void setDateColumnCellFactory() {
dateColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
dateColumn.setCellFactory(new Callback<TableColumn<WithdrawalListItem, WithdrawalListItem>,
TableCell<WithdrawalListItem, WithdrawalListItem>>() {
@Override
public TableCell<WithdrawalListItem, WithdrawalListItem> call(TableColumn<WithdrawalListItem,
WithdrawalListItem> column) {
return new TableCell<WithdrawalListItem, WithdrawalListItem>() {
@Override
public void updateItem(final WithdrawalListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (getTradable(item).isPresent())
setText(formatter.formatDateTime(getTradable(item).get().getDate()));
else
setText("No date available");
} else {
setText("");
}
}
};
}
});
}
private void setDetailsColumnCellFactory() {
detailsColumn.setCellValueFactory((addressListItem) -> new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
detailsColumn.setCellFactory(new Callback<TableColumn<WithdrawalListItem, WithdrawalListItem>,
TableCell<WithdrawalListItem, WithdrawalListItem>>() {
@Override
public TableCell<WithdrawalListItem, WithdrawalListItem> call(TableColumn<WithdrawalListItem,
WithdrawalListItem> column) {
return new TableCell<WithdrawalListItem, WithdrawalListItem>() {
private HyperlinkWithIcon field;
@Override
public void updateItem(final WithdrawalListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
if (detailsAvailable(item)) {
Hyperlink hyperlink = new Hyperlink(item.getLabel());
if (item.getAddressEntry().getOfferId() != null) {
Tooltip tooltip = new Tooltip(item.getAddressEntry().getShortOfferId());
Tooltip.install(hyperlink, tooltip);
if (isTradableAvailable(item)) {
AddressEntry addressEntry = item.getAddressEntry();
String details;
if (addressEntry.getContext() == AddressEntry.Context.TRADE) {
String prefix;
if (getTradable(item).isPresent()) {
Tradable tradable = getTradable(item).get();
if (tradable instanceof Trade)
prefix = "Trade ID: ";
else if (tradable instanceof OpenOffer)
prefix = "Offer ID: ";
else
prefix = "";
} else {
prefix = "";
}
hyperlink.setOnAction(event -> openDetails(item));
setGraphic(hyperlink);
details = prefix + addressEntry.getShortOfferId();
} else if (addressEntry.getContext() == AddressEntry.Context.ARBITRATOR) {
details = "Arbitration fee";
} else {
details = "-";
}
field = new HyperlinkWithIcon(details, AwesomeIcon.INFO_SIGN);
field.setOnAction(event -> openDetailPopup(item));
field.setTooltip(new Tooltip("Open popup for details"));
setGraphic(field);
} else if (item.getAddressEntry().getContext() == AddressEntry.Context.ARBITRATOR) {
setGraphic(new Label("Arbitrators fee"));
} else {
Label label = new Label("No info available");
setGraphic(label);
setGraphic(new Label("No details available"));
}
} else {
setGraphic(null);
setId(null);
if (field != null)
field.setOnAction(null);
}
}
};
@ -308,19 +475,23 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
public TableCell<WithdrawalListItem, WithdrawalListItem> call(TableColumn<WithdrawalListItem,
WithdrawalListItem> column) {
return new TableCell<WithdrawalListItem, WithdrawalListItem>() {
private Hyperlink hyperlink;
private HyperlinkWithIcon hyperlinkWithIcon;
@Override
public void updateItem(final WithdrawalListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
hyperlink = new Hyperlink(item.getAddressString());
hyperlink.setOnAction(event -> openTxDetails(item));
setGraphic(hyperlink);
String address = item.getAddressString();
hyperlinkWithIcon = new HyperlinkWithIcon(address, AwesomeIcon.EXTERNAL_LINK);
hyperlinkWithIcon.setOnAction(event -> openBlockExplorer(item));
hyperlinkWithIcon.setTooltip(new Tooltip("Open external blockchain explorer for " +
"address: " + address));
setGraphic(hyperlinkWithIcon);
} else {
setGraphic(null);
setId(null);
if (hyperlinkWithIcon != null)
hyperlinkWithIcon.setOnAction(null);
}
}
};
@ -348,33 +519,6 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
});
}
private void setConfidenceColumnCellFactory() {
confidenceColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
confidenceColumn.setCellFactory(
new Callback<TableColumn<WithdrawalListItem, WithdrawalListItem>, TableCell<WithdrawalListItem,
WithdrawalListItem>>() {
@Override
public TableCell<WithdrawalListItem, WithdrawalListItem> call(TableColumn<WithdrawalListItem,
WithdrawalListItem> column) {
return new TableCell<WithdrawalListItem, WithdrawalListItem>() {
@Override
public void updateItem(final WithdrawalListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
setGraphic(item.getProgressIndicator());
} else {
setGraphic(null);
}
}
};
}
});
}
private void setSelectColumnCellFactory() {
selectColumn.setCellValueFactory((addressListItem) ->
new ReadOnlyObjectWrapper<>(addressListItem.getValue()));
@ -387,46 +531,29 @@ public class WithdrawalView extends ActivatableView<VBox, Void> {
WithdrawalListItem> column) {
return new TableCell<WithdrawalListItem, WithdrawalListItem>() {
Button button = new Button("Select");
CheckBox checkBox;
@Override
public void updateItem(final WithdrawalListItem item, boolean empty) {
super.updateItem(item, empty);
if (item != null && !empty) {
button.setDefaultButton(true);
button.setMouseTransparent(true);
setGraphic(button);
if (checkBox == null) {
checkBox = new CheckBox("Select for withdrawal");
checkBox.setOnAction(e -> selectForWithdrawal(item, checkBox.isSelected()));
setGraphic(checkBox);
}
} else {
setGraphic(null);
if (checkBox != null) {
checkBox.setOnAction(null);
checkBox = null;
}
}
}
};
}
});
}
private boolean detailsAvailable(WithdrawalListItem item) {
String offerId = item.getAddressEntry().getOfferId();
return closedTradableManager.getTradableById(offerId).isPresent() ||
failedTradesManager.getTradeById(offerId).isPresent();
}
private void openDetails(WithdrawalListItem item) {
String offerId = item.getAddressEntry().getOfferId();
Optional<Tradable> tradableOptional = closedTradableManager.getTradableById(offerId);
if (tradableOptional.isPresent()) {
Tradable tradable = tradableOptional.get();
if (tradable instanceof Trade) {
tradeDetailsPopup.show((Trade) tradable);
} else if (tradable instanceof OpenOffer) {
offerDetailsPopup.show(tradable.getOffer());
}
} else if (failedTradesManager.getTradeById(offerId).isPresent()) {
tradeDetailsPopup.show(failedTradesManager.getTradeById(offerId).get());
} else {
log.warn("no details available. A test with detailsAvailable() is missing.");
}
}
}

View File

@ -102,7 +102,6 @@ class CreateOfferDataModel extends ActivatableDataModel {
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, TradeWalletService tradeWalletService,
Preferences preferences, User user, KeyRing keyRing, P2PService p2PService,

View File

@ -17,6 +17,7 @@
package io.bitsquare.gui.popups;
import io.bitsquare.arbitration.DisputeManager;
import io.bitsquare.gui.util.BSFormatter;
import io.bitsquare.gui.util.Layout;
import io.bitsquare.locale.BSResources;
@ -43,6 +44,7 @@ public class TradeDetailsPopup extends Popup {
protected static final Logger log = LoggerFactory.getLogger(TradeDetailsPopup.class);
private final BSFormatter formatter;
private DisputeManager disputeManager;
private Trade trade;
@ -51,8 +53,9 @@ public class TradeDetailsPopup extends Popup {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public TradeDetailsPopup(BSFormatter formatter) {
public TradeDetailsPopup(BSFormatter formatter, DisputeManager disputeManager) {
this.formatter = formatter;
this.disputeManager = disputeManager;
}
public TradeDetailsPopup show(Trade trade) {
@ -119,7 +122,7 @@ public class TradeDetailsPopup extends Popup {
if (buyerPaymentAccountContractData == null && sellerPaymentAccountContractData == null)
rows++;
if (contract.takeOfferFeeTxID != null)
if (trade.getTakeOfferFeeTx() != null)
rows++;
}
@ -127,6 +130,8 @@ public class TradeDetailsPopup extends Popup {
rows++;
if (trade.getPayoutTx() != null)
rows++;
if (disputeManager.findOwnDispute(trade.getId()).isPresent())
rows++;
if (trade.errorMessageProperty().get() != null)
rows += 2;
@ -151,13 +156,15 @@ public class TradeDetailsPopup extends Popup {
}
addLabelTxIdTextField(gridPane, ++rowIndex, "Offer fee transaction ID:", offer.getOfferFeePaymentTxID());
if (contract != null && contract.takeOfferFeeTxID != null)
addLabelTxIdTextField(gridPane, ++rowIndex, "Trading fee transaction ID:", contract.takeOfferFeeTxID);
if (contract != null && trade.getTakeOfferFeeTx() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, "Taker fee transaction ID:", trade.getTakeOfferFeeTx().getHashAsString());
if (trade.getDepositTx() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, "Deposit transaction ID:", trade.getDepositTx().getHashAsString());
if (trade.getPayoutTx() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, "Payout transaction ID:", trade.getPayoutTx().getHashAsString());
if (disputeManager.findOwnDispute(trade.getId()).isPresent() && disputeManager.findOwnDispute(trade.getId()).get().getDisputePayoutTx() != null)
addLabelTxIdTextField(gridPane, ++rowIndex, "Disputed payout transaction ID:", disputeManager.findOwnDispute(trade.getId()).get().getDisputePayoutTx().getHashAsString());
if (contract != null) {
TextArea textArea = addLabelTextArea(gridPane, ++rowIndex, "Contract in JSON format:", trade.getContractAsJson()).second;

Binary file not shown.

After

Width:  |  Height:  |  Size: 338 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 B