Remove listeners/unbind at views. Add more error handling

This commit is contained in:
Manfred Karrer 2015-04-22 18:38:53 +02:00
parent 919e31f0d5
commit 3f6f8dd160
90 changed files with 1940 additions and 1610 deletions

View File

@ -23,8 +23,7 @@ import org.slf4j.LoggerFactory;
public abstract class Task<T extends Model> {
private static final Logger log = LoggerFactory.getLogger(Task.class);
public static Class<? extends Task> taskToInterceptBeforeRun;
public static Class<? extends Task> taskToInterceptAfterRun;
public static Class<? extends Task> taskToIntercept;
private final TaskRunner taskHandler;
protected final T model;
@ -35,26 +34,11 @@ public abstract class Task<T extends Model> {
this.model = model;
}
protected void run() {
try {
interceptBeforeRun();
doRun();
} catch (Throwable t) {
appendExceptionToErrorMessage(t);
failed();
}
}
abstract protected void run();
abstract protected void doRun();
private void interceptBeforeRun() {
if (getClass() == taskToInterceptBeforeRun)
throw new InterceptTaskException("Task intercepted before run got executed. Task = " + getClass().getSimpleName());
}
private void interceptBeforeComplete() {
if (getClass() == taskToInterceptAfterRun)
throw new InterceptTaskException("Task intercepted before complete was called. Task = " + getClass().getSimpleName());
protected void runInterceptHook() {
if (getClass() == taskToIntercept)
throw new InterceptTaskException("Task intercepted for testing purpose. Task = " + getClass().getSimpleName());
}
protected void appendToErrorMessage(String message) {
@ -69,12 +53,6 @@ public abstract class Task<T extends Model> {
}
protected void complete() {
try {
interceptBeforeComplete();
} catch (Throwable t) {
appendExceptionToErrorMessage(t);
failed();
}
taskHandler.handleComplete();
}
@ -84,6 +62,7 @@ public abstract class Task<T extends Model> {
}
protected void failed(Throwable t) {
t.printStackTrace();
appendExceptionToErrorMessage(t);
failed();
}

View File

@ -21,8 +21,6 @@ import com.google.common.base.Throwables;
import java.io.File;
import java.io.IOException;
import java.io.InvalidClassException;
import java.io.InvalidObjectException;
import java.io.Serializable;
import java.util.concurrent.TimeUnit;
@ -131,7 +129,7 @@ public class Storage<T extends Serializable> {
log.info("Backup {} completed in {}msec", serializable.getClass().getSimpleName(), System.currentTimeMillis() - now);
return persistedObject;
} catch (InvalidClassException | InvalidObjectException | ClassCastException | ClassNotFoundException e) {
} catch (ClassCastException | ClassNotFoundException | IOException e) {
e.printStackTrace();
log.error("Version of persisted class has changed. We cannot read the persisted data anymore. We make a backup and remove the inconsistent " +
"file.");

View File

@ -33,6 +33,8 @@ import io.bitsquare.storage.Storage;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.protocol.trade.ProcessModel;
import io.bitsquare.trade.protocol.trade.TradeProtocol;
import io.bitsquare.trade.states.BuyerTradeState;
import io.bitsquare.trade.states.SellerTradeState;
import io.bitsquare.trade.states.TradeState;
import io.bitsquare.user.User;
@ -245,6 +247,13 @@ abstract public class Trade implements Tradable, Model, Serializable {
storage.queueUpForSave();
}
public void setFaultState() {
if (this instanceof SellerTrade)
setProcessState(SellerTradeState.ProcessState.FAULT);
else if (this instanceof BuyerTrade)
setProcessState(BuyerTradeState.ProcessState.FAULT);
}
public void setLifeCycleState(Trade.LifeCycleState lifeCycleState) {
this.lifeCycleState = lifeCycleState;
lifeCycleStateProperty.set(lifeCycleState);

View File

@ -59,7 +59,10 @@ public class Offer implements Serializable {
AVAILABLE,
NOT_AVAILABLE,
REMOVED,
OFFERER_OFFLINE
OFFERER_OFFLINE,
TIMEOUT,
FAULT
}

View File

@ -23,7 +23,7 @@ import io.bitsquare.p2p.DHTService;
import java.util.List;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyLongProperty;
public interface OfferBookService extends DHTService {
@ -39,7 +39,7 @@ public interface OfferBookService extends DHTService {
void removeListener(Listener listener);
LongProperty invalidationTimestampProperty();
ReadOnlyLongProperty invalidationTimestampProperty();
void requestInvalidationTimeStampFromDHT(String fiatCode);

View File

@ -21,14 +21,12 @@ import io.bitsquare.app.Version;
import io.bitsquare.storage.Storage;
import io.bitsquare.trade.Tradable;
import io.bitsquare.trade.TradableList;
import org.bitcoinj.utils.Threading;
import io.bitsquare.util.Utilities;
import java.io.Serializable;
import java.util.Date;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -95,26 +93,16 @@ public class OpenOffer implements Tradable, Serializable {
private void startTimeout() {
log.trace("startTimeout");
stopTimeout();
timeoutTimer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> {
timeoutTimer = Utilities.setTimeout(TIMEOUT, () -> {
log.debug("Timeout reached");
if (state == State.RESERVED)
setState(State.AVAILABLE);
});
}
};
timeoutTimer.schedule(task, TIMEOUT);
}
protected void stopTimeout() {
log.trace("stopTimeout");
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;

View File

@ -34,6 +34,7 @@ import java.util.Map;
import javax.inject.Inject;
import javafx.beans.property.LongProperty;
import javafx.beans.property.ReadOnlyLongProperty;
import javafx.beans.property.SimpleLongProperty;
import net.tomp2p.dht.FutureGet;
@ -220,7 +221,7 @@ public class TomP2POfferBookService extends TomP2PDHTService implements OfferBoo
if (offerDataObject instanceof Offer) {
offers.add((Offer) offerDataObject);
}
} catch (ClassNotFoundException | IOException e) {
} catch (ClassCastException | ClassNotFoundException | IOException e) {
e.printStackTrace();
log.warn(e.getMessage());
}
@ -293,7 +294,7 @@ public class TomP2POfferBookService extends TomP2PDHTService implements OfferBoo
}
}
public LongProperty invalidationTimestampProperty() {
public ReadOnlyLongProperty invalidationTimestampProperty() {
return invalidationTimestamp;
}

View File

@ -30,11 +30,9 @@ import io.bitsquare.trade.protocol.availability.messages.OfferMessage;
import io.bitsquare.trade.protocol.availability.tasks.GetPeerAddress;
import io.bitsquare.trade.protocol.availability.tasks.ProcessOfferAvailabilityResponse;
import io.bitsquare.trade.protocol.availability.tasks.SendOfferAvailabilityRequest;
import org.bitcoinj.utils.Threading;
import io.bitsquare.util.Utilities;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -52,7 +50,6 @@ public class OfferAvailabilityProtocol {
private final DecryptedMessageHandler decryptedMessageHandler;
private Timer timeoutTimer;
private boolean isCanceled;
private TaskRunner<OfferAvailabilityModel> taskRunner;
@ -84,8 +81,14 @@ public class OfferAvailabilityProtocol {
model.messageService.addDecryptedMessageHandler(decryptedMessageHandler);
taskRunner = new TaskRunner<>(model,
() -> log.debug("sequence at onCheckOfferAvailability completed"),
log::error
() -> {
log.debug("sequence at onCheckOfferAvailability completed");
stopTimeout();
},
(errorMessage) -> {
log.error(errorMessage);
stopTimeout();
}
);
taskRunner.addTasks(
GetPeerAddress.class,
@ -96,7 +99,6 @@ public class OfferAvailabilityProtocol {
}
public void cancel() {
isCanceled = true;
taskRunner.cancel();
cleanup();
}
@ -119,15 +121,18 @@ public class OfferAvailabilityProtocol {
private void handle(OfferAvailabilityResponse message) {
stopTimeout();
startTimeout();
model.setMessage(message);
taskRunner = new TaskRunner<>(model,
() -> {
log.debug("sequence at handleReportOfferAvailabilityMessage completed");
log.debug("sequence at handle OfferAvailabilityResponse completed");
stopTimeout();
resultHandler.handleResult();
},
(errorMessage) -> {
log.error(errorMessage);
stopTimeout();
errorMessageHandler.handleErrorMessage(errorMessage);
}
);
@ -136,26 +141,16 @@ public class OfferAvailabilityProtocol {
}
protected void startTimeout() {
log.debug("startTimeout");
stopTimeout();
timeoutTimer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> {
log.debug("Timeout reached");
timeoutTimer = Utilities.setTimeout(TIMEOUT, () -> {
log.warn("Timeout reached");
errorMessageHandler.handleErrorMessage("Timeout reached: Peer has not responded.");
model.offer.setState(Offer.State.OFFERER_OFFLINE);
model.offer.setState(Offer.State.TIMEOUT);
});
}
};
timeoutTimer.schedule(task, TIMEOUT);
}
protected void stopTimeout() {
log.debug("stopTimeout");
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;

View File

@ -37,13 +37,13 @@ public class GetPeerAddress extends Task<OfferAvailabilityModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
model.addressService.findPeerAddress(model.offer.getPubKeyRing(), new GetPeerAddressListener() {
@Override
public void onResult(Peer peer) {
model.setPeer(peer);
complete();
}
@ -55,6 +55,7 @@ public class GetPeerAddress extends Task<OfferAvailabilityModel> {
}
});
} catch (Throwable t) {
model.offer.setState(Offer.State.FAULT);
failed(t);
}
}

View File

@ -34,8 +34,9 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
OfferAvailabilityResponse offerAvailabilityResponse = (OfferAvailabilityResponse) model.getMessage();
if (model.offer.getState() != Offer.State.REMOVED) {
@ -47,6 +48,7 @@ public class ProcessOfferAvailabilityResponse extends Task<OfferAvailabilityMode
complete();
} catch (Throwable t) {
model.offer.setState(Offer.State.FAULT);
failed(t);
}
}

View File

@ -35,8 +35,9 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
OfferAvailabilityRequest message = new OfferAvailabilityRequest(model.offer.getId(), model.getPubKeyRing());
model.messageService.sendEncryptedMessage(model.getPeer(),
model.offer.getPubKeyRing(),
@ -55,6 +56,7 @@ public class SendOfferAvailabilityRequest extends Task<OfferAvailabilityModel> {
}
});
} catch (Throwable t) {
model.offer.setState(Offer.State.FAULT);
failed(t);
}
}

View File

@ -35,7 +35,7 @@ public class PlaceOfferModel implements Model {
public final WalletService walletService;
public final TradeWalletService tradeWalletService;
public final OfferBookService offerBookService;
public boolean offerAddedToOfferBook;
private Transaction transaction;
public PlaceOfferModel(Offer offer,

View File

@ -61,6 +61,15 @@ public class PlaceOfferProtocol {
},
(errorMessage) -> {
log.error(errorMessage);
if (model.offerAddedToOfferBook) {
model.offerBookService.removeOffer(model.offer,
() -> {
model.offerAddedToOfferBook = false;
log.debug("Offer removed from offer book.");
},
(message, throwable) -> log.error(message));
}
errorMessageHandler.handleErrorMessage(errorMessage);
}
);

View File

@ -33,9 +33,17 @@ public class AddOfferToRemoteOfferBook extends Task<PlaceOfferModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
model.offerBookService.addOffer(model.offer,
this::complete,
() -> {
model.offerAddedToOfferBook = true;
complete();
},
(message, throwable) -> failed(throwable));
} catch (Throwable t) {
failed(t);
}
}
}

View File

@ -47,13 +47,13 @@ public class BroadcastCreateOfferFeeTx extends Task<PlaceOfferModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Coin totalsNeeded = model.offer.getSecurityDeposit().add(FeePolicy.CREATE_OFFER_FEE).add(FeePolicy.TX_FEE);
AddressEntry addressEntry = model.walletService.getAddressEntry(model.offer.getId());
Coin balance = model.walletService.getBalanceForAddress(addressEntry.getAddress());
if (balance.compareTo(totalsNeeded) >= 0) {
model.tradeWalletService.broadcastTx(model.getTransaction(), new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {
@ -100,6 +100,9 @@ public class BroadcastCreateOfferFeeTx extends Task<PlaceOfferModel> {
failed("Not enough balance for placing the offer.");
updateStateOnFault();
}
} catch (Throwable t) {
failed(t);
}
}
private void updateStateOnFault() {

View File

@ -34,8 +34,9 @@ public class CreateOfferFeeTx extends Task<PlaceOfferModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Transaction transaction = model.tradeWalletService.createOfferFeeTx(
model.walletService.getAddressEntry(model.offer.getId()));

View File

@ -32,8 +32,9 @@ public class ValidateOffer extends Task<PlaceOfferModel> {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
model.offer.validate();
complete();

View File

@ -102,8 +102,8 @@ public class BuyerAsOffererProtocol extends TradeProtocol implements BuyerProtoc
CreateDepositTxInputs.class,
SendPayDepositRequest.class
);
taskRunner.run();
startTimeout();
taskRunner.run();
}
@Override

View File

@ -116,8 +116,8 @@ public class BuyerAsTakerProtocol extends TradeProtocol implements BuyerProtocol
CreateDepositTxInputs.class,
SendPayDepositRequest.class
);
taskRunner.run();
startTimeout();
taskRunner.run();
}

View File

@ -100,8 +100,8 @@ public class SellerAsOffererProtocol extends TradeProtocol implements SellerProt
CreateAndSignDepositTx.class,
SendPublishDepositTxRequest.class
);
taskRunner.run();
startTimeout();
taskRunner.run();
}
@Override

View File

