diff --git a/.env.nightly b/.env.nightly index 585705eb29..0587ef9b3a 100644 --- a/.env.nightly +++ b/.env.nightly @@ -19,3 +19,4 @@ INSTABUG_SURVEYS=1 GROUP_CHATS_ENABLED=0 FORCE_SENT_RECEIVED_TRACKING=1 ADD_CUSTOM_MAILSERVERS_ENABLED=0 +BOOTNODES_SETTINGS_ENABLED=0 diff --git a/.env.prod b/.env.prod index db23138e7d..39ae2f4735 100644 --- a/.env.prod +++ b/.env.prod @@ -20,3 +20,4 @@ GROUP_CHATS_ENABLED=0 FORCE_SENT_RECEIVED_TRACKING=0 USE_SYM_KEY=0 ADD_CUSTOM_MAILSERVERS_ENABLED=0 +BOOTNODES_SETTINGS_ENABLED=0 diff --git a/modules/react-native-status/android/build.gradle b/modules/react-native-status/android/build.gradle index cc04a52211..65d5b30366 100644 --- a/modules/react-native-status/android/build.gradle +++ b/modules/react-native-status/android/build.gradle @@ -17,7 +17,7 @@ dependencies { implementation 'com.github.ericwlange:AndroidJSCore:3.0.1' implementation 'status-im:function:0.0.1' - String statusGoVersion = 'develop-g5aae87ab' + String statusGoVersion = 'develop-gbc14e6fa' final String statusGoGroup = 'status-im', statusGoName = 'status-go' // Check if the local status-go jar exists, and compile against that if it does diff --git a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java index 2de0d53190..93df40fc9d 100644 --- a/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java +++ b/modules/react-native-status/android/src/main/java/im/status/ethereum/module/StatusModule.java @@ -183,6 +183,8 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } } + + int testnetNetworkId = 3; String testnetDataDir = root + "/ethereum/testnet"; String oldKeystoreDir = testnetDataDir + "/keystore"; String newKeystoreDir = root + "/keystore"; @@ -204,14 +206,19 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } } - String config = Statusgo.GenerateConfig(testnetDataDir, 3); + String config; try { JSONObject customConfig = new JSONObject(defaultConfig); + + String dataDir = root + customConfig.get("DataDir"); + + config = Statusgo.GenerateConfig(dataDir, customConfig.getInt("NetworkId")); + JSONObject jsonConfig = new JSONObject(config); String gethLogFilePath = prepareLogsFile(); boolean logsEnabled = (gethLogFilePath != null) && !TextUtils.isEmpty(this.logLevel); - String dataDir = root + customConfig.get("DataDir"); + jsonConfig.put("LogEnabled", (gethLogFilePath != null && logsEnabled)); jsonConfig.put("LogFile", gethLogFilePath); jsonConfig.put("LogLevel", TextUtils.isEmpty(this.logLevel) ? "ERROR" : this.logLevel.toUpperCase()); @@ -236,10 +243,22 @@ class StatusModule extends ReactContextBaseJavaModule implements LifecycleEventL } catch (Exception e) { } + try { + Object clusterConfig = customConfig.get("ClusterConfig"); + if (clusterConfig != null) { + Log.d(TAG, "ClusterConfig is not null"); + jsonConfig.put("ClusterConfig", clusterConfig); + } + } catch (Exception e) { + Log.w(TAG, "Something went wrong parsing cluster config" + e.getMessage()); + } + + jsonConfig.put("KeyStoreDir", newKeystoreDir); config = jsonConfig.toString(); } catch (JSONException e) { + config = Statusgo.GenerateConfig(testnetDataDir, testnetNetworkId); Log.d(TAG, "Something went wrong " + e.getMessage()); Log.d(TAG, "Default configuration will be used"); } diff --git a/modules/react-native-status/ios/RCTStatus/RCTStatus.m b/modules/react-native-status/ios/RCTStatus/RCTStatus.m index 324c3cbdf1..2e5129770c 100644 --- a/modules/react-native-status/ios/RCTStatus/RCTStatus.m +++ b/modules/react-native-status/ios/RCTStatus/RCTStatus.m @@ -15,7 +15,7 @@ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:(NSJSONWritingOptions) (prettyPrint ? NSJSONWritingPrettyPrinted : 0) error:&error]; - + if (! jsonData) { NSLog(@"bv_jsonStringWithPrettyPrint: error: %@", error.localizedDescription); return @"{}"; @@ -35,7 +35,7 @@ NSData *jsonData = [NSJSONSerialization dataWithJSONObject:self options:(NSJSONWritingOptions) (prettyPrint ? NSJSONWritingPrettyPrinted : 0) error:&error]; - + if (! jsonData) { NSLog(@"bv_jsonStringWithPrettyPrint: error: %@", error.localizedDescription); return @"[]"; @@ -90,7 +90,7 @@ RCT_EXPORT_METHOD(parseJail:(NSString *)chatId } NSDictionary *result = [_jail parseJail:chatId withCode:js]; stringResult = [result bv_jsonStringWithPrettyPrint:NO]; - + callback(@[stringResult]); } @@ -103,7 +103,7 @@ RCT_EXPORT_METHOD(callJail:(NSString *)chatId NSLog(@"CallJail() method called"); #endif dispatch_async( dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{ - + NSString *stringResult; if(_jail == nil) { _jail = [Jail new]; @@ -130,12 +130,12 @@ RCT_EXPORT_METHOD(startNode:(NSString *)configString) { URLsForDirectory:NSDocumentDirectory inDomains:NSUserDomainMask] lastObject]; NSURL *testnetFolderName = [rootUrl URLByAppendingPathComponent:@"ethereum/testnet"]; - + if (![fileManager fileExistsAtPath:testnetFolderName.path]) [fileManager createDirectoryAtPath:testnetFolderName.path withIntermediateDirectories:YES attributes:nil error:&error]; - + NSURL *flagFolderUrl = [rootUrl URLByAppendingPathComponent:@"ropsten_flag"]; - + if(![fileManager fileExistsAtPath:flagFolderUrl.path]){ NSLog(@"remove lightchaindata"); NSURL *lightChainData = [testnetFolderName URLByAppendingPathComponent:@"StatusIM/lightchaindata"]; @@ -148,9 +148,9 @@ RCT_EXPORT_METHOD(startNode:(NSString *)configString) { attributes:nil error:&error]; } - + NSLog(@"after remove lightchaindata"); - + NSURL *oldKeystoreUrl = [testnetFolderName URLByAppendingPathComponent:@"keystore"]; NSURL *newKeystoreUrl = [rootUrl URLByAppendingPathComponent:@"keystore"]; if([fileManager fileExistsAtPath:oldKeystoreUrl.path]){ @@ -158,15 +158,16 @@ RCT_EXPORT_METHOD(startNode:(NSString *)configString) { [fileManager copyItemAtPath:oldKeystoreUrl.path toPath:newKeystoreUrl.path error:nil]; [fileManager removeItemAtPath:oldKeystoreUrl.path error:nil]; } - + NSLog(@"after lightChainData"); - + NSLog(@"preconfig: %@", configString); NSData *configData = [configString dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary *configJSON = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingMutableContainers error:nil]; int networkId = [configJSON[@"NetworkId"] integerValue]; NSString *dataDir = [configJSON objectForKey:@"DataDir"]; NSString *upstreamURL = [configJSON valueForKeyPath:@"UpstreamConfig.URL"]; + NSArray *bootnodes = [configJSON valueForKeyPath:@"ClusterConfig.BootNodes"]; NSString *networkDir = [rootUrl.path stringByAppendingString:dataDir]; NSString *devCluster = [ReactNativeConfig envFor:@"ETHEREUM_DEV_CLUSTER"]; NSString *logLevel = [[ReactNativeConfig envFor:@"LOG_LEVEL_STATUS_GO"] uppercaseString]; @@ -180,19 +181,27 @@ RCT_EXPORT_METHOD(startNode:(NSString *)configString) { [resultingConfigJson setValue:[NSNumber numberWithBool:[logLevel length] != 0] forKey:@"LogEnabled"]; [resultingConfigJson setValue:logUrl.path forKey:@"LogFile"]; [resultingConfigJson setValue:([logLevel length] == 0 ? [NSString stringWithUTF8String: "ERROR"] : logLevel) forKey:@"LogLevel"]; - + [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"WhisperConfig.LightClient"]; + if(upstreamURL != nil) { [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"UpstreamConfig.Enabled"]; [resultingConfigJson setValue:upstreamURL forKeyPath:@"UpstreamConfig.URL"]; } + + if(bootnodes != nil) { + [resultingConfigJson setValue:[NSNumber numberWithBool:YES] forKeyPath:@"ClusterConfig.Enabled"]; + [resultingConfigJson setValue:bootnodes forKeyPath:@"ClusterConfig.BootNodes"]; + } + + NSString *resultingConfig = [resultingConfigJson bv_jsonStringWithPrettyPrint:NO]; NSLog(@"node config %@", resultingConfig); - + if(![fileManager fileExistsAtPath:networkDirUrl.path]) { [fileManager createDirectoryAtPath:networkDirUrl.path withIntermediateDirectories:YES attributes:nil error:nil]; } - + NSLog(@"logUrlPath %@", logUrl.path); if(![fileManager fileExistsAtPath:logUrl.path]) { NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; @@ -353,7 +362,7 @@ RCT_EXPORT_METHOD(clearCookies) { RCT_EXPORT_METHOD(clearStorageAPIs) { [[NSURLCache sharedURLCache] removeAllCachedResponses]; - + NSString *path = [NSSearchPathForDirectoriesInDomains(NSCachesDirectory, NSUserDomainMask, YES) lastObject]; NSArray *array = [[NSFileManager defaultManager] contentsOfDirectoryAtPath:path error:nil]; for (NSString *string in array) { @@ -408,7 +417,7 @@ RCT_EXPORT_METHOD(getDeviceUUID:(RCTResponseSenderBlock)callback) { NSLog(@"getDeviceUUID() method called"); #endif NSString* Identifier = [[[UIDevice currentDevice] identifierForVendor] UUIDString]; - + callback(@[Identifier]); } @@ -420,7 +429,7 @@ RCT_EXPORT_METHOD(getDeviceUUID:(RCTResponseSenderBlock)callback) { #endif return; } - + NSString *sig = [NSString stringWithUTF8String:signal]; #if DEBUG NSLog(@"SignalEvent"); @@ -428,7 +437,7 @@ RCT_EXPORT_METHOD(getDeviceUUID:(RCTResponseSenderBlock)callback) { #endif [bridge.eventDispatcher sendAppEventWithName:@"gethEvent" body:@{@"jsonEvent": sig}]; - + return; } @@ -447,7 +456,7 @@ RCT_EXPORT_METHOD(getDeviceUUID:(RCTResponseSenderBlock)callback) { #endif [bridge.eventDispatcher sendAppEventWithName:@"gethEvent" body:@{@"jsonEvent": signal}]; - + return; } diff --git a/modules/react-native-status/ios/RCTStatus/pom.xml b/modules/react-native-status/ios/RCTStatus/pom.xml index 7e1a1896f2..b2c73093da 100644 --- a/modules/react-native-status/ios/RCTStatus/pom.xml +++ b/modules/react-native-status/ios/RCTStatus/pom.xml @@ -25,7 +25,7 @@ status-im status-go-ios-simulator - develop-g5aae87ab + develop-gbc14e6fa zip true ./ diff --git a/src/status_im/data_store/accounts.cljs b/src/status_im/data_store/accounts.cljs index 1d85fbf0e7..4b17f38d8c 100644 --- a/src/status_im/data_store/accounts.cljs +++ b/src/status_im/data_store/accounts.cljs @@ -10,9 +10,22 @@ (core/single-clj :account) (update :settings core/deserialize))) +(defn- deserialize-bootnodes [bootnodes] + (reduce-kv + (fn [acc id {:keys [chain] :as bootnode}] + (assoc-in acc [chain id] bootnode)) + {} + bootnodes)) + +(defn- serialize-bootnodes [bootnodes] + (->> bootnodes + vals + (mapcat vals))) + (defn- deserialize-account [account] (-> account (update :settings core/deserialize) + (update :bootnodes deserialize-bootnodes) (update :networks (partial reduce-kv (fn [acc network-id props] (assoc acc network-id @@ -31,6 +44,7 @@ (defn- serialize-account [account] (-> account (update :settings core/serialize) + (update :bootnodes serialize-bootnodes) (update :networks (partial map (fn [[_ props]] (update props :config types/clj->json)))))) diff --git a/src/status_im/data_store/realm/schemas/base/core.cljs b/src/status_im/data_store/realm/schemas/base/core.cljs index 6c08f5466a..0a5d7d34c1 100644 --- a/src/status_im/data_store/realm/schemas/base/core.cljs +++ b/src/status_im/data_store/realm/schemas/base/core.cljs @@ -1,7 +1,8 @@ (ns status-im.data-store.realm.schemas.base.core (:require [status-im.data-store.realm.schemas.base.v1.core :as v1] [status-im.data-store.realm.schemas.base.v2.core :as v2] - [status-im.data-store.realm.schemas.base.v3.core :as v3])) + [status-im.data-store.realm.schemas.base.v3.core :as v3] + [status-im.data-store.realm.schemas.base.v4.core :as v4])) ;; put schemas ordered by version (def schemas [{:schema v1/schema @@ -12,4 +13,7 @@ :migration v2/migration} {:schema v3/schema :schemaVersion 3 - :migration v3/migration}]) + :migration v3/migration} + {:schema v4/schema + :schemaVersion 4 + :migration v4/migration}]) diff --git a/src/status_im/data_store/realm/schemas/base/v4/account.cljs b/src/status_im/data_store/realm/schemas/base/v4/account.cljs new file mode 100644 index 0000000000..a62ff56132 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/base/v4/account.cljs @@ -0,0 +1,29 @@ +(ns status-im.data-store.realm.schemas.base.v4.account) + +(def schema {:name :account + :primaryKey :address + :properties {:address :string + :public-key :string + :name {:type :string :optional true} + :email {:type :string :optional true} + :status {:type :string :optional true} + :debug? {:type :bool :default false} + :photo-path :string + :signing-phrase {:type :string} + :mnemonic {:type :string :optional true} + :last-updated {:type :int :default 0} + :last-sign-in {:type :int :default 0} + :signed-up? {:type :bool + :default false} + :network :string + :networks {:type :list + :objectType :network} + :bootnodes {:type :list + :objectType :bootnode} + :last-request {:type :int :optional true} + :settings {:type :string} + :sharing-usage-data? {:type :bool :default false} + :dev-mode? {:type :bool :default false} + :seed-backed-up? {:type :bool :default false} + :wallet-set-up-passed? {:type :bool + :default false}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs b/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs new file mode 100644 index 0000000000..7975d74735 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/base/v4/bootnode.cljs @@ -0,0 +1,8 @@ +(ns status-im.data-store.realm.schemas.base.v4.bootnode) + +(def schema {:name :bootnode + :primaryKey :id + :properties {:id :string + :name {:type :string} + :chain {:type :string} + :address {:type :string}}}) diff --git a/src/status_im/data_store/realm/schemas/base/v4/core.cljs b/src/status_im/data_store/realm/schemas/base/v4/core.cljs new file mode 100644 index 0000000000..2202000d67 --- /dev/null +++ b/src/status_im/data_store/realm/schemas/base/v4/core.cljs @@ -0,0 +1,12 @@ +(ns status-im.data-store.realm.schemas.base.v4.core + (:require [status-im.data-store.realm.schemas.base.v1.network :as network] + [status-im.data-store.realm.schemas.base.v4.account :as account] + [status-im.data-store.realm.schemas.base.v4.bootnode :as bootnode] + [taoensso.timbre :as log])) + +(def schema [network/schema + bootnode/schema + account/schema]) + +(defn migration [old-realm new-realm] + (log/debug "migrating base database v4: " old-realm new-realm)) diff --git a/src/status_im/translations/en.cljs b/src/status_im/translations/en.cljs index cfa89a7f48..c367ab7295 100644 --- a/src/status_im/translations/en.cljs +++ b/src/status_im/translations/en.cljs @@ -622,6 +622,14 @@ :close-app-button "Confirm" :connect-wnode-content "Connect to {{name}}?" + ;; Bootnodes + :bootnodes "Bootnodes" + :bootnodes-settings "Bootnodes settings" + :bootnodes-enabled "Bootnodes enabled" + :bootnode-address "Bootnode address" + :add-bootnode "Add bootnode" + :specify-bootnode-address "Specify bootnode address" + :mainnet-warning-title "Warning!" :mainnet-warning-text "While we highly appreciate your contribution as a tester of Status, we’d like to point out the dangers. You’re switching to Mainnet mode which is still in Alpha. This means it is still in development and has not been audited yet. Some of the risks you may be exposed to include:\n\n- Accounts may be unrecoverable due to breaking changes\n- Loss of ETH and tokens\n- Failure to send or receive messages\n\nSwitching to Mainnet should be done for testing purposes only. By tapping \"I understand\", you confirm that you assume the full responsibility for all risks concerning your data and funds. " :mainnet-warning-ok-text "I understand" diff --git a/src/status_im/ui/screens/accounts/db.cljs b/src/status_im/ui/screens/accounts/db.cljs index c7ef1c350f..68581841ff 100644 --- a/src/status_im/ui/screens/accounts/db.cljs +++ b/src/status_im/ui/screens/accounts/db.cljs @@ -3,6 +3,7 @@ (:require [cljs.spec.alpha :as spec] status-im.utils.db status-im.ui.screens.network-settings.db + status-im.ui.screens.bootnodes-settings.db [status-im.constants :as const])) (defn valid-length? [password] @@ -24,6 +25,7 @@ (spec/def :account/status (spec/nilable string?)) (spec/def :account/network (spec/nilable string?)) (spec/def :account/networks (spec/nilable :networks/networks)) +(spec/def :account/bootnodes (spec/nilable :bootnodes/bootnodes)) (spec/def :account/wnode (spec/nilable string?)) (spec/def :account/settings (spec/nilable (spec/map-of keyword? any?))) (spec/def :account/signing-phrase :global/not-empty-string) @@ -41,7 +43,8 @@ :account/networks :account/settings :account/wnode :account/last-sign-in :account/sharing-usage-data? :account/dev-mode? :account/seed-backed-up? :account/mnemonic - :account/wallet-set-up-passed? :account/last-request])) + :account/wallet-set-up-passed? :account/last-request + :account/bootnodes])) (spec/def :accounts/accounts (spec/nilable (spec/map-of :account/address :accounts/account))) diff --git a/src/status_im/ui/screens/accounts/login/events.cljs b/src/status_im/ui/screens/accounts/login/events.cljs index 377b1cb691..6382d86226 100644 --- a/src/status_im/ui/screens/accounts/login/events.cljs +++ b/src/status_im/ui/screens/accounts/login/events.cljs @@ -63,12 +63,32 @@ (assoc db :node/after-start nil) address password))) +(defn add-custom-bootnodes [config network all-bootnodes] + (let [bootnodes (as-> all-bootnodes $ + (get $ network) + (vals $) + (map :address $))] + (if (seq bootnodes) + (assoc config :ClusterConfig {:Enabled true + :BootNodes bootnodes}) + config))) + (defn get-network-by-address [db address] (let [accounts (get db :accounts/accounts) - {:keys [network networks]} (get accounts address) - config (get-in networks [network :config])] - {:network network - :config config})) + {:keys [network + settings + bootnodes + networks]} (get accounts address) + use-custom-bootnodes (get-in settings [:bootnodes network]) + config (cond-> (get-in networks [network :config]) + + (and + config/bootnodes-settings-enabled? + use-custom-bootnodes) + (add-custom-bootnodes network bootnodes))] + {:use-custom-bootnodes use-custom-bootnodes + :network network + :config config})) (defn wrap-with-initialize-geth-fx [db address password] (let [{:keys [network config]} (get-network-by-address db address)] @@ -87,21 +107,31 @@ {:db (assoc db :node/after-stop [::start-node address password]) ::stop-node nil}) +(defn- restart-node? [account-network network use-custom-bootnodes] + (or (not= account-network network) + (and config/bootnodes-settings-enabled? + use-custom-bootnodes))) + +(defn login-account [{{:keys [network status-node-started?] :as db} :db} [_ address password]] + (let [{use-custom-bootnodes :use-custom-bootnodes + account-network :network} (get-network-by-address db address) + db' (-> db + (assoc-in [:accounts/login :processing] true)) + wrap-fn (cond (not status-node-started?) + wrap-with-initialize-geth-fx + + (not (restart-node? account-network + network + use-custom-bootnodes)) + wrap-with-login-account-fx + + :else + wrap-with-stop-node-fx)] + (wrap-fn db' address password))) + (register-handler-fx :login-account - (fn [{{:keys [network status-node-started?] :as db} :db} [_ address password]] - (let [{account-network :network} (get-network-by-address db address) - db' (-> db - (assoc-in [:accounts/login :processing] true)) - wrap-fn (cond (not status-node-started?) - wrap-with-initialize-geth-fx - - (= account-network network) - wrap-with-login-account-fx - - :else - wrap-with-stop-node-fx)] - (wrap-fn db' address password)))) + login-account) (register-handler-fx :login-handler diff --git a/src/status_im/ui/screens/bootnodes_settings/db.cljs b/src/status_im/ui/screens/bootnodes_settings/db.cljs new file mode 100644 index 0000000000..42e723359d --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/db.cljs @@ -0,0 +1,18 @@ +(ns status-im.ui.screens.bootnodes-settings.db + (:require-macros [status-im.utils.db :refer [allowed-keys]]) + (:require + [clojure.string :as string] + [cljs.spec.alpha :as spec])) + +(spec/def ::not-blank-string (complement string/blank?)) + +(spec/def :bootnode/address ::not-blank-string) +(spec/def :bootnode/name ::not-blank-string) +(spec/def :bootnode/id ::not-blank-string) +(spec/def :bootnode/chain ::not-blank-string) +(spec/def :bootnode/bootnode (allowed-keys :req-un [:bootnode/chain + :bootnode/address + :bootnode/name + :bootnode/id])) + +(spec/def :bootnodes/bootnodes (spec/nilable (spec/map-of :bootnode/id (spec/map-of :bootnode/id :bootnode/bootnode)))) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs new file mode 100644 index 0000000000..d03aec22a1 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/events.cljs @@ -0,0 +1,51 @@ +(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.events + (:require [clojure.string :as string] + [re-frame.core :as re-frame] + [status-im.utils.handlers :refer [register-handler] :as handlers] + [status-im.utils.handlers-macro :as handlers-macro] + [status-im.ui.screens.accounts.utils :as accounts.utils] + [status-im.utils.ethereum.core :as ethereum] + [status-im.utils.types :as types] + [status-im.utils.inbox :as utils.inbox])) + +(defn- new-bootnode [id bootnode-name address chain] + {:address address + :chain chain + :id (string/replace id "-" "") + :name bootnode-name}) + +(defn save-new-bootnode [{{:bootnodes/keys [manage] :account/keys [account] :as db} :db :as cofx} _] + (let [{:keys [name url]} manage + network (:network db) + bootnode (new-bootnode + (:random-id cofx) + (:value name) + (:value url) + network) + new-bootnodes (assoc-in (:bootnodes account) [network (:id bootnode)] bootnode)] + + (handlers-macro/merge-fx cofx + {:db (dissoc db :bootnodes/manage) + :dispatch [:navigate-back]} + (accounts.utils/account-update {:bootnodes new-bootnodes})))) + +(handlers/register-handler-fx + :save-new-bootnode + [(re-frame/inject-cofx :random-id)] + save-new-bootnode) + +(handlers/register-handler-fx + :bootnode-set-input + (fn [{db :db} [_ input-key value]] + {:db (update db :bootnodes/manage assoc input-key {:value value + :error (if (= input-key :name) + (string/blank? value) + (not (utils.inbox/valid-enode-address? value)))})})) + +(handlers/register-handler-fx + :edit-bootnode + (fn [{db :db} _] + {:db (update-in db [:bootnodes/manage] assoc + :name {:error true} + :url {:error true}) + :dispatch [:navigate-to :edit-bootnode]})) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs new file mode 100644 index 0000000000..e5f881dbb0 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/styles.cljs @@ -0,0 +1,15 @@ +(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.styles + (:require-macros [status-im.utils.styles :refer [defstyle]])) + +(def edit-bootnode-view + {:flex 1 + :margin-horizontal 16 + :margin-vertical 15}) + +(def input-container + {:margin-bottom 15}) + +(def bottom-container + {:flex-direction :row + :margin-horizontal 12 + :margin-vertical 15}) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs new file mode 100644 index 0000000000..49e41fdc82 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/subs.cljs @@ -0,0 +1,14 @@ +(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.subs + (:require [re-frame.core :refer [reg-sub]])) + +(reg-sub + :get-manage-bootnode + :<- [:get :bootnodes/manage] + (fn [manage] + manage)) + +(reg-sub + :manage-bootnode-valid? + :<- [:get-manage-bootnode] + (fn [manage] + (not-any? :error (vals manage)))) diff --git a/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs new file mode 100644 index 0000000000..02440ef671 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/edit_bootnode/views.cljs @@ -0,0 +1,42 @@ +(ns status-im.ui.screens.bootnodes-settings.edit-bootnode.views + (:require-macros [status-im.utils.views :as views]) + (:require + [re-frame.core :as re-frame] + [status-im.ui.components.react :as react] + [status-im.i18n :as i18n] + [status-im.ui.components.styles :as components.styles] + [status-im.ui.components.common.common :as components.common] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.text-input.view :as text-input] + [status-im.ui.screens.bootnodes-settings.edit-bootnode.styles :as styles])) + +(views/defview edit-bootnode [] + (views/letsubs [manage-bootnode [:get-manage-bootnode] + is-valid? [:manage-bootnode-valid?]] + [react/view components.styles/flex + [status-bar/status-bar] + [react/keyboard-avoiding-view components.styles/flex + [toolbar/simple-toolbar (i18n/label :t/add-bootnode)] + [react/scroll-view + [react/view styles/edit-bootnode-view + [text-input/text-input-with-label + {:label (i18n/label :t/name) + :placeholder (i18n/label :t/specify-name) + :container styles/input-container + :default-value (get-in manage-bootnode [:name :value]) + :on-change-text #(re-frame/dispatch [:bootnode-set-input :name %]) + :auto-focus true}] + [text-input/text-input-with-label + {:label (i18n/label :t/bootnode-address) + :placeholder (i18n/label :t/specify-bootnode-address) + :container styles/input-container + :default-value (get-in manage-bootnode [:url :value]) + :on-change-text #(re-frame/dispatch [:bootnode-set-input :url %])}]]] + [react/view styles/bottom-container + [react/view components.styles/flex] + [components.common/bottom-button + {:forward? true + :label (i18n/label :t/save) + :disabled? (not is-valid?) + :on-press #(re-frame/dispatch [:save-new-bootnode])}]]]])) diff --git a/src/status_im/ui/screens/bootnodes_settings/events.cljs b/src/status_im/ui/screens/bootnodes_settings/events.cljs new file mode 100644 index 0000000000..213a5279b4 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/events.cljs @@ -0,0 +1,22 @@ +(ns status-im.ui.screens.bootnodes-settings.events + (:require [re-frame.core :as re-frame] + [status-im.utils.handlers :as handlers] + [status-im.utils.handlers-macro :as handlers-macro] + [status-im.ui.screens.accounts.events :as accounts-events] + [status-im.i18n :as i18n] + [status-im.transport.core :as transport] + status-im.ui.screens.bootnodes-settings.edit-bootnode.events + [status-im.utils.ethereum.core :as ethereum])) + +(defn toggle-custom-bootnodes [value {:keys [db] :as cofx}] + (let [network (get-in db [:account/account :network]) + settings (get-in db [:account/account :settings])] + (handlers-macro/merge-fx cofx + (accounts-events/update-settings + (assoc-in settings [:bootnodes network] value) + [:logout])))) + +(handlers/register-handler-fx + :toggle-custom-bootnodes + (fn [cofx [_ value]] + (toggle-custom-bootnodes value cofx))) diff --git a/src/status_im/ui/screens/bootnodes_settings/styles.cljs b/src/status_im/ui/screens/bootnodes_settings/styles.cljs new file mode 100644 index 0000000000..fd69219d6f --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/styles.cljs @@ -0,0 +1,30 @@ +(ns status-im.ui.screens.bootnodes-settings.styles + (:require [status-im.ui.components.colors :as colors]) + (:require-macros [status-im.utils.styles :refer [defstyle]])) + +(def wrapper + {:flex 1 + :background-color :white}) + +(def bootnode-item-inner + {:padding-horizontal 16}) + +(defstyle bootnode-item + {:flex-direction :row + :background-color :white + :align-items :center + :padding-horizontal 16 + :ios {:height 64} + :android {:height 56}}) + +(defstyle bootnode-item-name-text + {:color colors/black + :ios {:font-size 17 + :letter-spacing -0.2 + :line-height 20} + :android {:font-size 16}}) + +(defstyle switch-container + {:height 50 + :background-color colors/white + :padding-left 15}) diff --git a/src/status_im/ui/screens/bootnodes_settings/subs.cljs b/src/status_im/ui/screens/bootnodes_settings/subs.cljs new file mode 100644 index 0000000000..5daa5c8500 --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/subs.cljs @@ -0,0 +1,15 @@ +(ns status-im.ui.screens.bootnodes-settings.subs + (:require [re-frame.core :as re-frame] + status-im.ui.screens.bootnodes-settings.edit-bootnode.subs + [status-im.utils.ethereum.core :as ethereum])) + +(re-frame/reg-sub :settings/bootnodes-enabled + :<- [:get :account/account] + (fn [account] + (let [{:keys [network settings]} account] + (get-in settings [:bootnodes network])))) + +(re-frame/reg-sub :settings/network-bootnodes + :<- [:get :account/account] + (fn [account] + (get-in account [:bootnodes (:network account)]))) diff --git a/src/status_im/ui/screens/bootnodes_settings/views.cljs b/src/status_im/ui/screens/bootnodes_settings/views.cljs new file mode 100644 index 0000000000..ed603601fd --- /dev/null +++ b/src/status_im/ui/screens/bootnodes_settings/views.cljs @@ -0,0 +1,46 @@ +(ns status-im.ui.screens.bootnodes-settings.views + (:require-macros [status-im.utils.views :as views]) + (:require [re-frame.core :as re-frame] + [status-im.i18n :as i18n] + [status-im.utils.config :as config] + [status-im.ui.components.colors :as colors] + [status-im.ui.components.icons.vector-icons :as vector-icons] + [status-im.ui.components.list.views :as list] + [status-im.ui.components.react :as react] + [status-im.ui.components.status-bar.view :as status-bar] + [status-im.ui.components.toolbar.view :as toolbar] + [status-im.ui.components.toolbar.actions :as toolbar.actions] + [status-im.ui.screens.profile.components.views :as profile.components] + [status-im.ui.screens.bootnodes-settings.styles :as styles])) + +(defn navigate-to-add-bootnode [] + (re-frame/dispatch [:edit-bootnode])) + +(defn render-row [{:keys [name id]}] + [react/view + {:accessibility-label :bootnode-item} + [react/view styles/bootnode-item + [react/view styles/bootnode-item-inner + [react/text {:style styles/bootnode-item-name-text} + name]]]]) + +(views/defview bootnodes-settings [] + (views/letsubs [bootnodes-enabled [:settings/bootnodes-enabled] + bootnodes [:settings/network-bootnodes]] + [react/view {:flex 1} + [status-bar/status-bar] + [toolbar/toolbar {} + toolbar/default-nav-back + [toolbar/content-title (i18n/label :t/bootnodes-settings)] + [toolbar/actions + [(toolbar.actions/add false navigate-to-add-bootnode)]]] + [react/view styles/switch-container + [profile.components/settings-switch-item + {:label-kw :t/bootnodes-enabled + :value bootnodes-enabled + :action-fn #(re-frame/dispatch [:toggle-custom-bootnodes %])}]] + [react/view styles/wrapper + [list/flat-list {:data (vals bootnodes) + :default-separator? false + :key-fn :id + :render-fn render-row}]]])) diff --git a/src/status_im/ui/screens/db.cljs b/src/status_im/ui/screens/db.cljs index 77191fe1ca..772cfd46ad 100644 --- a/src/status_im/ui/screens/db.cljs +++ b/src/status_im/ui/screens/db.cljs @@ -170,6 +170,7 @@ :networks/networks :networks/manage :mailservers/manage + :bootnodes/manage :node/after-start :node/after-stop :inbox/wnodes diff --git a/src/status_im/ui/screens/events.cljs b/src/status_im/ui/screens/events.cljs index fdc970d52e..76057989c5 100644 --- a/src/status_im/ui/screens/events.cljs +++ b/src/status_im/ui/screens/events.cljs @@ -24,6 +24,7 @@ status-im.ui.screens.wallet.choose-recipient.events status-im.ui.screens.browser.events status-im.ui.screens.offline-messaging-settings.events + status-im.ui.screens.bootnodes-settings.events status-im.ui.screens.currency-settings.events status-im.ui.screens.usage-data.events [re-frame.core :as re-frame] diff --git a/src/status_im/ui/screens/network_settings/events.cljs b/src/status_im/ui/screens/network_settings/events.cljs index 0e048c5a38..7af6a38039 100644 --- a/src/status_im/ui/screens/network_settings/events.cljs +++ b/src/status_im/ui/screens/network_settings/events.cljs @@ -2,7 +2,6 @@ (:require [re-frame.core :refer [dispatch dispatch-sync after] :as re-frame] [status-im.utils.handlers :refer [register-handler] :as handlers] status-im.ui.screens.network-settings.edit-network.events - status-im.ui.screens.offline-messaging-settings.edit-mailserver.events [status-im.utils.handlers-macro :as handlers-macro] [status-im.ui.screens.accounts.utils :as accounts.utils] [status-im.i18n :as i18n] diff --git a/src/status_im/ui/screens/offline_messaging_settings/events.cljs b/src/status_im/ui/screens/offline_messaging_settings/events.cljs index 3b40450e18..a7403101e0 100644 --- a/src/status_im/ui/screens/offline_messaging_settings/events.cljs +++ b/src/status_im/ui/screens/offline_messaging_settings/events.cljs @@ -5,6 +5,7 @@ [status-im.ui.screens.accounts.events :as accounts-events] [status-im.i18n :as i18n] [status-im.transport.core :as transport] + status-im.ui.screens.offline-messaging-settings.edit-mailserver.events [status-im.utils.ethereum.core :as ethereum])) (handlers/register-handler-fx diff --git a/src/status_im/ui/screens/profile/user/views.cljs b/src/status_im/ui/screens/profile/user/views.cljs index fcf8f80235..384fb51d82 100644 --- a/src/status_im/ui/screens/profile/user/views.cljs +++ b/src/status_im/ui/screens/profile/user/views.cljs @@ -152,6 +152,11 @@ {:label-kw :t/offline-messaging :action-fn #(re-frame/dispatch [:navigate-to :offline-messaging-settings]) :accessibility-label :offline-messages-settings-button}]) + (when config/bootnodes-settings-enabled? + [profile.components/settings-item + {:label-kw :t/bootnodes + :action-fn #(re-frame/dispatch [:navigate-to :bootnodes-settings]) + :accessibility-label :bootnodes-settings-button}]) [profile.components/settings-item-separator] [profile.components/settings-item {:label-kw :t/help-improve? diff --git a/src/status_im/ui/screens/subs.cljs b/src/status_im/ui/screens/subs.cljs index 6535b26e31..d14eaca773 100644 --- a/src/status_im/ui/screens/subs.cljs +++ b/src/status_im/ui/screens/subs.cljs @@ -13,6 +13,7 @@ status-im.ui.screens.wallet.transactions.subs status-im.ui.screens.network-settings.subs status-im.ui.screens.offline-messaging-settings.subs + status-im.ui.screens.bootnodes-settings.subs status-im.ui.screens.currency-settings.subs status-im.ui.screens.browser.subs status-im.bots.subs diff --git a/src/status_im/ui/screens/views.cljs b/src/status_im/ui/screens/views.cljs index 291dd6ac43..fc9d969a8b 100644 --- a/src/status_im/ui/screens/views.cljs +++ b/src/status_im/ui/screens/views.cljs @@ -39,6 +39,8 @@ [status-im.ui.screens.network-settings.edit-network.views :refer [edit-network]] [status-im.ui.screens.offline-messaging-settings.views :refer [offline-messaging-settings]] [status-im.ui.screens.offline-messaging-settings.edit-mailserver.views :refer [edit-mailserver]] + [status-im.ui.screens.bootnodes-settings.views :refer [bootnodes-settings]] + [status-im.ui.screens.bootnodes-settings.edit-bootnode.views :refer [edit-bootnode]] [status-im.ui.screens.currency-settings.views :refer [currency-settings]] [status-im.ui.screens.browser.views :refer [browser]] [status-im.ui.screens.add-new.open-dapp.views :refer [open-dapp dapp-description]] @@ -85,6 +87,8 @@ :edit-network edit-network :offline-messaging-settings offline-messaging-settings :edit-mailserver edit-mailserver + :bootnodes-settings bootnodes-settings + :edit-bootnode edit-bootnode :currency-settings currency-settings :recent-recipients recent-recipients :recipient-qr-code recipient-qr-code diff --git a/src/status_im/utils/config.cljs b/src/status_im/utils/config.cljs index 5085bd7fd6..1a39e2f686 100644 --- a/src/status_im/utils/config.cljs +++ b/src/status_im/utils/config.cljs @@ -22,6 +22,7 @@ (def stub-status-go? (enabled? (get-config :STUB_STATUS_GO 0))) (def mainnet-warning-enabled? (enabled? (get-config :MAINNET_WARNING_ENABLED 0))) (def offline-inbox-enabled? (enabled? (get-config :OFFLINE_INBOX_ENABLED "1"))) +(def bootnodes-settings-enabled? (enabled? (get-config :BOOTNODES_SETTINGS_ENABLED "1"))) (def log-level (-> (get-config :LOG_LEVEL "error") string/lower-case diff --git a/test/cljs/status_im/test/runner.cljs b/test/cljs/status_im/test/runner.cljs index f54992247c..dad674a4fa 100644 --- a/test/cljs/status_im/test/runner.cljs +++ b/test/cljs/status_im/test/runner.cljs @@ -35,7 +35,9 @@ [status-im.test.utils.datetime] [status-im.test.utils.mixpanel] [status-im.test.utils.prices] - [status-im.test.ui.screens.network-settings.edit-network.events])) + [status-im.test.ui.screens.network-settings.edit-network.events] + [status-im.test.ui.screens.bootnodes-settings.edit-bootnode.events] + [status-im.test.ui.screens.accounts.login.events])) (enable-console-print!) @@ -81,4 +83,6 @@ 'status-im.test.utils.datetime 'status-im.test.utils.mixpanel 'status-im.test.utils.prices - 'status-im.test.ui.screens.network-settings.edit-network.events) + 'status-im.test.ui.screens.network-settings.edit-network.events + 'status-im.test.ui.screens.bootnodes-settings.edit-bootnode.events + 'status-im.test.ui.screens.accounts.login.events) diff --git a/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs b/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs new file mode 100644 index 0000000000..bb7016ee69 --- /dev/null +++ b/test/cljs/status_im/test/ui/screens/accounts/login/events.cljs @@ -0,0 +1,132 @@ +(ns status-im.test.ui.screens.accounts.login.events + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.utils.config :as config] + [status-im.ui.screens.accounts.login.events :as events])) + +(deftest login-account + (let [mainnet-account {:network "mainnet_rpc" + :networks {"mainnet_rpc" {:config {:NetworkId 1}}}} + testnet-account {:network "testnet_rpc" + :networks {"testnet_rpc" {:config {:NetworkId 3}}}} + accounts {"mainnet" mainnet-account + "testnet" testnet-account} + initial-db {:db {:network "mainnet_rpc" + :accounts/accounts accounts}}] + + (testing "status-go has not started" + (let [actual (events/login-account initial-db [nil "testnet" "password"])] + (testing "it starts status-node if it has not started" + (is (= {:NetworkId 3} + (:initialize-geth-fx + actual)))) + (testing "it logins the user after the node started" + (is (= [::events/login-account "testnet" "password"] (get-in actual [:db :node/after-start])))))) + + (testing "status-go has started & the user is on mainnet" + (let [db (assoc-in initial-db [:db :status-node-started?] true) + actual (events/login-account + db + [nil "mainnet" "password"])] + (testing "it does not start status-node if it has already started" + (is (not (:initialize-geth-fx actual)))) + (testing "it logs in the user" + (is (= ["mainnet" "password"] (::events/login actual)))))) + + (testing "the user has selected a different network" + (testing "status-go has started" + (let [db (assoc-in initial-db [:db :status-node-started?] true) + actual (events/login-account + db + [nil "testnet" "password"])] + (testing "it dispatches start-node" + (is (get-in actual [:db :node/after-stop] [::events/start-node "testnet" "password"]))) + (testing "it stops status-node" + (is (contains? actual ::events/stop-node))))) + + (testing "status-go has not started" + (let [actual (events/login-account + initial-db + [nil "testnet" "password"])] + (testing "it starts status-node" + (is (= {:NetworkId 3} (:initialize-geth-fx actual)))) + (testing "it logins the user after the node started" + (is (= [::events/login-account "testnet" "password"] (get-in actual [:db :node/after-start]))))))) + + (testing "custom bootnodes" + (let [custom-bootnodes {"a" {:id "a" + :name "name-a" + :address "address-a"} + "b" {:id "b" + :name "name-b" + :address "address-b"}} + bootnodes-db (assoc-in + initial-db + [:db :accounts/accounts "mainnet" :bootnodes] + {"mainnet_rpc" custom-bootnodes})] + + (testing "custom bootnodes enabled" + (let [bootnodes-enabled-db (assoc-in + bootnodes-db + [:db :accounts/accounts "mainnet" :settings] + {:bootnodes {"mainnet_rpc" true}}) + actual (events/login-account + bootnodes-enabled-db + [nil "mainnet" "password"])] + (testing "status-node has started" + (let [db (assoc-in bootnodes-enabled-db [:db :status-node-started?] true) + actual (events/login-account + db + [nil "mainnet" "password"])] + (testing "it dispatches start-node" + (is (get-in actual [:db :node/after-stop] [::events/start-node "testnet" "password"]))) + (testing "it stops status-node" + (is (contains? actual ::events/stop-node))))) + (testing "status-node has not started" + (let [actual (events/login-account + bootnodes-enabled-db + [nil "mainnet" "password"])] + (testing "it adds bootnodes to the config" + (is (= {:ClusterConfig {:Enabled true + :BootNodes ["address-a" "address-b"]} + :NetworkId 1} (:initialize-geth-fx actual)))) + (testing "it logins the user after the node started" + (is (= [::events/login-account "mainnet" "password"] (get-in actual [:db :node/after-start])))))))) + + (testing "custom bootnodes not enabled" + (testing "status-node has started" + (let [db (assoc-in bootnodes-db [:db :status-node-started?] true) + actual (events/login-account + db + [nil "mainnet" "password"])] + (testing "it does not start status-node if it has already started" + (is (not (:initialize-geth-fx actual)))) + (testing "it logs in the user" + (is (= ["mainnet" "password"] (::events/login actual)))))) + (testing "status-node has not started" + (let [actual (events/login-account + bootnodes-db + [nil "mainnet" "password"])] + (testing "it starts status-node without custom bootnodes" + (is (= {:NetworkId 1} (:initialize-geth-fx actual)))) + (testing "it logins the user after the node started" + (is (= [::events/login-account "mainnet" "password"] (get-in actual [:db :node/after-start]))))))))))) + +(deftest restart-node? + (testing "custom bootnodes is toggled off" + (with-redefs [config/bootnodes-settings-enabled? false] + (testing "it returns true when the network is different" + (is (events/restart-node? "mainnet_rpc" "mainnet" true))) + (testing "it returns false when the network is the same" + (is (not (events/restart-node? "mainnet" "mainnet" true)))))) + (testing "custom bootnodes is toggled on" + (with-redefs [config/bootnodes-settings-enabled? true] + (testing "the user is not using custom bootnodes" + (testing "it returns true when the network is different" + (is (events/restart-node? "mainnet_rpc" "mainnet" false))) + (testing "it returns false when the network is the same" + (is (not (events/restart-node? "mainnet" "mainnet" false))))) + (testing "the user is using custom bootnodes" + (testing "it returns true when the network is different" + (is (events/restart-node? "mainnet" "mainnet" true))) + (testing "it returns true when the network is the same" + (is (events/restart-node? "mainnet_rpc" "mainnet" true))))))) diff --git a/test/cljs/status_im/test/ui/screens/bootnodes_settings/edit_bootnode/events.cljs b/test/cljs/status_im/test/ui/screens/bootnodes_settings/edit_bootnode/events.cljs new file mode 100644 index 0000000000..4d781a664b --- /dev/null +++ b/test/cljs/status_im/test/ui/screens/bootnodes_settings/edit_bootnode/events.cljs @@ -0,0 +1,19 @@ +(ns status-im.test.ui.screens.bootnodes-settings.edit-bootnode.events + (:require [cljs.test :refer-macros [deftest is testing]] + [status-im.ui.screens.bootnodes-settings.edit-bootnode.events :as events])) + +(deftest add-new-bootnode + (testing "adding a bootnode" + (let [new-bootnode {:name {:value "name"} + :url {:value "url"}} + expected {"mainnet_rpc" {"someid" {:name "name" + :address "url" + :chain "mainnet_rpc" + :id "someid"}}} + actual (events/save-new-bootnode + {:random-id "some-id" + :db {:bootnodes/manage new-bootnode + :network "mainnet_rpc" + :account/account {}}} + nil)] + (is (= expected (get-in actual [:db :account/account :bootnodes]))))))