diff --git a/src/main/java/bisq/core/CoreModule.java b/src/main/java/bisq/core/CoreModule.java
index fed52f5a..047de63e 100644
--- a/src/main/java/bisq/core/CoreModule.java
+++ b/src/main/java/bisq/core/CoreModule.java
@@ -29,6 +29,15 @@
import bisq.core.filter.FilterModule;
import bisq.core.network.p2p.seed.DefaultSeedNodeRepository;
import bisq.core.network.p2p.seed.SeedNodeAddressLookup;
+import bisq.core.notifications.MobileMessageEncryption;
+import bisq.core.notifications.MobileModel;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.notifications.MobileNotificationValidator;
+import bisq.core.notifications.alerts.DisputeMsgEvents;
+import bisq.core.notifications.alerts.MyOfferTakenEvents;
+import bisq.core.notifications.alerts.TradeEvents;
+import bisq.core.notifications.alerts.market.MarketAlerts;
+import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OfferModule;
import bisq.core.presentation.CorePresentationModule;
import bisq.core.proto.network.CoreNetworkProtoResolver;
@@ -104,6 +113,16 @@ protected void configure() {
String referralId = environment.getProperty(AppOptionKeys.REFERRAL_ID, String.class, "");
bind(String.class).annotatedWith(Names.named(AppOptionKeys.REFERRAL_ID)).toInstance(referralId);
+ bind(MobileNotificationService.class).in(Singleton.class);
+ bind(MobileMessageEncryption.class).in(Singleton.class);
+ bind(MobileNotificationValidator.class).in(Singleton.class);
+ bind(MobileModel.class).in(Singleton.class);
+ bind(MyOfferTakenEvents.class).in(Singleton.class);
+ bind(TradeEvents.class).in(Singleton.class);
+ bind(DisputeMsgEvents.class).in(Singleton.class);
+ bind(PriceAlert.class).in(Singleton.class);
+ bind(MarketAlerts.class).in(Singleton.class);
+
// ordering is used for shut down sequence
install(tradeModule());
install(encryptionServiceModule());
diff --git a/src/main/java/bisq/core/app/BisqSetup.java b/src/main/java/bisq/core/app/BisqSetup.java
index b88b9c71..1f5a4d89 100644
--- a/src/main/java/bisq/core/app/BisqSetup.java
+++ b/src/main/java/bisq/core/app/BisqSetup.java
@@ -32,6 +32,12 @@
import bisq.core.dao.DaoSetup;
import bisq.core.filter.FilterManager;
import bisq.core.locale.Res;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.notifications.alerts.DisputeMsgEvents;
+import bisq.core.notifications.alerts.MyOfferTakenEvents;
+import bisq.core.notifications.alerts.TradeEvents;
+import bisq.core.notifications.alerts.market.MarketAlerts;
+import bisq.core.notifications.alerts.price.PriceAlert;
import bisq.core.offer.OpenOffer;
import bisq.core.offer.OpenOfferManager;
import bisq.core.payment.AccountAgeWitnessService;
@@ -134,6 +140,12 @@ public interface BisqSetupCompleteListener {
private final KeyRing keyRing;
private final BisqEnvironment bisqEnvironment;
private final AccountAgeWitnessService accountAgeWitnessService;
+ private final MobileNotificationService mobileNotificationService;
+ private final MyOfferTakenEvents myOfferTakenEvents;
+ private final TradeEvents tradeEvents;
+ private final DisputeMsgEvents disputeMsgEvents;
+ private final PriceAlert priceAlert;
+ private final MarketAlerts marketAlerts;
private final BSFormatter formatter;
@Setter
@Nullable
@@ -197,7 +209,15 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup,
KeyRing keyRing,
BisqEnvironment bisqEnvironment,
AccountAgeWitnessService accountAgeWitnessService,
+ MobileNotificationService mobileNotificationService,
+ MyOfferTakenEvents myOfferTakenEvents,
+ TradeEvents tradeEvents,
+ DisputeMsgEvents disputeMsgEvents,
+ PriceAlert priceAlert,
+ MarketAlerts marketAlerts,
BSFormatter formatter) {
+
+
this.p2PNetworkSetup = p2PNetworkSetup;
this.walletAppSetup = walletAppSetup;
@@ -224,6 +244,12 @@ public BisqSetup(P2PNetworkSetup p2PNetworkSetup,
this.keyRing = keyRing;
this.bisqEnvironment = bisqEnvironment;
this.accountAgeWitnessService = accountAgeWitnessService;
+ this.mobileNotificationService = mobileNotificationService;
+ this.myOfferTakenEvents = myOfferTakenEvents;
+ this.tradeEvents = tradeEvents;
+ this.disputeMsgEvents = disputeMsgEvents;
+ this.priceAlert = priceAlert;
+ this.marketAlerts = marketAlerts;
this.formatter = formatter;
}
@@ -602,6 +628,13 @@ public void onBalanceChanged(Coin balance, Transaction tx) {
}
});
+ mobileNotificationService.onAllServicesInitialized();
+ myOfferTakenEvents.onAllServicesInitialized();
+ tradeEvents.onAllServicesInitialized();
+ disputeMsgEvents.onAllServicesInitialized();
+ priceAlert.onAllServicesInitialized();
+ marketAlerts.onAllServicesInitialized();
+
allBasicServicesInitialized = true;
}
diff --git a/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java b/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java
index c5b7a4d5..fa6ae9de 100644
--- a/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java
+++ b/src/main/java/bisq/core/arbitration/messages/DisputeCommunicationMessage.java
@@ -22,6 +22,7 @@
import bisq.network.p2p.NodeAddress;
import bisq.common.app.Version;
+import bisq.common.util.Utilities;
import io.bisq.generated.protobuffer.PB;
@@ -241,6 +242,10 @@ public String getTradeId() {
return tradeId;
}
+ public String getShortId() {
+ return Utilities.getShortId(tradeId);
+ }
+
public void addWeakMessageStateListener(Listener listener) {
this.listener = new WeakReference<>(listener);
}
diff --git a/src/main/java/bisq/core/btc/BitcoinModule.java b/src/main/java/bisq/core/btc/BitcoinModule.java
index 5b5b9dd4..0ff0ebfc 100644
--- a/src/main/java/bisq/core/btc/BitcoinModule.java
+++ b/src/main/java/bisq/core/btc/BitcoinModule.java
@@ -23,13 +23,12 @@
import bisq.core.btc.wallet.BtcWalletService;
import bisq.core.btc.wallet.TradeWalletService;
import bisq.core.btc.wallet.WalletsSetup;
+import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.ProvidersRepository;
import bisq.core.provider.fee.FeeProvider;
import bisq.core.provider.fee.FeeService;
import bisq.core.provider.price.PriceFeedService;
-import bisq.network.http.HttpClient;
-
import bisq.common.app.AppModule;
import org.springframework.core.env.Environment;
@@ -75,7 +74,8 @@ protected void configure() {
bind(BitcoinNodes.class).in(Singleton.class);
bind(BalanceModel.class).in(Singleton.class);
- bind(HttpClient.class).in(Singleton.class);
+ bind(PriceNodeHttpClient.class).in(Singleton.class);
+
bind(ProvidersRepository.class).in(Singleton.class);
bind(FeeProvider.class).in(Singleton.class);
bind(PriceFeedService.class).in(Singleton.class);
diff --git a/src/main/java/bisq/core/notifications/MobileMessage.java b/src/main/java/bisq/core/notifications/MobileMessage.java
new file mode 100644
index 00000000..1d122390
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileMessage.java
@@ -0,0 +1,53 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import bisq.common.util.JsonExclude;
+
+import java.util.Date;
+
+import lombok.Value;
+
+@Value
+public class MobileMessage {
+ private long sentDate;
+ private String txId;
+ private String title;
+ private String message;
+ @JsonExclude
+ transient private MobileMessageType mobileMessageType;
+ private String type;
+ private String actionRequired;
+ private int version;
+
+ public MobileMessage(String title, String message, MobileMessageType mobileMessageType) {
+ this(title, message, "", mobileMessageType);
+ }
+
+ public MobileMessage(String title, String message, String txId, MobileMessageType mobileMessageType) {
+ this.title = title;
+ this.message = message;
+ this.txId = txId;
+ this.mobileMessageType = mobileMessageType;
+
+ this.type = mobileMessageType.name();
+ actionRequired = "";
+ sentDate = new Date().getTime();
+ version = 1;
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/MobileMessageEncryption.java b/src/main/java/bisq/core/notifications/MobileMessageEncryption.java
new file mode 100644
index 00000000..04223643
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileMessageEncryption.java
@@ -0,0 +1,78 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import javax.inject.Inject;
+
+import org.apache.commons.codec.binary.Base64;
+
+import javax.crypto.Cipher;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import java.security.NoSuchAlgorithmException;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MobileMessageEncryption {
+ private SecretKeySpec keySpec;
+ private Cipher cipher;
+
+ @Inject
+ public MobileMessageEncryption() {
+ }
+
+ public void setKey(String key) {
+ keySpec = new SecretKeySpec(key.getBytes(), "AES");
+ try {
+ cipher = Cipher.getInstance("AES/CBC/NOPadding");
+ } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
+ e.printStackTrace();
+ }
+ }
+
+ public String encrypt(String valueToEncrypt, String iv) throws Exception {
+ while (valueToEncrypt.length() % 16 != 0) {
+ valueToEncrypt = valueToEncrypt + " ";
+ }
+
+ if (iv.length() != 16) {
+ throw new Exception("iv not 16 characters");
+ }
+ IvParameterSpec ivSpec = new IvParameterSpec(iv.getBytes());
+ byte[] encryptedBytes = doEncrypt(valueToEncrypt, ivSpec);
+ return Base64.encodeBase64String(encryptedBytes);
+ }
+
+ private byte[] doEncrypt(String text, IvParameterSpec ivSpec) throws Exception {
+ if (text == null || text.length() == 0) {
+ throw new Exception("Empty string");
+ }
+
+ byte[] encrypted;
+ try {
+ cipher.init(Cipher.ENCRYPT_MODE, keySpec, ivSpec);
+ encrypted = cipher.doFinal(text.getBytes());
+ } catch (Exception e) {
+ throw new Exception("[encrypt] " + e.getMessage());
+ }
+ return encrypted;
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/MobileMessageType.java b/src/main/java/bisq/core/notifications/MobileMessageType.java
new file mode 100644
index 00000000..5562fac8
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileMessageType.java
@@ -0,0 +1,28 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+public enum MobileMessageType {
+ SETUP_CONFIRMATION,
+ OFFER,
+ TRADE,
+ DISPUTE,
+ PRICE,
+ MARKET,
+ ERASE
+}
diff --git a/src/main/java/bisq/core/notifications/MobileModel.java b/src/main/java/bisq/core/notifications/MobileModel.java
new file mode 100644
index 00000000..d4d50b4b
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileModel.java
@@ -0,0 +1,147 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import javax.inject.Inject;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import lombok.Data;
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import javax.annotation.Nullable;
+
+@Data
+@Slf4j
+public class MobileModel {
+ public static final String PHONE_SEPARATOR_ESCAPED = "\\|"; // see https://stackoverflow.com/questions/5675704/java-string-split-not-returning-the-right-values
+ public static final String PHONE_SEPARATOR_WRITING = "|";
+
+ public enum OS {
+ UNDEFINED(""),
+ IOS("iOS"),
+ IOS_DEV("iOSDev"),
+ ANDROID("android");
+
+ @Getter
+ private String magicString;
+
+ OS(String magicString) {
+ this.magicString = magicString;
+ }
+ }
+
+ @Nullable
+ private OS os;
+ @Nullable
+ private String descriptor;
+ @Nullable
+ private String key;
+ @Nullable
+ private String token;
+ private boolean isContentAvailable = true;
+
+ @Inject
+ public MobileModel() {
+ }
+
+ public void reset() {
+ os = null;
+ key = null;
+ token = null;
+ }
+
+ public void applyKeyAndToken(String keyAndToken) {
+ log.info("phoneId={}", keyAndToken);
+ String[] tokens = keyAndToken.split(PHONE_SEPARATOR_ESCAPED);
+ String magic = tokens[0];
+ descriptor = tokens[1];
+ key = tokens[2];
+ token = tokens[3];
+ if (magic.equals(OS.IOS.getMagicString()))
+ os = OS.IOS;
+ else if (magic.equals(OS.IOS_DEV.getMagicString()))
+ os = OS.IOS_DEV;
+ else if (magic.equals(OS.ANDROID.getMagicString()))
+ os = OS.ANDROID;
+
+ isContentAvailable = parseDescriptor(descriptor);
+ }
+
+ @VisibleForTesting
+ boolean parseDescriptor(String descriptor) {
+ // phone descriptors
+ /*
+ iPod Touch 5
+ iPod Touch 6
+ iPhone 4
+ iPhone 4s
+ iPhone 5
+ iPhone 5c
+ iPhone 5s
+ iPhone 6
+ iPhone 6 Plus
+ iPhone 6s
+ iPhone 6s Plus
+ iPhone 7
+ iPhone 7 Plus
+ iPhone SE
+ iPhone 8
+ iPhone 8 Plus
+ iPhone X
+ iPad 2
+ iPad 3
+ iPad 4
+ iPad Air
+ iPad Air 2
+ iPad 5
+ iPad 6
+ iPad Mini
+ iPad Mini 2
+ iPad Mini 3
+ iPad Mini 4
+ iPad Pro 9.7 Inch
+ iPad Pro 12.9 Inch
+ iPad Pro 12.9 Inch 2. Generation
+ iPad Pro 10.5 Inch
+ */
+ // iPhone 6 does not support isContentAvailable, iPhone 7 does.
+ // We don't know for other versions, but lets assume all above iPhone 6 are ok.
+ if (descriptor != null) {
+ String[] descriptorTokens = descriptor.split(" ");
+ if (descriptorTokens.length >= 1) {
+ String model = descriptorTokens[0];
+ if (model.equals("iPhone")) {
+ String versionString = descriptorTokens[1];
+ versionString = versionString.substring(0, 1);
+ if (versionString.equals("X") || versionString.equals("SE"))
+ return true;
+ try {
+ int version = Integer.parseInt(versionString);
+ return version > 5;
+ } catch (Throwable ignore) {
+ }
+ } else {
+ return (model.equals("iPad")) && descriptorTokens[1].equals("Pro");
+ }
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/MobileNotificationService.java b/src/main/java/bisq/core/notifications/MobileNotificationService.java
new file mode 100644
index 00000000..e7481e76
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileNotificationService.java
@@ -0,0 +1,253 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import bisq.core.user.Preferences;
+
+import bisq.network.NetworkOptionKeys;
+import bisq.network.http.HttpClient;
+
+import bisq.common.app.Version;
+
+import com.google.gson.Gson;
+
+import com.google.inject.Inject;
+
+import javax.inject.Named;
+
+import org.apache.commons.codec.binary.Hex;
+
+import javafx.beans.property.BooleanProperty;
+import javafx.beans.property.SimpleBooleanProperty;
+
+import java.util.UUID;
+
+import lombok.Getter;
+import lombok.extern.slf4j.Slf4j;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+@Slf4j
+public class MobileNotificationService {
+ // Used in Relay app to response of a success state. We won't want a code dependency just for that string so we keep it
+ // duplicated in relay and here. Must not be changed.
+ private static final String SUCCESS = "success";
+ private static final String DEV_URL_LOCALHOST = "http://localhost:8080/";
+ private static final String DEV_URL = "http://198.211.125.72:8080/";
+ private static final String URL = "http://jtboonrvwmq7frkj.onion/";
+ private static final String BISQ_MESSAGE_IOS_MAGIC = "BisqMessageiOS";
+ private static final String BISQ_MESSAGE_ANDROID_MAGIC = "BisqMessageAndroid";
+
+ private final Preferences preferences;
+ private final MobileMessageEncryption mobileMessageEncryption;
+ private final MobileNotificationValidator mobileNotificationValidator;
+ private final HttpClient httpClient;
+ @Getter
+ private final MobileModel mobileModel;
+
+ @Getter
+ private boolean setupConfirmationSent;
+ @Getter
+ private BooleanProperty useSoundProperty = new SimpleBooleanProperty();
+ @Getter
+ private BooleanProperty useTradeNotificationsProperty = new SimpleBooleanProperty();
+ @Getter
+ private BooleanProperty useMarketNotificationsProperty = new SimpleBooleanProperty();
+ @Getter
+ private BooleanProperty usePriceNotificationsProperty = new SimpleBooleanProperty();
+
+ @Inject
+ public MobileNotificationService(Preferences preferences,
+ MobileMessageEncryption mobileMessageEncryption,
+ MobileNotificationValidator mobileNotificationValidator,
+ MobileModel mobileModel,
+ HttpClient httpClient,
+ @Named(NetworkOptionKeys.USE_LOCALHOST_FOR_P2P) Boolean useLocalHost) {
+ this.preferences = preferences;
+ this.mobileMessageEncryption = mobileMessageEncryption;
+ this.mobileNotificationValidator = mobileNotificationValidator;
+ this.httpClient = httpClient;
+ this.mobileModel = mobileModel;
+
+ // httpClient.setBaseUrl(useLocalHost ? DEV_URL_LOCALHOST : URL);
+
+ httpClient.setBaseUrl(useLocalHost ? DEV_URL : URL);
+ httpClient.setIgnoreSocks5Proxy(false);
+ }
+
+ public void onAllServicesInitialized() {
+ String keyAndToken = preferences.getPhoneKeyAndToken();
+ if (mobileNotificationValidator.isValid(keyAndToken)) {
+ setupConfirmationSent = true;
+ mobileModel.applyKeyAndToken(keyAndToken);
+ mobileMessageEncryption.setKey(mobileModel.getKey());
+ }
+ useTradeNotificationsProperty.set(preferences.isUseTradeNotifications());
+ useMarketNotificationsProperty.set(preferences.isUseMarketNotifications());
+ usePriceNotificationsProperty.set(preferences.isUsePriceNotifications());
+ useSoundProperty.set(preferences.isUseSoundForMobileNotifications());
+ }
+
+ public boolean sendMessage(MobileMessage message) throws Exception {
+ return sendMessage(message, useSoundProperty.get());
+ }
+
+ public boolean applyKeyAndToken(String keyAndToken) {
+ if (mobileNotificationValidator.isValid(keyAndToken)) {
+ mobileModel.applyKeyAndToken(keyAndToken);
+ mobileMessageEncryption.setKey(mobileModel.getKey());
+ preferences.setPhoneKeyAndToken(keyAndToken);
+ if (!setupConfirmationSent) {
+ try {
+ boolean success = sendConfirmationMessage();
+ if (success)
+ setupConfirmationSent = true;
+ else
+ log.warn("sendConfirmationMessage failed");
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public boolean sendMessage(MobileMessage message, boolean useSound) throws Exception {
+ log.info("sendMessage\n" +
+ "Title: " + message.getTitle() + "\nMessage: " + message.getMessage());
+ if (mobileModel.getKey() == null)
+ return false;
+
+ boolean doSend;
+ switch (message.getMobileMessageType()) {
+ case SETUP_CONFIRMATION:
+ doSend = true;
+ break;
+ case OFFER:
+ case TRADE:
+ case DISPUTE:
+ doSend = useTradeNotificationsProperty.get();
+ break;
+ case PRICE:
+ doSend = usePriceNotificationsProperty.get();
+ break;
+ case MARKET:
+ doSend = useMarketNotificationsProperty.get();
+ break;
+ case ERASE:
+ doSend = true;
+ break;
+ default:
+ doSend = false;
+ }
+
+ if (!doSend)
+ return false;
+
+ log.info("sendMessage message={}", message);
+ Gson gson = new Gson();
+ String json = gson.toJson(message);
+ log.info("json " + json);
+
+ StringBuilder padded = new StringBuilder(json);
+ while (padded.length() % 16 != 0) {
+ padded.append(" ");
+ }
+ json = padded.toString();
+
+ // generate 16 random characters for iv
+ String uuid = UUID.randomUUID().toString();
+ uuid = uuid.replace("-", "");
+ String iv = uuid.substring(0, 16);
+
+ String cipher = mobileMessageEncryption.encrypt(json, iv);
+ log.info("key = " + mobileModel.getKey());
+ log.info("iv = " + iv);
+ log.info("encryptedJson = " + cipher);
+ return doSendMessage(iv, cipher, useSound);
+ }
+
+ public boolean sendEraseMessage() throws Exception {
+ MobileMessage message = new MobileMessage("",
+ "",
+ MobileMessageType.ERASE);
+ return sendMessage(message, false);
+ }
+
+ public void reset() {
+ mobileModel.reset();
+ preferences.setPhoneKeyAndToken(null);
+ setupConfirmationSent = false;
+ }
+
+
+ private boolean sendConfirmationMessage() throws Exception {
+ log.info("sendConfirmationMessage");
+ MobileMessage message = new MobileMessage("",
+ "",
+ MobileMessageType.SETUP_CONFIRMATION);
+ return sendMessage(message, true);
+ }
+
+ private boolean doSendMessage(String iv, String cipher, boolean useSound) throws Exception {
+ String msg;
+ if (mobileModel.getOs() == null)
+ throw new RuntimeException("No mobileModel OS set");
+
+ switch (mobileModel.getOs()) {
+ case IOS:
+ msg = BISQ_MESSAGE_IOS_MAGIC;
+ break;
+ case IOS_DEV:
+ msg = BISQ_MESSAGE_IOS_MAGIC;
+ break;
+ case ANDROID:
+ msg = BISQ_MESSAGE_ANDROID_MAGIC;
+ break;
+ case UNDEFINED:
+ default:
+ throw new RuntimeException("No mobileModel OS set");
+ }
+ msg += MobileModel.PHONE_SEPARATOR_WRITING + iv + MobileModel.PHONE_SEPARATOR_WRITING + cipher;
+ boolean isAndroid = mobileModel.getOs() == MobileModel.OS.ANDROID;
+ boolean isProduction = mobileModel.getOs() == MobileModel.OS.IOS;
+
+ checkNotNull(mobileModel.getToken(), "mobileModel.getToken() must not be null");
+ String tokenAsHex = Hex.encodeHexString(mobileModel.getToken().getBytes("UTF-8"));
+ String msgAsHex = Hex.encodeHexString(msg.getBytes("UTF-8"));
+ String param = "relay?" +
+ "isAndroid=" + isAndroid +
+ "&isProduction=" + isProduction +
+ "&isContentAvailable=" + mobileModel.isContentAvailable() +
+ "&snd=" + useSound +
+ "&token=" + tokenAsHex + "&" +
+ "msg=" + msgAsHex;
+
+ log.info("Send: token={}", mobileModel.getToken());
+ log.info("Send: msg={}", msg);
+ log.info("Send: isAndroid={}\nuseSound={}\ntokenAsHex={}\nmsgAsHex={}",
+ isAndroid, useSound, tokenAsHex, msgAsHex);
+
+ String result = httpClient.requestWithGET(param, "User-Agent", "bisq/" +
+ Version.VERSION + ", uid:" + httpClient.getUid());
+ log.info("result: " + result);
+ return result.equals(SUCCESS);
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/MobileNotificationValidator.java b/src/main/java/bisq/core/notifications/MobileNotificationValidator.java
new file mode 100644
index 00000000..95eab45f
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/MobileNotificationValidator.java
@@ -0,0 +1,66 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import javax.inject.Inject;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MobileNotificationValidator {
+ @Inject
+ public MobileNotificationValidator() {
+ }
+
+ public boolean isValid(String keyAndToken) {
+ if (keyAndToken == null)
+ return false;
+
+ String[] tokens = keyAndToken.split(MobileModel.PHONE_SEPARATOR_ESCAPED);
+ if (tokens.length != 4) {
+ log.error("invalid pairing ID format: not 4 sections separated by " + MobileModel.PHONE_SEPARATOR_WRITING);
+ return false;
+ }
+ String magic = tokens[0];
+ String key = tokens[2];
+ String phoneId = tokens[3];
+
+ if (key.length() != 32) {
+ log.error("invalid pairing ID format: key not 32 bytes");
+ return false;
+ }
+
+ if (magic.equals(MobileModel.OS.IOS.getMagicString()) ||
+ magic.equals(MobileModel.OS.IOS_DEV.getMagicString())) {
+ if (phoneId.length() != 64) {
+ log.error("invalid Bisq MobileModel ID format: iOS token not 64 bytes");
+ return false;
+ }
+ } else if (magic.equals(MobileModel.OS.ANDROID.getMagicString())) {
+ if (phoneId.length() < 32) {
+ log.error("invalid Bisq MobileModel ID format: Android token too short (<32 bytes)");
+ return false;
+ }
+ } else {
+ log.error("invalid Bisq MobileModel ID format");
+ return false;
+ }
+
+ return true;
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java b/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java
new file mode 100644
index 00000000..a2829321
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/DisputeMsgEvents.java
@@ -0,0 +1,107 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts;
+
+import bisq.core.arbitration.Dispute;
+import bisq.core.arbitration.DisputeManager;
+import bisq.core.arbitration.messages.DisputeCommunicationMessage;
+import bisq.core.locale.Res;
+import bisq.core.notifications.MobileMessage;
+import bisq.core.notifications.MobileMessageType;
+import bisq.core.notifications.MobileNotificationService;
+
+import bisq.network.p2p.P2PService;
+
+import javax.inject.Inject;
+
+import javafx.collections.ListChangeListener;
+
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class DisputeMsgEvents {
+ private final P2PService p2PService;
+ private final MobileNotificationService mobileNotificationService;
+
+ @Inject
+ public DisputeMsgEvents(DisputeManager disputeManager, P2PService p2PService, MobileNotificationService mobileNotificationService) {
+ this.p2PService = p2PService;
+ this.mobileNotificationService = mobileNotificationService;
+
+ // We need to handle it here in the constructor otherwise we get repeated the messages sent.
+ disputeManager.getDisputesAsObservableList().addListener((ListChangeListener) c -> {
+ c.next();
+ if (c.wasAdded()) {
+ c.getAddedSubList().forEach(this::setDisputeListener);
+ }
+ });
+ disputeManager.getDisputesAsObservableList().forEach(this::setDisputeListener);
+ }
+
+ // We ignore that onAllServicesInitialized here
+ public void onAllServicesInitialized() {
+ }
+
+ private void setDisputeListener(Dispute dispute) {
+ //TODO use weak ref or remove listener
+ log.info("We got a dispute added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
+ dispute.getDisputeCommunicationMessages().addListener((ListChangeListener) c -> {
+ log.info("We got a DisputeCommunicationMessage added. id={}, tradeId={}", dispute.getId(), dispute.getTradeId());
+ c.next();
+ if (c.wasAdded()) {
+ c.getAddedSubList().forEach(this::setDisputeCommunicationMessage);
+ }
+ });
+
+ //TODO test
+ if (!dispute.getDisputeCommunicationMessages().isEmpty())
+ setDisputeCommunicationMessage(dispute.getDisputeCommunicationMessages().get(0));
+ }
+
+ private void setDisputeCommunicationMessage(DisputeCommunicationMessage disputeMsg) {
+ // TODO we need to prevent to send msg for old dispute messages again at restart
+ // Maybe we need a new property in DisputeCommunicationMessage
+ // As key is not set in initial iterations it seems we don't need an extra handling.
+ // the mailbox msg is set a bit later so that triggers a notification, but not the old messages.
+
+ // We only send msg in case we are not the sender
+ if (!disputeMsg.getSenderNodeAddress().equals(p2PService.getAddress())) {
+ String shortId = disputeMsg.getShortId();
+ MobileMessage message = new MobileMessage(Res.get("account.notifications.dispute.message.title"),
+ Res.get("account.notifications.dispute.message.msg", shortId),
+ shortId,
+ MobileMessageType.DISPUTE);
+ try {
+ mobileNotificationService.sendMessage(message);
+ } catch (Exception e) {
+ log.error(e.toString());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static MobileMessage getTestMsg() {
+ String shortId = UUID.randomUUID().toString().substring(0, 8);
+ return new MobileMessage(Res.get("account.notifications.dispute.message.title"),
+ Res.get("account.notifications.dispute.message.msg", shortId),
+ shortId,
+ MobileMessageType.DISPUTE);
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java b/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java
new file mode 100644
index 00000000..2ed8a7eb
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/MyOfferTakenEvents.java
@@ -0,0 +1,79 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts;
+
+import bisq.core.locale.Res;
+import bisq.core.notifications.MobileMessage;
+import bisq.core.notifications.MobileMessageType;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.offer.OpenOffer;
+import bisq.core.offer.OpenOfferManager;
+
+import javax.inject.Inject;
+
+import javafx.collections.ListChangeListener;
+
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MyOfferTakenEvents {
+ private final OpenOfferManager openOfferManager;
+ private final MobileNotificationService mobileNotificationService;
+
+ @Inject
+ public MyOfferTakenEvents(OpenOfferManager openOfferManager, MobileNotificationService mobileNotificationService) {
+ this.openOfferManager = openOfferManager;
+ this.mobileNotificationService = mobileNotificationService;
+ }
+
+ public void onAllServicesInitialized() {
+ openOfferManager.getObservableList().addListener((ListChangeListener) c -> {
+ c.next();
+ if (c.wasRemoved())
+ c.getRemoved().forEach(this::onOpenOfferRemoved);
+ });
+ openOfferManager.getObservableList().forEach(this::onOpenOfferRemoved);
+ }
+
+ private void onOpenOfferRemoved(OpenOffer openOffer) {
+ log.info("We got a offer removed. id={}, state={}", openOffer.getId(), openOffer.getState());
+ if (openOffer.getState() == OpenOffer.State.RESERVED) {
+ String shortId = openOffer.getShortId();
+ MobileMessage message = new MobileMessage(Res.get("account.notifications.offer.message.title"),
+ Res.get("account.notifications.offer.message.msg", shortId),
+ shortId,
+ MobileMessageType.OFFER);
+ try {
+ mobileNotificationService.sendMessage(message);
+ } catch (Exception e) {
+ log.error(e.toString());
+ e.printStackTrace();
+ }
+ }
+ }
+
+ public static MobileMessage getTestMsg() {
+ String shortId = UUID.randomUUID().toString().substring(0, 8);
+ return new MobileMessage(Res.get("account.notifications.offer.message.title"),
+ Res.get("account.notifications.offer.message.msg", shortId),
+ shortId,
+ MobileMessageType.OFFER);
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/TradeEvents.java b/src/main/java/bisq/core/notifications/alerts/TradeEvents.java
new file mode 100644
index 00000000..50bd4986
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/TradeEvents.java
@@ -0,0 +1,127 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts;
+
+import bisq.core.locale.Res;
+import bisq.core.notifications.MobileMessage;
+import bisq.core.notifications.MobileMessageType;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.trade.Trade;
+import bisq.core.trade.TradeManager;
+
+import bisq.common.crypto.KeyRing;
+import bisq.common.crypto.PubKeyRing;
+
+import javax.inject.Inject;
+
+import javafx.collections.ListChangeListener;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class TradeEvents {
+ private final PubKeyRing pubKeyRing;
+ private final TradeManager tradeManager;
+ private final MobileNotificationService mobileNotificationService;
+
+ @Inject
+ public TradeEvents(TradeManager tradeManager, KeyRing keyRing, MobileNotificationService mobileNotificationService) {
+ this.tradeManager = tradeManager;
+ this.mobileNotificationService = mobileNotificationService;
+ this.pubKeyRing = keyRing.getPubKeyRing();
+ }
+
+ public void onAllServicesInitialized() {
+ tradeManager.getTradableList().addListener((ListChangeListener) c -> {
+ c.next();
+ if (c.wasAdded()) {
+ c.getAddedSubList().forEach(this::setTradePhaseListener);
+ }
+ });
+ tradeManager.getTradableList().forEach(this::setTradePhaseListener);
+ }
+
+ private void setTradePhaseListener(Trade trade) {
+ log.info("We got a new trade. id={}", trade.getId());
+ if (!trade.isPayoutPublished()) {
+ trade.statePhaseProperty().addListener((observable, oldValue, newValue) -> {
+ String msg = null;
+ log.error("setTradePhaseListener phase " + newValue);
+ String shortId = trade.getShortId();
+ switch (newValue) {
+ case INIT:
+ case TAKER_FEE_PUBLISHED:
+ case DEPOSIT_PUBLISHED:
+ break;
+ case DEPOSIT_CONFIRMED:
+ if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing()))
+ msg = Res.get("account.notifications.trade.message.msg.conf", shortId);
+ break;
+ case FIAT_SENT:
+ // We only notify the seller
+ if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getSellerPubKeyRing()))
+ msg = Res.get("account.notifications.trade.message.msg.started", shortId);
+ break;
+ case FIAT_RECEIVED:
+ break;
+ case PAYOUT_PUBLISHED:
+ // We only notify the buyer
+ if (trade.getContract() != null && pubKeyRing.equals(trade.getContract().getBuyerPubKeyRing()))
+ msg = Res.get("account.notifications.trade.message.msg.completed", shortId);
+ break;
+ case WITHDRAWN:
+ break;
+ }
+ if (msg != null) {
+ MobileMessage message = new MobileMessage(Res.get("account.notifications.trade.message.title"),
+ msg,
+ shortId,
+ MobileMessageType.TRADE);
+ try {
+ mobileNotificationService.sendMessage(message);
+ } catch (Exception e) {
+ log.error(e.toString());
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ }
+
+ public static List getTestMessages() {
+ String shortId = UUID.randomUUID().toString().substring(0, 8);
+ List list = new ArrayList<>();
+ list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"),
+ Res.get("account.notifications.trade.message.msg.conf", shortId),
+ shortId,
+ MobileMessageType.TRADE));
+ list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"),
+ Res.get("account.notifications.trade.message.msg.started", shortId),
+ shortId,
+ MobileMessageType.TRADE));
+ list.add(new MobileMessage(Res.get("account.notifications.trade.message.title"),
+ Res.get("account.notifications.trade.message.msg.completed", shortId),
+ shortId,
+ MobileMessageType.TRADE));
+ return list;
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java b/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java
new file mode 100644
index 00000000..48daa041
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/market/MarketAlertFilter.java
@@ -0,0 +1,107 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts.market;
+
+import bisq.core.payment.PaymentAccount;
+import bisq.core.proto.CoreProtoResolver;
+
+import bisq.common.proto.persistable.PersistablePayload;
+
+import io.bisq.generated.protobuffer.PB;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import lombok.Value;
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+@Value
+public class MarketAlertFilter implements PersistablePayload {
+ private PaymentAccount paymentAccount;
+ private int triggerValue;
+ private boolean isBuyOffer;
+ private List alertIds;
+
+
+ public MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer) {
+ this(paymentAccount, triggerValue, isBuyOffer, new ArrayList<>());
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // PROTO BUFFER
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ /**
+ *
+ * @param paymentAccount // The payment account used for the filter
+ * @param triggerValue // Percentage distance from market price (100 for 1.00%)
+ * @param isBuyOffer // It the offer is a buy offer
+ * @param alertIds // List of offerIds for which we have sent already an alert
+ */
+ private MarketAlertFilter(PaymentAccount paymentAccount, int triggerValue, boolean isBuyOffer, List alertIds) {
+ this.paymentAccount = paymentAccount;
+ this.triggerValue = triggerValue;
+ this.isBuyOffer = isBuyOffer;
+ this.alertIds = alertIds;
+ }
+
+ @Override
+ public PB.MarketAlertFilter toProtoMessage() {
+ return PB.MarketAlertFilter.newBuilder()
+ .setPaymentAccount(paymentAccount.toProtoMessage())
+ .setTriggerValue(triggerValue)
+ .setIsBuyOffer(isBuyOffer)
+ .addAllAlertIds(alertIds)
+ .build();
+ }
+
+ public static MarketAlertFilter fromProto(PB.MarketAlertFilter proto, CoreProtoResolver coreProtoResolver) {
+ List list = proto.getAlertIdsList().isEmpty() ?
+ new ArrayList<>() : new ArrayList<>(proto.getAlertIdsList());
+ return new MarketAlertFilter(PaymentAccount.fromProto(proto.getPaymentAccount(), coreProtoResolver),
+ proto.getTriggerValue(),
+ proto.getIsBuyOffer(),
+ list);
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // API
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public void addAlertId(String alertId) {
+ if (notContainsAlertId(alertId))
+ alertIds.add(alertId);
+ }
+
+ public boolean notContainsAlertId(String alertId) {
+ return !alertIds.contains(alertId);
+ }
+
+ @Override
+ public String toString() {
+ return "MarketAlertFilter{" +
+ "\n paymentAccount=" + paymentAccount +
+ ",\n triggerValue=" + triggerValue +
+ ",\n isBuyOffer=" + isBuyOffer +
+ ",\n alertIds=" + alertIds +
+ "\n}";
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java b/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java
new file mode 100644
index 00000000..4720a803
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/market/MarketAlerts.java
@@ -0,0 +1,216 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts.market;
+
+import bisq.core.locale.CurrencyUtil;
+import bisq.core.locale.Res;
+import bisq.core.monetary.Altcoin;
+import bisq.core.monetary.Price;
+import bisq.core.notifications.MobileMessage;
+import bisq.core.notifications.MobileMessageType;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.offer.Offer;
+import bisq.core.offer.OfferBookService;
+import bisq.core.offer.OfferPayload;
+import bisq.core.provider.price.MarketPrice;
+import bisq.core.provider.price.PriceFeedService;
+import bisq.core.user.User;
+import bisq.core.util.BSFormatter;
+
+import bisq.common.crypto.KeyRing;
+import bisq.common.util.MathUtils;
+
+import org.bitcoinj.utils.Fiat;
+
+import javax.inject.Inject;
+
+import java.util.List;
+import java.util.UUID;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class MarketAlerts {
+ private final OfferBookService offerBookService;
+ private final MobileNotificationService mobileNotificationService;
+ private final User user;
+ private final PriceFeedService priceFeedService;
+ private final KeyRing keyRing;
+ private final BSFormatter formatter;
+
+ @Inject
+ public MarketAlerts(OfferBookService offerBookService, MobileNotificationService mobileNotificationService,
+ User user, PriceFeedService priceFeedService, KeyRing keyRing, BSFormatter formatter) {
+ this.offerBookService = offerBookService;
+ this.mobileNotificationService = mobileNotificationService;
+ this.user = user;
+ this.priceFeedService = priceFeedService;
+ this.keyRing = keyRing;
+ this.formatter = formatter;
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // API
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ public void onAllServicesInitialized() {
+ offerBookService.addOfferBookChangedListener(new OfferBookService.OfferBookChangedListener() {
+ @Override
+ public void onAdded(Offer offer) {
+ onOfferAdded(offer);
+ }
+
+ @Override
+ public void onRemoved(Offer offer) {
+ }
+ });
+ applyFilterOnAllOffers();
+ }
+
+ public void addMarketAlertFilter(MarketAlertFilter filter) {
+ user.addMarketAlertFilter(filter);
+ applyFilterOnAllOffers();
+ }
+
+ public void removeMarketAlertFilter(MarketAlertFilter filter) {
+ user.removeMarketAlertFilter(filter);
+ }
+
+ public List getMarketAlertFilters() {
+ return user.getMarketAlertFilters();
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // Private
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ private void applyFilterOnAllOffers() {
+ offerBookService.getOffers().forEach(this::onOfferAdded);
+ }
+
+ // We combine the offer ID and the price (either as % price or as fixed price) to get also updates for edited offers
+ // % price get multiplied by 10000 to have 0.12% be converted to 12. For fixed price we have precision of 8 for
+ // altcoins and precision of 4 for fiat.
+ private String getAlertId(Offer offer) {
+ double price = offer.isUseMarketBasedPrice() ? offer.getMarketPriceMargin() * 10000 : offer.getOfferPayload().getPrice();
+ String priceString = String.valueOf((long) price);
+ return offer.getId() + "|" + priceString;
+ }
+
+ private void onOfferAdded(Offer offer) {
+ String currencyCode = offer.getCurrencyCode();
+ MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
+ Price offerPrice = offer.getPrice();
+ if (marketPrice != null && offerPrice != null) {
+ boolean isSellOffer = offer.getDirection() == OfferPayload.Direction.SELL;
+ String shortOfferId = offer.getShortId();
+ boolean isFiatCurrency = CurrencyUtil.isFiatCurrency(currencyCode);
+ String alertId = getAlertId(offer);
+ user.getMarketAlertFilters().stream()
+ .filter(marketAlertFilter -> !offer.isMyOffer(keyRing))
+ .filter(marketAlertFilter -> offer.getPaymentMethod().equals(marketAlertFilter.getPaymentAccount().getPaymentMethod()))
+ .filter(marketAlertFilter -> marketAlertFilter.notContainsAlertId(alertId))
+ .forEach(marketAlertFilter -> {
+ int triggerValue = marketAlertFilter.getTriggerValue();
+ boolean isTriggerForBuyOffer = marketAlertFilter.isBuyOffer();
+ double marketPriceAsDouble1 = marketPrice.getPrice();
+ int precision = CurrencyUtil.isCryptoCurrency(currencyCode) ?
+ Altcoin.SMALLEST_UNIT_EXPONENT :
+ Fiat.SMALLEST_UNIT_EXPONENT;
+ double marketPriceAsDouble = MathUtils.scaleUpByPowerOf10(marketPriceAsDouble1, precision);
+ double offerPriceValue = offerPrice.getValue();
+ double ratio = offerPriceValue / marketPriceAsDouble;
+ ratio = 1 - ratio;
+ if (isFiatCurrency && isSellOffer)
+ ratio *= -1;
+ else if (!isFiatCurrency && !isSellOffer)
+ ratio *= -1;
+
+ ratio = ratio * 10000;
+ boolean triggered = ratio <= triggerValue;
+ if (!triggered)
+ return;
+
+ boolean isTriggerForBuyOfferAndTriggered = !isSellOffer && isTriggerForBuyOffer;
+ boolean isTriggerForSellOfferAndTriggered = isSellOffer && !isTriggerForBuyOffer;
+ if (isTriggerForBuyOfferAndTriggered || isTriggerForSellOfferAndTriggered) {
+ String direction = isSellOffer ? Res.get("shared.sell") : Res.get("shared.buy");
+ String marketDir;
+ if (isFiatCurrency) {
+ if (isSellOffer) {
+ marketDir = ratio > 0 ?
+ Res.get("account.notifications.marketAlert.message.msg.above") :
+ Res.get("account.notifications.marketAlert.message.msg.below");
+ } else {
+ marketDir = ratio < 0 ?
+ Res.get("account.notifications.marketAlert.message.msg.above") :
+ Res.get("account.notifications.marketAlert.message.msg.below");
+ }
+ } else {
+ if (isSellOffer) {
+ marketDir = ratio < 0 ?
+ Res.get("account.notifications.marketAlert.message.msg.above") :
+ Res.get("account.notifications.marketAlert.message.msg.below");
+ } else {
+ marketDir = ratio > 0 ?
+ Res.get("account.notifications.marketAlert.message.msg.above") :
+ Res.get("account.notifications.marketAlert.message.msg.below");
+ }
+ }
+
+ ratio = Math.abs(ratio);
+ String msg = Res.get("account.notifications.marketAlert.message.msg",
+ direction,
+ formatter.getCurrencyPair(currencyCode),
+ formatter.formatPrice(offerPrice),
+ formatter.formatToPercentWithSymbol(ratio / 10000d),
+ marketDir,
+ Res.get(offer.getPaymentMethod().getId()),
+ shortOfferId);
+ MobileMessage message = new MobileMessage(Res.get("account.notifications.marketAlert.message.title"),
+ msg,
+ shortOfferId,
+ MobileMessageType.MARKET);
+ try {
+ boolean success = mobileNotificationService.sendMessage(message);
+ if (success) {
+ // In case we have disabled alerts we do not get a success msg back and we do not
+ // persist the offer
+ marketAlertFilter.addAlertId(alertId);
+ user.persist();
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+ });
+ }
+ }
+
+ public static MobileMessage getTestMsg() {
+ String shortId = UUID.randomUUID().toString().substring(0, 8);
+ return new MobileMessage(Res.get("account.notifications.marketAlert.message.title"),
+ "A new 'sell BTC/USD' offer with price 6019.2744 (5.36% below market price) and payment method " +
+ "'Perfect Money' was published to the Bisq offerbook.\n" +
+ "Offer ID: wygiaw.",
+ shortId,
+ MobileMessageType.MARKET);
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java b/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java
new file mode 100644
index 00000000..c7b21aea
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/price/PriceAlert.java
@@ -0,0 +1,102 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts.price;
+
+import bisq.core.locale.CurrencyUtil;
+import bisq.core.locale.Res;
+import bisq.core.monetary.Altcoin;
+import bisq.core.notifications.MobileMessage;
+import bisq.core.notifications.MobileMessageType;
+import bisq.core.notifications.MobileNotificationService;
+import bisq.core.provider.price.MarketPrice;
+import bisq.core.provider.price.PriceFeedService;
+import bisq.core.user.User;
+import bisq.core.util.BSFormatter;
+
+import bisq.common.util.MathUtils;
+
+import org.bitcoinj.utils.Fiat;
+
+import javax.inject.Inject;
+
+import lombok.extern.slf4j.Slf4j;
+
+@Slf4j
+public class PriceAlert {
+ private final PriceFeedService priceFeedService;
+ private final MobileNotificationService mobileNotificationService;
+ private final User user;
+ private final BSFormatter formatter;
+
+ @Inject
+ public PriceAlert(PriceFeedService priceFeedService, MobileNotificationService mobileNotificationService, User user, BSFormatter formatter) {
+ this.priceFeedService = priceFeedService;
+ this.user = user;
+ this.mobileNotificationService = mobileNotificationService;
+ this.formatter = formatter;
+ }
+
+ public void onAllServicesInitialized() {
+ priceFeedService.updateCounterProperty().addListener((observable, oldValue, newValue) -> update());
+ }
+
+ private void update() {
+ if (user.getPriceAlertFilter() != null) {
+ PriceAlertFilter filter = user.getPriceAlertFilter();
+ String currencyCode = filter.getCurrencyCode();
+ MarketPrice marketPrice = priceFeedService.getMarketPrice(currencyCode);
+ if (marketPrice != null) {
+ int exp = CurrencyUtil.isCryptoCurrency(currencyCode) ? Altcoin.SMALLEST_UNIT_EXPONENT : Fiat.SMALLEST_UNIT_EXPONENT;
+ double priceAsDouble = marketPrice.getPrice();
+ long priceAsLong = MathUtils.roundDoubleToLong(MathUtils.scaleUpByPowerOf10(priceAsDouble, exp));
+ String currencyName = CurrencyUtil.getNameByCode(currencyCode);
+ if (priceAsLong > filter.getHigh() || priceAsLong < filter.getLow()) {
+ String msg = Res.get("account.notifications.priceAlert.message.msg",
+ currencyName,
+ formatter.formatMarketPrice(priceAsDouble, currencyCode),
+ formatter.getCurrencyPair(currencyCode));
+ MobileMessage message = new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName),
+ msg,
+ MobileMessageType.PRICE);
+ log.error(msg);
+ try {
+ mobileNotificationService.sendMessage(message);
+
+ // If we got triggered an alert we remove the filter.
+ user.removePriceAlertFilter();
+ } catch (Exception e) {
+ log.error(e.toString());
+ e.printStackTrace();
+ }
+ }
+ }
+ }
+ }
+
+ public static MobileMessage getTestMsg() {
+ String currencyCode = "USD";
+ String currencyName = CurrencyUtil.getNameByCode(currencyCode);
+ String msg = Res.get("account.notifications.priceAlert.message.msg",
+ currencyName,
+ "6023.34",
+ "BTC/USD");
+ return new MobileMessage(Res.get("account.notifications.priceAlert.message.title", currencyName),
+ msg,
+ MobileMessageType.PRICE);
+ }
+}
diff --git a/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java b/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java
new file mode 100644
index 00000000..712a8d61
--- /dev/null
+++ b/src/main/java/bisq/core/notifications/alerts/price/PriceAlertFilter.java
@@ -0,0 +1,54 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications.alerts.price;
+
+import bisq.common.proto.persistable.PersistablePayload;
+
+import io.bisq.generated.protobuffer.PB;
+
+import lombok.Value;
+
+@Value
+public class PriceAlertFilter implements PersistablePayload {
+ String currencyCode;
+ long high;
+ long low;
+
+ public PriceAlertFilter(String currencyCode, long high, long low) {
+ this.currencyCode = currencyCode;
+ this.high = high;
+ this.low = low;
+ }
+
+
+ ///////////////////////////////////////////////////////////////////////////////////////////
+ // PROTO BUFFER
+ ///////////////////////////////////////////////////////////////////////////////////////////
+
+ @Override
+ public PB.PriceAlertFilter toProtoMessage() {
+ return PB.PriceAlertFilter.newBuilder()
+ .setCurrencyCode(currencyCode)
+ .setHigh(high)
+ .setLow(low).build();
+ }
+
+ public static PriceAlertFilter fromProto(PB.PriceAlertFilter proto) {
+ return new PriceAlertFilter(proto.getCurrencyCode(), proto.getHigh(), proto.getLow());
+ }
+}
diff --git a/src/main/java/bisq/core/provider/PriceNodeHttpClient.java b/src/main/java/bisq/core/provider/PriceNodeHttpClient.java
new file mode 100644
index 00000000..1a9510f4
--- /dev/null
+++ b/src/main/java/bisq/core/provider/PriceNodeHttpClient.java
@@ -0,0 +1,32 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.provider;
+
+import bisq.network.Socks5ProxyProvider;
+import bisq.network.http.HttpClient;
+
+import javax.inject.Inject;
+
+import javax.annotation.Nullable;
+
+public class PriceNodeHttpClient extends HttpClient {
+ @Inject
+ public PriceNodeHttpClient(@Nullable Socks5ProxyProvider socks5ProxyProvider) {
+ super(socks5ProxyProvider);
+ }
+}
diff --git a/src/main/java/bisq/core/provider/fee/FeeProvider.java b/src/main/java/bisq/core/provider/fee/FeeProvider.java
index 9e047e49..f9f1a475 100644
--- a/src/main/java/bisq/core/provider/fee/FeeProvider.java
+++ b/src/main/java/bisq/core/provider/fee/FeeProvider.java
@@ -18,10 +18,9 @@
package bisq.core.provider.fee;
import bisq.core.provider.HttpClientProvider;
+import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.ProvidersRepository;
-import bisq.network.http.HttpClient;
-
import bisq.common.app.Version;
import bisq.common.util.Tuple2;
@@ -41,7 +40,7 @@
public class FeeProvider extends HttpClientProvider {
@Inject
- public FeeProvider(HttpClient httpClient, ProvidersRepository providersRepository) {
+ public FeeProvider(PriceNodeHttpClient httpClient, ProvidersRepository providersRepository) {
super(httpClient, providersRepository.getBaseUrl(), false);
}
diff --git a/src/main/java/bisq/core/provider/price/PriceFeedService.java b/src/main/java/bisq/core/provider/price/PriceFeedService.java
index bbbf7c50..c7b899d4 100644
--- a/src/main/java/bisq/core/provider/price/PriceFeedService.java
+++ b/src/main/java/bisq/core/provider/price/PriceFeedService.java
@@ -21,6 +21,7 @@
import bisq.core.locale.CurrencyUtil;
import bisq.core.locale.TradeCurrency;
import bisq.core.monetary.Price;
+import bisq.core.provider.PriceNodeHttpClient;
import bisq.core.provider.ProvidersRepository;
import bisq.core.trade.statistics.TradeStatistics2;
import bisq.core.user.Preferences;
@@ -98,7 +99,7 @@ public class PriceFeedService {
///////////////////////////////////////////////////////////////////////////////////////////
@Inject
- public PriceFeedService(@SuppressWarnings("SameParameterValue") HttpClient httpClient,
+ public PriceFeedService(@SuppressWarnings("SameParameterValue") PriceNodeHttpClient httpClient,
@SuppressWarnings("SameParameterValue") ProvidersRepository providersRepository,
@SuppressWarnings("SameParameterValue") Preferences preferences) {
this.httpClient = httpClient;
diff --git a/src/main/java/bisq/core/user/Preferences.java b/src/main/java/bisq/core/user/Preferences.java
index ef2e1cf1..731e0f9f 100644
--- a/src/main/java/bisq/core/user/Preferences.java
+++ b/src/main/java/bisq/core/user/Preferences.java
@@ -549,6 +549,31 @@ public void setReferralId(String referralId) {
persist();
}
+ public void setPhoneKeyAndToken(String phoneKeyAndToken) {
+ prefPayload.setPhoneKeyAndToken(phoneKeyAndToken);
+ persist();
+ }
+
+ public void setUseSoundForMobileNotifications(boolean value) {
+ prefPayload.setUseSoundForMobileNotifications(value);
+ persist();
+ }
+
+ public void setUseTradeNotifications(boolean value) {
+ prefPayload.setUseTradeNotifications(value);
+ persist();
+ }
+
+ public void setUseMarketNotifications(boolean value) {
+ prefPayload.setUseMarketNotifications(value);
+ persist();
+ }
+
+ public void setUsePriceNotifications(boolean value) {
+ prefPayload.setUsePriceNotifications(value);
+ persist();
+ }
+
///////////////////////////////////////////////////////////////////////////////////////////
// Getter
@@ -735,6 +760,16 @@ private interface ExcludesDelegateMethods {
void setReferralId(String referralId);
+ void setPhoneKeyAndToken(String phoneKeyAndToken);
+
+ void setUseSoundForMobileNotifications(boolean value);
+
+ void setUseTradeNotifications(boolean value);
+
+ void setUseMarketNotifications(boolean value);
+
+ void setUsePriceNotifications(boolean value);
+
List getBridgeAddresses();
long getWithdrawalTxFeeInBytes();
diff --git a/src/main/java/bisq/core/user/PreferencesPayload.java b/src/main/java/bisq/core/user/PreferencesPayload.java
index 86a8ac4c..524cb9b1 100644
--- a/src/main/java/bisq/core/user/PreferencesPayload.java
+++ b/src/main/java/bisq/core/user/PreferencesPayload.java
@@ -103,6 +103,12 @@ public final class PreferencesPayload implements PersistableEnvelope {
int bitcoinNodesOptionOrdinal;
@Nullable
String referralId;
+ @Nullable
+ String phoneKeyAndToken;
+ boolean useSoundForMobileNotifications = true;
+ boolean useTradeNotifications = true;
+ boolean useMarketNotifications = true;
+ boolean usePriceNotifications = true;
///////////////////////////////////////////////////////////////////////////////////////////
@@ -152,7 +158,11 @@ public Message toProtoMessage() {
.setPayFeeInBtc(payFeeInBtc)
.setBridgeOptionOrdinal(bridgeOptionOrdinal)
.setTorTransportOrdinal(torTransportOrdinal)
- .setBitcoinNodesOptionOrdinal(bitcoinNodesOptionOrdinal);
+ .setBitcoinNodesOptionOrdinal(bitcoinNodesOptionOrdinal)
+ .setUseSoundForMobileNotifications(useSoundForMobileNotifications)
+ .setUseTradeNotifications(useTradeNotifications)
+ .setUseMarketNotifications(useMarketNotifications)
+ .setUsePriceNotifications(usePriceNotifications);
Optional.ofNullable(backupDirectory).ifPresent(builder::setBackupDirectory);
Optional.ofNullable(preferredTradeCurrency).ifPresent(e -> builder.setPreferredTradeCurrency((PB.TradeCurrency) e.toProtoMessage()));
Optional.ofNullable(offerBookChartScreenCurrencyCode).ifPresent(builder::setOfferBookChartScreenCurrencyCode);
@@ -164,6 +174,8 @@ public Message toProtoMessage() {
Optional.ofNullable(bridgeAddresses).ifPresent(builder::addAllBridgeAddresses);
Optional.ofNullable(customBridges).ifPresent(builder::setCustomBridges);
Optional.ofNullable(referralId).ifPresent(builder::setReferralId);
+ Optional.ofNullable(phoneKeyAndToken).ifPresent(builder::setPhoneKeyAndToken);
+
return PB.PersistableEnvelope.newBuilder().setPreferencesPayload(builder).build();
}
@@ -218,6 +230,11 @@ public static PersistableEnvelope fromProto(PB.PreferencesPayload proto, CorePro
proto.getTorTransportOrdinal(),
ProtoUtil.stringOrNullFromProto(proto.getCustomBridges()),
proto.getBitcoinNodesOptionOrdinal(),
- proto.getReferralId().isEmpty() ? null : proto.getReferralId());
+ proto.getReferralId().isEmpty() ? null : proto.getReferralId(),
+ proto.getPhoneKeyAndToken().isEmpty() ? null : proto.getPhoneKeyAndToken(),
+ proto.getUseSoundForMobileNotifications(),
+ proto.getUseTradeNotifications(),
+ proto.getUseMarketNotifications(),
+ proto.getUsePriceNotifications());
}
}
diff --git a/src/main/java/bisq/core/user/User.java b/src/main/java/bisq/core/user/User.java
index 01db35d5..bbb42653 100644
--- a/src/main/java/bisq/core/user/User.java
+++ b/src/main/java/bisq/core/user/User.java
@@ -23,6 +23,8 @@
import bisq.core.filter.Filter;
import bisq.core.locale.LanguageUtil;
import bisq.core.locale.TradeCurrency;
+import bisq.core.notifications.alerts.market.MarketAlertFilter;
+import bisq.core.notifications.alerts.price.PriceAlertFilter;
import bisq.core.payment.PaymentAccount;
import bisq.network.p2p.NodeAddress;
@@ -108,11 +110,11 @@ public void readPersisted() {
userPayload.setCurrentPaymentAccount(currentPaymentAccountProperty.get());
persist();
});
-
}
- private void persist() {
- storage.queueUpForSave(userPayload);
+ public void persist() {
+ if (storage != null)
+ storage.queueUpForSave(userPayload);
}
@@ -134,10 +136,7 @@ public Arbitrator getAcceptedArbitratorByAddress(NodeAddress nodeAddress) {
Optional arbitratorOptional = acceptedArbitrators.stream()
.filter(e -> e.getNodeAddress().equals(nodeAddress))
.findFirst();
- if (arbitratorOptional.isPresent())
- return arbitratorOptional.get();
- else
- return null;
+ return arbitratorOptional.orElse(null);
} else {
return null;
}
@@ -150,10 +149,7 @@ public Mediator getAcceptedMediatorByAddress(NodeAddress nodeAddress) {
Optional mediatorOptionalOptional = acceptedMediators.stream()
.filter(e -> e.getNodeAddress().equals(nodeAddress))
.findFirst();
- if (mediatorOptionalOptional.isPresent())
- return mediatorOptionalOptional.get();
- else
- return null;
+ return mediatorOptionalOptional.orElse(null);
} else {
return null;
}
@@ -321,6 +317,25 @@ public void setDisplayedAlert(@Nullable Alert displayedAlert) {
persist();
}
+ public void addMarketAlertFilter(MarketAlertFilter filter) {
+ getMarketAlertFilters().add(filter);
+ persist();
+ }
+
+ public void removeMarketAlertFilter(MarketAlertFilter filter) {
+ getMarketAlertFilters().remove(filter);
+ persist();
+ }
+
+ public void setPriceAlertFilter(PriceAlertFilter filter) {
+ userPayload.setPriceAlertFilter(filter);
+ persist();
+ }
+
+ public void removePriceAlertFilter() {
+ userPayload.setPriceAlertFilter(null);
+ persist();
+ }
///////////////////////////////////////////////////////////////////////////////////////////
// Getters
@@ -330,11 +345,8 @@ public void setDisplayedAlert(@Nullable Alert displayedAlert) {
public PaymentAccount getPaymentAccount(String paymentAccountId) {
Optional optional = userPayload.getPaymentAccounts() != null ?
userPayload.getPaymentAccounts().stream().filter(e -> e.getId().equals(paymentAccountId)).findAny() :
- Optional.empty();
- if (optional.isPresent())
- return optional.get();
- else
- return null;
+ Optional.empty();
+ return optional.orElse(null);
}
public String getAccountId() {
@@ -414,4 +426,13 @@ public boolean isMyOwnRegisteredArbitrator(Arbitrator arbitrator) {
public boolean isMyOwnRegisteredMediator(Mediator mediator) {
return mediator.equals(userPayload.getRegisteredMediator());
}
+
+ public List getMarketAlertFilters() {
+ return userPayload.getMarketAlertFilters();
+ }
+
+ @Nullable
+ public PriceAlertFilter getPriceAlertFilter() {
+ return userPayload.getPriceAlertFilter();
+ }
}
diff --git a/src/main/java/bisq/core/user/UserPayload.java b/src/main/java/bisq/core/user/UserPayload.java
index 22c662c0..a475f25f 100644
--- a/src/main/java/bisq/core/user/UserPayload.java
+++ b/src/main/java/bisq/core/user/UserPayload.java
@@ -21,6 +21,8 @@
import bisq.core.arbitration.Arbitrator;
import bisq.core.arbitration.Mediator;
import bisq.core.filter.Filter;
+import bisq.core.notifications.alerts.market.MarketAlertFilter;
+import bisq.core.notifications.alerts.price.PriceAlertFilter;
import bisq.core.payment.PaymentAccount;
import bisq.core.proto.CoreProtoResolver;
@@ -68,6 +70,10 @@ public class UserPayload implements PersistableEnvelope {
private List acceptedArbitrators = new ArrayList<>();
@Nullable
private List acceptedMediators = new ArrayList<>();
+ @Nullable
+ private PriceAlertFilter priceAlertFilter;
+ @Nullable
+ private List marketAlertFilters = new ArrayList<>();
public UserPayload() {
}
@@ -98,6 +104,9 @@ public PB.PersistableEnvelope toProtoMessage() {
Optional.ofNullable(acceptedMediators)
.ifPresent(e -> builder.addAllAcceptedMediators(ProtoUtil.collectionToProto(acceptedMediators,
message -> ((PB.StoragePayload) message).getMediator())));
+ Optional.ofNullable(priceAlertFilter).ifPresent(priceAlertFilter -> builder.setPriceAlertFilter(priceAlertFilter.toProtoMessage()));
+ Optional.ofNullable(marketAlertFilters)
+ .ifPresent(e -> builder.addAllMarketAlertFilters(ProtoUtil.collectionToProto(marketAlertFilters)));
return PB.PersistableEnvelope.newBuilder().setUserPayload(builder).build();
}
@@ -119,7 +128,10 @@ public static UserPayload fromProto(PB.UserPayload proto, CoreProtoResolver core
.collect(Collectors.toList())),
proto.getAcceptedMediatorsList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getAcceptedMediatorsList().stream()
.map(Mediator::fromProto)
- .collect(Collectors.toList()))
- );
+ .collect(Collectors.toList())),
+ PriceAlertFilter.fromProto(proto.getPriceAlertFilter()),
+ proto.getMarketAlertFiltersList().isEmpty() ? new ArrayList<>() : new ArrayList<>(proto.getMarketAlertFiltersList().stream()
+ .map(e -> MarketAlertFilter.fromProto(e, coreProtoResolver))
+ .collect(Collectors.toSet())));
}
}
diff --git a/src/main/java/bisq/core/util/BSFormatter.java b/src/main/java/bisq/core/util/BSFormatter.java
index 1d70bd5c..0699df48 100644
--- a/src/main/java/bisq/core/util/BSFormatter.java
+++ b/src/main/java/bisq/core/util/BSFormatter.java
@@ -510,8 +510,8 @@ public String formatToPercent(double value) {
return decimalFormat.format(MathUtils.roundDouble(value * 100.0, 2)).replace(",", ".");
}
- public double parseNumberStringToDouble(String percentString) throws NumberFormatException {
- return Double.parseDouble(cleanDoubleInput(percentString));
+ public double parseNumberStringToDouble(String input) throws NumberFormatException {
+ return Double.parseDouble(cleanDoubleInput(input));
}
public double parsePercentStringToDouble(String percentString) throws NumberFormatException {
@@ -521,6 +521,24 @@ public double parsePercentStringToDouble(String percentString) throws NumberForm
return value / 100d;
}
+ public long parsePriceStringToLong(String currencyCode, String amount, int precision) {
+ if (amount == null || amount.isEmpty())
+ return 0;
+
+ long value = 0;
+ try {
+ double amountValue = Double.parseDouble(amount);
+ amount = formatRoundedDoubleWithPrecision(amountValue, precision);
+ value = Price.parse(currencyCode, amount).getValue();
+ } catch (NumberFormatException ignore) {
+ // expected NumberFormatException if input is not a number
+ } catch (Throwable t) {
+ log.error("parsePriceStringToLong: " + t.toString());
+ }
+
+ return value;
+ }
+
protected String cleanDoubleInput(String input) {
input = input.replace(",", ".");
input = StringUtils.deleteWhitespace(input);
diff --git a/src/main/resources/i18n/displayStrings.properties b/src/main/resources/i18n/displayStrings.properties
index cd3d17c7..dd540eaf 100644
--- a/src/main/resources/i18n/displayStrings.properties
+++ b/src/main/resources/i18n/displayStrings.properties
@@ -949,6 +949,7 @@ account.menu.arbitratorSelection=Arbitrator selection
account.menu.password=Wallet password
account.menu.seedWords=Wallet seed
account.menu.backup=Backup
+account.menu.notifications=Notifications
account.arbitratorRegistration.pubKey=Public key:
@@ -1065,6 +1066,79 @@ account.seed.restore.info=Please note that you cannot import a wallet from an ol
It is not a way for applying a backup! Please use a backup from the application data directory for restoring a previous application state.
account.seed.restore.ok=Ok, I understand and want to restore
+
+####################################################################
+# Mobile notifications
+####################################################################
+
+account.notifications.setup.title=Setup
+account.notifications.download.label=Download mobile app
+account.notifications.download.button=Download
+account.notifications.waitingForWebCam=Waiting for webcam...
+account.notifications.webCamWindow.headline=Scan QR-code from phone
+account.notifications.webcam.label=Use webcam
+account.notifications.webcam.button=Scan QR code
+account.notifications.noWebcam.button=I don't have a webcam
+account.notifications.testMsg.label=Send test notification:
+account.notifications.testMsg.title=Test
+account.notifications.erase.label=Clear notifications on phone:
+account.notifications.erase.title=Clear notifications
+account.notifications.email.label=Pairing token:
+account.notifications.email.prompt=Enter pairing token you received by email
+account.notifications.settings.title=Settings
+account.notifications.useSound.label=Play notification sound on phone:
+account.notifications.trade.label=Receive trade messages:
+account.notifications.market.label=Receive offer alerts:
+account.notifications.price.label=Receive price alerts:
+account.notifications.priceAlert.title=Price alerts
+account.notifications.priceAlert.high.label=Notify if BTC price is above
+account.notifications.priceAlert.low.label=Notify if BTC price is below
+account.notifications.priceAlert.setButton=Set price alert
+account.notifications.priceAlert.removeButton=Remove price alert
+account.notifications.trade.message.title=Trade state changed
+account.notifications.trade.message.msg.conf=The trade with ID {0} is confirmed.
+account.notifications.trade.message.msg.started=The BTC buyer has started the payment for the trade with ID {0}.
+account.notifications.trade.message.msg.completed=The trade with ID {0} is completed.
+account.notifications.offer.message.title=Your offer was taken
+account.notifications.offer.message.msg=Your offer with ID {0} was taken
+account.notifications.dispute.message.title=New dispute message
+account.notifications.dispute.message.msg=You received a dispute message for trade with ID {0}
+
+account.notifications.marketAlert.title=Offer alerts
+account.notifications.marketAlert.selectPaymentAccount=Offers matching payment account
+account.notifications.marketAlert.offerType.label=Offer type I am interested in
+account.notifications.marketAlert.offerType.buy=Buy offers (I want to sell BTC)
+account.notifications.marketAlert.offerType.sell=Sell offers (I want to buy BTC)
+account.notifications.marketAlert.trigger=Offer price distance (%)
+account.notifications.marketAlert.trigger.info=With a price distance set, you will only receive an alert when an offer \
+ that meets (or exceeds) your requirements is published. Example: you want to sell BTC, but you will only sell at \
+ a 2% premium to the current market price. Setting this field to 2% will ensure you only receive alerts for offers \
+ with prices that are 2% (or more) above the current market price.
+account.notifications.marketAlert.trigger.prompt=Percentage distance from market price (e.g. 2.50%, -0.50%, etc)
+account.notifications.marketAlert.addButton=Add offer alert
+account.notifications.marketAlert.manageAlertsButton=Manage offer alerts
+account.notifications.marketAlert.manageAlerts.title=Manage offer alerts
+account.notifications.marketAlert.manageAlerts.label=Offer alerts
+account.notifications.marketAlert.manageAlerts.item=Offer alert for {0} offer with trigger price {1} and payment account {2}
+account.notifications.marketAlert.manageAlerts.header.paymentAccount=Payment account
+account.notifications.marketAlert.manageAlerts.header.trigger=Trigger price
+account.notifications.marketAlert.manageAlerts.header.offerType=Offer type
+account.notifications.marketAlert.message.title=Offer alert
+account.notifications.marketAlert.message.msg.below=below
+account.notifications.marketAlert.message.msg.above=above
+account.notifications.marketAlert.message.msg=A new ''{0} {1}'' offer with price {2} ({3} {4} market price) and \
+ payment method ''{5}'' was published to the Bisq offerbook.\n\
+ Offer ID: {6}.
+account.notifications.priceAlert.message.title=Price alert for {0}
+account.notifications.priceAlert.message.msg=Your price alert got triggered. The current {0} price is {1} {2}
+account.notifications.noWebCamFound.warning=No webcam found.\n\n\
+ Please use the email option to send the token and encryption key from your mobile phone to the Bisq application.
+account.notifications.priceAlert.warning.highPriceTooLow=The higher price must be larger than the lower price.
+account.notifications.priceAlert.warning.lowerPriceTooHigh=The lower price must be lower than the higher price.
+
+
+
+
####################################################################
# DAO
####################################################################
@@ -2030,3 +2104,4 @@ validation.iban.checkSumInvalid=IBAN checksum is invalid
validation.iban.invalidLength=Number must have length 15 to 34 chars.
validation.interacETransfer.invalidAreaCode=Non-Canadian area code
validation.interacETransfer.invalidPhone=Invalid phone number format and not an email address
+validation.inputTooLarge=Input must not be larger than {0}
diff --git a/src/test/java/bisq/core/notifications/MobileModelTest.java b/src/test/java/bisq/core/notifications/MobileModelTest.java
new file mode 100644
index 00000000..c41c5eee
--- /dev/null
+++ b/src/test/java/bisq/core/notifications/MobileModelTest.java
@@ -0,0 +1,84 @@
+/*
+ * This file is part of Bisq.
+ *
+ * Bisq is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU Affero General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or (at
+ * your option) any later version.
+ *
+ * Bisq is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
+ * License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with Bisq. If not, see .
+ */
+
+package bisq.core.notifications;
+
+import bisq.common.util.Tuple2;
+
+import java.util.Arrays;
+import java.util.List;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+
+@Slf4j
+public class MobileModelTest {
+
+ @Test
+ public void testParseDescriptor() {
+ MobileModel mobileModel = new MobileModel();
+ List> list = Arrays.asList(
+ new Tuple2<>("iPod Touch 5", false),
+ new Tuple2<>("iPod Touch 6", false),
+ new Tuple2<>("iPhone 4", false),
+ new Tuple2<>("iPhone 4s", false),
+ new Tuple2<>("iPhone 5", false),
+ new Tuple2<>("iPhone 5c", false),
+ new Tuple2<>("iPhone 5s", false),
+ new Tuple2<>("iPhone 6", false),
+
+ // unclear
+ new Tuple2<>("iPhone 6 Plus", true),
+ new Tuple2<>("iPhone 6s", true),
+ new Tuple2<>("iPhone 6s Plus", true),
+
+ new Tuple2<>("iPhone 7", true),
+ new Tuple2<>("iPhone 7 Plus", true),
+ new Tuple2<>("iPhone SE", false), // unclear
+ new Tuple2<>("iPhone 8", true),
+ new Tuple2<>("iPhone 8 Plus", true),
+ new Tuple2<>("iPhone X", true),
+
+ new Tuple2<>("iPad 2", false),
+ new Tuple2<>("iPad 3", false),
+ new Tuple2<>("iPad 4", false),
+ new Tuple2<>("iPad Air", false),
+ new Tuple2<>("iPad Air 2", false),
+ new Tuple2<>("iPad 5", false),
+ new Tuple2<>("iPad 6", false),
+ new Tuple2<>("iPad Mini", false),
+ new Tuple2<>("iPad Mini 2", false),
+ new Tuple2<>("iPad Mini 3", false),
+ new Tuple2<>("iPad Mini 4", false),
+
+ new Tuple2<>("iPad Pro 9.7 Inch", true),
+ new Tuple2<>("iPad Pro 12.9 Inch", true),
+ new Tuple2<>("iPad Pro 12.9 Inch 2. Generation", true),
+ new Tuple2<>("iPad Pro 10.5 Inch", true)
+ );
+
+ list.forEach(tuple -> {
+ log.error(tuple.toString());
+
+ assertEquals("tuple: " + tuple, mobileModel.parseDescriptor(tuple.first), tuple.second);
+ });
+
+ }
+}