@ -128,8 +128,8 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
BroadcastTakeOfferFeeTx.class,
SendDepositTxInputsRequest.class
);
taskRunner.run();
startTimeout();
taskRunner.run();
}
@ -153,8 +153,8 @@ public class SellerAsTakerProtocol extends TradeProtocol implements SellerProtoc
CreateAndSignDepositTx.class,
SendPublishDepositTxRequest.class
);
taskRunner.run();
startTimeout();
taskRunner.run();
}
private void handle(DepositTxPublishedMessage tradeMessage) {

View File

@ -32,11 +32,9 @@ import io.bitsquare.trade.protocol.trade.messages.TradeMessage;
import io.bitsquare.trade.protocol.trade.tasks.shared.SetupPayoutTxLockTimeReachedListener;
import io.bitsquare.trade.states.BuyerTradeState;
import io.bitsquare.trade.states.SellerTradeState;
import org.bitcoinj.utils.Threading;
import io.bitsquare.util.Utilities;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -141,28 +139,18 @@ public abstract class TradeProtocol {
}
protected void startTimeout() {
log.debug("startTimeout");
stopTimeout();
timeoutTimer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> {
timeoutTimer = Utilities.setTimeout(TIMEOUT, () -> {
log.debug("Timeout reached");
/* if (trade instanceof SellerTrade)
if (trade instanceof SellerTrade)
trade.setProcessState(SellerTradeState.ProcessState.TIMEOUT);
else if (trade instanceof BuyerTrade)
trade.setProcessState(BuyerTradeState.ProcessState.TIMEOUT);*/
trade.setProcessState(BuyerTradeState.ProcessState.TIMEOUT);
});
}
};
timeoutTimer.schedule(task, TIMEOUT);
}
protected void stopTimeout() {
log.debug("stopTimeout");
if (timeoutTimer != null) {
timeoutTimer.cancel();
timeoutTimer = null;

View File

@ -38,6 +38,16 @@ public class TradeTask extends Task<Trade> {
}
@Override
protected void doRun() {
protected void run() {
}
@Override
protected void failed(Throwable t) {
t.printStackTrace();
appendExceptionToErrorMessage(t);
trade.setThrowable(t);
trade.setErrorMessage(errorMessage);
trade.setFaultState();
failed();
}
}

View File

@ -37,8 +37,9 @@ public class CreateDepositTxInputs extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
log.debug("trade.id" + trade.getId());
Coin inputAmount = trade.getSecurityDeposit().add(FeePolicy.TX_FEE);
TradeWalletService.Result result = processModel.getTradeWalletService().createDepositTxInputs(inputAmount,

View File

@ -37,8 +37,9 @@ public class ProcessDepositTxInputsRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
DepositTxInputsRequest message = (DepositTxInputsRequest) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -38,8 +38,9 @@ public class ProcessFinalizePayoutTxRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
FinalizePayoutTxRequest message = (FinalizePayoutTxRequest) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -37,8 +37,9 @@ public class ProcessPublishDepositTxRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
PublishDepositTxRequest message = (PublishDepositTxRequest) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -36,8 +36,9 @@ public class SendDepositTxPublishedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
DepositTxPublishedMessage tradeMessage = new DepositTxPublishedMessage(processModel.getId(), trade.getDepositTx());
processModel.getMessageService().sendEncryptedMessage(

View File

@ -36,8 +36,9 @@ public class SendFiatTransferStartedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
FiatTransferStartedMessage tradeMessage = new FiatTransferStartedMessage(processModel.getId(),
processModel.getAddressEntry().getAddressString()
);

View File

@ -36,8 +36,9 @@ public class SendPayDepositRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
boolean isInitialRequest = trade instanceof BuyerAsTakerTrade;
PayDepositRequest tradeMessage = new PayDepositRequest(
processModel.getId(),

View File

@ -36,8 +36,9 @@ public class SendPayoutTxFinalizedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
PayoutTxFinalizedMessage tradeMessage = new PayoutTxFinalizedMessage(processModel.getId(), trade.getPayoutTx());
processModel.getMessageService().sendEncryptedMessage(
trade.getTradingPeer(),

View File

@ -35,8 +35,9 @@ public class SignAndFinalizePayoutTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
assert trade.getTradeAmount() != null;
assert trade.getSecurityDeposit() != null;
Coin sellerPayoutAmount = trade.getSecurityDeposit();

View File

@ -44,8 +44,9 @@ public class SignAndPublishDepositTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Coin inputAmount = trade.getSecurityDeposit().add(FeePolicy.TX_FEE);
processModel.getTradeWalletService().signAndPublishDepositTx(

View File

@ -35,8 +35,9 @@ public class VerifyAndSignContract extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Contract contract = new Contract(
processModel.getOffer(),
trade.getTradeAmount(),

View File

@ -32,8 +32,9 @@ public class VerifyTakeOfferFeePayment extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
//TODO mocked yet, need a confidence listeners
int numOfPeersSeenTx = processModel.getWalletService().getNumOfPeersSeenTx(processModel.getTakeOfferFeeTxId());
/* if (numOfPeersSeenTx > 2) {

View File

@ -33,8 +33,9 @@ public class VerifyTakerAccount extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
//TODO mocked yet
if (processModel.getBlockChainService().verifyAccountRegistration()) {
if (processModel.getBlockChainService().isAccountBlackListed(processModel.tradingPeer.getAccountId(),

View File

@ -34,8 +34,9 @@ public class CommitDepositTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
// To access tx confidence we need to add that tx into our wallet.
Transaction depositTx = processModel.getTradeWalletService().commitTx(trade.getDepositTx());

View File

@ -34,8 +34,9 @@ public class CreateAndSignContract extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
assert processModel.getTakeOfferFeeTxId() != null;
Contract contract = new Contract(
processModel.getOffer(),

View File

@ -36,8 +36,9 @@ public class CreateAndSignDepositTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
assert trade.getTradeAmount() != null;
Coin inputAmount = trade.getSecurityDeposit().add(FeePolicy.TX_FEE).add(trade.getTradeAmount());
Coin msOutputAmount = inputAmount.add(trade.getSecurityDeposit());

View File

@ -37,8 +37,9 @@ public class ProcessDepositTxPublishedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
DepositTxPublishedMessage message = (DepositTxPublishedMessage) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -37,8 +37,9 @@ public class ProcessFiatTransferStartedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
FiatTransferStartedMessage message = (FiatTransferStartedMessage) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -36,8 +36,9 @@ public class ProcessPayDepositRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
PayDepositRequest message = (PayDepositRequest) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -37,8 +37,9 @@ public class ProcessPayoutTxFinalizedMessage extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
PayoutTxFinalizedMessage message = (PayoutTxFinalizedMessage) processModel.getTradeMessage();
checkTradeId(processModel.getId(), message);
checkNotNull(message);

View File

@ -39,8 +39,9 @@ public class SendDepositTxInputsRequest extends TradeTask {
private int retryCounter = 0;
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
assert processModel.getTakeOfferFeeTx() != null;
DepositTxInputsRequest message = new DepositTxInputsRequest(
processModel.getId(),
@ -67,7 +68,7 @@ public class SendDepositTxInputsRequest extends TradeTask {
// We try to repeat once and if that fails as well we persist the state for a later retry.
if (retryCounter == 0) {
retryCounter++;
Threading.USER_THREAD.execute(SendDepositTxInputsRequest.this::doRun);
Threading.USER_THREAD.execute(SendDepositTxInputsRequest.this::run);
}
else {
appendToErrorMessage("Sending TakeOfferFeePayedMessage to offerer failed. Maybe the network connection was " +

View File

@ -36,8 +36,9 @@ public class SendFinalizePayoutTxRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
FinalizePayoutTxRequest message = new FinalizePayoutTxRequest(
processModel.getId(),
processModel.getPayoutTxSignature(),

View File

@ -35,8 +35,9 @@ public class SendPublishDepositTxRequest extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
PublishDepositTxRequest tradeMessage = new PublishDepositTxRequest(
processModel.getId(),
processModel.getFiatAccount(),

View File

@ -34,8 +34,9 @@ public class SignPayoutTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
assert trade.getTradeAmount() != null;
assert trade.getSecurityDeposit() != null;
Coin sellerPayoutAmount = trade.getSecurityDeposit();

View File

@ -38,8 +38,9 @@ public class CommitPayoutTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Transaction transaction = processModel.getTradeWalletService().commitTx(trade.getPayoutTx());
trade.setPayoutTx(transaction);

View File

@ -47,8 +47,9 @@ public class SetupPayoutTxLockTimeReachedListener extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
log.debug("ChainHeight/LockTime: {} / {}", processModel.getTradeWalletService().getBestChainHeight(), trade.getLockTime());
if (processModel.getTradeWalletService().getBestChainHeight() >= trade.getLockTime()) {
broadcastTx();

View File

@ -38,35 +38,25 @@ public class BroadcastTakeOfferFeeTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
processModel.getTradeWalletService().broadcastTx(processModel.getTakeOfferFeeTx(),
new FutureCallback<Transaction>() {
@Override
public void onSuccess(Transaction transaction) {
log.debug("Take offer fee published successfully. Transaction ID = " + transaction.getHashAsString());
/* if (trade instanceof SellerTrade)
trade.setProcessState(TakerTradeState.ProcessState.TAKE_OFFER_FEE_PUBLISHED);*/
complete();
}
@Override
public void onFailure(@NotNull Throwable t) {
t.printStackTrace();
appendToErrorMessage("Take offer fee payment failed. Maybe your network connection was lost. Please try again.");
trade.setErrorMessage(errorMessage);
/* if (trade instanceof SellerTrade)
trade.setProcessState(TakerTradeState.ProcessState.TAKE_OFFER_FEE_PUBLISH_FAILED);*/
failed(t);
}
});
} catch (Throwable t) {
t.printStackTrace();
trade.setThrowable(t);
failed(t);
}
}

View File

@ -34,20 +34,16 @@ public class CreateTakeOfferFeeTx extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
Transaction createTakeOfferFeeTx = processModel.getTradeWalletService().createTakeOfferFeeTx(processModel.getAddressEntry());
processModel.setTakeOfferFeeTx(createTakeOfferFeeTx);
processModel.setTakeOfferFeeTxId(createTakeOfferFeeTx.getHashAsString());
/*if (trade instanceof SellerTrade)
trade.setProcessState(TakerTradeState.ProcessState.TAKE_OFFER_FEE_TX_CREATED);*/
complete();
} catch (Throwable t) {
t.printStackTrace();
trade.setThrowable(t);
failed(t);
}
}

View File

@ -32,8 +32,9 @@ public class VerifyOfferFeePayment extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
//TODO impl. missing
int numOfPeersSeenTx = processModel.getWalletService().getNumOfPeersSeenTx(processModel.getTakeOfferFeeTx().getHashAsString());
/* if (numOfPeersSeenTx > 2) {

View File

@ -32,8 +32,9 @@ public class VerifyOffererAccount extends TradeTask {
}
@Override
protected void doRun() {
protected void run() {
try {
runInterceptHook();
if (processModel.getBlockChainService().verifyAccountRegistration()) {
if (processModel.getBlockChainService().isAccountBlackListed(processModel.tradingPeer.getAccountId(),
processModel.tradingPeer.getFiatAccount())) {

View File

@ -38,6 +38,9 @@ public class BuyerTradeState {
PAYOUT_TX_COMMITTED,
PAYOUT_TX_SENT,
PAYOUT_BROAD_CASTED
PAYOUT_BROAD_CASTED,
TIMEOUT,
FAULT
}
}

View File

@ -37,6 +37,9 @@ public class SellerTradeState {
PAYOUT_TX_RECEIVED,
PAYOUT_TX_COMMITTED,
PAYOUT_BROAD_CASTED
PAYOUT_BROAD_CASTED,
TIMEOUT,
FAULT
}
}

View File

@ -0,0 +1,26 @@
/*
* This file is part of Bitsquare.
*
* Bitsquare is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or (at
* your option) any later version.
*
* Bitsquare is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
* License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with Bitsquare. If not, see <http://www.gnu.org/licenses/>.
*/
package io.bitsquare.trade.states;
public enum TradePhase {
PREPARATION, // No damage
TAKE_OFFER__FEE_PAID, // Offer fee can be lost
DEPOSIT_BROAD_CASTED, // Need arbitrator
PAYOUT_BROAD_CASTED // Only charge back risk open
}

View File

@ -54,9 +54,9 @@ public class Preferences implements Serializable {
// Persisted fields
private String btcDenomination = MonetaryFormat.CODE_BTC;
private Boolean useAnimations = true;
private Boolean useEffects = true;
private Boolean displaySecurityDepositInfo = true;
private boolean useAnimations = true;
private boolean useEffects = true;
private boolean displaySecurityDepositInfo = true;
// Observable wrappers
transient private final StringProperty btcDenominationProperty = new SimpleStringProperty(btcDenomination);
@ -111,7 +111,7 @@ public class Preferences implements Serializable {
this.useEffectsProperty.set(useEffectsProperty);
}
public void setDisplaySecurityDepositInfo(Boolean displaySecurityDepositInfo) {
public void setDisplaySecurityDepositInfo(boolean displaySecurityDepositInfo) {
this.displaySecurityDepositInfo = displaySecurityDepositInfo;
storage.queueUpForSave();
}
@ -133,7 +133,7 @@ public class Preferences implements Serializable {
return useAnimationsProperty.get();
}
public Boolean getDisplaySecurityDepositInfo() {
public boolean getDisplaySecurityDepositInfo() {
return displaySecurityDepositInfo;
}

View File

@ -17,6 +17,10 @@
package io.bitsquare.util;
import io.bitsquare.common.handlers.ResultHandler;
import org.bitcoinj.utils.Threading;
import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
@ -36,6 +40,9 @@ import java.io.Serializable;
import java.net.URI;
import java.util.Timer;
import java.util.TimerTask;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -47,6 +54,31 @@ public class Utilities {
private static final Logger log = LoggerFactory.getLogger(Utilities.class);
private static long lastTimeStamp = System.currentTimeMillis();
public static Timer setTimeout(long delay, ResultHandler handler) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> handler.handleResult());
}
};
timer.schedule(task, delay);
return timer;
}
public static Timer setInterval(long delay, ResultHandler handler) {
Timer timer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> handler.handleResult());
}
};
timer.scheduleAtFixedRate(task, delay, delay);
return timer;
}
public static String objectToJson(Object object) {
Gson gson =
new GsonBuilder().setFieldNamingPolicy(FieldNamingPolicy.UPPER_CAMEL_CASE).setPrettyPrinting().create();

View File

@ -158,7 +158,7 @@ public class BitsquareApp extends Application {
primaryStage.show();
//TODO just temp.
//showDebugWindow();
// showDebugWindow();
} catch (Throwable throwable) {
showErrorPopup(throwable, true);
}
@ -174,9 +174,8 @@ public class BitsquareApp extends Application {
throwable.printStackTrace();
Dialogs.create()
.owner(primaryStage)
.title("")
.message("")
.masthead("")
.title("Error")
.message("A fatal exception occurred at startup.")
.showException(throwable);
if (doShutDown)
stop();

View File

@ -17,7 +17,7 @@
package io.bitsquare.app;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.util.Utilities;
import com.google.inject.Inject;
@ -26,8 +26,8 @@ import java.io.File;
import java.nio.file.Path;
import java.util.List;
import java.util.Timer;
import javafx.animation.AnimationTimer;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
@ -65,7 +65,7 @@ public class UpdateProcess {
protected String errorMessage;
protected final Subject<State, State> process = BehaviorSubject.create();
protected AnimationTimer timeoutTimer;
protected Timer timeoutTimer;
@Inject
public UpdateProcess(Environment environment) {
@ -88,11 +88,7 @@ public class UpdateProcess {
log.info("UpdateFX current version " + Version.PATCH_VERSION);
// process.timeout() will cause an error state back but we don't want to break startup in case of an timeout
timeoutTimer = GUIUtil.setTimeout(10000, animationTimer -> {
process.onCompleted();
return null;
});
timeoutTimer.start();
timeoutTimer = Utilities.setTimeout(10000, () -> process.onCompleted());
String agent = environment.getProperty(BitsquareEnvironment.APP_NAME_KEY) + Version.VERSION;
Path dataDirPath = new File(environment.getProperty(BitsquareEnvironment.APP_DATA_DIR_KEY)).toPath();
@ -123,12 +119,12 @@ public class UpdateProcess {
state.set(State.UPDATE_AVAILABLE);
// We stop the timeout and treat it not completed.
// The user should click the restart button manually if there are updates available.
timeoutTimer.stop();
timeoutTimer.cancel();
}
else if (summary.highestVersion == Version.PATCH_VERSION) {
log.info("UP_TO_DATE");
state.set(State.UP_TO_DATE);
timeoutTimer.stop();
timeoutTimer.cancel();
process.onCompleted();
}
} catch (Throwable e) {
@ -138,7 +134,7 @@ public class UpdateProcess {
// so we use state.onCompleted() instead of state.onError()
errorMessage = "Exception at processing UpdateSummary: " + e.getMessage();
state.set(State.FAILURE);
timeoutTimer.stop();
timeoutTimer.cancel();
process.onCompleted();
}
});
@ -150,7 +146,7 @@ public class UpdateProcess {
// so we use state.onCompleted() instead of state.onError()
errorMessage = "Update failed: " + updater.getException();
state.set(State.FAILURE);
timeoutTimer.stop();
timeoutTimer.cancel();
process.onCompleted();
});

View File

@ -64,6 +64,7 @@ public class InputTextField extends TextField {
private InputValidator validator;
///////////////////////////////////////////////////////////////////////////////////////////
// Static
///////////////////////////////////////////////////////////////////////////////////////////
@ -72,6 +73,8 @@ public class InputTextField extends TextField {
if (errorMessageDisplay != null)
errorMessageDisplay.hide();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////

View File

@ -34,13 +34,11 @@ import io.bitsquare.trade.Trade;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.offer.OpenOfferManager;
import io.bitsquare.user.User;
import org.bitcoinj.utils.Threading;
import io.bitsquare.util.Utilities;
import com.google.inject.Inject;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.TimeoutException;
import javafx.application.Platform;
@ -345,7 +343,7 @@ class MainViewModel implements ViewModel {
if (numPendingTrades > 0)
numPendingTradesAsString.set(String.valueOf(numPendingTrades));
if (numPendingTrades > 9)
numPendingTradesAsString.set("*");
numPendingTradesAsString.set("-");
showPendingTradesNotification.set(numPendingTrades > 0);
}
@ -371,18 +369,11 @@ class MainViewModel implements ViewModel {
log.trace("startBlockchainSyncTimeout");
stopBlockchainSyncTimeout();
blockchainSyncTimeoutTimer = new Timer();
TimerTask task = new TimerTask() {
@Override
public void run() {
Threading.USER_THREAD.execute(() -> {
log.trace("Timeout reached");
Platform.runLater(() -> setWalletServiceException(new TimeoutException()));
});
}
};
blockchainSyncTimeoutTimer.schedule(task, BLOCKCHAIN_SYNC_TIMEOUT);
blockchainSyncTimeoutTimer = Utilities.setTimeout(BLOCKCHAIN_SYNC_TIMEOUT, () -> {
log.trace("Timeout reached");
setWalletServiceException(new TimeoutException());
});
}
private void stopBlockchainSyncTimeout() {

View File

@ -44,39 +44,11 @@
</GridPane.margin>
</ComboBox>
<!-- <Label text="P2P network connection:" GridPane.rowIndex="1"/>
<TextField fx:id="connectionType" GridPane.rowIndex="1" GridPane.columnIndex="1"
mouseTransparent="true" focusTraversable="false"/>
<Label text="My external visible P2P network address:" GridPane.rowIndex="2"/>
<TextField fx:id="nodeAddress" GridPane.rowIndex="2" GridPane.columnIndex="1"
mouseTransparent="true" focusTraversable="false"/>
-->
<Label text="Intercept before run?:" GridPane.rowIndex="3">
<GridPane.margin>
<Insets bottom="-15"/>
</GridPane.margin>
</Label>
<CheckBox fx:id="interceptBeforeCheckBox" onAction="#onCheckBoxChanged" GridPane.rowIndex="3" GridPane.columnIndex="1">
<GridPane.margin>
<Insets bottom="-15"/>
</GridPane.margin>
</CheckBox>
<columnConstraints>
<ColumnConstraints hgrow="SOMETIMES" halignment="RIGHT" minWidth="200.0"/>
<ColumnConstraints hgrow="ALWAYS" minWidth="300.0"/>
</columnConstraints>
<rowConstraints>
<RowConstraints vgrow="NEVER"/>
<RowConstraints vgrow="NEVER"/>
<RowConstraints vgrow="NEVER"/>
</rowConstraints>
</GridPane>

View File

@ -21,6 +21,7 @@ import io.bitsquare.common.taskrunner.Task;
import io.bitsquare.gui.common.view.FxmlView;
import io.bitsquare.gui.common.view.InitializableView;
import io.bitsquare.trade.protocol.availability.OfferAvailabilityProtocol;
import io.bitsquare.trade.protocol.availability.tasks.GetPeerAddress;
import io.bitsquare.trade.protocol.availability.tasks.ProcessOfferAvailabilityResponse;
import io.bitsquare.trade.protocol.availability.tasks.SendOfferAvailabilityRequest;
import io.bitsquare.trade.protocol.placeoffer.PlaceOfferProtocol;
@ -32,6 +33,7 @@ import io.bitsquare.trade.protocol.trade.BuyerAsOffererProtocol;
import io.bitsquare.trade.protocol.trade.SellerAsTakerProtocol;
import io.bitsquare.trade.protocol.trade.tasks.buyer.CreateDepositTxInputs;
import io.bitsquare.trade.protocol.trade.tasks.buyer.ProcessDepositTxInputsRequest;
import io.bitsquare.trade.protocol.trade.tasks.buyer.ProcessFinalizePayoutTxRequest;
import io.bitsquare.trade.protocol.trade.tasks.buyer.ProcessPublishDepositTxRequest;
import io.bitsquare.trade.protocol.trade.tasks.buyer.SendDepositTxPublishedMessage;
import io.bitsquare.trade.protocol.trade.tasks.buyer.SendFiatTransferStartedMessage;
@ -39,16 +41,23 @@ import io.bitsquare.trade.protocol.trade.tasks.buyer.SendPayDepositRequest;
import io.bitsquare.trade.protocol.trade.tasks.buyer.SendPayoutTxFinalizedMessage;
import io.bitsquare.trade.protocol.trade.tasks.buyer.SignAndFinalizePayoutTx;
import io.bitsquare.trade.protocol.trade.tasks.buyer.SignAndPublishDepositTx;
import io.bitsquare.trade.protocol.trade.tasks.buyer.VerifyAndSignContract;
import io.bitsquare.trade.protocol.trade.tasks.offerer.VerifyTakeOfferFeePayment;
import io.bitsquare.trade.protocol.trade.tasks.offerer.VerifyTakerAccount;
import io.bitsquare.trade.protocol.trade.tasks.seller.CommitDepositTx;
import io.bitsquare.trade.protocol.trade.tasks.seller.CreateAndSignContract;
import io.bitsquare.trade.protocol.trade.tasks.seller.CreateAndSignDepositTx;
import io.bitsquare.trade.protocol.trade.tasks.seller.ProcessDepositTxPublishedMessage;
import io.bitsquare.trade.protocol.trade.tasks.seller.ProcessFiatTransferStartedMessage;
import io.bitsquare.trade.protocol.trade.tasks.seller.ProcessPayDepositRequest;
import io.bitsquare.trade.protocol.trade.tasks.seller.ProcessPayoutTxFinalizedMessage;
import io.bitsquare.trade.protocol.trade.tasks.seller.SendDepositTxInputsRequest;
import io.bitsquare.trade.protocol.trade.tasks.seller.SendFinalizePayoutTxRequest;
import io.bitsquare.trade.protocol.trade.tasks.seller.SendPublishDepositTxRequest;
import io.bitsquare.trade.protocol.trade.tasks.seller.SignPayoutTx;
import io.bitsquare.trade.protocol.trade.tasks.shared.CommitPayoutTx;
import io.bitsquare.trade.protocol.trade.tasks.shared.SetupPayoutTxLockTimeReachedListener;
import io.bitsquare.trade.protocol.trade.tasks.taker.BroadcastTakeOfferFeeTx;
import io.bitsquare.trade.protocol.trade.tasks.taker.CreateTakeOfferFeeTx;
import io.bitsquare.trade.protocol.trade.tasks.taker.VerifyOfferFeePayment;
import io.bitsquare.trade.protocol.trade.tasks.taker.VerifyOffererAccount;
@ -68,7 +77,6 @@ public class DebugView extends InitializableView {
@FXML ComboBox<Class> taskComboBox;
@FXML CheckBox interceptBeforeCheckBox;
@Inject
public DebugView() {
@ -76,12 +84,10 @@ public class DebugView extends InitializableView {
@Override
public void initialize() {
interceptBeforeCheckBox.setSelected(true);
final ObservableList<Class> items = FXCollections.observableArrayList(Arrays.asList(
/*---- Protocol ----*/
OfferAvailabilityProtocol.class,
io.bitsquare.trade.protocol.availability.tasks.GetPeerAddress.class,
GetPeerAddress.class,
SendOfferAvailabilityRequest.class,
ProcessOfferAvailabilityResponse.class,
Boolean.class, /* used as seperator*/
@ -104,34 +110,45 @@ public class DebugView extends InitializableView {
ProcessPublishDepositTxRequest.class,
VerifyTakerAccount.class,
VerifyAndSignContract.class,
SignAndPublishDepositTx.class,
SendDepositTxPublishedMessage.class,
SignPayoutTx.class,
VerifyTakeOfferFeePayment.class,
SendFiatTransferStartedMessage.class,
ProcessPayoutTxFinalizedMessage.class,
ProcessFinalizePayoutTxRequest.class,
SignAndFinalizePayoutTx.class,
CommitPayoutTx.class,
SendPayoutTxFinalizedMessage.class,
SetupPayoutTxLockTimeReachedListener.class,
Boolean.class, /* used as seperator*/
/*---- Protocol ----*/
SellerAsTakerProtocol.class,
CreateTakeOfferFeeTx.class,
BroadcastTakeOfferFeeTx.class,
SendDepositTxInputsRequest.class,
ProcessPayDepositRequest.class,
VerifyOffererAccount.class,
CreateAndSignContract.class,
CreateAndSignDepositTx.class,
SendPublishDepositTxRequest.class,
ProcessDepositTxPublishedMessage.class,
CommitDepositTx.class,
ProcessFiatTransferStartedMessage.class,
SignAndFinalizePayoutTx.class,
VerifyOfferFeePayment.class,
SendPayoutTxFinalizedMessage.class
SignPayoutTx.class,
SendFinalizePayoutTxRequest.class,
ProcessPayoutTxFinalizedMessage.class,
CommitPayoutTx.class,
SetupPayoutTxLockTimeReachedListener.class
)
);
@ -160,20 +177,8 @@ public class DebugView extends InitializableView {
void onSelectTask() {
Class item = taskComboBox.getSelectionModel().getSelectedItem();
if (!item.getSimpleName().contains("Protocol")) {
if (interceptBeforeCheckBox.isSelected()) {
Task.taskToInterceptBeforeRun = item;
Task.taskToInterceptAfterRun = null;
Task.taskToIntercept = item;
}
else {
Task.taskToInterceptAfterRun = item;
Task.taskToInterceptBeforeRun = null;
}
}
}
@FXML
void onCheckBoxChanged() {
onSelectTask();
}
}

View File

@ -18,7 +18,6 @@
package io.bitsquare.gui.main.offer.createoffer;
import io.bitsquare.arbitration.Arbitrator;
import io.bitsquare.arbitration.ArbitratorService;
import io.bitsquare.btc.AddressEntry;
import io.bitsquare.btc.FeePolicy;
import io.bitsquare.btc.WalletService;
@ -48,17 +47,15 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkNotNull;
/**
* Domain for that UI element.
* Note that the create offer domain has a deeper scope in the application domain (TradeManager).
@ -67,16 +64,22 @@ import static com.google.common.base.Preconditions.checkNotNull;
class CreateOfferDataModel implements Activatable, DataModel {
private static final Logger log = LoggerFactory.getLogger(CreateOfferDataModel.class);
private OpenOfferManager openOfferManager;
private final OpenOfferManager openOfferManager;
private final WalletService walletService;
private final AccountSettings accountSettings;
private final Preferences preferences;
private final User user;
private final BSFormatter formatter;
private final String offerId;
private final AddressEntry addressEntry;
final ObjectProperty<Coin> offerFeeAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> networkFeeAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> securityDepositAsCoin = new SimpleObjectProperty<>();
private final BalanceListener balanceListener;
private final ChangeListener<FiatAccount> currentFiatAccountListener;
private Offer.Direction direction;
private AddressEntry addressEntry;
final StringProperty requestPlaceOfferErrorMessage = new SimpleStringProperty();
final StringProperty transactionId = new SimpleStringProperty();
@ -95,57 +98,53 @@ class CreateOfferDataModel implements Activatable, DataModel {
final ObjectProperty<Fiat> priceAsFiat = new SimpleObjectProperty<>();
final ObjectProperty<Fiat> volumeAsFiat = new SimpleObjectProperty<>();
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> offerFeeAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> networkFeeAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> securityDepositAsCoin = new SimpleObjectProperty<>();
final ObservableList<Country> acceptedCountries = FXCollections.observableArrayList();
final ObservableList<String> acceptedLanguageCodes = FXCollections.observableArrayList();
final ObservableList<Arbitrator> acceptedArbitrators = FXCollections.observableArrayList();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
// non private for testing
@Inject
public CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService, ArbitratorService arbitratorService,
public CreateOfferDataModel(OpenOfferManager openOfferManager, WalletService walletService,
AccountSettings accountSettings, Preferences preferences, User user, BSFormatter formatter) {
this.openOfferManager = openOfferManager;
this.walletService = walletService;
this.accountSettings = accountSettings;
this.preferences = preferences;
this.user = user;
this.formatter = formatter;
this.offerId = UUID.randomUUID().toString();
addressEntry = walletService.getAddressEntry(offerId);
offerFeeAsCoin.set(FeePolicy.CREATE_OFFER_FEE);
networkFeeAsCoin.set(FeePolicy.TX_FEE);
if (walletService != null && walletService.getWallet() != null) {
addressEntry = walletService.getAddressEntry(offerId);
// we need to set it here already as it is used before activate
securityDepositAsCoin.set(accountSettings.getSecurityDeposit());
walletService.addBalanceListener(new BalanceListener(getAddressEntry().getAddress()) {
balanceListener = new BalanceListener(getAddressEntry().getAddress()) {
@Override
public void onBalanceChanged(@NotNull Coin balance) {
updateBalance(balance);
}
});
updateBalance(walletService.getBalanceForAddress(getAddressEntry().getAddress()));
}
};
if (user != null) {
user.currentFiatAccountProperty().addListener((ov, oldValue, newValue) -> applyBankAccount(newValue));
applyBankAccount(user.currentFiatAccountProperty().get());
}
if (accountSettings != null)
btcCode.bind(preferences.btcDenominationProperty());
// we need to set it here already as initWithData is called before activate
if (accountSettings != null)
securityDepositAsCoin.set(accountSettings.getSecurityDeposit());
currentFiatAccountListener = (observable, oldValue, newValue) -> {
applyBankAccount(newValue);
};
}
@Override
public void activate() {
addBindings();
addListeners();
// might be changed after screen change
if (accountSettings != null) {
// set it here again to cover the case of an securityDeposit change after a screen change
@ -155,13 +154,50 @@ class CreateOfferDataModel implements Activatable, DataModel {
acceptedLanguageCodes.setAll(accountSettings.getAcceptedLanguageLocaleCodes());
acceptedArbitrators.setAll(accountSettings.getAcceptedArbitrators());
}
updateBalance(walletService.getBalanceForAddress(getAddressEntry().getAddress()));
applyBankAccount(user.currentFiatAccountProperty().get());
}
@Override
public void deactivate() {
// no-op
removeBindings();
removeListeners();
}
private void addBindings() {
btcCode.bind(preferences.btcDenominationProperty());
}
private void removeBindings() {
btcCode.unbind();
}
private void addListeners() {
walletService.addBalanceListener(balanceListener);
user.currentFiatAccountProperty().addListener(currentFiatAccountListener);
}
private void removeListeners() {
walletService.removeBalanceListener(balanceListener);
user.currentFiatAccountProperty().removeListener(currentFiatAccountListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void initWithData(Offer.Direction direction) {
this.direction = direction;
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
void onPlaceOffer() {
// data validation is done in the trade domain
openOfferManager.onPlaceOffer(offerId,
@ -177,47 +213,14 @@ class CreateOfferDataModel implements Activatable, DataModel {
);
}
void calculateVolume() {
try {
if (priceAsFiat.get() != null &&
amountAsCoin.get() != null &&
!amountAsCoin.get().isZero() &&
!priceAsFiat.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
void onSecurityDepositInfoDisplayed() {
preferences.setDisplaySecurityDepositInfo(false);
}
void calculateAmount() {
try {
if (volumeAsFiat.get() != null &&
priceAsFiat.get() != null &&
!volumeAsFiat.get().isZero() &&
!priceAsFiat.get().isZero()) {
// If we got a btc value with more then 4 decimals we convert it to max 4 decimals
amountAsCoin.set(formatter.reduceTo4Decimals(new ExchangeRate(priceAsFiat.get()).fiatToCoin
(volumeAsFiat.get())));
calculateTotalToPay();
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
void calculateTotalToPay() {
if (securityDepositAsCoin.get() != null) {
if (direction == Offer.Direction.BUY)
totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(securityDepositAsCoin.get()));
else
totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(securityDepositAsCoin.get()).add(amountAsCoin.get()));
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
boolean isMinAmountLessOrEqualAmount() {
@ -227,12 +230,6 @@ class CreateOfferDataModel implements Activatable, DataModel {
return true;
}
void securityDepositInfoDisplayed() {
preferences.setDisplaySecurityDepositInfo(false);
}
@Nullable
Offer.Direction getDirection() {
return direction;
}
@ -245,12 +242,51 @@ class CreateOfferDataModel implements Activatable, DataModel {
return offerId;
}
private void updateBalance(@NotNull Coin balance) {
isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0);
AddressEntry getAddressEntry() {
return addressEntry;
}
public AddressEntry getAddressEntry() {
return addressEntry;
boolean getDisplaySecurityDepositInfo() {
return preferences.getDisplaySecurityDepositInfo();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
void calculateVolume() {
if (priceAsFiat.get() != null &&
amountAsCoin.get() != null &&
!amountAsCoin.get().isZero() &&
!priceAsFiat.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
}
}
void calculateAmount() {
if (volumeAsFiat.get() != null &&
priceAsFiat.get() != null &&
!volumeAsFiat.get().isZero() &&
!priceAsFiat.get().isZero()) {
// If we got a btc value with more then 4 decimals we convert it to max 4 decimals
amountAsCoin.set(formatter.reduceTo4Decimals(new ExchangeRate(priceAsFiat.get()).fiatToCoin(volumeAsFiat.get())));
calculateTotalToPay();
}
}
void calculateTotalToPay() {
if (securityDepositAsCoin.get() != null) {
if (direction == Offer.Direction.BUY)
totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(securityDepositAsCoin.get()));
else
totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(securityDepositAsCoin.get()).add(amountAsCoin.get()));
}
}
private void updateBalance(Coin balance) {
isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0);
}
private void applyBankAccount(FiatAccount fiatAccount) {
@ -262,13 +298,4 @@ class CreateOfferDataModel implements Activatable, DataModel {
fiatCode.set(fiatAccount.currencyCode);
}
}
public Boolean getDisplaySecurityDepositInfo() {
return preferences.getDisplaySecurityDepositInfo();
}
public void initWithData(Offer.Direction direction, Coin amount, Fiat price) {
checkNotNull(direction);
this.direction = direction;
}
}

View File

@ -30,7 +30,7 @@
<AnchorPane fx:id="root" fx:controller="io.bitsquare.gui.main.offer.createoffer.CreateOfferView"
xmlns:fx="http://javafx.com/fxml">
<ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" vbarPolicy="NEVER" fitToWidth="true" fitToHeight="true"
<ScrollPane fx:id="scrollPane" onScroll="#onScroll" hbarPolicy="NEVER" vbarPolicy="NEVER" fitToWidth="true" fitToHeight="true"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"
AnchorPane.bottomAnchor="0.0">

View File

@ -48,6 +48,7 @@ import java.util.List;
import javax.inject.Inject;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.geometry.HPos;
@ -75,6 +76,9 @@ import static javafx.beans.binding.Bindings.createStringBinding;
@FxmlView
public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateOfferViewModel> {
private final Navigation navigation;
private final OverlayManager overlayManager;
@FXML ScrollPane scrollPane;
@FXML ImageView imageView;
@FXML AddressTextField addressTextField;
@ -100,32 +104,258 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
private boolean detailsVisible;
private boolean advancedScreenInited;
private final Navigation navigation;
private final OverlayManager overlayManager;
private OfferView.CloseHandler closeHandler;
private ChangeListener<Boolean> amountFocusedListener;
private ChangeListener<Boolean> minAmountFocusedListener;
private ChangeListener<Boolean> priceFocusedListener;
private ChangeListener<Boolean> volumeFocusedListener;
private ChangeListener<Boolean> showWarningInvalidBtcDecimalPlacesListener;
private ChangeListener<Boolean> showWarningInvalidFiatDecimalPlacesPlacesListener;
private ChangeListener<Boolean> showWarningAdjustedVolumeListener;
private ChangeListener<String> requestPlaceOfferErrorMessageListener;
private ChangeListener<Boolean> isPlaceOfferSpinnerVisibleListener;
private ChangeListener<Boolean> showTransactionPublishedScreen;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private CreateOfferView(CreateOfferViewModel model, Navigation navigation,
OverlayManager overlayManager) {
super(model);
this.navigation = navigation;
this.overlayManager = overlayManager;
}
@Override
protected void initialize() {
setupListeners();
setupBindings();
createListeners();
balanceTextField.setup(model.getWalletService(), model.address.get(), model.getFormatter());
volumeTextField.setPromptText(BSResources.get("createOffer.volume.prompt", model.fiatCode.get()));
}
@Override
protected void doDeactivate() {
protected void doActivate() {
addBindings();
addListeners();
}
@Override
protected void doDeactivate() {
removeBindings();
removeListeners();
}
private void addBindings() {
amountBtcLabel.textProperty().bind(model.btcCode);
priceFiatLabel.textProperty().bind(model.fiatCode);
volumeFiatLabel.textProperty().bind(model.fiatCode);
minAmountBtcLabel.textProperty().bind(model.btcCode);
priceDescriptionLabel.textProperty().bind(createStringBinding(() ->
BSResources.get("createOffer.amountPriceBox.priceDescription", model.fiatCode.get()), model.fiatCode));
volumeDescriptionLabel.textProperty().bind(createStringBinding(() -> model.volumeDescriptionLabel.get(), model.fiatCode, model.volumeDescriptionLabel));
buyLabel.textProperty().bind(model.directionLabel);
amountToTradeLabel.textProperty().bind(model.amountToTradeLabel);
amountTextField.textProperty().bindBidirectional(model.amount);
minAmountTextField.textProperty().bindBidirectional(model.minAmount);
priceTextField.textProperty().bindBidirectional(model.price);
volumeTextField.textProperty().bindBidirectional(model.volume);
amountPriceBoxInfo.textProperty().bind(model.amountPriceBoxInfo);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin);
addressTextField.paymentLabelProperty().bind(model.paymentLabel);
addressTextField.addressProperty().bind(model.addressAsString);
bankAccountTypeTextField.textProperty().bind(model.bankAccountType);
bankAccountCurrencyTextField.textProperty().bind(model.bankAccountCurrency);
bankAccountCountyTextField.textProperty().bind(model.bankAccountCounty);
acceptedCountriesTextField.textProperty().bind(model.acceptedCountries);
acceptedLanguagesTextField.textProperty().bind(model.acceptedLanguages);
acceptedArbitratorsTextField.textProperty().bind(model.acceptedArbitrators);
// Validation
amountTextField.validationResultProperty().bind(model.amountValidationResult);
minAmountTextField.validationResultProperty().bind(model.minAmountValidationResult);
priceTextField.validationResultProperty().bind(model.priceValidationResult);
volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
// buttons
placeOfferButton.visibleProperty().bind(model.isPlaceOfferButtonVisible);
placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled);
placeOfferSpinnerInfoLabel.visibleProperty().bind(model.isPlaceOfferSpinnerVisible);
}
private void removeBindings() {
amountBtcLabel.textProperty().unbind();
priceFiatLabel.textProperty().unbind();
volumeFiatLabel.textProperty().unbind();
minAmountBtcLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind();
volumeDescriptionLabel.textProperty().unbind();
buyLabel.textProperty().unbind();
amountToTradeLabel.textProperty().unbind();
amountTextField.textProperty().unbindBidirectional(model.amount);
minAmountTextField.textProperty().unbindBidirectional(model.minAmount);
priceTextField.textProperty().unbindBidirectional(model.price);
volumeTextField.textProperty().unbindBidirectional(model.volume);
amountPriceBoxInfo.textProperty().unbind();
totalToPayTextField.textProperty().unbind();
addressTextField.amountAsCoinProperty().unbind();
addressTextField.paymentLabelProperty().unbind();
addressTextField.addressProperty().unbind();
bankAccountTypeTextField.textProperty().unbind();
bankAccountCurrencyTextField.textProperty().unbind();
bankAccountCountyTextField.textProperty().unbind();
acceptedCountriesTextField.textProperty().unbind();
acceptedLanguagesTextField.textProperty().unbind();
acceptedArbitratorsTextField.textProperty().unbind();
amountTextField.validationResultProperty().unbind();
minAmountTextField.validationResultProperty().unbind();
priceTextField.validationResultProperty().unbind();
volumeTextField.validationResultProperty().unbind();
placeOfferButton.visibleProperty().unbind();
placeOfferButton.disableProperty().unbind();
placeOfferSpinnerInfoLabel.visibleProperty().unbind();
}
private void createListeners() {
amountFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(model.amount.get());
};
minAmountFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutMinAmountTextField(oldValue, newValue, minAmountTextField.getText());
minAmountTextField.setText(model.minAmount.get());
};
priceFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutPriceTextField(oldValue, newValue, priceTextField.getText());
priceTextField.setText(model.price.get());
};
volumeFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText());
volumeTextField.setText(model.volume.get());
};
showWarningInvalidBtcDecimalPlacesListener = (o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.invalidBtcDecimalPlaces"));
model.showWarningInvalidBtcDecimalPlaces.set(false);
}
};
showWarningInvalidFiatDecimalPlacesPlacesListener = (o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.invalidFiatDecimalPlaces"));
model.showWarningInvalidFiatDecimalPlaces.set(false);
}
};
showWarningAdjustedVolumeListener = (o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.adjustedVolume"));
model.showWarningAdjustedVolume.set(false);
volumeTextField.setText(model.volume.get());
}
};
requestPlaceOfferErrorMessageListener = (o, oldValue, newValue) -> {
if (newValue != null) {
Popups.openErrorPopup(BSResources.get("shared.error"), BSResources.get("createOffer.amountPriceBox.error.message",
model.requestPlaceOfferErrorMessage.get()));
Popups.removeBlurContent();
}
};
isPlaceOfferSpinnerVisibleListener = (ov, oldValue, newValue) -> {
placeOfferSpinner.setProgress(newValue ? -1 : 0);
placeOfferSpinner.setVisible(newValue);
};
showTransactionPublishedScreen = (o, oldValue, newValue) -> {
// TODO temp just for testing
newValue = false;
close();
navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class);
if (newValue) {
overlayManager.blurContent();
// Dialogs are a bit limited. There is no callback for the InformationDialog button click, so we added
// our own actions.
List<Action> actions = new ArrayList<>();
/* actions.add(new AbstractAction(BSResources.get("shared.copyTxId")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "COPY");
Utilities.copyToClipboard(model.transactionId.get());
}
});*/
actions.add(new AbstractAction(BSResources.get("shared.close")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "CLOSE");
try {
close();
navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class);
} catch (Exception e) {
e.printStackTrace();
}
Dialog.Actions.CLOSE.handle(actionEvent);
}
});
Popups.openInfoPopup(BSResources.get("createOffer.success.headline"),
BSResources.get("createOffer.success.info"),
actions);
}
};
}
private void addListeners() {
// focus out
amountTextField.focusedProperty().addListener(amountFocusedListener);
minAmountTextField.focusedProperty().addListener(minAmountFocusedListener);
priceTextField.focusedProperty().addListener(priceFocusedListener);
volumeTextField.focusedProperty().addListener(volumeFocusedListener);
// warnings
model.showWarningInvalidBtcDecimalPlaces.addListener(showWarningInvalidBtcDecimalPlacesListener);
model.showWarningInvalidFiatDecimalPlaces.addListener(showWarningInvalidFiatDecimalPlacesPlacesListener);
model.showWarningAdjustedVolume.addListener(showWarningAdjustedVolumeListener);
model.requestPlaceOfferErrorMessage.addListener(requestPlaceOfferErrorMessageListener);
model.isPlaceOfferSpinnerVisible.addListener(isPlaceOfferSpinnerVisibleListener);
model.showTransactionPublishedScreen.addListener(showTransactionPublishedScreen);
}
private void removeListeners() {
// focus out
amountTextField.focusedProperty().removeListener(amountFocusedListener);
minAmountTextField.focusedProperty().removeListener(minAmountFocusedListener);
priceTextField.focusedProperty().removeListener(priceFocusedListener);
volumeTextField.focusedProperty().removeListener(volumeFocusedListener);
// warnings
model.showWarningInvalidBtcDecimalPlaces.removeListener(showWarningInvalidBtcDecimalPlacesListener);
model.showWarningInvalidFiatDecimalPlaces.removeListener(showWarningInvalidFiatDecimalPlacesPlacesListener);
model.showWarningAdjustedVolume.removeListener(showWarningAdjustedVolumeListener);
model.requestPlaceOfferErrorMessage.removeListener(requestPlaceOfferErrorMessageListener);
model.isPlaceOfferSpinnerVisible.removeListener(isPlaceOfferSpinnerVisibleListener);
model.showTransactionPublishedScreen.removeListener(showTransactionPublishedScreen);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initWithData(Offer.Direction direction, Coin amount, Fiat price) {
model.initWithData(direction, amount, price);
@ -139,11 +369,21 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
this.closeHandler = closeHandler;
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
@FXML
void onPlaceOffer() {
model.onPlaceOffer();
}
@FXML
void onScroll() {
InputTextField.hideErrorMessageDisplay();
}
@FXML
void onShowPayFundsScreen() {
// TODO deactivate for testing the moment
@ -226,6 +466,11 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
Help.openWindow(HelpId.CREATE_OFFER_ADVANCED);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Navigation
///////////////////////////////////////////////////////////////////////////////////////////
private void openAccountSettings() {
navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, RestrictionsView.class);
}
@ -235,150 +480,10 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
closeHandler.close();
}
private void setupListeners() {
scrollPane.setOnScroll(e -> InputTextField.hideErrorMessageDisplay());
// focus out
amountTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(model.amount.get());
});
minAmountTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
model.onFocusOutMinAmountTextField(oldValue, newValue, minAmountTextField.getText());
minAmountTextField.setText(model.minAmount.get());
});
priceTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
model.onFocusOutPriceTextField(oldValue, newValue, priceTextField.getText());
priceTextField.setText(model.price.get());
});
volumeTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
model.onFocusOutVolumeTextField(oldValue, newValue, volumeTextField.getText());
volumeTextField.setText(model.volume.get());
});
// warnings
model.showWarningInvalidBtcDecimalPlaces.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.invalidBtcDecimalPlaces"));
model.showWarningInvalidBtcDecimalPlaces.set(false);
}
});
model.showWarningInvalidFiatDecimalPlaces.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.invalidFiatDecimalPlaces"));
model.showWarningInvalidFiatDecimalPlaces.set(false);
}
});
model.showWarningAdjustedVolume.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"), BSResources.get("createOffer.amountPriceBox.warning.adjustedVolume"));
model.showWarningAdjustedVolume.set(false);
volumeTextField.setText(model.volume.get());
}
});
model.requestPlaceOfferErrorMessage.addListener((o, oldValue, newValue) -> {
if (newValue != null) {
Popups.openErrorPopup(BSResources.get("shared.error"), BSResources.get("createOffer.amountPriceBox.error.message",
model.requestPlaceOfferErrorMessage.get()));
Popups.removeBlurContent();
}
});
model.showTransactionPublishedScreen.addListener((o, oldValue, newValue) -> {
// TODO temp just for testing
newValue = false;
close();
navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class);
if (newValue) {
overlayManager.blurContent();
// Dialogs are a bit limited. There is no callback for the InformationDialog button click, so we added
// our own actions.
List<Action> actions = new ArrayList<>();
/* actions.add(new AbstractAction(BSResources.get("shared.copyTxId")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "COPY");
Utilities.copyToClipboard(model.transactionId.get());
}
});*/
actions.add(new AbstractAction(BSResources.get("shared.close")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "CLOSE");
try {
close();
navigation.navigateTo(MainView.class, PortfolioView.class, OpenOffersView.class);
} catch (Exception e) {
e.printStackTrace();
}
Dialog.Actions.CLOSE.handle(actionEvent);
}
});
Popups.openInfoPopup(BSResources.get("createOffer.success.headline"),
BSResources.get("createOffer.success.info"),
actions);
}
});
}
private void setupBindings() {
amountBtcLabel.textProperty().bind(model.btcCode);
priceFiatLabel.textProperty().bind(model.fiatCode);
volumeFiatLabel.textProperty().bind(model.fiatCode);
minAmountBtcLabel.textProperty().bind(model.btcCode);
priceDescriptionLabel.textProperty().bind(createStringBinding(() ->
BSResources.get("createOffer.amountPriceBox.priceDescription", model.fiatCode.get()), model.fiatCode));
volumeDescriptionLabel.textProperty().bind(createStringBinding(() -> model.volumeDescriptionLabel.get(), model.fiatCode, model.volumeDescriptionLabel));
buyLabel.textProperty().bind(model.directionLabel);
amountToTradeLabel.textProperty().bind(model.amountToTradeLabel);
amountTextField.textProperty().bindBidirectional(model.amount);
minAmountTextField.textProperty().bindBidirectional(model.minAmount);
priceTextField.textProperty().bindBidirectional(model.price);
volumeTextField.textProperty().bindBidirectional(model.volume);
amountPriceBoxInfo.textProperty().bind(model.amountPriceBoxInfo);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin);
addressTextField.paymentLabelProperty().bind(model.paymentLabel);
addressTextField.addressProperty().bind(model.addressAsString);
bankAccountTypeTextField.textProperty().bind(model.bankAccountType);
bankAccountCurrencyTextField.textProperty().bind(model.bankAccountCurrency);
bankAccountCountyTextField.textProperty().bind(model.bankAccountCounty);
acceptedCountriesTextField.textProperty().bind(model.acceptedCountries);
acceptedLanguagesTextField.textProperty().bind(model.acceptedLanguages);
acceptedArbitratorsTextField.textProperty().bind(model.acceptedArbitrators);
// Validation
amountTextField.validationResultProperty().bind(model.amountValidationResult);
minAmountTextField.validationResultProperty().bind(model.minAmountValidationResult);
priceTextField.validationResultProperty().bind(model.priceValidationResult);
volumeTextField.validationResultProperty().bind(model.volumeValidationResult);
// buttons
placeOfferButton.visibleProperty().bind(model.isPlaceOfferButtonVisible);
placeOfferButton.disableProperty().bind(model.isPlaceOfferButtonDisabled);
placeOfferSpinnerInfoLabel.visibleProperty().bind(model.isPlaceOfferSpinnerVisible);
model.isPlaceOfferSpinnerVisible.addListener((ov, oldValue, newValue) -> {
placeOfferSpinner.setProgress(newValue ? -1 : 0);
placeOfferSpinner.setVisible(newValue);
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// State
///////////////////////////////////////////////////////////////////////////////////////////
private void showDetailsScreen() {
payFundsPane.setInactive();
@ -433,6 +538,11 @@ public class CreateOfferView extends ActivatableViewAndModel<AnchorPane, CreateO
advancedInfoDisplay.setVisible(visible);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void initEditIcons() {
acceptedCountriesLabelIcon.setId("clickable-icon");
AwesomeDude.setIcon(acceptedCountriesLabelIcon, AwesomeIcon.EDIT_SIGN);

View File

@ -33,6 +33,7 @@ import org.bitcoinj.utils.Fiat;
import javax.inject.Inject;
import javafx.beans.InvalidationListener;
import javafx.beans.Observable;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ObjectProperty;
@ -40,6 +41,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import static javafx.beans.binding.Bindings.createStringBinding;
@ -49,7 +51,6 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
private final BSFormatter formatter;
private final FiatValidator fiatValidator;
final StringProperty amount = new SimpleStringProperty();
final StringProperty minAmount = new SimpleStringProperty();
final StringProperty price = new SimpleStringProperty();
@ -94,6 +95,25 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Address> address = new SimpleObjectProperty<>();
private ChangeListener<String> amountListener;
private ChangeListener<String> minAmountListener;
private ChangeListener<String> priceListener;
private ChangeListener<String> volumeListener;
private ChangeListener<Coin> amountAsCoinListener;
private ChangeListener<Coin> minAmountAsCoinListener;
private ChangeListener<Fiat> priceAsFiatListener;
private ChangeListener<Fiat> volumeAsFiatListener;
private ChangeListener<Boolean> isWalletFundedListener;
private ChangeListener<Boolean> requestPlaceOfferSuccessListener;
private ChangeListener<String> requestPlaceOfferErrorMessageListener;
private InvalidationListener acceptedCountriesListener;
private InvalidationListener acceptedLanguageCodesListener;
private InvalidationListener acceptedArbitratorsListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public CreateOfferViewModel(CreateOfferDataModel dataModel, FiatValidator fiatValidator, BtcValidator btcValidator,
@ -110,234 +130,22 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
addressAsString.set(dataModel.getAddressEntry().getAddress().toString());
address.set(dataModel.getAddressEntry().getAddress());
}
setupBindings();
setupListeners();
createListeners();
}
// setOfferBookFilter is a one time call
void initWithData(Offer.Direction direction, Coin amount, Fiat price) {
dataModel.initWithData(direction, amount, price);
if (dataModel.getDirection() == Offer.Direction.BUY) {
directionLabel.set(BSResources.get("shared.buyBitcoin"));
amountToTradeLabel.set(BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.buy")));
volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.buy.volumeDescription", fiatCode.get()));
amountPriceBoxInfo.set(BSResources.get("createOffer.amountPriceBox.buy.info"));
}
else {
directionLabel.set(BSResources.get("shared.sellBitcoin"));
amountToTradeLabel.set(BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.sell")));
volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", fiatCode.get()));
amountPriceBoxInfo.set(BSResources.get("createOffer.amountPriceBox.sell.info"));
@Override
protected void doActivate() {
addBindings();
addListeners();
}
// apply only if valid
boolean amountValid = false;
if (amount != null && isBtcInputValid(amount.toPlainString())
.isValid) {
dataModel.amountAsCoin.set(amount);
dataModel.minAmountAsCoin.set(amount);
amountValid = true;
@Override
protected void doDeactivate() {
removeBindings();
removeListeners();
}
// apply only if valid
boolean priceValid = false;
if (price != null && isBtcInputValid(price.toPlainString()).isValid) {
dataModel.priceAsFiat.set(formatter.parseToFiatWith2Decimals(price.toPlainString()));
priceValid = true;
}
if (amountValid && priceValid)
dataModel.calculateTotalToPay();
}
void onPlaceOffer() {
dataModel.requestPlaceOfferErrorMessage.set(null);
dataModel.requestPlaceOfferSuccess.set(false);
isPlaceOfferSpinnerVisible.set(true);
dataModel.onPlaceOffer();
}
void onShowPayFundsScreen() {
isPlaceOfferButtonVisible.set(true);
}
// On focus out we do validation and apply the data to the model
void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(amount.get());
amountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!formatter.hasBtcValidDecimals(userInput));
// only allow max 4 decimal places for btc values
setAmountToModel();
// reformat input
amount.set(formatter.formatCoin(dataModel.amountAsCoin.get()));
calculateVolume();
// handle minAmount/amount relationship
if (!dataModel.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.amountSmallerThanMinAmount")));
}
else {
amountValidationResult.set(result);
if (minAmount.get() != null)
minAmountValidationResult.set(isBtcInputValid(minAmount.get()));
}
}
}
}
void onFocusOutMinAmountTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(minAmount.get());
minAmountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!formatter.hasBtcValidDecimals(userInput));
setMinAmountToModel();
minAmount.set(formatter.formatCoin(dataModel.minAmountAsCoin.get()));
if (!dataModel.isMinAmountLessOrEqualAmount()) {
minAmountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.minAmountLargerThanAmount")));
}
else {
minAmountValidationResult.set(result);
if (amount.get() != null)
amountValidationResult.set(isBtcInputValid(amount.get()));
}
}
}
}
void onFocusOutPriceTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isFiatInputValid(price.get());
boolean isValid = result.isValid;
priceValidationResult.set(result);
if (isValid) {
showWarningInvalidFiatDecimalPlaces.set(!formatter.hasFiatValidDecimals(userInput));
setPriceToModel();
price.set(formatter.formatFiat(dataModel.priceAsFiat.get()));
calculateVolume();
}
}
}
void onFocusOutVolumeTextField(Boolean oldValue, Boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(volume.get());
volumeValidationResult.set(result);
if (result.isValid) {
showWarningInvalidFiatDecimalPlaces.set(!formatter.hasFiatValidDecimals(userInput));
setVolumeToModel();
volume.set(formatter.formatFiat(dataModel.volumeAsFiat.get()));
calculateAmount();
// must be placed after calculateAmount (btc value has been adjusted in case the calculation leads to
// invalid decimal places for the amount value
showWarningAdjustedVolume.set(!formatter.formatFiat(formatter.parseToFiatWith2Decimals(userInput))
.equals(volume
.get()));
}
}
}
void securityDepositInfoDisplayed() {
dataModel.securityDepositInfoDisplayed();
}
WalletService getWalletService() {
return dataModel.getWalletService();
}
BSFormatter getFormatter() {
return formatter;
}
Boolean getDisplaySecurityDepositInfo() {
return dataModel.getDisplaySecurityDepositInfo();
}
private void setupListeners() {
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener((ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
setAmountToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
});
minAmount.addListener((ov, oldValue, newValue) -> {
setMinAmountToModel();
updateButtonDisableState();
});
price.addListener((ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setPriceToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
});
volume.addListener((ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setVolumeToModel();
setPriceToModel();
dataModel.calculateAmount();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
});
dataModel.isWalletFunded.addListener((ov, oldValue, newValue) -> {
updateButtonDisableState();
});
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoin.addListener((ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue)));
dataModel.minAmountAsCoin.addListener((ov, oldValue, newValue) -> minAmount.set(formatter.formatCoin(newValue)));
dataModel.priceAsFiat.addListener((ov, oldValue, newValue) -> price.set(formatter.formatFiat(newValue)));
dataModel.volumeAsFiat.addListener((ov, oldValue, newValue) -> volume.set(formatter.formatFiat(newValue)));
dataModel.requestPlaceOfferErrorMessage.addListener((ov, oldValue, newValue) -> {
if (newValue != null) {
isPlaceOfferSpinnerVisible.set(false);
}
});
dataModel.requestPlaceOfferSuccess.addListener((ov, oldValue, newValue) -> {
isPlaceOfferButtonVisible.set(!newValue);
isPlaceOfferSpinnerVisible.set(false);
});
// ObservableLists
dataModel.acceptedCountries.addListener((Observable o) -> acceptedCountries.set(formatter
.countryLocalesToString(dataModel.acceptedCountries)));
dataModel.acceptedLanguageCodes.addListener((Observable o) -> acceptedLanguages.set(formatter
.languageCodesToString(dataModel.acceptedLanguageCodes)));
dataModel.acceptedArbitrators.addListener((Observable o) ->
acceptedArbitrators.set(formatter.arbitratorsToNames(dataModel.acceptedArbitrators)));
}
private void setupBindings() {
private void addBindings() {
totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()),
dataModel.totalToPayAsCoin));
securityDeposit.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.securityDepositAsCoin.get()),
@ -366,6 +174,307 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
fiatCode.bind(dataModel.fiatCode);
}
private void removeBindings() {
totalToPay.unbind();
securityDeposit.unbind();
tradeAmount.unbind();
totalToPayAsCoin.unbind();
offerFee.unbind();
networkFee.unbind();
bankAccountType.unbind();
bankAccountCurrency.unbind();
bankAccountCounty.unbind();
requestPlaceOfferErrorMessage.unbind();
showTransactionPublishedScreen.unbind();
transactionId.unbind();
btcCode.unbind();
fiatCode.unbind();
}
private void createListeners() {
amountListener = (ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
setAmountToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
};
minAmountListener = (ov, oldValue, newValue) -> {
setMinAmountToModel();
updateButtonDisableState();
};
priceListener = (ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setPriceToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
};
volumeListener = (ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setVolumeToModel();
setPriceToModel();
dataModel.calculateAmount();
dataModel.calculateTotalToPay();
}
updateButtonDisableState();
};
amountAsCoinListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue));
minAmountAsCoinListener = (ov, oldValue, newValue) -> minAmount.set(formatter.formatCoin(newValue));
priceAsFiatListener = (ov, oldValue, newValue) -> price.set(formatter.formatFiat(newValue));
volumeAsFiatListener = (ov, oldValue, newValue) -> volume.set(formatter.formatFiat(newValue));
isWalletFundedListener = (ov, oldValue, newValue) -> {
updateButtonDisableState();
};
requestPlaceOfferSuccessListener = (ov, oldValue, newValue) -> {
isPlaceOfferButtonVisible.set(!newValue);
isPlaceOfferSpinnerVisible.set(false);
};
requestPlaceOfferErrorMessageListener = (ov, oldValue, newValue) -> {
if (newValue != null) {
isPlaceOfferSpinnerVisible.set(false);
}
};
acceptedCountriesListener = (Observable o) ->
acceptedCountries.set(formatter.countryLocalesToString(dataModel.acceptedCountries));
acceptedLanguageCodesListener = (Observable o) -> acceptedLanguages.set(formatter.languageCodesToString(dataModel.acceptedLanguageCodes));
acceptedArbitratorsListener = (Observable o) -> acceptedArbitrators.set(formatter.arbitratorsToNames(dataModel.acceptedArbitrators));
}
private void addListeners() {
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener(amountListener);
minAmount.addListener(minAmountListener);
price.addListener(priceListener);
volume.addListener(volumeListener);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoin.addListener(amountAsCoinListener);
dataModel.minAmountAsCoin.addListener(minAmountAsCoinListener);
dataModel.priceAsFiat.addListener(priceAsFiatListener);
dataModel.volumeAsFiat.addListener(volumeAsFiatListener);
dataModel.isWalletFunded.addListener(isWalletFundedListener);
dataModel.requestPlaceOfferSuccess.addListener(requestPlaceOfferSuccessListener);
dataModel.requestPlaceOfferErrorMessage.addListener(requestPlaceOfferErrorMessageListener);
// ObservableLists
dataModel.acceptedCountries.addListener(acceptedCountriesListener);
dataModel.acceptedLanguageCodes.addListener(acceptedLanguageCodesListener);
dataModel.acceptedArbitrators.addListener(acceptedArbitratorsListener);
}
private void removeListeners() {
amount.removeListener(amountListener);
minAmount.removeListener(minAmountListener);
price.removeListener(priceListener);
volume.removeListener(volumeListener);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoin.removeListener(amountAsCoinListener);
dataModel.minAmountAsCoin.removeListener(minAmountAsCoinListener);
dataModel.priceAsFiat.removeListener(priceAsFiatListener);
dataModel.volumeAsFiat.removeListener(volumeAsFiatListener);
dataModel.isWalletFunded.removeListener(isWalletFundedListener);
dataModel.requestPlaceOfferSuccess.removeListener(requestPlaceOfferSuccessListener);
dataModel.requestPlaceOfferErrorMessage.removeListener(requestPlaceOfferErrorMessageListener);
// ObservableLists
dataModel.acceptedCountries.removeListener(acceptedCountriesListener);
dataModel.acceptedLanguageCodes.removeListener(acceptedLanguageCodesListener);
dataModel.acceptedArbitrators.removeListener(acceptedArbitratorsListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void initWithData(Offer.Direction direction, Coin amountAsCoin, Fiat priceAsFiat) {
addListeners();
dataModel.initWithData(direction);
if (dataModel.getDirection() == Offer.Direction.BUY) {
directionLabel.set(BSResources.get("shared.buyBitcoin"));
amountToTradeLabel.set(BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.buy")));
volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.buy.volumeDescription", fiatCode.get()));
amountPriceBoxInfo.set(BSResources.get("createOffer.amountPriceBox.buy.info"));
}
else {
directionLabel.set(BSResources.get("shared.sellBitcoin"));
amountToTradeLabel.set(BSResources.get("createOffer.amountPriceBox.amountDescription", BSResources.get("shared.sell")));
volumeDescriptionLabel.set(BSResources.get("createOffer.amountPriceBox.sell.volumeDescription", fiatCode.get()));
amountPriceBoxInfo.set(BSResources.get("createOffer.amountPriceBox.sell.info"));
}
// apply only if valid
boolean amountValid = false;
if (amountAsCoin != null && isBtcInputValid(amountAsCoin.toPlainString())
.isValid) {
dataModel.amountAsCoin.set(amountAsCoin);
dataModel.minAmountAsCoin.set(amountAsCoin);
amountValid = true;
}
// apply only if valid
boolean priceValid = false;
if (priceAsFiat != null && isBtcInputValid(priceAsFiat.toPlainString()).isValid) {
dataModel.priceAsFiat.set(formatter.parseToFiatWith2Decimals(priceAsFiat.toPlainString()));
priceValid = true;
}
if (amountValid && priceValid)
dataModel.calculateTotalToPay();
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
void onPlaceOffer() {
dataModel.requestPlaceOfferErrorMessage.set(null);
dataModel.requestPlaceOfferSuccess.set(false);
isPlaceOfferSpinnerVisible.set(true);
dataModel.onPlaceOffer();
}
void onShowPayFundsScreen() {
isPlaceOfferButtonVisible.set(true);
}
void onSecurityDepositInfoDisplayed() {
dataModel.onSecurityDepositInfoDisplayed();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Handle focus
///////////////////////////////////////////////////////////////////////////////////////////
// On focus out we do validation and apply the data to the model
void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(amount.get());
amountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!formatter.hasBtcValidDecimals(userInput));
// only allow max 4 decimal places for btc values
setAmountToModel();
// reformat input
amount.set(formatter.formatCoin(dataModel.amountAsCoin.get()));
calculateVolume();
// handle minAmount/amount relationship
if (!dataModel.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.amountSmallerThanMinAmount")));
}
else {
amountValidationResult.set(result);
if (minAmount.get() != null)
minAmountValidationResult.set(isBtcInputValid(minAmount.get()));
}
}
}
}
void onFocusOutMinAmountTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(minAmount.get());
minAmountValidationResult.set(result);
if (result.isValid) {
showWarningInvalidBtcDecimalPlaces.set(!formatter.hasBtcValidDecimals(userInput));
setMinAmountToModel();
minAmount.set(formatter.formatCoin(dataModel.minAmountAsCoin.get()));
if (!dataModel.isMinAmountLessOrEqualAmount()) {
minAmountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("createOffer.validation.minAmountLargerThanAmount")));
}
else {
minAmountValidationResult.set(result);
if (amount.get() != null)
amountValidationResult.set(isBtcInputValid(amount.get()));
}
}
}
}
void onFocusOutPriceTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isFiatInputValid(price.get());
boolean isValid = result.isValid;
priceValidationResult.set(result);
if (isValid) {
showWarningInvalidFiatDecimalPlaces.set(!formatter.hasFiatValidDecimals(userInput));
setPriceToModel();
price.set(formatter.formatFiat(dataModel.priceAsFiat.get()));
calculateVolume();
}
}
}
void onFocusOutVolumeTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(volume.get());
volumeValidationResult.set(result);
if (result.isValid) {
showWarningInvalidFiatDecimalPlaces.set(!formatter.hasFiatValidDecimals(userInput));
setVolumeToModel();
volume.set(formatter.formatFiat(dataModel.volumeAsFiat.get()));
calculateAmount();
// must be placed after calculateAmount (btc value has been adjusted in case the calculation leads to
// invalid decimal places for the amount value
showWarningAdjustedVolume.set(!formatter.formatFiat(formatter.parseToFiatWith2Decimals(userInput))
.equals(volume
.get()));
}
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
WalletService getWalletService() {
return dataModel.getWalletService();
}
BSFormatter getFormatter() {
return formatter;
}
boolean getDisplaySecurityDepositInfo() {
return dataModel.getDisplaySecurityDepositInfo();
}
boolean isSeller() {
return dataModel.getDirection() == Offer.Direction.SELL;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void calculateVolume() {
setAmountToModel();
setPriceToModel();
@ -417,16 +526,11 @@ class CreateOfferViewModel extends ActivatableWithDataModel<CreateOfferDataModel
}
private InputValidator.ValidationResult isBtcInputValid(String input) {
return btcValidator.validate(input);
}
private InputValidator.ValidationResult isFiatInputValid(String input) {
return fiatValidator.validate(input);
}
boolean isSeller() {
return dataModel.getDirection() == Offer.Direction.SELL;
}
}

View File

@ -18,19 +18,19 @@
package io.bitsquare.gui.main.offer.offerbook;
import io.bitsquare.fiat.FiatAccount;
import io.bitsquare.gui.util.GUIUtil;
import io.bitsquare.locale.Country;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.trade.TradeManager;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.offer.OfferBookService;
import io.bitsquare.user.User;
import io.bitsquare.util.Utilities;
import java.util.List;
import java.util.Timer;
import javax.inject.Inject;
import javafx.animation.AnimationTimer;
import javafx.beans.value.ChangeListener;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
@ -38,8 +38,6 @@ import javafx.collections.ObservableList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static com.google.common.base.Preconditions.checkArgument;
/**
* Holds and manages the unsorted and unfiltered offerbook list of both buy and sell offers.
* It is handled as singleton by Guice and is used by 2 instances of OfferBookDataModel (one for Buy one for Sell).
@ -54,15 +52,15 @@ public class OfferBook {
private final OfferBookService offerBookService;
private final User user;
private final ObservableList<OfferBookListItem> offerBookListItems = FXCollections.observableArrayList();
private final OfferBookService.Listener offerBookServiceListener;
private final ChangeListener<FiatAccount> bankAccountChangeListener;
private final ChangeListener<Number> invalidationListener;
private final OfferBookService.Listener offerBookServiceListener;
private final ObservableList<OfferBookListItem> offerBookListItems = FXCollections.observableArrayList();
private String fiatCode;
private AnimationTimer pollingTimer;
private Timer pollingTimer;
private Country country;
private int numClients = 0;
///////////////////////////////////////////////////////////////////////////////////////////
@ -75,7 +73,7 @@ public class OfferBook {
this.user = user;
bankAccountChangeListener = (observableValue, oldValue, newValue) -> setBankAccount(newValue);
invalidationListener = (ov, oldValue, newValue) -> requestGetOffers();
invalidationListener = (ov, oldValue, newValue) -> offerBookService.getOffers(fiatCode);
offerBookServiceListener = new OfferBookService.Listener() {
@Override
@ -105,20 +103,31 @@ public class OfferBook {
///////////////////////////////////////////////////////////////////////////////////////////
// Package scope
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void addClient() {
numClients++;
if (numClients == 1)
startPolling();
void startPolling() {
addListeners();
setBankAccount(user.currentFiatAccountProperty().get());
pollingTimer = Utilities.setInterval(POLLING_INTERVAL, () -> offerBookService.requestInvalidationTimeStampFromDHT(fiatCode));
offerBookService.getOffers(fiatCode);
}
public void removeClient() {
numClients--;
checkArgument(numClients >= 0);
if (numClients == 0)
stopPolling();
void stopPolling() {
pollingTimer.cancel();
removeListeners();
}
private void addListeners() {
user.currentFiatAccountProperty().addListener(bankAccountChangeListener);
offerBookService.addListener(offerBookServiceListener);
offerBookService.invalidationTimestampProperty().addListener(invalidationListener);
}
private void removeListeners() {
user.currentFiatAccountProperty().removeListener(bankAccountChangeListener);
offerBookService.removeListener(offerBookServiceListener);
offerBookService.invalidationTimestampProperty().removeListener(invalidationListener);
}
@ -126,13 +135,13 @@ public class OfferBook {
// Getter
///////////////////////////////////////////////////////////////////////////////////////////
public ObservableList<OfferBookListItem> getOfferBookListItems() {
ObservableList<OfferBookListItem> getOfferBookListItems() {
return offerBookListItems;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void setBankAccount(FiatAccount fiatAccount) {
@ -149,50 +158,9 @@ public class OfferBook {
}
}
private void addListeners() {
log.debug("addListeners ");
user.currentFiatAccountProperty().addListener(bankAccountChangeListener);
offerBookService.addListener(offerBookServiceListener);
offerBookService.invalidationTimestampProperty().addListener(invalidationListener);
}
private void removeListeners() {
log.debug("removeListeners ");
user.currentFiatAccountProperty().removeListener(bankAccountChangeListener);
offerBookService.removeListener(offerBookServiceListener);
offerBookService.invalidationTimestampProperty().removeListener(invalidationListener);
}
private void addOfferToOfferBookListItems(Offer offer) {
if (offer != null) {
offerBookListItems.add(new OfferBookListItem(offer, country));
}
}
private void requestGetOffers() {
offerBookService.getOffers(fiatCode);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Polling
///////////////////////////////////////////////////////////////////////////////////////////
// TODO Just temporary, will be removed later when we have a push solution
private void startPolling() {
addListeners();
setBankAccount(user.currentFiatAccountProperty().get());
pollingTimer = GUIUtil.setInterval(POLLING_INTERVAL, (animationTimer) -> {
offerBookService.requestInvalidationTimeStampFromDHT(fiatCode);
return null;
});
offerBookService.getOffers(fiatCode);
}
private void stopPolling() {
pollingTimer.stop();
removeListeners();
}
}

View File

@ -54,16 +54,16 @@ import org.slf4j.LoggerFactory;
class OfferBookDataModel implements Activatable, DataModel {
private static final Logger log = LoggerFactory.getLogger(OfferBookDataModel.class);
private final OpenOfferManager openOfferManager;
private final User user;
private final OfferBook offerBook;
private final Preferences preferences;
private final BSFormatter formatter;
private final OpenOfferManager openOfferManager;
private final FilteredList<OfferBookListItem> filteredItems;
private final SortedList<OfferBookListItem> sortedItems;
// private OfferBookInfo offerBookInfo;
private final ChangeListener<FiatAccount> bankAccountChangeListener;
private ChangeListener<FiatAccount> bankAccountChangeListener;
private final ObjectProperty<Coin> amountAsCoin = new SimpleObjectProperty<>();
private final ObjectProperty<Fiat> priceAsFiat = new SimpleObjectProperty<>();
@ -76,6 +76,10 @@ class OfferBookDataModel implements Activatable, DataModel {
private Offer.Direction direction;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public OfferBookDataModel(User user, OpenOfferManager openOfferManager, OfferBook offerBook, Preferences preferences,
BSFormatter formatter) {
@ -87,63 +91,81 @@ class OfferBookDataModel implements Activatable, DataModel {
this.filteredItems = new FilteredList<>(offerBook.getOfferBookListItems());
this.sortedItems = new SortedList<>(filteredItems);
this.bankAccountChangeListener = (observableValue, oldValue, newValue) -> setBankAccount(newValue);
createListeners();
}
@Override
public void activate() {
addBindings();
addListeners();
/*
amountAsCoin.set(null);
priceAsFiat.set(null);
volumeAsFiat.set(null);
offerBook.addClient();
user.currentFiatAccountProperty().addListener(bankAccountChangeListener);
btcCode.bind(preferences.btcDenominationProperty());
//TODO temp for testing
amountAsCoin.set(Coin.COIN);
priceAsFiat.set(Fiat.valueOf("EUR", 300*10000));
// volumeAsFiat.set(Fiat.valueOf("EUR", 300));*/
setBankAccount(user.currentFiatAccountProperty().get());
applyFilter();
offerBook.startPolling();
}
@Override
public void deactivate() {
offerBook.removeClient();
user.currentFiatAccountProperty().removeListener(bankAccountChangeListener);
removeBindings();
removeListeners();
offerBook.stopPolling();
}
private void addBindings() {
btcCode.bind(preferences.btcDenominationProperty());
}
private void removeBindings() {
btcCode.unbind();
}
private void createListeners() {
this.bankAccountChangeListener = (observableValue, oldValue, newValue) -> setBankAccount(newValue);
}
private void addListeners() {
user.currentFiatAccountProperty().addListener(bankAccountChangeListener);
}
private void removeListeners() {
user.currentFiatAccountProperty().removeListener(bankAccountChangeListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void setDirection(Offer.Direction direction) {
this.direction = direction;
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
void onCancelOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
openOfferManager.onCancelOpenOffer(offer, resultHandler, errorMessageHandler);
}
void calculateVolume() {
try {
if (priceAsFiat.get() != null &&
amountAsCoin.get() != null &&
!amountAsCoin.get().isZero() &&
!priceAsFiat.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
void calculateAmount() {
try {
if (volumeAsFiat.get() != null &&
priceAsFiat.get() != null &&
!volumeAsFiat.get().isZero() &&
!priceAsFiat.get().isZero()) {
// If we got a btc value with more then 4 decimals we convert it to max 4 decimals
amountAsCoin.set(formatter.reduceTo4Decimals(new ExchangeRate(priceAsFiat.get()).fiatToCoin
(volumeAsFiat.get())));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
boolean isTradable(Offer offer) {
// if user has not registered yet we display all
@ -180,27 +202,6 @@ class OfferBookDataModel implements Activatable, DataModel {
return countryResult;
}
void setDirection(Offer.Direction direction) {
this.direction = direction;
}
void setAmount(Coin amount) {
amountAsCoin.set(amount);
applyFilter();
}
void setPrice(Fiat price) {
priceAsFiat.set(price);
applyFilter();
}
void setVolume(Fiat volume) {
volumeAsFiat.set(volume);
applyFilter();
}
SortedList<OfferBookListItem> getOfferList() {
return sortedItems;
}
@ -241,6 +242,57 @@ class OfferBookDataModel implements Activatable, DataModel {
return direction;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
void calculateVolume() {
try {
if (priceAsFiat.get() != null &&
amountAsCoin.get() != null &&
!amountAsCoin.get().isZero() &&
!priceAsFiat.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(priceAsFiat.get()).coinToFiat(amountAsCoin.get()));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
void calculateAmount() {
try {
if (volumeAsFiat.get() != null &&
priceAsFiat.get() != null &&
!volumeAsFiat.get().isZero() &&
!priceAsFiat.get().isZero()) {
// If we got a btc value with more then 4 decimals we convert it to max 4 decimals
amountAsCoin.set(formatter.reduceTo4Decimals(new ExchangeRate(priceAsFiat.get()).fiatToCoin
(volumeAsFiat.get())));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
void setAmount(Coin amount) {
amountAsCoin.set(amount);
applyFilter();
}
void setPrice(Fiat price) {
priceAsFiat.set(price);
applyFilter();
}
void setVolume(Fiat volume) {
volumeAsFiat.set(volume);
applyFilter();
}
private void setBankAccount(FiatAccount fiatAccount) {
if (fiatAccount != null) {
fiatCode.set(fiatAccount.currencyCode);
@ -274,5 +326,4 @@ class OfferBookDataModel implements Activatable, DataModel {
return directionResult && amountResult && priceResult;
});
}
}

View File

@ -27,30 +27,15 @@ public class OfferBookListItem {
private final Offer offer;
private final ObjectProperty<Country> bankAccountCountry = new SimpleObjectProperty<>();
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
public OfferBookListItem(Offer offer, Country bankAccountCountry) {
this.offer = offer;
setBankAccountCountry(bankAccountCountry);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setters
///////////////////////////////////////////////////////////////////////////////////////////
void setBankAccountCountry(Country bankAccountCountry) {
this.bankAccountCountry.set(bankAccountCountry);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
public Offer getOffer() {
return offer;
}
@ -62,7 +47,5 @@ public class OfferBookListItem {
ObjectProperty<Country> bankAccountCountryProperty() {
return bankAccountCountry;
}
}

View File

@ -35,6 +35,7 @@ import io.bitsquare.gui.util.ImageUtil;
import io.bitsquare.gui.util.validation.OptionalBtcValidator;
import io.bitsquare.gui.util.validation.OptionalFiatValidator;
import io.bitsquare.locale.BSResources;
import io.bitsquare.locale.Country;
import io.bitsquare.trade.offer.Offer;
import java.util.ArrayList;
@ -43,7 +44,7 @@ import java.util.List;
import javax.inject.Inject;
import javafx.beans.property.ReadOnlyObjectWrapper;
import javafx.collections.transformation.SortedList;
import javafx.beans.value.ChangeListener;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
@ -84,6 +85,11 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
private final OptionalFiatValidator optionalFiatValidator;
private OfferView.OfferActionHandler offerActionHandler;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
OfferBookView(OfferBookViewModel model,
Navigation navigation,
@ -114,7 +120,6 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
placeholder.setWrapText(true);
table.setPlaceholder(placeholder);
setupValidators();
setupComparators();
@ -129,24 +134,12 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
@Override
public void doActivate() {
amountTextField.setText("");
priceTextField.setText("");
volumeTextField.setText("");
setupBindings();
addBindings();
// setOfferBookInfo has been called before
SortedList<OfferBookListItem> offerList = model.getOfferList();
table.setItems(offerList);
offerList.comparatorProperty().bind(table.comparatorProperty());
priceColumn.setSortType((model.getDirection() == Offer.Direction.BUY) ?
TableColumn.SortType.ASCENDING : TableColumn.SortType.DESCENDING);
table.setItems(model.getOfferList());
priceColumn.setSortType((model.getDirection() == Offer.Direction.BUY) ? TableColumn.SortType.ASCENDING : TableColumn.SortType.DESCENDING);
table.sort();
//TODO temp for testing
amountTextField.setText("1");
priceTextField.setText("300");
volumeTextField.setText("300");
}
@Override
@ -154,6 +147,56 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
removeBindings();
}
private void addBindings() {
amountTextField.textProperty().bindBidirectional(model.amount);
priceTextField.textProperty().bindBidirectional(model.price);
volumeTextField.textProperty().bindBidirectional(model.volume);
amountBtcLabel.textProperty().bind(model.btcCode);
priceFiatLabel.textProperty().bind(model.fiatCode);
volumeFiatLabel.textProperty().bind(model.fiatCode);
priceDescriptionLabel.textProperty().bind(createStringBinding(() -> BSResources.get("Filter by price in {0}", model.fiatCode.get()), model.fiatCode));
volumeDescriptionLabel.textProperty().bind(createStringBinding(() -> BSResources.get("Filter by amount in {0}", model.fiatCode.get()), model.fiatCode));
volumeTextField.promptTextProperty().bind(createStringBinding(() -> BSResources.get("Amount in {0}", model.fiatCode.get()), model.fiatCode));
model.getOfferList().comparatorProperty().bind(table.comparatorProperty());
}
private void removeBindings() {
amountTextField.textProperty().unbind();
priceTextField.textProperty().unbind();
volumeTextField.textProperty().unbind();
amountBtcLabel.textProperty().unbind();
priceFiatLabel.textProperty().unbind();
volumeFiatLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind();
volumeDescriptionLabel.textProperty().unbind();
volumeTextField.promptTextProperty().unbind();
model.getOfferList().comparatorProperty().unbind();
}
private void setupValidators() {
amountTextField.setValidator(optionalBtcValidator);
priceTextField.setValidator(optionalFiatValidator);
volumeTextField.setValidator(optionalFiatValidator);
}
private void setupComparators() {
priceColumn.setComparator((o1, o2) -> o1.getOffer().getPrice().compareTo(o2.getOffer().getPrice()));
amountColumn.setComparator((o1, o2) -> o1.getOffer().getAmount().compareTo(o2.getOffer().getAmount()));
volumeColumn.setComparator((o1, o2) ->
o1.getOffer().getOfferVolume().compareTo(o2.getOffer().getOfferVolume()));
/* countryColumn.setComparator((o1, o2) -> o1.getOffer().getBankAccountCountry().getName().compareTo(o2
.getOffer()
.getBankAccountCountry().getName()));*/
bankAccountTypeColumn.setComparator((o1, o2) -> o1.getOffer().getFiatAccountType().compareTo(o2.getOffer()
.getFiatAccountType()));
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void enableCreateOfferButton() {
createOfferButton.setDisable(false);
}
@ -162,6 +205,15 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
model.setDirection(direction);
}
public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) {
this.offerActionHandler = offerActionHandler;
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
@FXML
void createOffer() {
if (model.isRegistered()) {
@ -173,6 +225,23 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
}
}
private void openSetupScreen() {
overlayManager.blurContent();
List<Action> actions = new ArrayList<>();
actions.add(new AbstractAction(BSResources.get("shared.ok")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "OK");
Dialog.Actions.OK.handle(actionEvent);
navigation.setReturnPath(navigation.getCurrentPath());
navigation.navigateTo(MainView.class, AccountView.class, AccountSetupWizard.class);
}
});
Popups.openInfoPopup("You don't have setup a trading account.",
"You need to setup your trading account before you can trade.",
actions);
}
@FXML
void onToggleShowAdvancedSettings() {
detailsVisible = !detailsVisible;
@ -203,23 +272,6 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
Popups.openWarningPopup("Under construction", "This feature is not implemented yet.");
}
private void openSetupScreen() {
overlayManager.blurContent();
List<Action> actions = new ArrayList<>();
actions.add(new AbstractAction(BSResources.get("shared.ok")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "OK");
Dialog.Actions.OK.handle(actionEvent);
navigation.setReturnPath(navigation.getCurrentPath());
navigation.navigateTo(MainView.class, AccountView.class, AccountSetupWizard.class);
}
});
Popups.openInfoPopup("You don't have setup a trading account.",
"You need to setup your trading account before you can trade.",
actions);
}
private void takeOffer(Offer offer) {
if (model.isRegistered()) {
if (offer.getDirection() == Offer.Direction.BUY) {
@ -248,36 +300,10 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
}
private void openRestrictionsWarning(String restrictionsInfo) {
overlayManager.blurContent();
List<Action> actions = new ArrayList<>();
actions.add(new AbstractAction(BSResources.get("shared.yes")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "YES");
Dialog.Actions.YES.handle(actionEvent);
}
});
actions.add(new AbstractAction(BSResources.get("shared.no")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "NO");
Dialog.Actions.NO.handle(actionEvent);
}
});
Action response = Popups.openConfirmPopup("Information",
"You do not fulfill the requirements for that offer.",
restrictionsInfo,
actions);
Popups.removeBlurContent();
if (Popups.isYes(response))
navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, RestrictionsView.class);
else
table.getSelectionModel().clearSelection();
}
///////////////////////////////////////////////////////////////////////////////////////////
// State
///////////////////////////////////////////////////////////////////////////////////////////
private void showDetailsScreen() {
if (!advancedScreenInited) {
@ -313,61 +339,46 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
showOnlyMatchingCheckBox.setManaged(visible);
}
private void setupBindings() {
amountTextField.textProperty().bindBidirectional(model.amount);
priceTextField.textProperty().bindBidirectional(model.price);
volumeTextField.textProperty().bindBidirectional(model.volume);
amountBtcLabel.textProperty().bind(model.btcCode);
priceFiatLabel.textProperty().bind(model.fiatCode);
volumeFiatLabel.textProperty().bind(model.fiatCode);
priceDescriptionLabel.textProperty().bind(model.fiatCode);
volumeDescriptionLabel.textProperty().bind(model.fiatCode);//Price per Bitcoin in EUR
priceDescriptionLabel.textProperty().bind(createStringBinding(() ->
BSResources.get("Filter by price in {0}", model.fiatCode.get()),
model.fiatCode));
volumeDescriptionLabel.textProperty().bind(createStringBinding(() ->
BSResources.get("Filter by amount in {0}", model.fiatCode.get()),
model.fiatCode));
volumeTextField.promptTextProperty().bind(createStringBinding(() ->
BSResources.get("Amount in {0}", model.fiatCode.get()),
model.fiatCode));
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void openRestrictionsWarning(String restrictionsInfo) {
overlayManager.blurContent();
List<Action> actions = new ArrayList<>();
actions.add(new AbstractAction(BSResources.get("shared.yes")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "YES");
Dialog.Actions.YES.handle(actionEvent);
}
});
actions.add(new AbstractAction(BSResources.get("shared.no")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "NO");
Dialog.Actions.NO.handle(actionEvent);
}
});
Action response = Popups.openConfirmPopup("Information",
"You do not fulfill the requirements for that offer.",
restrictionsInfo,
actions);
Popups.removeBlurContent();
if (Popups.isYes(response))
navigation.navigateTo(MainView.class, AccountView.class, AccountSettingsView.class, RestrictionsView.class);
else
table.getSelectionModel().clearSelection();
}
private void removeBindings() {
amountTextField.textProperty().unbind();
priceTextField.textProperty().unbind();
volumeTextField.textProperty().unbind();
amountBtcLabel.textProperty().unbind();
priceFiatLabel.textProperty().unbind();
volumeFiatLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind();
volumeDescriptionLabel.textProperty().unbind();
priceDescriptionLabel.textProperty().unbind();
volumeDescriptionLabel.textProperty().unbind();
volumeTextField.promptTextProperty().unbind();
}
private void setupValidators() {
amountTextField.setValidator(optionalBtcValidator);
priceTextField.setValidator(optionalFiatValidator);
volumeTextField.setValidator(optionalFiatValidator);
}
private void setupComparators() {
priceColumn.setComparator((o1, o2) -> o1.getOffer().getPrice().compareTo(o2.getOffer().getPrice()));
amountColumn.setComparator((o1, o2) -> o1.getOffer().getAmount().compareTo(o2.getOffer().getAmount()));
volumeColumn.setComparator((o1, o2) ->
o1.getOffer().getOfferVolume().compareTo(o2.getOffer().getOfferVolume()));
/* countryColumn.setComparator((o1, o2) -> o1.getOffer().getBankAccountCountry().getName().compareTo(o2
.getOffer()
.getBankAccountCountry().getName()));*/
bankAccountTypeColumn.setComparator((o1, o2) -> o1.getOffer().getFiatAccountType().compareTo(o2.getOffer()
.getFiatAccountType()));
}
public void setOfferActionHandler(OfferView.OfferActionHandler offerActionHandler) {
this.offerActionHandler = offerActionHandler;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Table
///////////////////////////////////////////////////////////////////////////////////////////
private void setAmountColumnCellFactory() {
amountColumn.setCellValueFactory((offer) -> new ReadOnlyObjectWrapper<>(offer.getValue()));
@ -438,6 +449,8 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
return new TableCell<OfferBookListItem, OfferBookListItem>() {
final ImageView iconView = new ImageView();
final Button button = new Button();
ChangeListener<Country> countryChangeListener;
OfferBookListItem item;
{
button.setGraphic(iconView);
@ -493,14 +506,26 @@ public class OfferBookView extends ActivatableViewAndModel<GridPane, OfferBookVi
button.setOnAction(event -> takeOffer(item.getOffer()));
}
//TODO remove listener
item.bankAccountCountryProperty().addListener((ov, o, n) -> verifyIfTradable(item));
if (countryChangeListener != null && this.item != null)
item.bankAccountCountryProperty().removeListener(countryChangeListener);
this.item = item;
countryChangeListener = (ov, o, n) -> verifyIfTradable(this.item);
item.bankAccountCountryProperty().addListener(countryChangeListener);
verifyIfTradable(item);
button.setText(title);
setGraphic(button);
}
else {
if (this.item != null) {
this.item.bankAccountCountryProperty().removeListener(countryChangeListener);
this.item = null;
countryChangeListener = null;
}
setGraphic(null);
}
}

View File

@ -35,6 +35,7 @@ import com.google.inject.Inject;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.collections.transformation.SortedList;
import org.slf4j.Logger;
@ -54,6 +55,17 @@ class OfferBookViewModel extends ActivatableWithDataModel<OfferBookDataModel> im
final StringProperty fiatCode = new SimpleStringProperty();
final StringProperty restrictionsInfo = new SimpleStringProperty();
private ChangeListener<String> amountListener;
private ChangeListener<String> priceListener;
private ChangeListener<String> volumeListener;
private ChangeListener<Coin> amountAsCoinListener;
private ChangeListener<Fiat> priceAsFiatListener;
private ChangeListener<Fiat> volumeAsFiatListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public OfferBookViewModel(OfferBookDataModel dataModel, OptionalFiatValidator optionalFiatValidator,
@ -64,58 +76,116 @@ class OfferBookViewModel extends ActivatableWithDataModel<OfferBookDataModel> im
this.optionalBtcValidator = optionalBtcValidator;
this.formatter = formatter;
createListeners();
}
@Override
protected void doActivate() {
addBindings();
addListeners();
}
@Override
protected void doDeactivate() {
removeBindings();
removeListeners();
}
private void addBindings() {
btcCode.bind(dataModel.btcCode);
fiatCode.bind(dataModel.fiatCode);
restrictionsInfo.bind(dataModel.restrictionsInfo);
}
// Bidirectional bindings are used for all input fields: amount, price and volume
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener((ov, oldValue, newValue) -> {
private void removeBindings() {
btcCode.unbind();
fiatCode.unbind();
restrictionsInfo.unbind();
}
private void createListeners() {
amountAsCoinListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue));
priceAsFiatListener = (ov, oldValue, newValue) -> price.set(formatter.formatFiat(newValue));
volumeAsFiatListener = (ov, oldValue, newValue) -> volume.set(formatter.formatFiat(newValue));
amountListener = (ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
setAmountToModel();
setPriceToModel();
dataModel.calculateVolume();
}
});
price.addListener((ov, oldValue, newValue) -> {
};
priceListener = (ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setAmountToModel();
setPriceToModel();
dataModel.calculateVolume();
}
});
volume.addListener((ov, oldValue, newValue) -> {
};
volumeListener = (ov, oldValue, newValue) -> {
if (isFiatInputValid(newValue).isValid) {
setPriceToModel();
setVolumeToModel();
dataModel.calculateAmount();
}
});
};
}
private void addListeners() {
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoinProperty().addListener((ov, oldValue, newValue) -> amount.set(formatter.formatCoin
(newValue)));
dataModel.priceAsFiatProperty().addListener((ov, oldValue, newValue) -> price.set(formatter.formatFiat(newValue)));
dataModel.volumeAsFiatProperty().addListener((ov, oldValue, newValue) -> volume.set(formatter.formatFiat
(newValue)));
dataModel.amountAsCoinProperty().addListener(amountAsCoinListener);
dataModel.priceAsFiatProperty().addListener(priceAsFiatListener);
dataModel.volumeAsFiatProperty().addListener(volumeAsFiatListener);
// Bidirectional bindings are used for all input fields: amount, price and volume
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener(amountListener);
price.addListener(priceListener);
volume.addListener(volumeListener);
amount.set("1");
price.set("300");
setAmountToModel();
setPriceToModel();
}
void onCancelOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
dataModel.onCancelOpenOffer(offer, resultHandler, errorMessageHandler);
private void removeListeners() {
amount.removeListener(amountListener);
price.removeListener(priceListener);
volume.removeListener(volumeListener);
dataModel.amountAsCoinProperty().removeListener(amountAsCoinListener);
dataModel.priceAsFiatProperty().removeListener(priceAsFiatListener);
dataModel.volumeAsFiatProperty().removeListener(volumeAsFiatListener);
}
boolean isTradable(Offer offer) {
return dataModel.isTradable(offer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void setDirection(Offer.Direction direction) {
dataModel.setDirection(direction);
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
void onCancelOpenOffer(Offer offer, ResultHandler resultHandler, ErrorMessageHandler errorMessageHandler) {
dataModel.onCancelOpenOffer(offer, resultHandler, errorMessageHandler);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
boolean isTradable(Offer offer) {
return dataModel.isTradable(offer);
}
SortedList<OfferBookListItem> getOfferList() {
return dataModel.getOfferList();
}
@ -162,6 +232,11 @@ class OfferBookViewModel extends ActivatableWithDataModel<OfferBookDataModel> im
return dataModel.getPriceAsFiat();
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private InputValidator.ValidationResult isBtcInputValid(String input) {
return optionalBtcValidator.validate(input);
}

View File

@ -46,11 +46,6 @@ import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Domain for that UI element.
* Note that the create offer domain has a deeper scope in the application domain (TradeManager).
* That model is just responsible for the domain specific parts displayed needed in that UI element.
*/
class TakeOfferDataModel implements Activatable, DataModel {
private static final Logger log = LoggerFactory.getLogger(TakeOfferDataModel.class);
@ -73,6 +68,13 @@ class TakeOfferDataModel implements Activatable, DataModel {
final ObjectProperty<Coin> offerFeeAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<Coin> networkFeeAsCoin = new SimpleObjectProperty<>();
private BalanceListener balanceListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public TakeOfferDataModel(TradeManager tradeManager,
WalletService walletService,
@ -87,59 +89,90 @@ class TakeOfferDataModel implements Activatable, DataModel {
@Override
public void activate() {
btcCode.bind(preferences.btcDenominationProperty());
addBindings();
addListeners();
}
@Override
public void deactivate() {
btcCode.unbind();
removeBindings();
removeListeners();
tradeManager.onCancelAvailabilityRequest(offer);
}
private void addBindings() {
btcCode.bind(preferences.btcDenominationProperty());
}
private void removeBindings() {
btcCode.unbind();
}
private void addListeners() {
walletService.addBalanceListener(balanceListener);
}
private void removeListeners() {
if (addressEntry != null)
walletService.removeBalanceListener(balanceListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void initWithData(Coin amount, Offer offer) {
this.offer = offer;
securityDepositAsCoin.set(offer.getSecurityDeposit());
if (amount != null &&
!amount.isGreaterThan(offer.getAmount()) &&
!offer.getMinAmount().isGreaterThan(amount)) {
if (amount != null && !amount.isGreaterThan(offer.getAmount()) && !offer.getMinAmount().isGreaterThan(amount))
amountAsCoin.set(amount);
}
else {
else
amountAsCoin.set(offer.getAmount());
}
calculateVolume();
calculateTotalToPay();
addressEntry = walletService.getAddressEntry(offer.getId());
walletService.addBalanceListener(new BalanceListener(addressEntry.getAddress()) {
assert addressEntry != null;
balanceListener = new BalanceListener(addressEntry.getAddress()) {
@Override
public void onBalanceChanged(@NotNull Coin balance) {
updateBalance(balance);
}
});
};
updateBalance(walletService.getBalanceForAddress(addressEntry.getAddress()));
tradeManager.onCheckOfferAvailability(offer);
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI calls
///////////////////////////////////////////////////////////////////////////////////////////
void onTakeOffer(TakeOfferResultHandler handler) {
tradeManager.onTakeOffer(amountAsCoin.get(), offer, handler::handleResult);
tradeManager.onTakeOffer(amountAsCoin.get(), offer, handler);
}
void onSecurityDepositInfoDisplayed() {
preferences.setDisplaySecurityDepositInfo(false);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
void calculateVolume() {
try {
if (offer != null &&
offer.getPrice() != null &&
amountAsCoin.get() != null &&
!amountAsCoin.get().isZero()) {
volumeAsFiat.set(new ExchangeRate(offer.getPrice()).coinToFiat(amountAsCoin.get()));
}
} catch (Throwable t) {
// Should be never reached
log.error(t.toString());
}
}
void calculateTotalToPay() {
@ -149,6 +182,15 @@ class TakeOfferDataModel implements Activatable, DataModel {
totalToPayAsCoin.set(offerFeeAsCoin.get().add(networkFeeAsCoin.get()).add(securityDepositAsCoin.get()).add(amountAsCoin.get()));
}
private void updateBalance(@NotNull Coin balance) {
isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
Offer.Direction getDirection() {
return offer.getDirection();
}
@ -168,15 +210,10 @@ class TakeOfferDataModel implements Activatable, DataModel {
return true;
}
Boolean getDisplaySecurityDepositInfo() {
boolean getDisplaySecurityDepositInfo() {
return preferences.getDisplaySecurityDepositInfo();
}
void securityDepositInfoDisplayed() {
preferences.setDisplaySecurityDepositInfo(false);
}
WalletService getWalletService() {
return walletService;
}
@ -184,10 +221,4 @@ class TakeOfferDataModel implements Activatable, DataModel {
AddressEntry getAddressEntry() {
return addressEntry;
}
private void updateBalance(@NotNull Coin balance) {
isWalletFunded.set(totalToPayAsCoin.get() != null && balance.compareTo(totalToPayAsCoin.get()) >= 0);
}
}

View File

@ -32,7 +32,7 @@
AnchorPane.rightAnchor="10.0" AnchorPane.topAnchor="10.0"
xmlns:fx="http://javafx.com/fxml">
<ScrollPane fx:id="scrollPane" hbarPolicy="NEVER" vbarPolicy="NEVER" fitToWidth="true" fitToHeight="true"
<ScrollPane fx:id="scrollPane" onScroll="#onScroll" hbarPolicy="NEVER" vbarPolicy="NEVER" fitToWidth="true" fitToHeight="true"
AnchorPane.leftAnchor="0.0" AnchorPane.rightAnchor="0.0" AnchorPane.topAnchor="0.0"
AnchorPane.bottomAnchor="0.0">

View File

@ -72,6 +72,9 @@ import static javafx.beans.binding.Bindings.createStringBinding;
@FxmlView
public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOfferViewModel> {
private final Navigation navigation;
private final OverlayManager overlayManager;
@FXML ScrollPane scrollPane;
@FXML ImageView imageView;
@FXML InputTextField amountTextField;
@ -94,11 +97,19 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
private ImageView collapse;
private PopOver totalToPayInfoPopover;
private final Navigation navigation;
private final OverlayManager overlayManager;
private OfferView.CloseHandler closeHandler;
private ChangeListener<String> errorMessageChangeListener;
private ChangeListener<Boolean> amountFocusedListener;
private ChangeListener<Boolean> isTakeOfferSpinnerVisibleListener;
private ChangeListener<TakeOfferViewModel.State> stateListener;
private ChangeListener<Boolean> showWarningInvalidBtcDecimalPlacesListener;
private ChangeListener<Boolean> showTransactionPublishedScreenListener;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
private TakeOfferView(TakeOfferViewModel model, Navigation navigation,
@ -107,19 +118,153 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
this.navigation = navigation;
this.overlayManager = overlayManager;
createListeners();
}
@Override
public void initialize() {
setupListeners();
setupBindings();
}
@Override
protected void doActivate() {
addListeners();
addBindings();
}
@Override
protected void doDeactivate() {
model.errorMessage.removeListener(errorMessageChangeListener);
removeBindings();
removeListeners();
}
private void addBindings() {
amountBtcLabel.textProperty().bind(model.btcCode);
amountTextField.textProperty().bindBidirectional(model.amount);
volumeTextField.textProperty().bindBidirectional(model.volume);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin);
amountDescriptionLabel.textProperty().bind(model.amountDescription);
amountTextField.validationResultProperty().bind(model.amountValidationResult);
takeOfferButton.disableProperty().bind(model.takeOfferButtonDisabled);
takeOfferSpinnerInfoLabel.visibleProperty().bind(model.isTakeOfferSpinnerVisible);
}
private void removeBindings() {
amountBtcLabel.textProperty().unbind();
amountTextField.textProperty().unbindBidirectional(model.amount);
volumeTextField.textProperty().unbindBidirectional(model.volume);
totalToPayTextField.textProperty().unbind();
addressTextField.amountAsCoinProperty().unbind();
amountDescriptionLabel.textProperty().unbind();
amountTextField.validationResultProperty().unbind();
takeOfferButton.disableProperty().unbind();
takeOfferSpinnerInfoLabel.visibleProperty().unbind();
}
private void createListeners() {
errorMessageChangeListener = (o, oldValue, newValue) -> {
if (newValue != null) {
Popups.openErrorPopup(BSResources.get("shared.error"), BSResources.get("takeOffer.error.message", model.errorMessage.get()));
Popups.removeBlurContent();
Platform.runLater(this::close);
}
};
amountFocusedListener = (o, oldValue, newValue) -> {
model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(model.amount.get());
};
isTakeOfferSpinnerVisibleListener = (ov, oldValue, newValue) -> {
takeOfferSpinner.setProgress(newValue ? -1 : 0);
takeOfferSpinner.setVisible(newValue);
};
stateListener = (ov, oldValue, newValue) -> {
switch (newValue) {
case CHECK_AVAILABILITY:
showCheckAvailabilityScreen();
break;
case AMOUNT_SCREEN:
showAmountScreen();
break;
case PAYMENT_SCREEN:
setupPaymentScreen();
break;
case DETAILS_SCREEN:
showDetailsScreen();
break;
}
};
showWarningInvalidBtcDecimalPlacesListener = (o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"),
BSResources.get("takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces"));
model.showWarningInvalidBtcDecimalPlaces.set(false);
}
};
showTransactionPublishedScreenListener = (o, oldValue, newValue) -> {
// TODO temp just for testing
newValue = false;
close();
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
if (newValue) {
overlayManager.blurContent();
// Dialogs are a bit limited. There is no callback for the InformationDialog button click, so we added
// our own actions.
List<Action> actions = new ArrayList<>();
/* actions.add(new AbstractAction(BSResources.get("shared.copyTxId")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "COPY");
Utilities.copyToClipboard(model.transactionId.get());
}
});*/
actions.add(new AbstractAction(BSResources.get("shared.close")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "CLOSE");
try {
close();
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
} catch (Exception e) {
e.printStackTrace();
}
Dialog.Actions.CLOSE.handle(actionEvent);
}
});
Popups.openInfoPopup(BSResources.get("takeOffer.success.headline"),
BSResources.get("takeOffer.success.info"),
actions);
}
};
}
private void addListeners() {
amountTextField.focusedProperty().addListener(amountFocusedListener);
model.isTakeOfferSpinnerVisible.addListener(isTakeOfferSpinnerVisibleListener);
model.state.addListener(stateListener);
model.showWarningInvalidBtcDecimalPlaces.addListener(showWarningInvalidBtcDecimalPlacesListener);
model.errorMessage.addListener(errorMessageChangeListener);
model.showTransactionPublishedScreen.addListener(showTransactionPublishedScreenListener);
}
private void removeListeners() {
amountTextField.focusedProperty().removeListener(amountFocusedListener);
model.isTakeOfferSpinnerVisible.removeListener(isTakeOfferSpinnerVisibleListener);
model.state.removeListener(stateListener);
model.showWarningInvalidBtcDecimalPlaces.removeListener(showWarningInvalidBtcDecimalPlacesListener);
model.errorMessage.removeListener(errorMessageChangeListener);
model.showTransactionPublishedScreen.removeListener(showTransactionPublishedScreenListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
public void initWithData(Coin amount, Offer offer) {
model.initWithData(amount, offer);
@ -156,6 +301,11 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
this.closeHandler = closeHandler;
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
@FXML
void onTakeOffer() {
model.onTakeOffer();
@ -166,11 +316,15 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
model.onShowPaymentScreen();
}
@FXML
void onScroll() {
InputTextField.hideErrorMessageDisplay();
}
@FXML
void onToggleShowAdvancedSettings() {
model.detailsVisible = !model.detailsVisible;
if (model.detailsVisible) {
model.onToggleShowAdvancedSettings();
if (model.isDetailsVisible()) {
showAdvancedSettingsButton.setText(BSResources.get("takeOffer.fundsBox.hideAdvanced"));
showAdvancedSettingsButton.setGraphic(collapse);
showDetailsScreen();
@ -197,117 +351,10 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
Help.openWindow(HelpId.TAKE_OFFER_ADVANCED);
}
private void close() {
if (closeHandler != null)
closeHandler.close();
}
private void setupListeners() {
scrollPane.setOnScroll(e -> InputTextField.hideErrorMessageDisplay());
// focus out
amountTextField.focusedProperty().addListener((o, oldValue, newValue) -> {
model.onFocusOutAmountTextField(oldValue, newValue, amountTextField.getText());
amountTextField.setText(model.amount.get());
});
model.state.addListener((ov, oldValue, newValue) -> {
switch (newValue) {
case CHECK_AVAILABILITY:
showCheckAvailabilityScreen();
break;
case AMOUNT_SCREEN:
showAmountScreen();
break;
case PAYMENT_SCREEN:
setupPaymentScreen();
break;
case DETAILS_SCREEN:
showDetailsScreen();
break;
}
});
// warnings
model.showWarningInvalidBtcDecimalPlaces.addListener((o, oldValue, newValue) -> {
if (newValue) {
Popups.openWarningPopup(BSResources.get("shared.warning"),
BSResources.get("takeOffer.amountPriceBox.warning.invalidBtcDecimalPlaces"));
model.showWarningInvalidBtcDecimalPlaces.set(false);
}
});
errorMessageChangeListener = (o, oldValue, newValue) -> {
if (newValue != null) {
Popups.openErrorPopup(BSResources.get("shared.error"), BSResources.get("takeOffer.error.message", model.errorMessage.get()));
Popups.removeBlurContent();
Platform.runLater(this::close);
}
};
model.errorMessage.addListener(errorMessageChangeListener);
model.showTransactionPublishedScreen.addListener((o, oldValue, newValue) -> {
// TODO temp just for testing
newValue = false;
close();
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
if (newValue) {
overlayManager.blurContent();
// Dialogs are a bit limited. There is no callback for the InformationDialog button click, so we added
// our own actions.
List<Action> actions = new ArrayList<>();
/* actions.add(new AbstractAction(BSResources.get("shared.copyTxId")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "COPY");
Utilities.copyToClipboard(model.transactionId.get());
}
});*/
actions.add(new AbstractAction(BSResources.get("shared.close")) {
@Override
public void handle(ActionEvent actionEvent) {
getProperties().put("type", "CLOSE");
try {
close();
navigation.navigateTo(MainView.class, PortfolioView.class, PendingTradesView.class);
} catch (Exception e) {
e.printStackTrace();
}
Dialog.Actions.CLOSE.handle(actionEvent);
}
});
Popups.openInfoPopup(BSResources.get("takeOffer.success.headline"),
BSResources.get("takeOffer.success.info"),
actions);
}
});
}
private void setupBindings() {
amountBtcLabel.textProperty().bind(model.btcCode);
amountTextField.textProperty().bindBidirectional(model.amount);
volumeTextField.textProperty().bindBidirectional(model.volume);
totalToPayTextField.textProperty().bind(model.totalToPay);
addressTextField.amountAsCoinProperty().bind(model.totalToPayAsCoin);
amountDescriptionLabel.textProperty().bind(model.amountDescription);
// Validation
amountTextField.validationResultProperty().bind(model.amountValidationResult);
// buttons
takeOfferButton.disableProperty().bind(model.takeOfferButtonDisabled);
takeOfferSpinnerInfoLabel.visibleProperty().bind(model.isTakeOfferSpinnerVisible);
model.isTakeOfferSpinnerVisible.addListener((ov, oldValue, newValue) -> {
takeOfferSpinner.setProgress(newValue ? -1 : 0);
takeOfferSpinner.setVisible(newValue);
});
}
///////////////////////////////////////////////////////////////////////////////////////////
// States/screens
///////////////////////////////////////////////////////////////////////////////////////////
private void showCheckAvailabilityScreen() {
@ -376,12 +423,9 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.AS_NEEDED);
scrollPane.layout();
model.advancedScreenInited = !model.advancedScreenInited;
toggleDetailsScreen(true);
}
private void hideDetailsScreen() {
payFundsPane.setActive();
scrollPane.setVbarPolicy(ScrollPane.ScrollBarPolicy.NEVER);
@ -418,6 +462,16 @@ public class TakeOfferView extends ActivatableViewAndModel<AnchorPane, TakeOffer
advancedInfoDisplay.setVisible(visible);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void close() {
if (closeHandler != null)
closeHandler.close();
}
private void setupTotalToPayInfoIconLabel() {
totalToPayInfoIconLabel.setId("clickable-icon");
AwesomeDude.setIcon(totalToPayInfoIconLabel, AwesomeIcon.QUESTION_SIGN);

View File

@ -27,9 +27,11 @@ import io.bitsquare.locale.BSResources;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.trade.BuyerAsTakerTrade;
import io.bitsquare.trade.SellerAsTakerTrade;
import io.bitsquare.trade.Trade;
import io.bitsquare.trade.offer.Offer;
import io.bitsquare.trade.states.BuyerTradeState;
import io.bitsquare.trade.states.SellerTradeState;
import io.bitsquare.trade.states.TradeState;
import org.bitcoinj.core.Address;
import org.bitcoinj.core.Coin;
@ -42,6 +44,7 @@ import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@ -58,6 +61,12 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
DETAILS_SCREEN
}
private final BtcValidator btcValidator;
private final BSFormatter formatter;
private final String offerFee;
private final String networkFee;
// static fields
private String amountRange;
private String price;
private String directionLabel;
@ -69,17 +78,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
private String acceptedArbitratorIds;
private String addressAsString;
private String paymentLabel;
private boolean detailsVisible;
// Needed for the addressTextField
final ObjectProperty<Address> address = new SimpleObjectProperty<>();
final ObjectProperty<State> state = new SimpleObjectProperty<>(State.CHECK_AVAILABILITY);
private final BtcValidator btcValidator;
private final BSFormatter formatter;
private final String offerFee;
private final String networkFee;
boolean detailsVisible;
boolean advancedScreenInited;
final StringProperty amount = new SimpleStringProperty();
final StringProperty volume = new SimpleStringProperty();
@ -99,12 +99,30 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
final BooleanProperty showWarningInvalidBtcDecimalPlaces = new SimpleBooleanProperty();
final BooleanProperty showTransactionPublishedScreen = new SimpleBooleanProperty();
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
// Needed for the addressTextField
final ObjectProperty<Address> address = new SimpleObjectProperty<>();
final ObjectProperty<Coin> totalToPayAsCoin = new SimpleObjectProperty<>();
final ObjectProperty<State> state = new SimpleObjectProperty<>(State.CHECK_AVAILABILITY);
final ObjectProperty<InputValidator.ValidationResult> amountValidationResult = new SimpleObjectProperty<>();
private boolean takeOfferRequested;
private boolean isAmountAndPriceValidAndWalletFunded;
// listeners
private ChangeListener<String> amountChangeListener;
private ChangeListener<Boolean> isWalletFundedChangeListener;
private ChangeListener<Coin> amountAsCoinChangeListener;
private ChangeListener<Offer.State> offerStateChangeListener;
private ChangeListener<TradeState.ProcessState> tradeStateChangeListener;
// Offer and trade are stored only for remove listener at deactivate
private Offer offer;
private Trade trade;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor, lifecycle
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public TakeOfferViewModel(TakeOfferDataModel dataModel, BtcValidator btcValidator, BSFormatter formatter) {
@ -116,23 +134,94 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
this.offerFee = formatter.formatCoinWithCode(dataModel.offerFeeAsCoin.get());
this.networkFee = formatter.formatCoinWithCode(dataModel.networkFeeAsCoin.get());
setupBindings();
setupListeners();
applyTakeOfferRequestResult(false);
createListeners();
}
// setOfferBookFilter is a one time call
@Override
protected void doActivate() {
addBindings();
addListeners();
isTakeOfferSpinnerVisible.set(false);
showTransactionPublishedScreen.set(false);
}
@Override
protected void doDeactivate() {
removeBindings();
removeListeners();
}
private void addBindings() {
volume.bind(createStringBinding(() -> formatter.formatFiatWithCode(dataModel.volumeAsFiat.get()), dataModel.volumeAsFiat));
totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()), dataModel.totalToPayAsCoin));
securityDeposit.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.securityDepositAsCoin.get()), dataModel.securityDepositAsCoin));
totalToPayAsCoin.bind(dataModel.totalToPayAsCoin);
btcCode.bind(dataModel.btcCode);
}
private void removeBindings() {
volume.unbind();
totalToPay.unbind();
securityDeposit.unbind();
totalToPayAsCoin.unbind();
btcCode.unbind();
}
private void createListeners() {
amountChangeListener = (ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
setAmountToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
evaluateViewState();
};
isWalletFundedChangeListener = (ov, oldValue, newValue) -> evaluateViewState();
amountAsCoinChangeListener = (ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue));
offerStateChangeListener = (ov, oldValue, newValue) -> applyOfferState(newValue);
tradeStateChangeListener = (ov, oldValue, newValue) -> applyTradeState(newValue);
}
private void addListeners() {
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener(amountChangeListener);
dataModel.isWalletFunded.addListener(isWalletFundedChangeListener);
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoin.addListener(amountAsCoinChangeListener);
amountChangeListener.changed(null, null, amount.get());
isWalletFundedChangeListener.changed(null, null, dataModel.isWalletFunded.get());
amountAsCoinChangeListener.changed(null, null, dataModel.amountAsCoin.get());
}
private void removeListeners() {
amount.removeListener(amountChangeListener);
dataModel.isWalletFunded.removeListener(isWalletFundedChangeListener);
dataModel.amountAsCoin.removeListener(amountAsCoinChangeListener);
if (offer != null)
offer.stateProperty().removeListener(offerStateChangeListener);
if (trade != null)
trade.processStateProperty().removeListener(tradeStateChangeListener);
}
///////////////////////////////////////////////////////////////////////////////////////////
// API
///////////////////////////////////////////////////////////////////////////////////////////
void initWithData(Coin amount, Offer offer) {
dataModel.initWithData(amount, offer);
directionLabel = offer.getDirection() == Offer.Direction.SELL ?
BSResources.get("shared.buyBitcoin") : BSResources.get("shared.sellBitcoin");
this.offer = offer;
directionLabel = offer.getDirection() == Offer.Direction.SELL ? BSResources.get("shared.buyBitcoin") : BSResources.get("shared.sellBitcoin");
fiatCode.set(offer.getCurrencyCode());
if (!dataModel.isMinAmountLessOrEqualAmount()) {
amountValidationResult.set(new InputValidator.ValidationResult(false,
BSResources.get("takeOffer.validation.amountSmallerThanMinAmount")));
}
if (!dataModel.isMinAmountLessOrEqualAmount())
amountValidationResult.set(new InputValidator.ValidationResult(false, BSResources.get("takeOffer.validation.amountSmallerThanMinAmount")));
if (dataModel.getDirection() == Offer.Direction.BUY) {
amountDescription.set(BSResources.get("takeOffer.amountPriceBox.buy.amountDescription", offer.getId()));
@ -147,17 +236,13 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
fundsBoxInfoDisplay.set(BSResources.get("takeOffer.fundsBox.sell.info"));
}
//model.volumeAsFiat.set(offer.getVolumeByAmount(model.amountAsCoin.get()));
amountRange = formatter.formatCoinWithCode(offer.getMinAmount()) + " - " +
formatter.formatCoinWithCode(offer.getAmount());
amountRange = formatter.formatCoinWithCode(offer.getMinAmount()) + " - " + formatter.formatCoinWithCode(offer.getAmount());
price = formatter.formatFiatWithCode(offer.getPrice());
paymentLabel = BSResources.get("takeOffer.fundsBox.paymentLabel", offer.getId());
if (dataModel.getAddressEntry() != null) {
assert dataModel.getAddressEntry() != null;
addressAsString = dataModel.getAddressEntry().getAddress().toString();
address.set(dataModel.getAddressEntry().getAddress());
}
acceptedCountries = formatter.countryLocalesToString(offer.getAcceptedCountries());
acceptedLanguages = formatter.languageCodesToString(offer.getAcceptedLanguageCodes());
@ -166,12 +251,43 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
bankAccountCurrency = BSResources.get(CurrencyUtil.getDisplayName(offer.getCurrencyCode()));
bankAccountCounty = BSResources.get(offer.getBankAccountCountry().name);
offer.stateProperty().addListener((ov, oldValue, newValue) -> applyOfferState(newValue));
offer.stateProperty().addListener(offerStateChangeListener);
applyOfferState(offer.stateProperty().get());
}
///////////////////////////////////////////////////////////////////////////////////////////
// UI actions
///////////////////////////////////////////////////////////////////////////////////////////
void onTakeOffer() {
takeOfferRequested = true;
applyOnTakeOfferResult(false);
isTakeOfferSpinnerVisible.set(true);
dataModel.onTakeOffer((trade) -> {
this.trade = trade;
trade.processStateProperty().addListener(tradeStateChangeListener);
applyTradeState(trade.processStateProperty().get());
evaluateViewState();
});
}
void onShowPaymentScreen() {
state.set(State.PAYMENT_SCREEN);
}
void onToggleShowAdvancedSettings() {
detailsVisible = !detailsVisible;
}
///////////////////////////////////////////////////////////////////////////////////////////
// States
///////////////////////////////////////////////////////////////////////////////////////////
private void applyOfferState(Offer.State state) {
log.debug("offer state = " + state);
log.debug("applyOfferState state = " + state);
switch (state) {
case UNDEFINED:
@ -201,14 +317,20 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
errorMessage.set("You cannot take that offer because the offerer is offline.");
takeOfferRequested = false;
break;
/* case FAULT:
case FAULT:
if (takeOfferRequested)
errorMessage.set("Take offer request failed.");
else
errorMessage.set("The check for the offer availability failed.");
takeOfferRequested = false;
break;*/
break;
case TIMEOUT:
if (takeOfferRequested)
errorMessage.set("Take offer request failed due a timeout.");
else
errorMessage.set("The check for the offer availability failed due a timeout.");
takeOfferRequested = false;
break;
default:
log.error("Unhandled offer state: " + state);
break;
@ -218,31 +340,24 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
isTakeOfferSpinnerVisible.set(false);
}
evaluateState();
evaluateViewState();
}
void onTakeOffer() {
takeOfferRequested = true;
applyTakeOfferRequestResult(false);
private void applyTradeState(TradeState.ProcessState state) {
log.debug("applyTradeState state = " + state);
isTakeOfferSpinnerVisible.set(true);
dataModel.onTakeOffer((trade) -> {
trade.processStateProperty().addListener((ov, oldValue, newValue) -> {
log.debug("trade state = " + newValue);
String msg = "";
String msg = "An error occurred.";
if (trade.getErrorMessage() != null)
msg = "\nError message: " + trade.getErrorMessage();
msg = "Error message: " + trade.getErrorMessage();
if (trade instanceof SellerAsTakerTrade) {
switch ((SellerTradeState.ProcessState) newValue) {
switch ((SellerTradeState.ProcessState) state) {
case UNDEFINED:
break;
case DEPOSIT_PUBLISHED_MSG_RECEIVED:
assert trade.getDepositTx() != null;
transactionId.set(trade.getDepositTx().getHashAsString());
applyTakeOfferRequestResult(true);
applyOnTakeOfferResult(true);
break;
case DEPOSIT_CONFIRMED:
case FIAT_PAYMENT_STARTED_MSG_RECEIVED:
@ -252,32 +367,28 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
case PAYOUT_TX_COMMITTED:
case PAYOUT_BROAD_CASTED:
break;
/* case TAKE_OFFER_FEE_PUBLISH_FAILED:
errorMessage.set("An error occurred when paying the trade fee." + msg);
case TIMEOUT:
errorMessage.set("A timeout occurred. Maybe there are connection problems. " +
"Please try later again.\n" + msg);
takeOfferRequested = false;
break;
case MESSAGE_SENDING_FAILED:
errorMessage.set("An error occurred when sending a message to the offerer. Maybe there are connection problems. " +
"Please try later again." + msg);
takeOfferRequested = false;
break;
case EXCEPTION:
case FAULT:
errorMessage.set(msg);
takeOfferRequested = false;
break;*/
break;
default:
log.warn("Unhandled trade state: " + newValue);
log.warn("Unhandled trade state: " + state);
break;
}
}
else if (trade instanceof BuyerAsTakerTrade) {
switch ((BuyerTradeState.ProcessState) newValue) {
switch ((BuyerTradeState.ProcessState) state) {
case UNDEFINED:
break;
case DEPOSIT_PUBLISHED:
assert trade.getDepositTx() != null;
transactionId.set(trade.getDepositTx().getHashAsString());
applyTakeOfferRequestResult(true);
applyOnTakeOfferResult(true);
break;
case DEPOSIT_PUBLISHED_MSG_SENT:
case DEPOSIT_CONFIRMED:
@ -288,55 +399,32 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
case PAYOUT_TX_SENT:
case PAYOUT_BROAD_CASTED:
break;
/* case TAKE_OFFER_FEE_PUBLISH_FAILED:
errorMessage.set("An error occurred when paying the trade fee." + msg);
takeOfferRequested = false;
break;
case MESSAGE_SENDING_FAILED:
errorMessage.set("Sending a message to the offerer failed. Maybe there are connection problems. " +
"Please try later again." + msg);
takeOfferRequested = false;
break;
case TIMEOUT:
errorMessage.set("Timeout: We did not received a message from the offerer. Maybe there are connection problems. " +
"Please try later again." + msg);
errorMessage.set("A timeout occurred. Maybe there are connection problems. " +
"Please try later again.\n" + msg);
takeOfferRequested = false;
break;
case EXCEPTION:
case FAULT:
errorMessage.set(msg);
takeOfferRequested = false;
break;*/
break;
default:
log.warn("Unhandled trade state: " + newValue);
log.warn("Unhandled trade state: " + state);
break;
}
}
if (errorMessage.get() != null) {
isAmountAndPriceValidAndWalletFunded = false;
if (errorMessage.get() != null)
isTakeOfferSpinnerVisible.set(false);
}
});
evaluateState();
});
}
boolean isSeller() {
return dataModel.getDirection() == Offer.Direction.BUY;
}
void securityDepositInfoDisplayed() {
dataModel.securityDepositInfoDisplayed();
}
void onShowPaymentScreen() {
state.set(State.PAYMENT_SCREEN);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Focus handling
///////////////////////////////////////////////////////////////////////////////////////////
// On focus out we do validation and apply the data to the model
void onFocusOutAmountTextField(Boolean oldValue, Boolean newValue, String userInput) {
void onFocusOutAmountTextField(boolean oldValue, boolean newValue, String userInput) {
if (oldValue && !newValue) {
InputValidator.ValidationResult result = isBtcInputValid(amount.get());
amountValidationResult.set(result);
@ -361,6 +449,18 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
}
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
///////////////////////////////////////////////////////////////////////////////////////////
boolean isSeller() {
return dataModel.getDirection() == Offer.Direction.BUY;
}
void securityDepositInfoDisplayed() {
dataModel.onSecurityDepositInfoDisplayed();
}
WalletService getWalletService() {
return dataModel.getWalletService();
}
@ -429,50 +529,24 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
return paymentLabel;
}
Boolean getDisplaySecurityDepositInfo() {
boolean getDisplaySecurityDepositInfo() {
return dataModel.getDisplaySecurityDepositInfo();
}
private void setupListeners() {
// Bidirectional bindings are used for all input fields: amount, price, volume and minAmount
// We do volume/amount calculation during input, so user has immediate feedback
amount.addListener((ov, oldValue, newValue) -> {
if (isBtcInputValid(newValue).isValid) {
setAmountToModel();
calculateVolume();
dataModel.calculateTotalToPay();
}
evaluateState();
});
dataModel.isWalletFunded.addListener((ov, oldValue, newValue) -> {
evaluateState();
});
// Binding with Bindings.createObjectBinding does not work because of bi-directional binding
dataModel.amountAsCoin.addListener((ov, oldValue, newValue) -> amount.set(formatter.formatCoin(newValue)));
boolean isDetailsVisible() {
return detailsVisible;
}
private void applyTakeOfferRequestResult(boolean success) {
///////////////////////////////////////////////////////////////////////////////////////////
// Utils
///////////////////////////////////////////////////////////////////////////////////////////
private void applyOnTakeOfferResult(boolean success) {
isTakeOfferSpinnerVisible.set(false);
showTransactionPublishedScreen.set(success);
}
private void setupBindings() {
volume.bind(createStringBinding(() -> formatter.formatFiatWithCode(dataModel.volumeAsFiat.get()),
dataModel.volumeAsFiat));
totalToPay.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.totalToPayAsCoin.get()),
dataModel.totalToPayAsCoin));
securityDeposit.bind(createStringBinding(() -> formatter.formatCoinWithCode(dataModel.securityDepositAsCoin
.get()),
dataModel.securityDepositAsCoin));
totalToPayAsCoin.bind(dataModel.totalToPayAsCoin);
btcCode.bind(dataModel.btcCode);
}
private void calculateVolume() {
setAmountToModel();
dataModel.calculateVolume();
@ -482,8 +556,8 @@ class TakeOfferViewModel extends ActivatableWithDataModel<TakeOfferDataModel> im
dataModel.amountAsCoin.set(formatter.parseToCoinWith4Decimals(amount.get()));
}
private void evaluateState() {
isAmountAndPriceValidAndWalletFunded = isBtcInputValid(amount.get()).isValid &&
private void evaluateViewState() {
boolean isAmountAndPriceValidAndWalletFunded = isBtcInputValid(amount.get()).isValid &&
dataModel.isMinAmountLessOrEqualAmount() &&
!dataModel.isAmountLargerThanOfferAmount() &&
dataModel.isWalletFunded.get();

View File

@ -17,10 +17,6 @@
package io.bitsquare.gui.util;
import java.util.function.Function;
import javafx.animation.AnimationTimer;
import javafx.application.Platform;
import javafx.scene.input.*;
import org.slf4j.Logger;
@ -37,36 +33,4 @@ public class GUIUtil {
clipboard.setContent(clipboardContent);
}
}
public static AnimationTimer setTimeout(int delay, Function<AnimationTimer, Void> callback) {
AnimationTimer animationTimer = new AnimationTimer() {
final long lastTimeStamp = System.currentTimeMillis();
@Override
public void handle(long arg0) {
if (System.currentTimeMillis() > delay + lastTimeStamp) {
Platform.runLater(() -> callback.apply(this));
this.stop();
}
}
};
animationTimer.start();
return animationTimer;
}
public static AnimationTimer setInterval(int delay, Function<AnimationTimer, Void> callback) {
AnimationTimer animationTimer = new AnimationTimer() {
long lastTimeStamp = System.currentTimeMillis();
@Override
public void handle(long arg0) {
if (System.currentTimeMillis() > delay + lastTimeStamp) {
lastTimeStamp = System.currentTimeMillis();
callback.apply(this);
}
}
};
animationTimer.start();
return animationTimer;
}
}

View File

@ -24,30 +24,15 @@ import org.bitcoinj.core.AddressFormatException;
import javax.inject.Inject;
/**
* BtcValidator for validating BTC values.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
*/
public final class BtcAddressValidator extends InputValidator {
private final BitcoinNetwork bitcoinNetwork;
///////////////////////////////////////////////////////////////////////////////////////////
// Constructor
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
public BtcAddressValidator(BitcoinNetwork bitcoinNetwork) {
this.bitcoinNetwork = bitcoinNetwork;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public ValidationResult validate(String input) {
@ -58,11 +43,6 @@ public final class BtcAddressValidator extends InputValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateBtcAddress(String input) {
try {
new Address(bitcoinNetwork.getParameters(), input);

View File

@ -23,16 +23,7 @@ import org.bitcoinj.core.NetworkParameters;
import java.math.BigDecimal;
/**
* BtcValidator for validating BTC values.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
*/
public final class BtcValidator extends NumberValidator {
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public class BtcValidator extends NumberValidator {
@Override
public ValidationResult validate(String input) {
@ -52,12 +43,7 @@ public final class BtcValidator extends NumberValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateIfNotFractionalBtcValue(String input) {
protected ValidationResult validateIfNotFractionalBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(8);
if (satoshis.scale() > 0)
@ -66,7 +52,7 @@ public final class BtcValidator extends NumberValidator {
return new ValidationResult(true);
}
private ValidationResult validateIfNotExceedsMaxBtcValue(String input) {
protected ValidationResult validateIfNotExceedsMaxBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(8);
if (satoshis.longValue() > NetworkParameters.MAX_MONEY.longValue())

View File

@ -23,18 +23,12 @@ import io.bitsquare.user.User;
import javax.inject.Inject;
/**
* FiatNumberValidator for validating fiat values.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
*/
public final class FiatValidator extends NumberValidator {
public class FiatValidator extends NumberValidator {
//TODO Find appropriate values - depends on currencies
public static final double MIN_FIAT_VALUE = 0.01; // usually a cent is the smallest currency unit
public static final double MAX_FIAT_VALUE = 1000000;
private String currencyCode = "Fiat";
protected String currencyCode = "Fiat";
@Inject
public FiatValidator(User user) {
@ -51,11 +45,6 @@ public final class FiatValidator extends NumberValidator {
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public ValidationResult validate(String input) {
ValidationResult result = validateIfNotEmpty(input);
@ -74,21 +63,11 @@ public final class FiatValidator extends NumberValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setter
///////////////////////////////////////////////////////////////////////////////////////////
public void setFiatCurrencyCode(String currencyCode) {
this.currencyCode = currencyCode;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateIfNotExceedsMinFiatValue(String input) {
protected ValidationResult validateIfNotExceedsMinFiatValue(String input) {
double d = Double.parseDouble(input);
if (d < MIN_FIAT_VALUE)
return new ValidationResult(false, BSResources.get("validation.fiat.toSmall", currencyCode));
@ -96,7 +75,7 @@ public final class FiatValidator extends NumberValidator {
return new ValidationResult(true);
}
private ValidationResult validateIfNotExceedsMaxFiatValue(String input) {
protected ValidationResult validateIfNotExceedsMaxFiatValue(String input) {
double d = Double.parseDouble(input);
if (d > MAX_FIAT_VALUE)
return new ValidationResult(false, BSResources.get("validation.fiat.toLarge", currencyCode));

View File

@ -19,25 +19,12 @@ package io.bitsquare.gui.util.validation;
import io.bitsquare.locale.BSResources;
/**
* Base class for other specialized validators.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
*/
public class InputValidator {
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public ValidationResult validate(String input) {
return validateIfNotEmpty(input);
}
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
protected ValidationResult validateIfNotEmpty(String input) {
if (input == null || input.length() == 0)
return new ValidationResult(false, BSResources.get("validation.empty"));
@ -45,10 +32,6 @@ public class InputValidator {
return new ValidationResult(true);
}
///////////////////////////////////////////////////////////////////////////////////////////
// ValidationResult
///////////////////////////////////////////////////////////////////////////////////////////
public static class ValidationResult {
public final boolean isValid;
public final String errorMessage;

View File

@ -24,15 +24,9 @@ import io.bitsquare.locale.BSResources;
* Localisation not supported at the moment
* The decimal mark can be either "." or ",". Thousand separators are not supported yet,
* but might be added alter with Local support.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
*/
public abstract class NumberValidator extends InputValidator {
///////////////////////////////////////////////////////////////////////////////////////////
// Protected methods
///////////////////////////////////////////////////////////////////////////////////////////
protected String cleanInput(String input) {
return input.replace(",", ".").trim();
}

View File

@ -17,23 +17,10 @@
package io.bitsquare.gui.util.validation;
import io.bitsquare.locale.BSResources;
import org.bitcoinj.core.NetworkParameters;
import java.math.BigDecimal;
/**
* BtcValidator for validating BTC values.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
* That validator accepts empty inputs
*/
public final class OptionalBtcValidator extends NumberValidator {
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
public class OptionalBtcValidator extends BtcValidator {
@Override
public ValidationResult validate(String input) {
@ -63,27 +50,4 @@ public final class OptionalBtcValidator extends NumberValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateIfNotFractionalBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(8);
if (satoshis.scale() > 0)
return new ValidationResult(false, BSResources.get("validation.btc.toSmall"));
else
return new ValidationResult(true);
}
private ValidationResult validateIfNotExceedsMaxBtcValue(String input) {
BigDecimal bd = new BigDecimal(input);
final BigDecimal satoshis = bd.movePointRight(8);
if (satoshis.longValue() > NetworkParameters.MAX_MONEY.longValue())
return new ValidationResult(false, BSResources.get("validation.btc.toLarge"));
else
return new ValidationResult(true);
}
}

View File

@ -17,44 +17,19 @@
package io.bitsquare.gui.util.validation;
import io.bitsquare.locale.BSResources;
import io.bitsquare.locale.CurrencyUtil;
import io.bitsquare.user.User;
import javax.inject.Inject;
/**
* FiatNumberValidator for validating fiat values.
* <p/>
* That class implements just what we need for the moment. It is not intended as a general purpose library class.
* That validator accepts empty inputs
*/
public final class OptionalFiatValidator extends NumberValidator {
//TODO Find appropriate values - depends on currencies
public static final double MIN_FIAT_VALUE = 0.01; // usually a cent is the smallest currency unit
public static final double MAX_FIAT_VALUE = 1000000;
private String currencyCode = "Fiat";
public class OptionalFiatValidator extends FiatValidator {
@Inject
public OptionalFiatValidator(User user) {
if (user != null) {
if (user.currentFiatAccountProperty().get() == null)
setFiatCurrencyCode(CurrencyUtil.getDefaultCurrencyAsCode());
else if (user.currentFiatAccountProperty().get() != null)
setFiatCurrencyCode(user.currentFiatAccountProperty().get().currencyCode);
user.currentFiatAccountProperty().addListener((ov, oldValue, newValue) -> {
if (newValue != null)
setFiatCurrencyCode(newValue.currencyCode);
});
super(user);
}
}
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public ValidationResult validate(String input) {
@ -84,34 +59,4 @@ public final class OptionalFiatValidator extends NumberValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Setter
///////////////////////////////////////////////////////////////////////////////////////////
public void setFiatCurrencyCode(String currencyCode) {
this.currencyCode = currencyCode;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateIfNotExceedsMinFiatValue(String input) {
double d = Double.parseDouble(input);
if (d < MIN_FIAT_VALUE)
return new ValidationResult(false, BSResources.get("validation.fiat.toSmall", currencyCode));
else
return new ValidationResult(true);
}
private ValidationResult validateIfNotExceedsMaxFiatValue(String input) {
double d = Double.parseDouble(input);
if (d > MAX_FIAT_VALUE)
return new ValidationResult(false, BSResources.get("validation.fiat.toLarge", currencyCode));
else
return new ValidationResult(true);
}
}

View File

@ -21,10 +21,6 @@ import io.bitsquare.locale.BSResources;
public final class PasswordValidator extends InputValidator {
///////////////////////////////////////////////////////////////////////////////////////////
// Public methods
///////////////////////////////////////////////////////////////////////////////////////////
@Override
public ValidationResult validate(String input) {
ValidationResult result = validateIfNotEmpty(input);
@ -33,11 +29,6 @@ public final class PasswordValidator extends InputValidator {
return result;
}
///////////////////////////////////////////////////////////////////////////////////////////
// Private methods
///////////////////////////////////////////////////////////////////////////////////////////
private ValidationResult validateMinLength(String input) {
if (input.length() > 7)
return new ValidationResult(true);

View File

@ -44,7 +44,7 @@ public class CreateOfferViewModelTest {
BSFormatter formatter = new BSFormatter(new User(), null);
formatter.setLocale(Locale.US);
formatter.setFiatCurrencyCode("USD");
model = new CreateOfferDataModel(null, null, null, null, null, null, formatter);
model = new CreateOfferDataModel(null, null, null, null, null, formatter);
presenter = new CreateOfferViewModel(model, new FiatValidator(null), new BtcValidator(), formatter);
}