diff --git a/browserslist b/.browserslistrc similarity index 68% rename from browserslist rename to .browserslistrc index b15c7fae..0066735a 100644 --- a/browserslist +++ b/.browserslistrc @@ -2,6 +2,9 @@ # For additional information regarding the format and rule options, please see: # https://github.com/browserslist/browserslist#queries +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + # You can see what browsers were selected by your queries by running: # npx browserslist @@ -9,4 +12,4 @@ last 2 versions Firefox ESR not dead -not IE 9-11 # For IE 9-11 support, remove 'not'. +not IE 9-11 # For IE 9-11 support, remove 'not'. \ No newline at end of file diff --git a/.eslintrc b/.eslintrc index 11643ef3..91b976e0 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,6 +1,9 @@ { "root": true, "parser": "@typescript-eslint/parser", + "parserOptions": { + "project": "./tsconfig.json" + }, "plugins": [ "@typescript-eslint" ], @@ -8,5 +11,19 @@ "eslint:recommended", "plugin:@typescript-eslint/eslint-recommended", "plugin:@typescript-eslint/recommended" - ] + ], + "rules": { + "@typescript-eslint/await-thenable": "error", + "require-await": "off", + "@typescript-eslint/require-await": "error", + "@typescript-eslint/no-misused-promises": [ + "error", + { + "checksVoidReturn": false + } + ], + "no-shadow": "off", + "@typescript-eslint/no-shadow": "error" + // "@typescript-eslint/no-floating-promises": "error" + } } diff --git a/README.md b/README.md index 16fd2006..60e42fa9 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ # Beaconchain Dashboard App -Beaconchain Dashboard is an open source ethereum validator performance tracker app for Android and iOS. It utilizes the beaconcha.in API. +Beaconchain Dashboard is an open source ethereum and gnosis validator performance tracker app for Android and iOS. It utilizes the beaconcha.in API. [![Get it on Google Play](https://beaconcha.in/img/android.png)](https://play.google.com/store/apps/details?id=in.beaconcha.mobile) @@ -15,6 +15,7 @@ Beaconchain Dashboard is an Angular app written in Typescript, HTML & CSS. It ut ## Features +- Ethereum and Gnosis supported - Keep track on your validators online status, balances, returns and more - Various notification alerts for your validators - Execution block rewards overview @@ -70,7 +71,7 @@ Build the the app at least once before proceeding: Make sure port 8100 is accessable on your computer and use the following command to run a livereload server -`ionic capacitor run android --livereload --external --host=192.168.0.124 --disableHostCheck` +`ionic cap run android --livereload --external --host=192.168.1.64 --disableHostCheck --configuration=development` Adapt the --host param to match your computers IP. @@ -98,7 +99,7 @@ Build the the app at least once before proceeding: Make sure port 8100 is accessable on your mac and use the following command to run a livereload server -`ionic capacitor run ios --livereload --external --host=192.168.0.124 --disableHostCheck` +`ionic cap run ios --livereload --external --host=192.168.1.64 --disableHostCheck --configuration=development` Adapt the --host param to match your macs IP. diff --git a/android/app/build.gradle b/android/app/build.gradle index 65ea11a4..ee949e5f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -8,8 +8,8 @@ android { namespace "in.beaconcha.mobile" minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion - versionCode 101 - versionName "4.3.2" + versionCode 107 + versionName "4.5.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 447d6c50..5d050c31 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,90 +1,43 @@ - + - - - - - - - - - - + + + - - + - - - + - - + - - + - - - - - + + - - - diff --git a/android/app/src/main/java/in/beaconcha/mobile/widget b/android/app/src/main/java/in/beaconcha/mobile/widget index 9a947695..63d7292e 160000 --- a/android/app/src/main/java/in/beaconcha/mobile/widget +++ b/android/app/src/main/java/in/beaconcha/mobile/widget @@ -1 +1 @@ -Subproject commit 9a947695b1213cc6ac5cf5cad9efd42a96d83d69 +Subproject commit 63d7292e397db12b1545b9ccc2f071858add6702 diff --git a/angular.json b/angular.json index ca614137..4832ba80 100644 --- a/angular.json +++ b/angular.json @@ -19,6 +19,11 @@ "main": "src/main.ts", "polyfills": "src/polyfills.ts", "tsConfig": "tsconfig.app.json", + "allowedCommonJsDependencies": [ + "highcharts", + "ethereum-blockies", + "magic-snowflakes" + ], "assets": [ { "glob": "**/*", diff --git a/ios/App/App.xcodeproj/project.pbxproj b/ios/App/App.xcodeproj/project.pbxproj index 8af810dd..ac03c77e 100644 --- a/ios/App/App.xcodeproj/project.pbxproj +++ b/ios/App/App.xcodeproj/project.pbxproj @@ -500,17 +500,17 @@ CODE_SIGN_ENTITLEMENTS = App/App.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 103; DEVELOPMENT_TEAM = 3HYX3N9WTV; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.5.2; + MARKETING_VERSION = 4.5.0; OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\""; PRODUCT_BUNDLE_IDENTIFIER = in.beaconcha.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; SWIFT_OBJC_BRIDGING_HEADER = "App/App-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -528,12 +528,12 @@ CODE_SIGN_ENTITLEMENTS = App/AppRelease.entitlements; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 103; DEVELOPMENT_TEAM = 3HYX3N9WTV; INFOPLIST_FILE = App/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 13.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - MARKETING_VERSION = 3.5.2; + MARKETING_VERSION = 4.5.0; PRODUCT_BUNDLE_IDENTIFIER = in.beaconcha.mobile; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -556,12 +556,12 @@ CODE_SIGN_ENTITLEMENTS = "Beaconchain WidgetExtension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 103; DEVELOPMENT_TEAM = 3HYX3N9WTV; INFOPLIST_FILE = "Beaconchain Widget/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 3.5.2; + MARKETING_VERSION = 4.5.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "in.beaconcha.mobile.Beaconchain-Widget"; @@ -584,12 +584,12 @@ CODE_SIGN_ENTITLEMENTS = "Beaconchain WidgetExtension.entitlements"; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 75; + CURRENT_PROJECT_VERSION = 103; DEVELOPMENT_TEAM = 3HYX3N9WTV; INFOPLIST_FILE = "Beaconchain Widget/Info.plist"; - IPHONEOS_DEPLOYMENT_TARGET = 13.0; + IPHONEOS_DEPLOYMENT_TARGET = 14.0; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @executable_path/../../Frameworks"; - MARKETING_VERSION = 3.5.2; + MARKETING_VERSION = 4.5.0; MTL_FAST_MATH = YES; PRODUCT_BUNDLE_IDENTIFIER = "in.beaconcha.mobile.Beaconchain-Widget"; PRODUCT_NAME = "$(TARGET_NAME)"; diff --git a/ios/App/Podfile.lock b/ios/App/Podfile.lock new file mode 100644 index 00000000..4a86b7bf --- /dev/null +++ b/ios/App/Podfile.lock @@ -0,0 +1,199 @@ +PODS: + - ByteowlsCapacitorOauth2 (5.0.0): + - Capacitor + - OAuthSwift (= 2.2.0) + - Capacitor (5.4.2): + - CapacitorCordova + - CapacitorApp (5.0.6): + - Capacitor + - CapacitorBrowser (5.1.0): + - Capacitor + - CapacitorClipboard (5.0.6): + - Capacitor + - CapacitorCordova (5.4.2) + - CapacitorDevice (5.0.6): + - Capacitor + - CapacitorHaptics (5.0.6): + - Capacitor + - CapacitorKeyboard (5.0.6): + - Capacitor + - CapacitorLocalNotifications (5.0.6): + - Capacitor + - CapacitorNavigationbarnx (0.0.2): + - Capacitor + - CapacitorPreferences (5.0.6): + - Capacitor + - CapacitorPushNotifications (5.1.0): + - Capacitor + - CapacitorSplashScreen (5.0.6): + - Capacitor + - CapacitorStatusBar (5.0.6): + - Capacitor + - CapacitorToast (5.0.6): + - Capacitor + - CordovaPlugins (5.4.2): + - CapacitorCordova + - Firebase/CoreOnly (9.2.0): + - FirebaseCore (= 9.2.0) + - Firebase/Messaging (9.2.0): + - Firebase/CoreOnly + - FirebaseMessaging (~> 9.2.0) + - FirebaseCore (9.2.0): + - FirebaseCoreDiagnostics (~> 9.0) + - FirebaseCoreInternal (~> 9.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - FirebaseCoreDiagnostics (9.2.0): + - GoogleDataTransport (< 10.0.0, >= 9.1.4) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Logger (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - FirebaseCoreInternal (9.2.0): + - "GoogleUtilities/NSData+zlib (~> 7.7)" + - FirebaseInstallations (9.2.0): + - FirebaseCore (~> 9.0) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - PromisesObjC (~> 2.1) + - FirebaseMessaging (9.2.0): + - FirebaseCore (~> 9.0) + - FirebaseInstallations (~> 9.0) + - GoogleDataTransport (< 10.0.0, >= 9.1.4) + - GoogleUtilities/AppDelegateSwizzler (~> 7.7) + - GoogleUtilities/Environment (~> 7.7) + - GoogleUtilities/Reachability (~> 7.7) + - GoogleUtilities/UserDefaults (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - GoogleDataTransport (9.1.4): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Logger + - GoogleUtilities/Network + - GoogleUtilities/Environment (7.7.0): + - PromisesObjC (< 3.0, >= 1.2) + - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Environment + - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Logger + - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Reachability + - "GoogleUtilities/NSData+zlib (7.7.0)" + - GoogleUtilities/Reachability (7.7.0): + - GoogleUtilities/Logger + - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Logger + - nanopb (2.30909.0): + - nanopb/decode (= 2.30909.0) + - nanopb/encode (= 2.30909.0) + - nanopb/decode (2.30909.0) + - nanopb/encode (2.30909.0) + - OAuthSwift (2.2.0) + - PromisesObjC (2.1.1) + +DEPENDENCIES: + - "ByteowlsCapacitorOauth2 (from `../../node_modules/@byteowls/capacitor-oauth2`)" + - "Capacitor (from `../../node_modules/@capacitor/ios`)" + - "CapacitorApp (from `../../node_modules/@capacitor/app`)" + - "CapacitorBrowser (from `../../node_modules/@capacitor/browser`)" + - "CapacitorClipboard (from `../../node_modules/@capacitor/clipboard`)" + - "CapacitorCordova (from `../../node_modules/@capacitor/ios`)" + - "CapacitorDevice (from `../../node_modules/@capacitor/device`)" + - "CapacitorHaptics (from `../../node_modules/@capacitor/haptics`)" + - "CapacitorKeyboard (from `../../node_modules/@capacitor/keyboard`)" + - "CapacitorLocalNotifications (from `../../node_modules/@capacitor/local-notifications`)" + - CapacitorNavigationbarnx (from `../../node_modules/capacitor-navigationbarnx`) + - "CapacitorPreferences (from `../../node_modules/@capacitor/preferences`)" + - "CapacitorPushNotifications (from `../../node_modules/@capacitor/push-notifications`)" + - "CapacitorSplashScreen (from `../../node_modules/@capacitor/splash-screen`)" + - "CapacitorStatusBar (from `../../node_modules/@capacitor/status-bar`)" + - "CapacitorToast (from `../../node_modules/@capacitor/toast`)" + - CordovaPlugins (from `../capacitor-cordova-ios-plugins`) + - Firebase/Messaging + - FirebaseCore + +SPEC REPOS: + trunk: + - Firebase + - FirebaseCore + - FirebaseCoreDiagnostics + - FirebaseCoreInternal + - FirebaseInstallations + - FirebaseMessaging + - GoogleDataTransport + - GoogleUtilities + - nanopb + - OAuthSwift + - PromisesObjC + +EXTERNAL SOURCES: + ByteowlsCapacitorOauth2: + :path: "../../node_modules/@byteowls/capacitor-oauth2" + Capacitor: + :path: "../../node_modules/@capacitor/ios" + CapacitorApp: + :path: "../../node_modules/@capacitor/app" + CapacitorBrowser: + :path: "../../node_modules/@capacitor/browser" + CapacitorClipboard: + :path: "../../node_modules/@capacitor/clipboard" + CapacitorCordova: + :path: "../../node_modules/@capacitor/ios" + CapacitorDevice: + :path: "../../node_modules/@capacitor/device" + CapacitorHaptics: + :path: "../../node_modules/@capacitor/haptics" + CapacitorKeyboard: + :path: "../../node_modules/@capacitor/keyboard" + CapacitorLocalNotifications: + :path: "../../node_modules/@capacitor/local-notifications" + CapacitorNavigationbarnx: + :path: "../../node_modules/capacitor-navigationbarnx" + CapacitorPreferences: + :path: "../../node_modules/@capacitor/preferences" + CapacitorPushNotifications: + :path: "../../node_modules/@capacitor/push-notifications" + CapacitorSplashScreen: + :path: "../../node_modules/@capacitor/splash-screen" + CapacitorStatusBar: + :path: "../../node_modules/@capacitor/status-bar" + CapacitorToast: + :path: "../../node_modules/@capacitor/toast" + CordovaPlugins: + :path: "../capacitor-cordova-ios-plugins" + +SPEC CHECKSUMS: + ByteowlsCapacitorOauth2: dde6b8fdf2995bc7872434c407ee2492ce562638 + Capacitor: 8a9db42d105f55843cd8ed2a3cb54e2b78e7f102 + CapacitorApp: 963d90cb449803d58efc33bb515c81151e69159d + CapacitorBrowser: 31371ef981e378cb592be8cc203a9a4cbf43c486 + CapacitorClipboard: ae6864d6b4ff2243fc7ce19124df307d8d1bb78d + CapacitorCordova: cfcc06b698481da655415985eeb2b8da363f8451 + CapacitorDevice: 9233ba5f0c8f601bf4992006f5d2b2a86b7abc2e + CapacitorHaptics: 186982679c5f4005d1e6d2c719b8251c0d3a6df8 + CapacitorKeyboard: 2f3296af2bc929a5069f02ed264fdce58ed62028 + CapacitorLocalNotifications: 7ed607db672dc2834f22a25422b86e74129a032a + CapacitorNavigationbarnx: a22badd7d90d9715ecc08f3ae3824a57a37724b0 + CapacitorPreferences: 8053abfef3bd11e3e83834792a97e5a6387bf591 + CapacitorPushNotifications: 14425a66891d4134e73b6abe1932eeff37e4389d + CapacitorSplashScreen: f3c7de5468a52893e90f49d033772984270241b4 + CapacitorStatusBar: a73a9ae0dd2ca30ee429782d5d3ae82dcfa80207 + CapacitorToast: 61a9bec819ad3aeccc1ebe3fcbff565c22969c71 + CordovaPlugins: 13d1f5b0b81c2eba67b8f70432c3edb07483877c + Firebase: 4ba896cb8e5105d4b9e247e1c1b6222b548df55a + FirebaseCore: 0e27f2a15d8f7b7ef11e7d93e23b1cbab55d748c + FirebaseCoreDiagnostics: ad3f6c68b7c5b63b7cf15b0785d7137f05f32268 + FirebaseCoreInternal: cb966328b6985dbd6f535e1461291063e1c4a00f + FirebaseInstallations: 21186f0ca7849f90f4a3219fa31a5eca2e30f113 + FirebaseMessaging: 4eaf1b8a7464b2c5e619ad66e9b20ee3e3206b24 + GoogleDataTransport: 5fffe35792f8b96ec8d6775f5eccd83c998d5a3b + GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 + nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 + OAuthSwift: 75efbb5bd9a4b2b71a37bd7e986bf3f55ddd54c6 + PromisesObjC: ab77feca74fa2823e7af4249b8326368e61014cb + +PODFILE CHECKSUM: 0abccb83d0d503da1628c7d1ca5579739161bb33 + +COCOAPODS: 1.14.2 diff --git a/package-lock.json b/package-lock.json index b022ec93..9a87f113 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,8 +37,7 @@ "@capacitor/toast": "^5.0.6", "@ionic/angular": "^7.4.3", "async-mutex": "^0.2.6", - "axios": "^0.27.2", - "bignumber.js": "^9.0.2", + "bignumber.js": "^9.1.2", "canvas-confetti": "^1.3.3", "capacitor-navigationbarnx": "0.1.6", "cordova-plugin-purchase": "^13.8.6", @@ -5611,7 +5610,8 @@ "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "node_modules/at-least-node": { "version": "1.0.0", @@ -5669,28 +5669,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - } - }, - "node_modules/axios/node_modules/form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dependencies": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - }, - "engines": { - "node": ">= 6" - } - }, "node_modules/axobject-query": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", @@ -5849,9 +5827,9 @@ } }, "node_modules/bignumber.js": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", - "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", "engines": { "node": "*" } @@ -6627,6 +6605,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "dependencies": { "delayed-stream": "~1.0.0" }, @@ -7364,6 +7343,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, "engines": { "node": ">=0.4.0" } @@ -8805,6 +8785,7 @@ "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true, "funding": [ { "type": "individual", @@ -11390,6 +11371,7 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, "engines": { "node": ">= 0.6" } @@ -11398,6 +11380,7 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "dependencies": { "mime-db": "1.52.0" }, @@ -20933,7 +20916,8 @@ "asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", + "dev": true }, "at-least-node": { "version": "1.0.0", @@ -20966,27 +20950,6 @@ "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", - "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" - }, - "dependencies": { - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } - } - }, "axobject-query": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.0.2.tgz", @@ -21102,9 +21065,9 @@ "dev": true }, "bignumber.js": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.2.tgz", - "integrity": "sha512-GAcQvbpsM0pUb0zw1EI0KhQEZ+lRwR5fYaAp3vPOYuP7aDvGy6cVN6XHLauvF8SOga2y0dcLcjt3iQDTSEliyw==" + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==" }, "binary-extensions": { "version": "2.1.0", @@ -21675,6 +21638,7 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, "requires": { "delayed-stream": "~1.0.0" } @@ -22239,7 +22203,8 @@ "delayed-stream": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true }, "delegates": { "version": "1.0.0", @@ -23344,7 +23309,8 @@ "follow-redirects": { "version": "1.15.1", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.1.tgz", - "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==" + "integrity": "sha512-yLAMQs+k0b2m7cVxpS1VKJVvoz7SS9Td1zss3XRwXj+ZDH00RJgnuLx7E44wx02kQLrdM3aOOy+FpzS7+8OizA==", + "dev": true }, "foreground-child": { "version": "3.1.1", @@ -25307,12 +25273,14 @@ "mime-db": { "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true }, "mime-types": { "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, "requires": { "mime-db": "1.52.0" } diff --git a/package.json b/package.json index 58e5e9b8..2e80f069 100644 --- a/package.json +++ b/package.json @@ -43,8 +43,7 @@ "@capacitor/toast": "^5.0.6", "@ionic/angular": "^7.4.3", "async-mutex": "^0.2.6", - "axios": "^0.27.2", - "bignumber.js": "^9.0.2", + "bignumber.js": "^9.1.2", "canvas-confetti": "^1.3.3", "capacitor-navigationbarnx": "0.1.6", "cordova-plugin-purchase": "^13.8.6", diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 33e06820..1404150c 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -48,10 +48,10 @@ export class AppComponent { } initializeApp() { + BigNumber.config({ DECIMAL_PLACES: 25 }) this.platform.ready().then(() => { this.storage.migrateToCapacitor3().then(async () => { - BigNumber.config({ DECIMAL_PLACES: 25 }) - const networkName = await this.api.getNetworkName() + const networkName = this.api.getNetworkName() // migrate to 3.2+ const result = await this.storage.getBooleanSetting(networkName + 'migrated_to_3.2', false) if (!result) { @@ -66,11 +66,6 @@ export class AppComponent { }) // just initialize the theme service this.setAndroidBackButtonBehavior() - - /* AdMob.initialize({ - requestTrackingAuthorization: false, - testingDevices: [] - });*/ }) }) } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 23479878..1f1cd473 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -18,7 +18,7 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { NgModule } from '@angular/core' +import { APP_INITIALIZER, Injectable, NgModule } from '@angular/core' import { BrowserModule, HammerModule } from '@angular/platform-browser' import { RouteReuseStrategy } from '@angular/router' @@ -30,9 +30,12 @@ import { PipesModule } from './pipes/pipes.module' import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser' import { BrowserAnimationsModule } from '@angular/platform-browser/animations' import 'hammerjs' +import { ApiService } from './services/api.service' +import { BootPreloadService } from './services/boot-preload.service' // eslint-disable-next-line @typescript-eslint/no-explicit-any declare let Hammer: any +@Injectable() export class MyHammerConfig extends HammerGestureConfig { buildHammer(element: HTMLElement) { const mc = new Hammer(element, { @@ -59,7 +62,23 @@ export class MyHammerConfig extends HammerGestureConfig { provide: HAMMER_GESTURE_CONFIG, useClass: MyHammerConfig, }, + { + provide: APP_INITIALIZER, + useFactory: initializeApp, + multi: true, + deps: [ApiService, BootPreloadService], + }, ], bootstrap: [AppComponent], }) export class AppModule {} + +export function initializeApp(apiService: ApiService, bootPreloadService: BootPreloadService): () => Promise { + return async () => { + // Initialize ApiService first + await apiService.initialize() + + // Now that ApiService is initialized, you can preload using BootPreloadService + bootPreloadService.preload() + } +} diff --git a/src/app/components/block/block.component.html b/src/app/components/block/block.component.html index 1632d697..efbed75a 100644 --- a/src/app/components/block/block.component.html +++ b/src/app/components/block/block.component.html @@ -7,7 +7,7 @@
- Reward: {{ producerReward | mcurrency : 'WEI' : unit.pref }} + Reward: {{ producerReward | mcurrency : 'WEI' : unit.pref.Exec }} Proposer: {{ block.posConsensus.proposerIndex }} diff --git a/src/app/components/block/block.component.ts b/src/app/components/block/block.component.ts index bd811873..6247f1a2 100644 --- a/src/app/components/block/block.component.ts +++ b/src/app/components/block/block.component.ts @@ -31,7 +31,7 @@ export class BlockComponent implements OnInit { async ngOnChanges() { this.imgData = this.getBlockies() this.timestamp = this.block.timestamp * 1000 - this.producerReward = await await this.blockUtils.getBlockRewardWithShare(this.block) + this.producerReward = await this.blockUtils.getBlockRewardWithShare(this.block) this.resolvedName = null this.feeRecipient = this.block.feeRecipient @@ -47,7 +47,7 @@ export class BlockComponent implements OnInit { } } - async setInput(block: BlockResponse) { + setInput(block: BlockResponse) { this.block = block this.ngOnChanges() } diff --git a/src/app/components/dashboard/dashboard.component.html b/src/app/components/dashboard/dashboard.component.html index 3377148e..84b81852 100644 --- a/src/app/components/dashboard/dashboard.component.html +++ b/src/app/components/dashboard/dashboard.component.html @@ -160,60 +160,128 @@ - + + + - Staking + Rewards + + + Combined + + + Consensus + + + Blocks + + -
-
+
+
- - {{ data.overallBalance | mcurrency : 'GWEI' : unit.pref }} + + {{ data.combinedPerformance.performance1d | mcurrency : 'GWEI' : unit.pref.Cons }}
- Balance + + Today +
-
+
- {{ data.apr }} % + + {{ data.combinedPerformance.performance7d | mcurrency : 'GWEI' : unit.pref.Cons }} +
- + Last 7d +
+
+ +
+
+ + {{ data.combinedPerformance.performance31d | mcurrency : 'GWEI' : unit.pref.Cons }} + +
+
+ Last 31d +
+
+ +
+
+ + {{ data.combinedPerformance.total | mcurrency : 'GWEI' : unit.pref.Cons }} + +
+
+ Total +
+
+ +
+
+ {{ data.combinedPerformance.apr }} % +
+
+
- -
-
- - - {{ data.attrEffectiveness !== -1 ? data.attrEffectiveness + ' %' : 'NaN' }} - - - - - - +
+
+
+
+ + {{ data.consensusPerformance.performance1d | mcurrency : 'GWEI' : unit.pref.Cons }} +
-
+
- Effectiveness - + hideDelayAfterClick="5000" + >Today
-
+
- - {{ proposals.good }} / - {{ proposals.bad }} + + {{ data.consensusPerformance.performance7d | mcurrency : 'GWEI' : unit.pref.Cons }} + +
+
+ Last 7d +
+
+ +
+
+ + {{ data.consensusPerformance.performance31d | mcurrency : 'GWEI' : unit.pref.Cons }} + +
+
+ Last 31d +
+
+ +
+
+ + {{ data.consensusPerformance.total | mcurrency : 'GWEI' : unit.pref.Cons }}
- Proposals - + hideDelayAfterClick="5000" + >Total
-
+
+
+ {{ data.consensusPerformance.apr }} % +
+
+ + APR + +
+
+
+
+
- - {{ data.consensusPerformance.performance1d | mcurrency : 'GWEI' : unit.pref }} + + {{ data.executionPerformance.performance1d | mcurrency : 'GWEI' : unit.pref.Exec }}
Today
-
+
- - {{ data.consensusPerformance.performance7d | mcurrency : 'GWEI' : unit.pref }} + + {{ data.executionPerformance.performance7d | mcurrency : 'GWEI' : unit.pref.Exec }}
- Last Week + Last 7d
-
+
- - {{ data.consensusPerformance.performance31d | mcurrency : 'GWEI' : unit.pref }} + + {{ data.executionPerformance.performance31d | mcurrency : 'GWEI' : unit.pref.Exec }}
- Last Month + Last 31d
-
+
- - {{ data.consensusPerformance.total | mcurrency : 'GWEI' : unit.pref }} + + {{ data.executionPerformance.total | mcurrency : 'GWEI' : unit.pref.Exec }}
Total
-
- - - - - Transaction Fees - - - - -
-
-
- - {{ data.executionPerformance.performance31d | mcurrency : 'GWEI' : unit.pref }} - -
-
- - Last Month - -
-
- -
-
- {{ data.executionPerformance.apr }} % -
-
- - APR - -
+
+
+ {{ data.executionPerformance.apr }} % +
+
+ + APR +
- - +
+ @@ -376,8 +448,8 @@
- - {{ smoothingClaimed.plus(smoothingUnclaimed) | mcurrency : 'GWEI' : unit.pref }} + + {{ smoothingClaimed.plus(smoothingUnclaimed) | mcurrency : 'GWEI' : unit.pref.Exec }}
@@ -393,8 +465,8 @@
- - {{ smoothingUnclaimed | mcurrency : 'GWEI' : unit.pref }} + + {{ smoothingUnclaimed | mcurrency : 'GWEI' : unit.pref.Exec }}
@@ -424,7 +496,7 @@ {{ rplProjectedClaim | mcurrency : 'WEI' : 'RPL_NAKED' }} - {{ rplProjectedClaim | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ rplProjectedClaim | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }}
@@ -440,7 +512,7 @@ {{ totalRplEarned | mcurrency : 'WEI' : 'RPL_NAKED' }} - {{ totalRplEarned | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ totalRplEarned | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }}
@@ -462,7 +534,7 @@ {{ unclaimedRpl | mcurrency : 'WEI' : 'RPL_NAKED' }} - {{ unclaimedRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ unclaimedRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }}
@@ -501,10 +573,10 @@
-
+
- {{ 1 | mcurrency : 'ETHER' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ 1 | mcurrency : 'ETHER' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }}
@@ -535,7 +607,7 @@ class="value left" *ngIf="rplState == 'conv'" [class]="data.rocketpool.currentRpl | valuestyle : data.rocketpool.minRpl : data.rocketpool.maxRpl : -1"> - {{ data.rocketpool.currentRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ data.rocketpool.currentRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }} - {{ data.rocketpool.minRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} - - {{ data.rocketpool.maxRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref }} + {{ data.rocketpool.minRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }} - + {{ data.rocketpool.maxRpl | mcurrency : 'WEI' : 'RPL' : false | mcurrency : 'ETHER' : unit.pref.RPL }}
@@ -632,8 +704,8 @@ data.foreignValidator ">
- - {{ data.foreignValidatorItem.rocketpool.node_deposit_balance | mcurrency : 'WEI' : unit.pref }} + + {{ data.foreignValidatorItem.rocketpool.node_deposit_balance | mcurrency : 'WEI' : unit.pref.Cons }}
@@ -676,6 +748,38 @@
+
+ + Blocks + + + + + + Proposals + + + + {{ proposals.good }} / + {{ proposals.bad }} + + + + Luck + + {{ data.proposalLuckResponse.proposal_luck * 100 | number : '1.0-1' }}% + - + + + +
+
Sync Committees @@ -713,7 +817,7 @@ tooltip="Sync committees expected: {{ data.syncCommitteesStats.committeesExpected }}" trigger="click" hideDelayAfterClick="5000"> - {{ data.syncCommitteesStats.luck | number : '1.0-2' }}% + {{ data.syncCommitteesStats.luck | number : '1.0-1' }}% - @@ -812,8 +916,8 @@ Effective Balance - - {{ data.effectiveBalance | mcurrency : 'GWEI' : 'ETHER' }} + + {{ data.effectiveBalance | mcurrency : 'GWEI' : unit.getNetworkDefaultCurrency('cons') }} @@ -846,15 +950,15 @@ Effective Balance - - {{ data.effectiveBalance | mcurrency : 'GWEI' : 'ETHER' }} + + {{ data.effectiveBalance | mcurrency : 'GWEI' : unit.getNetworkDefaultCurrency('cons') }} - - Ether Price - - {{ 1 | mcurrency : 'ETHER' : unit.pref }} + + {{ unit.getNetworkDefaultUnit('cons').display }} Price + + {{ unit.lastPrice.Cons | mcurrency : unit.getFiatCurrency('cons') : unit.getFiatCurrency('cons') }} @@ -877,7 +981,7 @@ - View on beaconcha.in + View on {{ api.getHostName() }}
diff --git a/src/app/components/dashboard/dashboard.component.scss b/src/app/components/dashboard/dashboard.component.scss index 535e41a6..65cced08 100644 --- a/src/app/components/dashboard/dashboard.component.scss +++ b/src/app/components/dashboard/dashboard.component.scss @@ -25,16 +25,29 @@ ion-segment-button { text-transform: none !important; --ripple-color: transparent !important; //transition: color 0.15s ease; - font-size: 1.05em !important; - letter-spacing: normal !important; + font-family: 'Varela'; + font-size: 0.75em; + --background-focused-opacity: 1; opacity: 1 !important; } +ion-segment-button.md::part(native) { + opacity: 0.7; +} + +ion-segment { + background: var(--ion-background-color); +} + .segment-button-checked { color: var(--segment-primary) !important; font-weight: 600 !important; } +.segment-button-checked.md::part(native) { + opacity: 1 !important; +} + .status ion-icon { width: 100%; display: flex; @@ -118,7 +131,15 @@ ion-segment-button { padding-bottom: 6px !important; } -ion-list { +ion-list-header { + padding-left: 0px; + text-align: center; +} +ion-list-header ion-label { + font-size: 0.9rem; + font-weight: 600 !important; + font-family: 'Varela'; + letter-spacing: 1.05px; } ion-item { @@ -149,9 +170,6 @@ ion-item { padding-bottom: 0px; } -.first-item-group { -} - .status-container { padding-bottom: 6px; } diff --git a/src/app/components/dashboard/dashboard.component.ts b/src/app/components/dashboard/dashboard.component.ts index 7d1df8bc..c1a94a10 100644 --- a/src/app/components/dashboard/dashboard.component.ts +++ b/src/app/components/dashboard/dashboard.component.ts @@ -19,7 +19,7 @@ */ import { Component, OnInit, Input, SimpleChange } from '@angular/core' -import { UnitconvService } from '../../services/unitconv.service' +import { RewardType, UnitconvService } from '../../services/unitconv.service' import { ApiService } from '../../services/api.service' import { DashboardDataRequest, EpochResponse, SyncCommitteeResponse } from '../../requests/requests' import * as HighCharts from 'highcharts' @@ -102,6 +102,8 @@ export class DashboardComponent implements OnInit { vacantMinipoolText = null showWithdrawalInfo = false + rewardTab: 'combined' | 'cons' | 'exec' = 'combined' + constructor( public unit: UnitconvService, public api: ApiService, @@ -114,7 +116,6 @@ export class DashboardComponent implements OnInit { private platform: Platform ) { this.randomChartId = getRandomInt(Number.MAX_SAFE_INTEGER) - //this.storage.setBooleanSetting("merge_list_dismissed", false) this.updateMergeListDismissed() } @@ -131,7 +132,6 @@ export class DashboardComponent implements OnInit { isAfterPotentialMergeTarget() { const now = Date.now() const target = 1663624800000 // target sept 20th to dismiss merge checklist - console.log('afterPotentialMerge', now, target, now >= target) return now >= target } @@ -161,7 +161,7 @@ export class DashboardComponent implements OnInit { this.drawProposalChart() }, 500) - this.beaconChainUrl = await this.getBaseBrowserUrl() + this.beaconChainUrl = this.api.getBaseUrl() await Promise.all([ this.updateRplDisplay(), @@ -177,8 +177,6 @@ export class DashboardComponent implements OnInit { this.updateWithdrawalInfo(), ]) - console.log('dashboard data', this.data) - if (!this.data.foreignValidator) { await Promise.all([this.checkForFinalization(), this.checkForGenesisOccurred()]) } @@ -188,15 +186,15 @@ export class DashboardComponent implements OnInit { } } - async updateWithdrawalInfo() { + updateWithdrawalInfo() { this.storage.getBooleanSetting('withdrawal_info_dismissed', false).then((result) => { this.showWithdrawalInfo = !this.data.withdrawalsEnabledForAll && !result }) } - async updateDepositCreditText() { + updateDepositCreditText() { if (this.data.rocketpool.depositCredit && this.data.rocketpool.depositCredit.gt(0)) { - this.depositCreditText = `You have ${this.unit.convert( + this.depositCreditText = `You have ${this.unit.convertNonFiat( this.data.rocketpool.depositCredit, 'WEI', 'ETHER', @@ -205,7 +203,7 @@ export class DashboardComponent implements OnInit { } } - async updateVacantMinipoolText() { + updateVacantMinipoolText() { if (this.data.rocketpool.vacantPools && this.data.rocketpool.vacantPools > 0) { this.vacantMinipoolText = `${this.data.rocketpool.vacantPools} of your ${ this.data.rocketpool.vacantPools == 1 ? 'minipool is' : 'minipools are' @@ -217,15 +215,15 @@ export class DashboardComponent implements OnInit { } } - async epochToTimestamp(epoch: number) { - const network = await this.api.getNetwork() - return (network.genesisTs + epoch * 32 * 12) * 1000 + epochToTimestamp(epoch: number) { + const network = this.api.getNetwork() + return (network.genesisTs + epoch * network.slotPerEpoch * network.slotsTime) * 1000 } - async updateActiveSyncCommitteeMessage(committee: SyncCommitteeResponse) { + updateActiveSyncCommitteeMessage(committee: SyncCommitteeResponse) { if (committee) { - const endTs = await this.epochToTimestamp(committee.end_epoch) - const startTs = await this.epochToTimestamp(committee.start_epoch) + const endTs = this.epochToTimestamp(committee.end_epoch) + const startTs = this.epochToTimestamp(committee.start_epoch) this.currentSyncCommitteeMessage = { title: 'Sync Committee', text: `Your validator${committee.validators.length > 1 ? 's' : ''} ${committee.validators.toString()} ${ @@ -241,10 +239,10 @@ export class DashboardComponent implements OnInit { } } - async updateNextSyncCommitteeMessage(committee: SyncCommitteeResponse) { + updateNextSyncCommitteeMessage(committee: SyncCommitteeResponse) { if (committee) { - const endTs = await this.epochToTimestamp(committee.end_epoch) - const startTs = await this.epochToTimestamp(committee.start_epoch) + const endTs = this.epochToTimestamp(committee.end_epoch) + const startTs = this.epochToTimestamp(committee.start_epoch) this.nextSyncCommitteeMessage = { title: 'Sync Committee Soon', text: `Your validator${committee.validators.length > 1 ? 's' : ''} ${committee.validators.toString()} ${ @@ -265,8 +263,8 @@ export class DashboardComponent implements OnInit { if (!this.validatorUtils.rocketpoolStats || !this.validatorUtils.rocketpoolStats.effective_rpl_staked) return this.hasNonSmoothingPoolAsWell = this.data.rocketpool.hasNonSmoothingPoolAsWell this.displaySmoothingPool = this.data.rocketpool.smoothingPool - this.smoothingClaimed = this.data.rocketpool.smoothingPoolClaimed.dividedBy(new BigNumber('1e9')) - this.smoothingUnclaimed = this.data.rocketpool.smoothingPoolUnclaimed.dividedBy(new BigNumber('1e9')) + this.smoothingClaimed = this.data.rocketpool.smoothingPoolClaimed + this.smoothingUnclaimed = this.data.rocketpool.smoothingPoolUnclaimed this.unclaimedRpl = this.data.rocketpool.rplUnclaimed this.totalRplEarned = this.data.rocketpool.totalClaims.plus(this.data.rocketpool.rplUnclaimed) } catch (e) { @@ -340,14 +338,14 @@ export class DashboardComponent implements OnInit { //this.doneLoading = false this.storage.getBooleanSetting('rank_percent_mode', false).then((result) => (this.rankPercentMode = result)) this.storage.getItem('rpl_pdisplay_mode').then((result) => (this.rplState = result ? result : 'rpl')) - highChartOptions(HighCharts) - highChartOptions(Highstock) + highChartOptions(HighCharts, this.api.getHostName()) + highChartOptions(Highstock, this.api.getHostName()) this.merchant.getCurrentPlanMaxValidator().then((result) => { this.currentPackageMaxValidators = result }) } - private async checkForGenesisOccurred() { + private checkForGenesisOccurred() { if (!this.data || !this.data.currentEpoch) return const currentEpoch = this.data.currentEpoch as EpochResponse this.awaitGenesis = currentEpoch.epoch == 0 && currentEpoch.proposedblocks <= 1 @@ -364,7 +362,7 @@ export class DashboardComponent implements OnInit { } } - const olderResult = await this.validatorUtils.getOlderEpoch() + const olderResult = this.validatorUtils.getOlderEpoch() if (!this.data || !this.data.currentEpoch || !olderResult) return console.log('checkForFinalization', olderResult) this.finalizationIssue = new BigNumber(olderResult.globalparticipationrate).isLessThan('0.664') && olderResult.epoch > 7 @@ -395,26 +393,6 @@ export class DashboardComponent implements OnInit { return await modal.present() } - switchCurrencyPipe() { - if (this.unit.pref == 'ETHER') { - if (UnitconvService.currencyPipe == null) return - this.unit.pref = UnitconvService.currencyPipe - } else { - UnitconvService.currencyPipe = this.unit.pref - this.unit.pref = 'ETHER' - } - } - - switchCurrencyPipeRocketpool() { - if (this.unit.prefRpl == 'RPL') { - if (UnitconvService.currencyPipe == null) return - this.unit.prefRpl = UnitconvService.currencyPipe - } else { - UnitconvService.currencyPipe = this.unit.pref - this.unit.prefRpl = 'RPL' - } - } - switchRplStake(canPercent = false) { if (this.rplState == 'rpl' && canPercent) { // next % @@ -438,7 +416,11 @@ export class DashboardComponent implements OnInit { updateRplDisplay() { if (this.rplState == '%') { - this.rplDisplay = this.data.rocketpool.currentRpl.dividedBy(this.data.rocketpool.maxRpl).multipliedBy(new BigNumber(150)).decimalPlaces(1) + const rplPrice = this.unit.getRPLPrice() + const currentETH = this.data.rocketpool.currentRpl.multipliedBy(rplPrice) + const minETH = this.data.rocketpool.minRpl.multipliedBy(rplPrice).multipliedBy(10) // since collateral is 10% of borrowed eth, multiply by 10 to get to the borrowed eth amount + + this.rplDisplay = currentETH.dividedBy(minETH).multipliedBy(100).decimalPlaces(1).toNumber() } else { this.rplDisplay = this.data.rocketpool.currentRpl } @@ -534,10 +516,10 @@ export class DashboardComponent implements OnInit { this.storage.setBooleanSetting('rank_percent_mode', this.rankPercentMode) } - private getChartToolTipCaption(timestamp: number, genesisTs: number, dataGroupLength: number) { + private getChartToolTipCaption(timestamp: number, genesisTs: number, slotTime: number, slotsPerEpoch: number, dataGroupLength: number) { const dateToEpoch = (ts: number): number => { - const slot = Math.floor((ts / 1000 - genesisTs) / 12) - const epoch = Math.floor(slot / 32) + const slot = Math.floor((ts / 1000 - genesisTs) / slotTime) + const epoch = Math.floor(slot / slotsPerEpoch) return Math.max(0, epoch) } @@ -555,8 +537,8 @@ export class DashboardComponent implements OnInit { } private proposalChart = null - async createProposedChart(proposed, missed, orphaned) { - const network = await this.api.getNetwork() + createProposedChart(proposed, missed, orphaned) { + const network = this.api.getNetwork() this.proposalChart = Highstock.chart( 'highchartsBlocks' + this.randomChartId, @@ -608,7 +590,13 @@ export class DashboardComponent implements OnInit { shared: true, formatter: (tooltip) => { // date and epoch - let text = this.getChartToolTipCaption(tooltip.chart.hoverPoints[0].x, network.genesisTs, tooltip.chart.hoverPoints[0].dataGroup.length) + let text = this.getChartToolTipCaption( + tooltip.chart.hoverPoints[0].x, + network.genesisTs, + network.slotsTime, + network.slotPerEpoch, + tooltip.chart.hoverPoints[0].dataGroup.length + ) // summary for (let i = 0; i < tooltip.chart.hoverPoints.length; i++) { @@ -674,17 +662,27 @@ export class DashboardComponent implements OnInit { } private balanceChart = null - async createBalanceChart(consensusIncome, executionIncome) { + createBalanceChart(consensusIncome, executionIncome) { executionIncome = executionIncome || [] const ticksDecimalPlaces = 3 - const network = await this.api.getNetwork() - - const getValueString = (value: BigNumber): string => { - let text = `${value.toFixed(5)} ETH` - if (this.unit.pref != 'ETHER' && network.key == 'main') { - text += ` (${this.unit.convertToPref(value, 'ETHER')})` + const network = this.api.getNetwork() + + const getValueString = (value: BigNumber, type: RewardType): string => { + let text + if (type == 'cons') { + text = `${value.toFixed(5)} ` + this.unit.getNetworkDefaultUnit(type).display + if (!this.unit.isDefaultCurrency(this.unit.pref.Cons)) { + text += ` (${this.unit.convertToPref(value, this.unit.getNetworkDefaultCurrency(type), type)})` + } + } else if (type == 'exec') { + // Gnosis: All values provided by the API are in the CL currency, including the el rewards + text = `${this.unit.convertCLtoEL(value).toFixed(5)} ` + this.unit.getNetworkDefaultUnit(type).display + if (!this.unit.isDefaultCurrency(this.unit.pref.Exec)) { + text += ` (${this.unit.convertToPref(this.unit.convertCLtoEL(value), this.unit.getNetworkDefaultCurrency(type), type)})` + } } + return text } @@ -730,21 +728,32 @@ export class DashboardComponent implements OnInit { shared: true, formatter: (tooltip) => { // date and epoch - let text = this.getChartToolTipCaption(tooltip.chart.hoverPoints[0].x, network.genesisTs, tooltip.chart.hoverPoints[0].dataGroup.length) + let text = this.getChartToolTipCaption( + tooltip.chart.hoverPoints[0].x, + network.genesisTs, + network.slotsTime, + network.slotPerEpoch, + tooltip.chart.hoverPoints[0].dataGroup.length + ) // income + // Gnosis: All values provided by the API are in the CL currency, including the el rewards which is why we can simply add them for total let total = new BigNumber(0) for (let i = 0; i < tooltip.chart.hoverPoints.length; i++) { + const type = tooltip.chart.hoverPoints[i].series.name == 'Execution' ? 'exec' : 'cons' + const value = new BigNumber(tooltip.chart.hoverPoints[i].y) text += ` ${ tooltip.chart.hoverPoints[i].series.name - }: ${getValueString(value)}
` + }: ${getValueString(value, type)}
` + total = total.plus(value) } // add total if hovered point contains rewards for both EL and CL + // only if both exec and cons currencies are the same if (tooltip.chart.hoverPoints.length > 1) { - text += `Total: ${getValueString(total)}` + text += `Total: ${getValueString(total, 'cons')}` } return text @@ -861,21 +870,16 @@ export class DashboardComponent implements OnInit { } async openBrowser() { - await Browser.open({ url: await this.getBrowserURL(), toolbarColor: '#2f2e42' }) + await Browser.open({ url: this.getBrowserURL(), toolbarColor: '#2f2e42' }) } - async getBrowserURL(): Promise { + getBrowserURL(): string { if (this.data.foreignValidator) { - return (await this.getBaseBrowserUrl()) + '/validator/' + this.data.foreignValidatorItem.pubkey + return this.api.getBaseUrl() + '/validator/' + this.data.foreignValidatorItem.pubkey } else { - return (await this.getBaseBrowserUrl()) + '/dashboard?validators=' + this.data.lazyChartValidators + return this.api.getBaseUrl() + '/dashboard?validators=' + this.data.lazyChartValidators } } - - async getBaseBrowserUrl() { - const net = (await this.api.networkConfig).net - return 'https://' + net + 'beaconcha.in' - } } function getRandomInt(max) { diff --git a/src/app/components/help/help.component.html b/src/app/components/help/help.component.html index 32804afe..baba21c9 100644 --- a/src/app/components/help/help.component.html +++ b/src/app/components/help/help.component.html @@ -26,7 +26,15 @@ - Or login with beaconcha.in + Or login with {{ api.getHostName() }} + + + + + {{ api.getNetwork().name }} Network + - {{ isGnosis ? 'Ethereum' : 'Gnosis' }} Network @@ -34,7 +42,7 @@ Guides & Help
- + Staking Guide & FAQ @@ -61,11 +69,25 @@ + + + + + Node Guide & FAQ + + + + + Enable Withdrawals Guide + + + + How to set up a Validator - + Setup Guide for Lighthouse @@ -92,13 +114,22 @@ + + + + + Interactive Setup Guide + + + + Useful Links - + - beaconcha.in Block Explorer + {{ api.getHostName() }} Block Explorer diff --git a/src/app/components/help/help.component.ts b/src/app/components/help/help.component.ts index 5cda2daf..43786707 100644 --- a/src/app/components/help/help.component.ts +++ b/src/app/components/help/help.component.ts @@ -25,6 +25,13 @@ import { OAuthUtils } from 'src/app/utils/OAuthUtils' import { ValidatorUtils } from 'src/app/utils/ValidatorUtils' import { Browser } from '@capacitor/browser' +import { ApiService } from 'src/app/services/api.service' +import { changeNetwork } from 'src/app/tab-preferences/tab-preferences.page' +import { UnitconvService } from 'src/app/services/unitconv.service' +import { NotificationBase } from 'src/app/tab-preferences/notification-base' +import ThemeUtils from 'src/app/utils/ThemeUtils' +import { AlertService } from 'src/app/services/alert.service' +import { MerchantUtils } from 'src/app/utils/MerchantUtils' @Component({ selector: 'app-help', @@ -35,12 +42,33 @@ export class HelpComponent implements OnInit { @Input() onlyGuides: boolean isAlreadyLoggedIn = false - constructor(private oauthUtils: OAuthUtils, private validator: ValidatorUtils, private storage: StorageService, private router: Router) {} + isGnosis: boolean + + private ethereumNetworkKey: string + + constructor( + private oauthUtils: OAuthUtils, + private validator: ValidatorUtils, + private storage: StorageService, + private router: Router, + public api: ApiService, + private validatorUtils: ValidatorUtils, + private unit: UnitconvService, + private notificationBase: NotificationBase, + private theme: ThemeUtils, + private alert: AlertService, + private merchant: MerchantUtils + ) {} ngOnInit() { this.storage.isLoggedIn().then((result) => { this.isAlreadyLoggedIn = result }) + this.isGnosis = this.api.isGnosis() + this.ethereumNetworkKey = this.api.getNetwork().key + if (this.ethereumNetworkKey == 'gnosis') { + this.ethereumNetworkKey = 'main' + } } async openBrowser(link) { @@ -57,4 +85,20 @@ export class HelpComponent implements OnInit { if (!hasValidators) this.router.navigate(['/tabs/validators']) } } + + async switchNetwork() { + await changeNetwork( + this.api.isGnosis() ? this.ethereumNetworkKey : 'gnosis', + this.storage, + this.api, + this.validatorUtils, + this.unit, + this.notificationBase, + this.theme, + this.alert, + this.merchant, + true + ) + this.isGnosis = this.api.isGnosis() + } } diff --git a/src/app/components/machinechart/machinechart.component.ts b/src/app/components/machinechart/machinechart.component.ts index 387f103d..2374c8fa 100644 --- a/src/app/components/machinechart/machinechart.component.ts +++ b/src/app/components/machinechart/machinechart.component.ts @@ -3,6 +3,7 @@ import { highChartOptions } from 'src/app/utils/HighchartOptions' import * as HighCharts from 'highcharts' import * as Highstock from 'highcharts/highstock' import { MachineChartData } from 'src/app/controllers/MachineController' +import { ApiService } from 'src/app/services/api.service' @Component({ selector: 'app-machinechart', @@ -26,6 +27,8 @@ export class MachinechartComponent implements OnInit { chartError = false specificError: string = null + constructor(private api: ApiService) {} + doClick() { this.clickAction() } @@ -58,7 +61,7 @@ export class MachinechartComponent implements OnInit { } ngOnInit() { - highChartOptions(Highstock) + highChartOptions(Highstock, this.api.getHostName()) this.id = makeid(6) } diff --git a/src/app/components/message/message.component.ts b/src/app/components/message/message.component.ts index 51738e43..df6fd5c4 100644 --- a/src/app/components/message/message.component.ts +++ b/src/app/components/message/message.component.ts @@ -99,13 +99,13 @@ export class MessageComponent implements OnInit { buttons: [ { text: 'Do not show again', - handler: async () => { + handler: () => { this.dismiss() }, }, { text: 'Decide Later', - handler: async () => { + handler: () => { this.notDismissed = false }, }, diff --git a/src/app/components/validator/validator.component.html b/src/app/components/validator/validator.component.html index 160f54ab..3e37e9ab 100644 --- a/src/app/components/validator/validator.component.html +++ b/src/app/components/validator/validator.component.html @@ -28,7 +28,7 @@
{{ name }} - {{ balance | mcurrency : 'GWEI' : unit.pref }} + {{ balance | mcurrency : 'GWEI' : unit.pref.Cons }} {{ validator.attrEffectiveness && validator.attrEffectiveness != -1 ? 'Eff.:' + validator.attrEffectiveness + '%' : '' }} diff --git a/src/app/controllers/MachineController.ts b/src/app/controllers/MachineController.ts index 2d07b37b..bb4bfaa3 100644 --- a/src/app/controllers/MachineController.ts +++ b/src/app/controllers/MachineController.ts @@ -598,18 +598,19 @@ export const bytes = (function () { tempLabel = [] let count - return function (bytes, label, isFirst, precision = 3) { + return function (byteData, label, isFirst, precision = 3) { let e, value - if (bytes == 0) return 0 + if (byteData == 0) return 0 if (isFirst) count = 0 - e = Math.floor(Math.log(bytes) / Math.log(1024)) - value = (bytes / Math.pow(1024, Math.floor(e))).toFixed(precision) + e = Math.floor(Math.log(byteData) / Math.log(1024)) + value = (byteData / Math.pow(1024, Math.floor(e))).toFixed(precision) tempLabel[count] = value - if (count > 0 && Math.abs(tempLabel[count - 1] - tempLabel[count]) < 0.0001) value = (bytes / Math.pow(1024, Math.floor(--e))).toFixed(precision) + if (count > 0 && Math.abs(tempLabel[count - 1] - tempLabel[count]) < 0.0001) + value = (byteData / Math.pow(1024, Math.floor(--e))).toFixed(precision) e = e < 0 ? -e : e if (label) value += ' ' + s[e] diff --git a/src/app/controllers/OverviewController.ts b/src/app/controllers/OverviewController.ts index 4ac95ab7..5ab41a48 100644 --- a/src/app/controllers/OverviewController.ts +++ b/src/app/controllers/OverviewController.ts @@ -18,12 +18,14 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { EpochResponse, SyncCommitteeResponse, ValidatorResponse } from '../requests/requests' +import { EpochResponse, ProposalLuckResponse, SyncCommitteeResponse, ValidatorResponse } from '../requests/requests' import { sumBigInt, findHighest, findLowest } from '../utils/MathUtils' import BigNumber from 'bignumber.js' import { getValidatorQueryString, ValidatorState, Validator } from '../utils/ValidatorUtils' import { formatDate } from '@angular/common' import { SyncCommitteesStatistics, SyncCommitteesStatisticsResponse } from '../requests/requests' +import { UnitconvService } from '../services/unitconv.service' +import { ApiNetwork } from '../models/StorageTypes' export type OverviewData = { overallBalance: BigNumber @@ -43,7 +45,7 @@ export type OverviewData = { worstTopPercentage: number displayAttrEffectiveness: boolean attrEffectiveness: number - + proposalLuckResponse: ProposalLuckResponse dashboardState: DashboardStatus lazyLoadChart: boolean lazyChartValidators: string @@ -57,6 +59,7 @@ export type OverviewData = { currentSyncCommittee: SyncCommitteeResponse nextSyncCommittee: SyncCommitteeResponse syncCommitteesStats: SyncCommitteesStatistics + proposalLuck: ProposalLuckResponse withdrawalsEnabledForAll: boolean } @@ -122,23 +125,36 @@ export type Description = { const VALIDATOR_32ETH = new BigNumber(32000000000) export default class OverviewController { - constructor(private refreshCallback: () => void = null, private userMaxValidators = 280) {} + constructor(private refreshCallback: () => void = null, private userMaxValidators = 280, private unit: UnitconvService = null) {} - public processDashboard(validators: Validator[], currentEpoch: EpochResponse, syncCommitteesStatsResponse = null) { - return this.process(validators, currentEpoch, false, syncCommitteesStatsResponse) + public processDashboard( + validators: Validator[], + currentEpoch: EpochResponse, + syncCommitteesStatsResponse = null, + proposalLuckResponse: ProposalLuckResponse = null, + network: ApiNetwork + ) { + return this.process(validators, currentEpoch, false, syncCommitteesStatsResponse, proposalLuckResponse, network) } - public processDetail(validators: Validator[], currentEpoch: EpochResponse) { - return this.process(validators, currentEpoch, true, null) + public processDetail(validators: Validator[], currentEpoch: EpochResponse, network: ApiNetwork) { + return this.process(validators, currentEpoch, true, null, null, network) } - private process(validators: Validator[], currentEpoch: EpochResponse, foreignValidator = false, syncCommitteesStatsResponse = null): OverviewData { + private process( + validators: Validator[], + currentEpoch: EpochResponse, + foreignValidator = false, + syncCommitteesStatsResponse = null, + proposalLuckResponse: ProposalLuckResponse = null, + network: ApiNetwork + ): OverviewData { if (!validators || validators.length <= 0 || currentEpoch == null) return null const effectiveBalance = sumBigInt(validators, (cur) => cur.data.effectivebalance) const validatorDepositActive = sumBigInt(validators, (cur) => { if (cur.data.activationepoch <= currentEpoch.epoch) { - if (!cur.rocketpool || !cur.rocketpool.node_address) return VALIDATOR_32ETH + if (!cur.rocketpool || !cur.rocketpool.node_address) return VALIDATOR_32ETH.multipliedBy(new BigNumber(cur.share == null ? 1 : cur.share)) let nodeDeposit if (cur.rocketpool.node_deposit_balance) { @@ -146,14 +162,14 @@ export default class OverviewController { } else { nodeDeposit = VALIDATOR_32ETH.dividedBy(new BigNumber(2)) } - return nodeDeposit + return nodeDeposit.multipliedBy(new BigNumber(cur.share == null ? 1 : cur.share)) } else { return new BigNumber(0) } }) const aprPerformance31dExecution = sumBigInt(validators, (cur) => - this.sumExcludeSmoothingPool(cur, (cur) => cur.execution.performance31d.toString()) + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performance31d.toString()) ) const overallBalance = this.sumBigIntBalanceRP(validators, (cur) => new BigNumber(cur.data.balance)) @@ -165,13 +181,27 @@ export default class OverviewController { const executionPerf = this.getExecutionPerformance(validators, validatorDepositActive, aprPerformance31dExecution) + const smoothingPoolClaimed = this.sumRocketpoolSmoothingBigIntPerNodeAddress( + true, + validators, + (cur) => cur.rocketpool.claimed_smoothing_pool, + (cur) => cur.execshare + ).dividedBy(new BigNumber('1e9')) + + const smoothingPoolUnclaimed = this.sumRocketpoolSmoothingBigIntPerNodeAddress( + true, + validators, + (cur) => cur.rocketpool.unclaimed_smoothing_pool, + (cur) => cur.execshare + ).dividedBy(new BigNumber('1e9')) + const combinedPerf = { - performance1d: consensusPerf.performance1d.plus(executionPerf.performance1d), - performance31d: consensusPerf.performance31d.plus(executionPerf.performance31d), - performance7d: consensusPerf.performance7d.plus(executionPerf.performance7d), - performance365d: consensusPerf.performance365d.plus(executionPerf.performance365d), - apr: this.getAPRFromMonth(validatorDepositActive, aprPerformance31dExecution.plus(consensusPerf.performance31d)), - total: consensusPerf.total.plus(executionPerf.total), + performance1d: consensusPerf.performance1d.plus(this.unit.convertELtoCL(executionPerf.performance1d)), + performance31d: consensusPerf.performance31d.plus(this.unit.convertELtoCL(executionPerf.performance31d)), + performance7d: consensusPerf.performance7d.plus(this.unit.convertELtoCL(executionPerf.performance7d)), + performance365d: consensusPerf.performance365d.plus(this.unit.convertELtoCL(executionPerf.performance365d)), + apr: this.getAPRFromMonth(validatorDepositActive, this.unit.convertELtoCL(aprPerformance31dExecution).plus(consensusPerf.performance31d)), + total: consensusPerf.total.plus(this.unit.convertELtoCL(executionPerf.total)).plus(smoothingPoolClaimed).plus(smoothingPoolUnclaimed), } let attrEffectiveness = 0 @@ -234,7 +264,7 @@ export default class OverviewController { consensusPerformance: consensusPerf, executionPerformance: executionPerf, combinedPerformance: combinedPerf, - dashboardState: this.getDashboardState(validators, currentEpoch, foreignValidator), + dashboardState: this.getDashboardState(validators, currentEpoch, foreignValidator, network), lazyLoadChart: true, lazyChartValidators: getValidatorQueryString(validators, 2000, this.userMaxValidators - 1), foreignValidator: foreignValidator, @@ -242,10 +272,11 @@ export default class OverviewController { foreignValidatorWithdrawalCredsAre0x01: foreignWCAre0x01, effectiveBalance: effectiveBalance, currentEpoch: currentEpoch, - apr: consensusPerf.apr, + apr: combinedPerf.apr, currentSyncCommittee: currentSync ? currentSync.currentSyncCommittee : null, nextSyncCommittee: nextSync ? nextSync.nextSyncCommittee : null, - syncCommitteesStats: this.calculateSyncCommitteeStats(syncCommitteesStatsResponse), + syncCommitteesStats: this.calculateSyncCommitteeStats(syncCommitteesStatsResponse, network), + proposalLuckResponse: proposalLuckResponse, withdrawalsEnabledForAll: validators.filter((cur) => (cur.data.withdrawalcredentials.startsWith('0x01') ? true : false)).length == validatorCount, rocketpool: { @@ -276,18 +307,8 @@ export default class OverviewController { fee: feeAvg, status: foreignValidator && validators[0].rocketpool ? validators[0].rocketpool.minipool_status : null, depositType: foreignValidator && validators[0].rocketpool ? validators[0].rocketpool.minipool_deposit_type : null, - smoothingPoolClaimed: this.sumRocketpoolSmoothingBigIntPerNodeAddress( - true, - validators, - (cur) => cur.rocketpool.claimed_smoothing_pool, - (cur) => cur.execshare - ), - smoothingPoolUnclaimed: this.sumRocketpoolSmoothingBigIntPerNodeAddress( - true, - validators, - (cur) => cur.rocketpool.unclaimed_smoothing_pool, - (cur) => cur.execshare - ), + smoothingPoolClaimed: smoothingPoolClaimed, + smoothingPoolUnclaimed: smoothingPoolUnclaimed, rplUnclaimed: this.sumRocketpoolBigIntPerNodeAddress( true, validators, @@ -311,17 +332,27 @@ export default class OverviewController { private getExecutionPerformance(validators: Validator[], validatorDepositActive: BigNumber, aprPerformance31dExecution: BigNumber) { const performance1d = this.sumBigIntPerformanceRP(validators, (cur) => - this.sumExcludeSmoothingPool(cur, (cur) => cur.execution.performance1d.toString()).multipliedBy( + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performance1d.toString()).multipliedBy( new BigNumber(cur.execshare == null ? 1 : cur.execshare) ) ) const performance31d = this.sumBigIntPerformanceRP(validators, (cur) => - this.sumExcludeSmoothingPool(cur, (cur) => cur.execution.performance31d.toString()).multipliedBy( + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performance31d.toString()).multipliedBy( new BigNumber(cur.execshare == null ? 1 : cur.execshare) ) ) const performance7d = this.sumBigIntPerformanceRP(validators, (cur) => - this.sumExcludeSmoothingPool(cur, (cur) => cur.execution.performance7d.toString()).multipliedBy( + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performance7d.toString()).multipliedBy( + new BigNumber(cur.execshare == null ? 1 : cur.execshare) + ) + ) + const performance365d = this.sumBigIntPerformanceRP(validators, (cur) => + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performance365d.toString()).multipliedBy( + new BigNumber(cur.execshare == null ? 1 : cur.execshare) + ) + ) + const total = this.sumBigIntPerformanceRP(validators, (cur) => + this.sumExcludeSmoothingPool(cur, (fieldCur) => fieldCur.execution.performanceTotal.toString()).multipliedBy( new BigNumber(cur.execshare == null ? 1 : cur.execshare) ) ) @@ -331,9 +362,9 @@ export default class OverviewController { performance1d: performance1d, performance31d: performance31d, performance7d: performance7d, - performance365d: new BigNumber(0), // not yet implemented + performance365d: performance365d, apr: aprExecution, - total: new BigNumber(0), // not yet implemented + total: total, } } @@ -539,7 +570,7 @@ export default class OverviewController { return new BigNumber(performance.toString()).multipliedBy('1177').dividedBy(validatorDepositActive).decimalPlaces(1).toNumber() } - private getDashboardState(validators: Validator[], currentEpoch: EpochResponse, foreignValidator): DashboardStatus { + private getDashboardState(validators: Validator[], currentEpoch: EpochResponse, foreignValidator, network: ApiNetwork): DashboardStatus { // collect data const validatorCount = validators.length const activeValidators = this.getActiveValidators(validators) @@ -569,7 +600,7 @@ export default class OverviewController { // The first one that matches will set the icon and its css as well as, if only one state matches, the extendedDescription // handle slashed validators - this.updateStateSlashed(dashboardStatus, slashedCount, validatorCount, foreignValidator, slashedValidators[0]?.data, currentEpoch) + this.updateStateSlashed(dashboardStatus, slashedCount, validatorCount, foreignValidator, slashedValidators[0]?.data, currentEpoch, network) // handle offline validators const offlineCount = validatorCount - activeValidatorCount - exitedValidatorsCount - slashedCount - awaitingCount - eligibilityCount @@ -577,19 +608,27 @@ export default class OverviewController { // handle awaiting activation validators if (awaitingCount > 0) { - this.updateStateAwaiting(dashboardStatus, awaitingCount, validatorCount, foreignValidator, awaitingActivation[0].data, currentEpoch) + this.updateStateAwaiting(dashboardStatus, awaitingCount, validatorCount, foreignValidator, awaitingActivation[0].data, currentEpoch, network) } // handle eligable validators if (eligibilityCount > 0) { - this.updateStateEligibility(dashboardStatus, eligibilityCount, validatorCount, foreignValidator, activationEligibility[0].data, currentEpoch) + this.updateStateEligibility( + dashboardStatus, + eligibilityCount, + validatorCount, + foreignValidator, + activationEligibility[0].data, + currentEpoch, + network + ) } // handle exited validators this.updateExitedState(dashboardStatus, exitedValidatorsCount, validatorCount, foreignValidator) // handle ok state, always call last - this.updateStateOk(dashboardStatus, activeValidatorCount, validatorCount, foreignValidator, activeValidators[0]?.data, currentEpoch) + this.updateStateOk(dashboardStatus, activeValidatorCount, validatorCount, foreignValidator, activeValidators[0]?.data, currentEpoch, network) if (dashboardStatus.state == StateType.mixed) { // remove extended description if more than one state is shown @@ -604,12 +643,12 @@ export default class OverviewController { return foreignValidator ? foreignText : myText } - private getExitingDescription(validatorResp: ValidatorResponse, currentEpoch: EpochResponse): Description { + private getExitingDescription(validatorResp: ValidatorResponse, currentEpoch: EpochResponse, network: ApiNetwork): Description { if (!validatorResp.exitepoch) return { extendedDescriptionPre: null, extendedDescription: null } const exitDiff = validatorResp.exitepoch - currentEpoch.epoch const isExiting = exitDiff >= 0 && exitDiff < 6480 // ~ 1 month - const exitingDate = isExiting ? this.getEpochDate(validatorResp.exitepoch, currentEpoch) : null + const exitingDate = isExiting ? this.getEpochDate(validatorResp.exitepoch, currentEpoch, network) : null return { extendedDescriptionPre: isExiting ? 'Exiting on ' : null, @@ -631,7 +670,8 @@ export default class OverviewController { validatorCount: number, foreignValidator: boolean, validatorResp: ValidatorResponse, - currentEpoch: EpochResponse + currentEpoch: EpochResponse, + network: ApiNetwork ) { if (slashedCount == 0) { return @@ -640,7 +680,7 @@ export default class OverviewController { if (dashboardStatus.state == StateType.none) { dashboardStatus.icon = 'alert-circle-outline' dashboardStatus.iconCss = 'err' - const exitingDescription = this.getExitingDescription(validatorResp, currentEpoch) + const exitingDescription = this.getExitingDescription(validatorResp, currentEpoch, network) dashboardStatus.extendedDescriptionPre = exitingDescription.extendedDescriptionPre dashboardStatus.extendedDescription = exitingDescription.extendedDescription dashboardStatus.state = StateType.slashed @@ -662,14 +702,15 @@ export default class OverviewController { validatorCount: number, foreignValidator: boolean, awaitingActivation: ValidatorResponse, - currentEpoch: EpochResponse + currentEpoch: EpochResponse, + network: ApiNetwork ) { if (awaitingCount == 0) { return } if (dashboardStatus.state == StateType.none) { - const estEta = this.getEpochDate(awaitingActivation.activationeligibilityepoch, currentEpoch) + const estEta = this.getEpochDate(awaitingActivation.activationeligibilityepoch, currentEpoch, network) dashboardStatus.icon = 'timer-outline' dashboardStatus.iconCss = 'waiting' @@ -696,13 +737,14 @@ export default class OverviewController { validatorCount: number, foreignValidator: boolean, eligbleState: ValidatorResponse, - currentEpoch: EpochResponse + currentEpoch: EpochResponse, + network: ApiNetwork ) { if (eligibilityCount == 0) { return } - const estEta = this.getEpochDate(eligbleState.activationeligibilityepoch, currentEpoch) + const estEta = this.getEpochDate(eligbleState.activationeligibilityepoch, currentEpoch, network) if (dashboardStatus.state == StateType.none) { dashboardStatus.icon = 'timer-outline' @@ -776,7 +818,8 @@ export default class OverviewController { validatorCount: number, foreignValidator: boolean, validatorResp: ValidatorResponse, - currentEpoch: EpochResponse + currentEpoch: EpochResponse, + network: ApiNetwork ) { if (dashboardStatus.state != StateType.mixed && dashboardStatus.state != StateType.none) { return @@ -790,14 +833,14 @@ export default class OverviewController { highlight: true, }) - const exitingDescription = this.getExitingDescription(validatorResp, currentEpoch) + const exitingDescription = this.getExitingDescription(validatorResp, currentEpoch, network) dashboardStatus.extendedDescriptionPre = exitingDescription.extendedDescriptionPre dashboardStatus.extendedDescription = exitingDescription.extendedDescription dashboardStatus.state = StateType.online } } - private getEpochDate(activationEpoch: number, currentEpoch: EpochResponse) { + private getEpochDate(activationEpoch: number, currentEpoch: EpochResponse, network: ApiNetwork) { try { const diff = activationEpoch - currentEpoch.epoch if (diff <= 0) { @@ -807,7 +850,7 @@ export default class OverviewController { const date = new Date(currentEpoch.lastCachedTimestamp) - const inEpochOffset = (32 - currentEpoch.scheduledblocks) * 12 // block time 12s + const inEpochOffset = (network.slotPerEpoch - currentEpoch.scheduledblocks) * network.slotsTime date.setSeconds(diff * 6.4 * 60 - inEpochOffset) @@ -855,15 +898,15 @@ export default class OverviewController { }) } - private calculateSyncCommitteeStats(stats: SyncCommitteesStatisticsResponse): SyncCommitteesStatistics { + private calculateSyncCommitteeStats(stats: SyncCommitteesStatisticsResponse, network: ApiNetwork): SyncCommitteesStatistics { if (stats) { // if no slots where expected yet, don't show any statistic as either no validator is subscribed or they have not been active in the selected timeframe if (stats.expectedSlots > 0) { const slotsTotal = stats.participatedSlots + stats.missedSlots - const slotsPerSyncPeriod = 32 * 256 + const slotsPerSyncPeriod = network.slotPerEpoch * network.epochsPerSyncPeriod const r: SyncCommitteesStatistics = { - committeesParticipated: Math.ceil(slotsTotal / 32 / 256), - committeesExpected: Math.round((stats.expectedSlots * 100) / 32 / 256) / 100, + committeesParticipated: Math.ceil(slotsTotal / network.slotPerEpoch / network.epochsPerSyncPeriod), + committeesExpected: Math.round((stats.expectedSlots * 100) / network.slotPerEpoch / network.epochsPerSyncPeriod) / 100, slotsPerSyncCommittee: slotsPerSyncPeriod, slotsLeftInSyncCommittee: slotsPerSyncPeriod - stats.scheduledSlots, slotsParticipated: stats.participatedSlots, diff --git a/src/app/models/StorageTypes.ts b/src/app/models/StorageTypes.ts index a5e34487..be7660c4 100644 --- a/src/app/models/StorageTypes.ts +++ b/src/app/models/StorageTypes.ts @@ -34,6 +34,27 @@ export interface ApiNetwork { onlyDebug: boolean active: boolean genesisTs: number + elCurrency: NetworkMainCurrency + clCurrency: NetworkMainCurrency + slotPerEpoch: number + slotsTime: number + epochsPerSyncPeriod: number + name: string +} + +export class NetworkMainCurrency { + static readonly ETH = new NetworkMainCurrency('ETHER', 'Ether', 'ETH') + static readonly GNO = new NetworkMainCurrency('GNO', 'GNO', 'GNO') + static readonly xDAI = new NetworkMainCurrency('xDAI', 'xDAI', 'DAI') + + public internalName: string + public formattedName: string + public coinbaseSpot: string + private constructor(internName: string, formattedName: string, coinbaseSpot: string) { + this.internalName = internName + this.coinbaseSpot = coinbaseSpot + this.formattedName = formattedName + } } export interface NetworkPreferences { diff --git a/src/app/pages/block-detail/block-detail.page.html b/src/app/pages/block-detail/block-detail.page.html index 4e9b6942..012c47da 100644 --- a/src/app/pages/block-detail/block-detail.page.html +++ b/src/app/pages/block-detail/block-detail.page.html @@ -23,8 +23,8 @@
- - {{ producerReward | mcurrency: "WEI":unit.pref }} + + {{ producerReward | mcurrency: "WEI":unit.pref.Exec }}
@@ -51,14 +51,14 @@ Producer Reward - - {{ block.producerReward | mcurrency: "WEI":unit.pref }} + + {{ block.producerReward | mcurrency: "WEI":unit.pref.Exec }} Burned - {{ burned | mcurrency: "WEI":unit.pref }} + {{ burned | mcurrency: "WEI":unit.pref.Exec }} diff --git a/src/app/pages/block-detail/block-detail.page.ts b/src/app/pages/block-detail/block-detail.page.ts index c77f482a..59511df1 100644 --- a/src/app/pages/block-detail/block-detail.page.ts +++ b/src/app/pages/block-detail/block-detail.page.ts @@ -78,32 +78,17 @@ export class BlockDetailPage implements OnInit { this.showGasUsedPercent = !this.showGasUsedPercent } - switchCurrencyPipe() { - if (this.unit.pref == 'ETHER') { - if (UnitconvService.currencyPipe == null) return - this.unit.pref = UnitconvService.currencyPipe - } else { - UnitconvService.currencyPipe = this.unit.pref - this.unit.pref = 'ETHER' - } - } - async openBlock() { await Browser.open({ - url: (await this.getBaseBrowserUrl()) + '/block/' + this.block.blockNumber, + url: this.api.getBaseUrl() + '/block/' + this.block.blockNumber, toolbarColor: '#2f2e42', }) } async openFeeRecipient() { await Browser.open({ - url: (await this.getBaseBrowserUrl()) + '/address/' + this.feeRecipient, + url: this.api.getBaseUrl() + '/address/' + this.feeRecipient, toolbarColor: '#2f2e42', }) } - - async getBaseBrowserUrl() { - const net = (await this.api.networkConfig).net - return 'https://' + net + 'beaconcha.in' - } } diff --git a/src/app/pages/dev/dev.page.ts b/src/app/pages/dev/dev.page.ts index ac036e37..68d2f767 100644 --- a/src/app/pages/dev/dev.page.ts +++ b/src/app/pages/dev/dev.page.ts @@ -4,7 +4,7 @@ import { CURRENT_TOKENKEY } from 'src/app/utils/FirebaseUtils' import { Tab3Page } from 'src/app/tab-preferences/tab-preferences.page' import { Toast } from '@capacitor/toast' import { Clients } from '../../utils/ClientUpdateUtils' -import { DevModeEnabled } from 'src/app/services/api.service' +import { DevModeEnabled } from 'src/app/services/storage.service' @Component({ selector: 'app-dev', @@ -31,11 +31,17 @@ export class DevPage extends Tab3Page implements OnInit { // --- Development methods --- - forceTokenRefresh() { - this.api.refreshToken() - Toast.show({ - text: 'Token refreshed', - }) + async forceTokenRefresh() { + const result = await this.api.refreshToken() + if (result) { + Toast.show({ + text: 'Token refreshed', + }) + } else { + Toast.show({ + text: 'Token refresh failed :(', + }) + } } clearApiCache() { diff --git a/src/app/pages/helppage/helppage.page.ts b/src/app/pages/helppage/helppage.page.ts index 5e534eab..309441b5 100644 --- a/src/app/pages/helppage/helppage.page.ts +++ b/src/app/pages/helppage/helppage.page.ts @@ -34,7 +34,7 @@ export class HelppagePage implements OnInit { ngOnInit() { const event = fromEvent(document, 'backbutton') - this.backbuttonSubscription = event.subscribe(async () => { + this.backbuttonSubscription = event.subscribe(() => { this.modalCtrl.dismiss() }) } diff --git a/src/app/pages/licences/licences.page.ts b/src/app/pages/licences/licences.page.ts index b589e3dc..a535469a 100644 --- a/src/app/pages/licences/licences.page.ts +++ b/src/app/pages/licences/licences.page.ts @@ -34,7 +34,7 @@ export class LicencesPage implements OnInit { ngOnInit() { this.populatePre('./3rdpartylicenses.txt') const event = fromEvent(document, 'backbutton') - this.backbuttonSubscription = event.subscribe(async () => { + this.backbuttonSubscription = event.subscribe(() => { this.modalCtrl.dismiss() }) } diff --git a/src/app/pages/machine-detail/machine-detail.page.ts b/src/app/pages/machine-detail/machine-detail.page.ts index f39c2b0e..124677cb 100644 --- a/src/app/pages/machine-detail/machine-detail.page.ts +++ b/src/app/pages/machine-detail/machine-detail.page.ts @@ -96,7 +96,7 @@ export class MachineDetailPage extends MachineController implements OnInit { async ngOnInit() { this.selectionTimeFrame = this.timeframe const event = fromEvent(document, 'backbutton') - this.backbuttonSubscription = event.subscribe(async () => { + this.backbuttonSubscription = event.subscribe(() => { this.modalCtrl.dismiss() }) diff --git a/src/app/pages/machines/machines.page.html b/src/app/pages/machines/machines.page.html index 83a9e918..eb0800db 100644 --- a/src/app/pages/machines/machines.page.html +++ b/src/app/pages/machines/machines.page.html @@ -94,10 +94,10 @@ Machine Monitoring

- Monitor your Ethereum staking machines on the go. + Monitor your {{ api.getNetwork().name }} staking machines on the go.

- Login - Login + Setup Monitoring
diff --git a/src/app/pages/machines/machines.page.ts b/src/app/pages/machines/machines.page.ts index ce6633cf..f432d6b1 100644 --- a/src/app/pages/machines/machines.page.ts +++ b/src/app/pages/machines/machines.page.ts @@ -11,6 +11,7 @@ import MachineUtils from 'src/app/utils/MachineUtils' import { Browser } from '@capacitor/browser' import { trigger, style, animate, transition } from '@angular/animations' +import { ApiService } from 'src/app/services/api.service' @Component({ selector: 'app-machines', @@ -60,7 +61,8 @@ export class MachinesPage extends MachineController implements OnInit { private storage: StorageService, private oauthUtils: OAuthUtils, private machineUtils: MachineUtils, - private ref: ChangeDetectorRef + private ref: ChangeDetectorRef, + protected api: ApiService ) { super(storage) } @@ -84,11 +86,12 @@ export class MachinesPage extends MachineController implements OnInit { } lastEnter = 0 - ionViewWillEnter() { + async ionViewWillEnter() { if (this.lastEnter == 0 || this.lastEnter + 5 * 60 * 1000 < Date.now()) { this.lastEnter = Date.now() this.getAndProcessData() } + this.loggedIn = await this.storage.isLoggedIn() } delegater(func) { diff --git a/src/app/pages/notifications/notifications.page.html b/src/app/pages/notifications/notifications.page.html index 5c7a7ea8..1e16da12 100644 --- a/src/app/pages/notifications/notifications.page.html +++ b/src/app/pages/notifications/notifications.page.html @@ -10,7 +10,7 @@
Note: You are using the No-Google version of this app. Push notifications will not work, but you can configure webhook - notifications on beaconcha.in directly or by clicking here. + notifications on {{ api.getHostName() }} directly or by clicking here.
diff --git a/src/app/pages/notifications/notifications.page.ts b/src/app/pages/notifications/notifications.page.ts index 09232113..b0329c7a 100644 --- a/src/app/pages/notifications/notifications.page.ts +++ b/src/app/pages/notifications/notifications.page.ts @@ -73,7 +73,7 @@ export class NotificationsPage extends NotificationBase implements OnInit { } async configureWebhooks() { - await Browser.open({ url: 'https://beaconcha.in/user/webhooks', toolbarColor: '#2f2e42' }) + await Browser.open({ url: this.api.getBaseUrl() + '/user/webhooks', toolbarColor: '#2f2e42' }) } async ionViewWillEnter() { @@ -84,9 +84,7 @@ export class NotificationsPage extends NotificationBase implements OnInit { }) this.storage.getAuthUser().then((result) => (this.authUser = result)) - this.api.getNetworkName().then((result) => { - this.network = this.api.capitalize(result) - }) + this.network = this.api.capitalize(this.api.getNetworkName()) this.merchantUtils.hasCustomizableNotifications().then((result) => { this.canCustomizeThresholds = result }) diff --git a/src/app/pages/subscribe/subscribe.page.ts b/src/app/pages/subscribe/subscribe.page.ts index 1a582937..9490e426 100644 --- a/src/app/pages/subscribe/subscribe.page.ts +++ b/src/app/pages/subscribe/subscribe.page.ts @@ -9,6 +9,7 @@ import { Toast } from '@capacitor/toast' import FlavorUtils from 'src/app/utils/FlavorUtils' import { Browser } from '@capacitor/browser' +import { ApiService } from 'src/app/services/api.service' @Component({ selector: 'app-subscribe', @@ -32,14 +33,15 @@ export class SubscribePage implements OnInit { private oauth: OAuthUtils, private alertService: AlertService, private platform: Platform, - private flavor: FlavorUtils + private flavor: FlavorUtils, + private api: ApiService ) { this.selectedPackage = this.merchant.PACKAGES[2] } ngOnInit() { const event = fromEvent(document, 'backbutton') - this.backbuttonSubscription = event.subscribe(async () => { + this.backbuttonSubscription = event.subscribe(() => { this.modalCtrl.dismiss() }) @@ -80,7 +82,7 @@ export class SubscribePage implements OnInit { 'Yes', async () => { if (isNoGoogle) { - await Browser.open({ url: 'https://beaconcha.in/premium', toolbarColor: '#2f2e42' }) + await Browser.open({ url: this.api.getBaseUrl() + '/premium', toolbarColor: '#2f2e42' }) } else { this.merchant.purchase(this.selectedPackage.purchaseKey) } @@ -90,18 +92,18 @@ export class SubscribePage implements OnInit { } if (isNoGoogle) { - await Browser.open({ url: 'https://beaconcha.in/premium', toolbarColor: '#2f2e42' }) + await Browser.open({ url: this.api.getBaseUrl() + '/premium', toolbarColor: '#2f2e42' }) } else { this.merchant.purchase(this.selectedPackage.purchaseKey) } } async purchaseIntern() { - const loggedIn = await this.storage.isLoggedIn() + let loggedIn = await this.storage.isLoggedIn() if (!loggedIn) { - this.alertService.confirmDialog('Login', 'You need to login to your beaconcha.in account first. Continue?', 'Login', () => { + this.alertService.confirmDialog('Login', 'You need to login to your ' + this.api.getHostName() + ' account first. Continue?', 'Login', () => { this.oauth.login().then(async () => { - const loggedIn = await this.storage.isLoggedIn() + loggedIn = await this.storage.isLoggedIn() if (loggedIn) this.continuePurchaseIntern() }) }) diff --git a/src/app/pages/validatordetail/validatordetail.page.ts b/src/app/pages/validatordetail/validatordetail.page.ts index c99ab379..2fb125fb 100644 --- a/src/app/pages/validatordetail/validatordetail.page.ts +++ b/src/app/pages/validatordetail/validatordetail.page.ts @@ -24,6 +24,8 @@ import { ModalController } from '@ionic/angular' import OverviewController, { OverviewData } from '../../controllers/OverviewController' import { fromEvent, Subscription } from 'rxjs' import { MerchantUtils } from 'src/app/utils/MerchantUtils' +import { UnitconvService } from 'src/app/services/unitconv.service' +import { ApiService } from 'src/app/services/api.service' @Component({ selector: 'app-validatordetail', @@ -44,7 +46,13 @@ export class ValidatordetailPage implements OnInit { scrolling = false - constructor(private validatorUtils: ValidatorUtils, private modalCtrl: ModalController, private merchant: MerchantUtils) {} + constructor( + private validatorUtils: ValidatorUtils, + private modalCtrl: ModalController, + private merchant: MerchantUtils, + private unit: UnitconvService, + private api: ApiService + ) {} setInput(validator: Validator) { this.item = validator @@ -52,7 +60,7 @@ export class ValidatordetailPage implements OnInit { ngOnInit() { const event = fromEvent(document, 'backbutton') - this.backbuttonSubscription = event.subscribe(async () => { + this.backbuttonSubscription = event.subscribe(() => { this.modalCtrl.dismiss() }) this.updateDetails(this.item) @@ -87,8 +95,8 @@ export class ValidatordetailPage implements OnInit { this.name = getDisplayName(item) const epoch = await this.validatorUtils.getRemoteCurrentEpoch() - const overviewController = new OverviewController(null, await this.merchant.getCurrentPlanMaxValidator()) - this.data = overviewController.processDetail([item], epoch) + const overviewController = new OverviewController(null, await this.merchant.getCurrentPlanMaxValidator(), this.unit) + this.data = overviewController.processDetail([item], epoch, this.api.getNetwork()) } tag() { diff --git a/src/app/pipes/mcurrency.pipe.ts b/src/app/pipes/mcurrency.pipe.ts index 75b1b7af..9aeadbf3 100644 --- a/src/app/pipes/mcurrency.pipe.ts +++ b/src/app/pipes/mcurrency.pipe.ts @@ -30,6 +30,12 @@ export class McurrencyPipe implements PipeTransform { transform(value: BigNumber | number | string, ...args: unknown[]): BigNumber | string | number { const displayAble = args.length == 2 - return this.unit.convert(value, args[0] as string, args[1] as string, displayAble) + if (typeof args[1] == 'string') { + return this.unit.convertNonFiat(value, args[0] as string, args[1] as string, displayAble) + } else if (typeof args[1] == 'object' && this.unit.isCurrency(args[1])) { + return this.unit.convert(value, args[0] as string, args[1], displayAble) + } else { + console.warn('illegal usage of mcurrency pipe. Usage: value | mcurrency:from:to or value | mcurrency:from:currency') + } } } diff --git a/src/app/requests/requests.ts b/src/app/requests/requests.ts index f51de761..b25571d5 100644 --- a/src/app/requests/requests.ts +++ b/src/app/requests/requests.ts @@ -45,7 +45,6 @@ export abstract class APIRequest { return this.parseBase(response) } - // Since we use native http and axios we have various response types // Usually you can expect either a Response or a boolean // response.status can be a string though depending on the type of http connector used // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -53,12 +52,7 @@ export abstract class APIRequest { if (typeof response === 'boolean') { return response } - if (this.nativeHttp) { - if (!response || !response.status) return false - return response.status == 200 && (response.data.status == 'OK' || !hasDataStatus) - } else { - return response && (response.status == 'OK' || response.status == 200 || !hasDataStatus) - } + return response && (response.status == 'OK' || response.status == 200 || !hasDataStatus) } protected parseBase(response: Response, hasDataStatus = true): T[] { @@ -87,7 +81,6 @@ export abstract class APIRequest { updatesLastRefreshState = false ignoreFails = false maxCacheAge = 6 * 60 * 1000 - nativeHttp = true // TODO: for some reason, native HTTP Post doesnt work on iOS.. } // ------------- Responses ------------- @@ -160,7 +153,6 @@ export interface CoinbaseExchangeResponse { export interface ETH1ValidatorResponse { publickey: string - valid_signature: boolean validatorindex: number } @@ -306,6 +298,8 @@ export interface ExecutionResponse { performance1d: number performance7d: number performance31d: number + performance365d: number + performanceTotal: number } export interface NotificationGetResponse { @@ -359,18 +353,33 @@ export class ValidatorRequest extends APIRequest { } } -export class ValidatorETH1Request extends APIRequest { +export class ValidatorViaDepositAddress extends APIRequest { resource = 'validator/eth1/' method = Method.GET /** - * @param validator Index or PubKey + * @param ethAddress Address */ constructor(ethAddress: string) { super() this.resource += ethAddress.replace(/\s/g, '') } } + +export class ValidatorViaWithdrawalAddress extends APIRequest { + resource = 'validator/withdrawalCredentials/' + method = Method.GET + + /** + * @param ethAddress Address or Withdrawal Credential + */ + constructor(ethAddress: string) { + super() + this.resource += ethAddress.replace(/\s/g, '') + this.resource += '?limit=300' + } +} + export class BlockProducedByRequest extends APIRequest { resource = 'execution/' method = Method.GET @@ -421,7 +430,6 @@ export class SetMobileSettingsRequest extends APIRequest requiresAuth = true ignoreFails = true - nativeHttp = false parse(response: Response): MobileSettingsResponse[] { if (!response || !response.data) return null @@ -453,7 +461,6 @@ export class PostMobileSubscription extends APIRequest { method = Method.POST requiresAuth = true ignoreFails = true - nativeHttp = false constructor(subscriptionData: SubscriptionData) { super() @@ -492,7 +499,6 @@ export class RemoveMyValidatorsRequest extends APIRequest { requiresAuth = true postData = {} ignoreFails = true - nativeHttp = false options = { url: null, // unused @@ -518,7 +524,6 @@ export class AddMyValidatorsRequest extends APIRequest { method = Method.POST requiresAuth = true ignoreFails = true - nativeHttp = false options = { url: null, // unused @@ -591,7 +596,6 @@ export class NotificationBundleSubsRequest extends APIRequest requiresAuth = true postData = {} ignoreFails = true - nativeHttp = false constructor(enabled: boolean, data: BundleSub[]) { super() @@ -606,18 +610,34 @@ export class RefreshTokenRequest extends APIRequest { requiresAuth = true ignoreFails = true maxCacheAge = 1000 + options = { + url: null, + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + Accept: 'application/json', + }, + } parse(response: Response): ApiTokenResponse[] { if (response && response.data) return [response.data] as ApiTokenResponse[] else return [] } - constructor(refreshToken: string) { + constructor(refreshToken: string, isIOS: boolean) { super() - this.postData = new FormDataContainer({ - grant_type: 'refresh_token', - refresh_token: refreshToken, - }) + // ¯\_(ツ)_/¯ + if (isIOS) { + this.postData = { + grant_type: 'refresh_token', + refresh_token: refreshToken, + } + } else { + const formBody = new FormData() + formBody.set('grant_type', 'refresh_token') + formBody.set('refresh_token', refreshToken) + this.postData = formBody + this.options.headers = undefined + } } } @@ -626,7 +646,6 @@ export class UpdateTokenRequest extends APIRequest { method = Method.POST requiresAuth = true ignoreFails = true - nativeHttp = false parse(response: Response): APIResponse[] { if (response && response.data) return response.data as APIResponse[] @@ -641,21 +660,6 @@ export class UpdateTokenRequest extends APIRequest { // ------------ Special external api requests ----------------- -export class AdSeenRequest extends APIRequest { - endPoint = 'https://request-global.czilladx.com' - - resource = '' - method = Method.GET - ignoreFails = true - maxCacheAge = 0 - nativeHttp = false - - constructor(url: string) { - super() - this.resource = url.replace('https://request-global.czilladx.com/', '') - } -} - export class BitflyAdRequest extends APIRequest { endPoint = 'https://ads.bitfly.at' @@ -663,7 +667,11 @@ export class BitflyAdRequest extends APIRequest { method = Method.GET ignoreFails = true maxCacheAge = 4 * 60 * 1000 - nativeHttp = false + + options = { + url: null, // unused + headers: undefined, + } parse(response: Response): BitflyAdResponse[] { if (!response || !response.data) { @@ -734,14 +742,3 @@ export class GithubReleaseRequest extends APIRequest { } export default class {} - -export class FormDataContainer { - private data: unknown - constructor(data: unknown) { - this.data = data - } - - getBody() { - return this.data - } -} diff --git a/src/app/services/alert.service.ts b/src/app/services/alert.service.ts index ba9066d3..8b05d2ae 100644 --- a/src/app/services/alert.service.ts +++ b/src/app/services/alert.service.ts @@ -21,7 +21,6 @@ import { Injectable } from '@angular/core' import { AlertController, LoadingController } from '@ionic/angular' -export const SETTINGS_PAGE = 100 export const VALIDATORUTILS = 140 export const PURCHASEUTILS = 150 diff --git a/src/app/services/api.service.ts b/src/app/services/api.service.ts index 47df85c3..4952cfb7 100644 --- a/src/app/services/api.service.ts +++ b/src/app/services/api.service.ts @@ -19,16 +19,13 @@ */ import { Injectable } from '@angular/core' -import { APIRequest, FormDataContainer, Method, RefreshTokenRequest } from '../requests/requests' +import { APIRequest, Method, RefreshTokenRequest } from '../requests/requests' import { StorageService } from './storage.service' import { ApiNetwork } from '../models/StorageTypes' -import { isDevMode } from '@angular/core' import { Mutex } from 'async-mutex' import { findConfigForKey, MAP } from '../utils/NetworkData' -import { CapacitorHttp, HttpResponse } from '@capacitor/core' import { CacheModule } from '../utils/CacheModule' -import axios, { AxiosResponse } from 'axios' -import { HttpOptions } from '@capacitor/core' +import { Capacitor, HttpOptions } from '@capacitor/core' const LOGTAG = '[ApiService]' @@ -38,44 +35,33 @@ const SERVER_TIMEOUT = 25000 providedIn: 'root', }) export class ApiService extends CacheModule { - networkConfig: Promise + networkConfig: ApiNetwork public connectionStateOK = true - public isAuthorized = false - - awaitingResponses: Map = new Map() + private awaitingResponses: Map = new Map() debug = false public lastRefreshed = 0 // only updated by calls that have the updatesLastRefreshState flag enabled - lastCacheInvalidate = 0 - - private httpLegacy = axios.create({ - timeout: SERVER_TIMEOUT, - }) - - forceNativeAll = false + private lastCacheInvalidate = 0 constructor(private storage: StorageService) { - super('api', 6 * 60 * 1000, storage) - this.storage.getBooleanSetting('migrated_4_3_0', false).then((migrated) => { + super('api', 6 * 60 * 1000, storage, 1000, false) + this.storage.getBooleanSetting('migrated_4_4_0', false).then((migrated) => { if (!migrated) { this.clearHardCache() - console.info('Cleared hard cache storage as part of 4.3.0 migration') - this.storage.setBooleanSetting('migrated_4_3_0', true) + console.info('Cleared hard cache storage as part of 4.4.0 migration') + this.storage.setBooleanSetting('migrated_4_4_0', true) } }) - this.isDebugMode().then((result) => { + this.storage.isDebugMode().then((result) => { this.debug = result window.localStorage.setItem('debug', this.debug ? 'true' : 'false') }) this.lastCacheInvalidate = Date.now() - //this.registerLogMiddleware() - this.updateNetworkConfig() - //this.isIOS15().then((result) => { this.forceNativeAll = result }) } mayInvalidateOnFaultyConnectionState() { @@ -91,27 +77,27 @@ export class ApiService extends CacheModule { } } - async updateNetworkConfig() { - this.networkConfig = this.storage.getNetworkPreferences().then((config) => { + async initialize() { + this.networkConfig = await this.storage.getNetworkPreferences().then((config) => { const temp = findConfigForKey(config.key) if (temp) { return temp } return config }) - - await this.networkConfig + await this.init() + console.log('API SERVICE INITIALISED') } networkName = null - async getNetworkName(): Promise { - const temp = (await this.networkConfig).key + getNetworkName(): string { + const temp = this.networkConfig.key this.networkName = temp return temp } - async getNetwork(): Promise { - const temp = await this.networkConfig + getNetwork(): ApiNetwork { + const temp = this.networkConfig return temp } @@ -153,22 +139,23 @@ export class ApiService extends CacheModule { } const now = Date.now() - const req = new RefreshTokenRequest(user.refreshToken) + const req = new RefreshTokenRequest(user.refreshToken, Capacitor.getPlatform() == 'ios') - const formBody = new FormData() - formBody.set('grant_type', 'refresh_token') - formBody.set('refresh_token', user.refreshToken) - const url = await this.getResourceUrl(req.resource, req.endPoint) + const resp = await this.execute(req) + const response = req.parse(resp) + const result = response[0] - // use js here for the request since the native http plugin performs inconsistent across platforms with non json requests - const resp = await fetch(url, { - method: 'POST', - body: formBody, - headers: await this.getAuthHeader(true), - }) - const result = await resp.json() + // Intention to not log access token in app logs + if (this.debug) { + console.log('Refresh token', result, resp) + } else { + if (result && result.access_token) { + console.log('Refresh token', 'success') + } else { + console.log('Refresh token', result, resp) + } + } - console.log('Refresh token', result, resp) if (!result || !result.access_token) { console.warn('could not refresh token', result) return null @@ -183,28 +170,29 @@ export class ApiService extends CacheModule { private async lockOrWait(resource) { if (!this.awaitingResponses[resource]) { - console.log('Locking ', resource) this.awaitingResponses[resource] = new Mutex() } await this.awaitingResponses[resource].acquire() } - private async unlock(resource) { - console.log('Unlocking ', resource) - + private unlock(resource) { this.awaitingResponses[resource].release() } - async isNotMainnet(): Promise { - const test = (await this.networkConfig).net != '' + isNotEthereumMainnet(): boolean { + const test = this.networkConfig.key != 'main' return test } - private async getCacheKey(request: APIRequest): Promise { + isEthereumMainnet(): boolean { + return !this.isNotEthereumMainnet() + } + + private getCacheKey(request: APIRequest): string { if (request.method == Method.GET) { - return request.method + (await this.getResourceUrl(request.resource, request.endPoint)) + return request.method + this.getResourceUrl(request.resource, request.endPoint) } else if (request.cacheablePOST) { - return request.method + (await this.getResourceUrl(request.resource, request.endPoint)) + JSON.stringify(request.postData) + return request.method + this.getResourceUrl(request.resource, request.endPoint) + JSON.stringify(request.postData) } return null } @@ -216,86 +204,74 @@ export class ApiService extends CacheModule { this.invalidateCache() } - // If cached and not stale, return cache - const cached = (await this.getCache(await this.getCacheKey(request))) as Response - if (cached) { - if (this.lastRefreshed == 0) this.lastRefreshed = Date.now() - cached.cached = true - return cached - } - - const options = request.options - - // second is special case for notifications - // notifications are rescheduled if response is != 200 - // but user can switch network in the mean time, so we need to reapply the network - // the user was currently on, when they set the notification toggle - // hence the additional request.requiresAuth - if (request.endPoint == 'default' || request.requiresAuth) { - const authHeader = await this.getAuthHeader(request instanceof RefreshTokenRequest) + await this.lockOrWait(request.resource) - if (authHeader) { - const headers = { ...options.headers, ...authHeader } - options.headers = headers + try { + // If cached and not stale, return cache + const cached = (await this.getCache(this.getCacheKey(request))) as Response + if (cached) { + if (this.lastRefreshed == 0) this.lastRefreshed = Date.now() + cached.cached = true + return cached } - } - await this.lockOrWait(request.resource) + const options = request.options - console.log(LOGTAG + ' Send request: ' + request.resource, request) - const startTs = Date.now() + // second is special case for notifications + // notifications are rescheduled if response is != 200 + // but user can switch network in the mean time, so we need to reapply the network + // the user was currently on, when they set the notification toggle + // hence the additional request.requiresAuth + if (request.endPoint == 'default' || request.requiresAuth) { + const authHeader = await this.getAuthHeader(request instanceof RefreshTokenRequest) - if (this.forceNativeAll) { - // android appears to have issues with native POST right now - console.log('force native all') - request.nativeHttp = false - } + if (authHeader) { + const headers = { ...options.headers, ...authHeader } + options.headers = headers + } + } + + console.log(LOGTAG + ' Send request: ' + request.resource, request.method, request) + const startTs = Date.now() - let response: Promise - switch (request.method) { - case Method.GET: - if (request.nativeHttp) { + let response: Promise + switch (request.method) { + case Method.GET: response = this.get(request.resource, request.endPoint, request.ignoreFails, options) - } else { - response = this.legacyGet(request.resource, request.endPoint, request.ignoreFails, options) - } - break - case Method.POST: - if (request.nativeHttp) { + break + case Method.POST: response = this.post(request.resource, request.postData, request.endPoint, request.ignoreFails, options) - } else { - response = this.legacyPost(request.resource, request.postData, request.endPoint, request.ignoreFails, options) - } - break - default: - throw 'Unsupported method: ' + request.method - } + break + default: + throw 'Unsupported method: ' + request.method + } - const result = await response - this.updateConnectionState(request.ignoreFails, result && result.data && !!result.url) + const result = await response + this.updateConnectionState(request.ignoreFails, result && result.data && !!result.url) - if (!result) { - this.unlock(request.resource) - console.log(LOGTAG + ' Empty Response: ' + request.resource, Date.now() - startTs) - return result - } + if (!result) { + console.log(LOGTAG + ' Empty Response: ' + request.resource, 'took ' + (Date.now() - startTs) + 'ms') + return result + } - if ((request.method == Method.GET || request.cacheablePOST) && result && result.status == 200 && result.data) { - this.putCache(await this.getCacheKey(request), result, request.maxCacheAge) - } + if ((request.method == Method.GET || request.cacheablePOST) && result && result.status == 200 && result.data) { + this.putCache(this.getCacheKey(request), result, request.maxCacheAge) + } - if (request.updatesLastRefreshState) this.updateLastRefreshed(result) + if (request.updatesLastRefreshState) this.updateLastRefreshed(result) - this.unlock(request.resource) - console.log(LOGTAG + ' Response: ' + result.url + '', result, Date.now() - startTs) + console.log(LOGTAG + ' Response: ' + result.url + '', 'took ' + (Date.now() - startTs) + 'ms', result) - result.cached = false + result.cached = false - return result + return result + } finally { + this.unlock(request.resource) + } } async clearSpecificCache(request: APIRequest) { - this.putCache(await this.getCacheKey(request), null, request.maxCacheAge) + await this.putCache(this.getCacheKey(request), null, request.maxCacheAge) } private updateLastRefreshed(response: Response) { @@ -305,87 +281,44 @@ export class ApiService extends CacheModule { } private async get(resource: string, endpoint = 'default', ignoreFails = false, options: HttpOptions = { url: null, headers: {} }) { - const getOptions = { - url: await this.getResourceUrl(resource, endpoint), - method: 'get', + const result = await fetch(this.getResourceUrl(resource, endpoint), { + method: 'GET', headers: options.headers, - } - return CapacitorHttp.get(getOptions) - .catch((err) => { - this.updateConnectionState(ignoreFails, false) - console.warn('Connection err', err) - }) - .then((response: Response) => this.validateResponse(ignoreFails, response)) + }).catch((err) => { + this.updateConnectionState(ignoreFails, false) + console.warn('Connection err', err) + }) + if (!result) return null + return await this.validateResponse(ignoreFails, result) } - private async post(resource: string, data: unknown, endpoint = 'default', ignoreFails = false, options: HttpOptions = { url: null, headers: {} }) { + private async post(resource: string, data, endpoint = 'default', ignoreFails = false, options: HttpOptions = { url: null, headers: {} }) { if (!Object.prototype.hasOwnProperty.call(options.headers, 'Content-Type')) { - options.headers = { ...options.headers, ...{ 'Content-Type': this.getContentType(data) } } - } - - const postOptions = { - url: await this.getResourceUrl(resource, endpoint), - headers: options.headers, - data: this.formatPostData(data), - method: 'post', + if (!(data instanceof FormData)) { + options.headers = { ...options.headers, ...{ 'Content-Type': this.getContentTypeBasedOnData(data) } } + } } - return CapacitorHttp.post(postOptions) //options) - .catch((err) => { - this.updateConnectionState(ignoreFails, false) - console.warn('Connection err', err) - }) - .then((response: Response) => this.validateResponse(ignoreFails, response)) - } - - private async legacyGet(resource: string, endpoint = 'default', ignoreFails = false, options: HttpOptions = { url: null, headers: {} }) { - return this.httpLegacy - .get(await this.getResourceUrl(resource, endpoint), options) - .catch((err) => { - this.updateConnectionState(ignoreFails, false) - console.warn('Connection err', err) - }) - .then((response: AxiosResponse) => this.validateResponseLegacy(ignoreFails, response)) - } - private async legacyPost( - resource: string, - data: unknown, - endpoint = 'default', - // eslint-disable-next-line @typescript-eslint/no-unused-vars - ignoreFails = false, - options: HttpOptions = { url: null, headers: {} } - ) { - if (!Object.prototype.hasOwnProperty.call(options.headers, 'Content-Type')) { - options.headers = { ...options.headers, ...{ 'Content-Type': this.getContentType(data) } } - } - /* return this.httpLegacy - .post(await this.getResourceUrl(resource, endpoint),JSON.stringify(this.formatPostData(data)), options) - .catch((err) => { - this.updateConnectionState(ignoreFails, false); - console.warn("Connection err", err) - }) - .then((response: AxiosResponse) => this.validateResponseLegacy(ignoreFails, response)); - */ - const resp = await fetch(await this.getResourceUrl(resource, endpoint), { + const result = await fetch(this.getResourceUrl(resource, endpoint), { method: 'POST', - body: JSON.stringify(this.formatPostData(data)), headers: options.headers, + body: this.formatPostData(data, resource), + }).catch((err) => { + this.updateConnectionState(ignoreFails, false) + console.warn('Connection err', err) }) - if (resp) { - return resp.json() - } else { - return null - } + if (!result) return null + return await this.validateResponse(ignoreFails, result) } - private getContentType(data: unknown): string { - if (data instanceof FormDataContainer) return 'application/x-www-form-urlencoded' + private getContentTypeBasedOnData(data: unknown): string { + if (data instanceof FormData) return 'application/x-www-form-urlencoded' return 'application/json' } - private formatPostData(data: unknown) { - if (data instanceof FormDataContainer) return data.getBody() - return data + private formatPostData(data, resource: string): BodyInit { + if (data instanceof FormData || resource.indexOf('user/token') != -1) return data + return JSON.stringify(data) } private updateConnectionState(ignoreFails: boolean, working: boolean) { @@ -394,72 +327,55 @@ export class ApiService extends CacheModule { console.log(LOGTAG + ' setting status', working) } - private validateResponseLegacy(ignoreFails, response: AxiosResponse): Response { - if (!response || !response.data) { - // || !response.data.data + private async validateResponse(ignoreFails, response: globalThis.Response): Promise { + if (!response) { + console.warn('can not get response', response) this.updateConnectionState(ignoreFails, false) - - return { - cached: false, - data: null, - status: response.status, - headers: response.headers, - url: null, - } - } - this.updateConnectionState(ignoreFails, true) - return { - cached: false, - data: response.data, - status: response.status, - headers: response.headers, - url: response.config.url, + return } - } - - private validateResponse(ignoreFails, response: Response): Response { - if (!response || !response.data) { - // || !response.data.data + const jsonData = await response.json() + if (!jsonData) { + console.warn('not json response', response, jsonData) this.updateConnectionState(ignoreFails, false) return } this.updateConnectionState(ignoreFails, true) - return response + return { + data: jsonData, + status: response.status, + headers: response.headers, + url: response.url, + cached: false, + } as Response } - async getResourceUrl(resource: string, endpoint = 'default'): Promise { - const base = await this.getBaseUrl() + getResourceUrl(resource: string, endpoint = 'default'): string { + const base = this.getBaseUrl() if (endpoint == 'default') { - return (await this.getApiBaseUrl()) + '/' + resource + return this.getApiBaseUrl() + '/' + resource } else { const substitute = endpoint.replace('{$BASE}', base) return substitute + '/' + resource } } - async getApiBaseUrl() { - const cfg = await this.networkConfig - return (await this.getBaseUrl()) + cfg.endpoint + cfg.version + getApiBaseUrl() { + const cfg = this.networkConfig + return this.getBaseUrl() + cfg.endpoint + cfg.version } - async getBaseUrl(): Promise { - const cfg = await this.networkConfig + getBaseUrl(): string { + const cfg = this.networkConfig return cfg.protocol + '://' + cfg.net + cfg.host } - private async isDebugMode() { - const devMode = isDevMode() - if (devMode) return true - const permanentDevMode = (await this.storage.getObject('dev_mode')) as DevModeEnabled - return permanentDevMode && permanentDevMode.enabled - } - async getAllTestNetNames() { - const debug = await this.isDebugMode() + const debug = await this.storage.isDebugMode() const re: string[][] = [] for (const entry of MAP) { if (entry.key == 'main') continue + if (entry.key == 'gnosis') continue if (!entry.active) continue if (entry.onlyDebug && !debug) continue re.push([this.capitalize(entry.key) + ' (Testnet)', entry.key]) @@ -470,12 +386,39 @@ export class ApiService extends CacheModule { capitalize(text) { return text.charAt(0).toUpperCase() + text.slice(1) } -} -export interface Response extends HttpResponse { - cached: boolean + getHostName() { + const network = this.networkConfig + return network.host + } + + /** + * Avoid whenever possible. Most of the time you can archive your goal by using the + * api.getNetwork().clCurrency or api.getNetwork().elCurrency for currencies. + * And api.getNetwork().name for the network name and api.getCurrenciesFormatted() + * for a formatted output of one/both currencies. + * @returns true if the current network is the mainnet + */ + isGnosis() { + return this.networkConfig.key == 'gnosis' + } + + /** + * Returns the formatted currencies for the network + */ + public getCurrenciesFormatted(): string { + const network = this.networkConfig + if (network.elCurrency.internalName == network.clCurrency.internalName) { + return network.clCurrency.formattedName + } + return network.clCurrency.formattedName + ' / ' + network.elCurrency.formattedName + } } -export interface DevModeEnabled { - enabled: boolean +export interface Response { + cached: boolean + data + headers: Headers + status: number + url: string } diff --git a/src/app/services/boot-preload.service.ts b/src/app/services/boot-preload.service.ts new file mode 100644 index 00000000..a972508f --- /dev/null +++ b/src/app/services/boot-preload.service.ts @@ -0,0 +1,40 @@ +/* + * // Copyright (C) 2020 - 2021 Bitfly GmbH + * // + * // This file is part of Beaconchain Dashboard. + * // + * // Beaconchain Dashboard is free software: you can redistribute it and/or modify + * // it under the terms of the GNU General Public License as published by + * // the Free Software Foundation, either version 3 of the License, or + * // (at your option) any later version. + * // + * // Beaconchain Dashboard 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 General Public License for more details. + * // + * // You should have received a copy of the GNU General Public License + * // along with Beaconchain Dashboard. If not, see . + */ + +import { Injectable } from '@angular/core' +import { ValidatorUtils } from '../utils/ValidatorUtils' +import ClientUpdateUtils from '../utils/ClientUpdateUtils' +import { BlockUtils } from '../utils/BlockUtils' + +@Injectable({ + providedIn: 'root', +}) +export class BootPreloadService { + constructor(private validatorUtils: ValidatorUtils, private clientUpdateUtils: ClientUpdateUtils, private blockUtils: BlockUtils) {} + + preload() { + try { + this.validatorUtils.getAllMyValidators() + this.clientUpdateUtils.checkAllUpdates() + this.blockUtils.getMyBlocks(0) // preload blocks + } catch (e) { + console.warn('can not preload', e) + } + } +} diff --git a/src/app/services/storage.service.ts b/src/app/services/storage.service.ts index e21022a3..a94c2fe5 100644 --- a/src/app/services/storage.service.ts +++ b/src/app/services/storage.service.ts @@ -18,10 +18,10 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { Injectable } from '@angular/core' +import { Injectable, isDevMode } from '@angular/core' import { Plugins } from '@capacitor/core' import * as StorageTypes from '../models/StorageTypes' -import { findConfigForKey } from '../utils/NetworkData' +import { MAP, findConfigForKey } from '../utils/NetworkData' import { CacheModule } from '../utils/CacheModule' import BigNumber from 'bignumber.js' import { Platform } from '@ionic/angular' @@ -76,6 +76,13 @@ export class StorageService extends CacheModule { return this.remove(AUTH_USER) } + public async isDebugMode() { + const devMode = isDevMode() + if (devMode) return true + const permanentDevMode = (await this.getObject('dev_mode')) as DevModeEnabled + return permanentDevMode && permanentDevMode.enabled + } + async getNetworkPreferences(): Promise { const result = await this.getObject(PREFERENCES) if (!result) { @@ -85,7 +92,7 @@ export class StorageService extends CacheModule { } async setNetworkPreferences(value: StorageTypes.ApiNetwork) { - return this.setObject(PREFERENCES, value) + return await this.setObject(PREFERENCES, value) } async loadPreferencesToggles(network: string): Promise { @@ -139,7 +146,7 @@ export class StorageService extends CacheModule { } async openLogSession(modalCtr, offset: number) { - let lastLogSession = parseInt(await window.localStorage.getItem('last_log_session')) + let lastLogSession = parseInt(window.localStorage.getItem('last_log_session')) if (isNaN(lastLogSession)) lastLogSession = 0 const modal = await modalCtr.create({ @@ -158,8 +165,8 @@ export class StorageService extends CacheModule { // --- Low level --- - async setObject(key: string, value: unknown) { - this.putCache(key, value) + async setObject(key: string, value: unknown, cache = true) { + if (cache) this.putCache(key, value) await this.setItem(key, JSON.stringify(value, replacer), false) } @@ -189,16 +196,15 @@ export class StorageService extends CacheModule { private reflectiOSStorage() { try { if (!this.platform.is('ios')) return + const reflectKeys = ['CapacitorStorage.prefered_unit', 'CapacitorStorage.network_preferences', 'CapacitorStorage.auth_user'] + for (let i = 0; i < MAP.length; i++) { + if (MAP[i].key.indexOf('invalid') > -1) continue + if (MAP[i].key.indexOf('local') > -1) continue + reflectKeys.push('CapacitorStorage.validators_' + MAP[i].key) + } + StorageMirror.reflect({ - keys: [ - 'CapacitorStorage.prefered_unit', - 'CapacitorStorage.network_preferences', - 'CapacitorStorage.validators_main', - 'CapacitorStorage.validators_pyrmont', - 'CapacitorStorage.validators_prater', - 'CapacitorStorage.validators_staging', - 'CapacitorStorage.auth_user', - ], + keys: reflectKeys, }) } catch (e) { console.warn('StorageMirror exception', e) @@ -235,7 +241,7 @@ export class StorageService extends CacheModule { } } -function replacer(key, value) { +export function replacer(key, value) { const originalObject = this[key] if (originalObject instanceof Map) { return { @@ -271,3 +277,7 @@ export interface StoredTimestamp { export interface StoredShare { share: number } + +export interface DevModeEnabled { + enabled: boolean +} diff --git a/src/app/services/sync.service.ts b/src/app/services/sync.service.ts index a80c7a30..e3f31e9e 100644 --- a/src/app/services/sync.service.ts +++ b/src/app/services/sync.service.ts @@ -220,7 +220,7 @@ export class SyncService { event_threshold: syncChange.eventThreshold, onComplete: superOnComplete, }) - return true + return Promise.resolve(true) } } @@ -293,7 +293,7 @@ export class SyncService { if (success) this.lastNotifySync = Date.now() for (let j = 0; j < splice.length; j++) { - await splice[j].onComplete(success) + splice[j].onComplete(success) } } } @@ -368,18 +368,18 @@ export class SyncService { async changeGeneralNotify(value: boolean, filter: string = null) { this.storage.setBooleanSetting(SETTING_NOTIFY, value) - this.setLastChanged(SETTING_NOTIFY, SETTING_NOTIFY, filter, null, value ? 'subscribe' : 'unsubscribe') + await this.setLastChanged(SETTING_NOTIFY, SETTING_NOTIFY, filter, null, value ? 'subscribe' : 'unsubscribe') } async changeNotifyEvent(key: string, event: string, value: boolean, filter: string = null, threshold: number = null) { - const net = (await this.api.networkConfig).net + const net = this.api.networkConfig.net this.storage.setBooleanSetting(net + key, value) - this.setLastChanged(net + key, event, filter, threshold, value ? 'subscribe' : 'unsubscribe') + await this.setLastChanged(net + key, event, filter, threshold, value ? 'subscribe' : 'unsubscribe') } async changeNotifyEventUser(key: string, event: string, value: boolean, filter: string = null, threshold: number = null) { this.storage.setBooleanSetting(key, value) - this.setLastChanged(key, event, filter, threshold, value ? 'subscribe' : 'unsubscribe') + await this.setLastChanged(key, event, filter, threshold, value ? 'subscribe' : 'unsubscribe') } async reapplyNotifyEvent(event: string, filter: string = null): Promise { @@ -404,7 +404,7 @@ export class SyncService { return true } - async changeNotifyClientUpdate(key: string, value: boolean) { + changeNotifyClientUpdate(key: string, value: boolean) { this.storage.setBooleanSetting(key, value) Clients.forEach(async (client) => { @@ -421,7 +421,7 @@ export class SyncService { lastChanged: current.lastChanged, lastSynced: value, eventName: event, - network: await this.api.getApiBaseUrl(), + network: this.api.getApiBaseUrl(), eventFilter: current.eventFilter, eventThreshold: current.eventThreshold, subscribeAction: subscribeAction, @@ -441,7 +441,7 @@ export class SyncService { lastChanged: value, lastSynced: current.lastSynced, eventName: event, - network: await this.api.getApiBaseUrl(), + network: this.api.getApiBaseUrl(), eventFilter: filter, eventThreshold: threshold, subscribeAction: subscribeAction, @@ -463,7 +463,7 @@ export class SyncService { lastChanged: 0, lastSynced: 0, eventName: '', - network: await this.api.getApiBaseUrl(), + network: this.api.getApiBaseUrl(), eventFilter: null, eventThreshold: null, subscribeAction: 'subscribe', diff --git a/src/app/services/unitconv.service.ts b/src/app/services/unitconv.service.ts index af0e9376..f2ac0aa8 100644 --- a/src/app/services/unitconv.service.ts +++ b/src/app/services/unitconv.service.ts @@ -25,179 +25,486 @@ import BigNumber from 'bignumber.js' import { ApiService } from './api.service' import { CoinbaseExchangeRequest, CoinbaseExchangeResponse } from '../requests/requests' -const STORAGE_KEY = 'prefered_unit' -const STORAGE_KEY_ROCKETPOOL = 'prefered_unit_rocketpool' +const STORAGE_KEY_CONS = 'prefered_unit' // cons +const STORAGE_KEY_EXEC = 'prefered_unit_exec' +const STORAGE_KEY_RPL = 'prefered_unit_rocketpool' + +export type RewardType = 'cons' | 'exec' | 'rpl' @Injectable({ providedIn: 'root', }) export class UnitconvService { - pref = 'ETHER' - prefRpl = 'RPL' - lastPrice: BigNumber + public pref: PreferredCurrency = { + Cons: { value: 'ETHER', type: 'cons', unit: Unit.ETHER } as Currency, + Exec: { value: 'ETHER', type: 'exec', unit: Unit.ETHER } as Currency, + RPL: { value: 'ETHER', type: 'rpl', unit: Unit.RPL } as Currency, + } + public lastPrice: LastPrice + private rplETHPrice: BigNumber = new BigNumber(1) - static currencyPipe = null + private mGNOXDAI = { value: 'GNOXDAI', type: 'cons', unit: Unit.DAI_GNO_HELPER } as Currency // dummy var to help us get the price of mGNO in xDAI + + /* + Users can quickly toggle between native currency and fiat currency by clicking on the value. + This variable holds the previous setting (native or fiat) for each currency type (cons, exec, rpl. + */ + static currencyPipe: CurrencyPipe = { Cons: null, Exec: null, RPL: null } constructor(private storage: StorageService, private api: ApiService) { - this.storage.getObject(STORAGE_KEY).then((unitPref) => this.init(unitPref)) - this.storage.getObject(STORAGE_KEY_ROCKETPOOL).then((unitPref) => (this.prefRpl = this.getPref(unitPref, 'RPL'))) + this.init() } - private async getExchangeRate(unitPair: string): Promise { - if (unitPair == 'ETH-BTC') return this.getExchangeRateBitcoin() + private async init() { + await this.migrateToGnosisEra() - const req = new CoinbaseExchangeRequest(unitPair) - const response = await this.api.execute(req).catch((e) => { - console.warn('error in response getExchangeRate', e) - return null - }) - const temp = req.parse(response) - if (temp.length <= 0) return null - return temp[0] + this.lastPrice = { + Cons: await this.getLastStoredPrice(this.pref.Cons), + Exec: await this.getLastStoredPrice(this.pref.Exec), + mGNOXDAI: await this.getLastStoredPrice(this.mGNOXDAI), + } as LastPrice + + this.pref.Cons = this.createCurrency(this.getPref(await this.loadStored(STORAGE_KEY_CONS), this.getNetworkDefaultCurrency('cons')), 'cons') + this.pref.Exec = this.createCurrency(this.getPref(await this.loadStored(STORAGE_KEY_EXEC), this.getNetworkDefaultCurrency('exec')), 'exec') + this.pref.RPL = this.createCurrency(this.getPref(await this.loadStored(STORAGE_KEY_RPL), this.getNetworkDefaultCurrency('rpl')), 'rpl') + + if (!(await this.storage.getBooleanSetting('UPDATED_CURRENCY_INTEROP', false))) { + this.storage.setBooleanSetting('UPDATED_CURRENCY_INTEROP', true) + this.save() + } + + await this.updatePriceData() } - // Special bitcoin case since coinbase doesn't have an ETH_BTC spot price api endpoint - private async getExchangeRateBitcoin(): Promise { - const reqEthUsd = new CoinbaseExchangeRequest('ETH-USD') - const reqBtcUsd = new CoinbaseExchangeRequest('BTC-USD') + public async networkSwitchReload() { + UnitconvService.currencyPipe = { Cons: null, Exec: null, RPL: null } + await this.init() + } - const responseEthUsdPromise = this.api.execute(reqEthUsd) - const responseBtcUsdPromise = this.api.execute(reqBtcUsd) + public hasSameCLAndELCurrency() { + return this.getNetworkDefaultCurrency('cons') == this.getNetworkDefaultCurrency('exec') + } - const responseEthUsd = reqEthUsd.parse(await responseEthUsdPromise) - const responseBtcUsd = reqBtcUsd.parse(await responseBtcUsdPromise) - if (responseEthUsd.length <= 0 || responseBtcUsd.length <= 0) return null + public async changeCurrency(value: string) { + UnitconvService.currencyPipe = { Cons: null, Exec: null, RPL: null } - const rate = new BigNumber(responseEthUsd[0].amount).dividedBy(new BigNumber(responseBtcUsd[0].amount)) + this.pref.Cons = this.createCurrency(value, 'cons') + if (this.isDefaultCurrency(this.pref.Cons)) { + this.pref.Exec = this.createCurrency(this.getNetworkDefaultCurrency(this.pref.Exec), 'exec') + } else { + this.pref.Exec = this.createCurrency(value, 'exec') + } - return { - base: 'ETH', - currency: 'BTC', - amount: rate.toString(), - } as CoinbaseExchangeResponse + await this.updatePriceData() } - async init(unitPref) { - const temp = this.getPref(unitPref) + public async getCurrentConsFiat() { + return this.createCurrency(this.getPref(await this.loadStored(STORAGE_KEY_CONS), this.getNetworkDefaultCurrency('cons')), 'cons').value + } - this.pref = temp + private async loadStored(key: string): Promise { + const result = await this.storage.getObject(key) + if (!result) return null + return result as StoredPref + } - const unit: Unit = this.getCurrentPrefAsUnit() + private createCurrency(value: string, type: RewardType): Currency { + const result = new Currency(value, type) + result.unit = this.getUnit(result) + return result + } - const lastUpdatedPrice = (await this.storage.getObject('last_price_' + this.pref)) as LastPrice + private getPref(unitPref: StoredPref, defaultva: string): string { + if (unitPref && unitPref.prefered) { + return unitPref.prefered + } + return defaultva + } - if (lastUpdatedPrice && lastUpdatedPrice.lastPrice) { - const price = new BigNumber(lastUpdatedPrice.lastPrice) - this.lastPrice = price - } else { - this.lastPrice = unit.value + private getUnit(currency: Currency): Unit { + const result = Object.assign({}, MAPPING.get(currency.value)) + if (!result) { + return Unit.ETHER } - if (!(await this.storage.getBooleanSetting('UPDATED_CURRENCY_INTEROP', false))) { - this.storage.setBooleanSetting('UPDATED_CURRENCY_INTEROP', true) - this.save() + // Price + let price = null + if (this.lastPrice) { + if (currency.type == 'cons' && this.lastPrice.Cons) { + price = this.lastPrice.Cons + } else if (currency.type == 'exec' && this.lastPrice.Exec) { + price = this.lastPrice.Exec + } else if (currency.type == 'rpl' && this.rplETHPrice && this.lastPrice.Cons) { + price = this.getUpdatedRPLPrice() + } + } + + if (price && !this.isDefaultCurrency(currency) && currency.value != 'mGNO') { + result.value = price + } + + // Coinbase spot name (for api calls) + if (result.coinbaseSpot) { + const network = this.api.getNetwork() + if (currency.type == 'cons') { + result.coinbaseSpot = result.coinbaseSpot.replace('XXX', network.clCurrency.coinbaseSpot) + } else if (currency.type == 'exec') { + result.coinbaseSpot = result.coinbaseSpot.replace('XXX', network.elCurrency.coinbaseSpot) + } + } + return result + } + + public isDefaultCurrency(currency: Currency) { + return currency.value == this.getNetworkDefaultCurrency(currency.type) + } + + public getNetworkDefaultUnit(type: RewardType): Unit { + return MAPPING.get(this.getNetworkDefaultCurrency(type)) + } + + public getNetworkDefaultCurrency(type: RewardType | Currency): string { + if (typeof type == 'object') { + type = type.type + } + + const network = this.api.getNetwork() + if (type == 'cons') { + return network.clCurrency.internalName + } else if (type == 'exec') { + return network.elCurrency.internalName + } else if (type == 'rpl') { + return 'RPL' } - this.updatePriceData() + return 'ETHER' } - private getCurrentPrefAsUnit() { - return this.getCurrentyAsUnit(this.pref) + public convertELtoCL(el: BigNumber) { + if (this.hasSameCLAndELCurrency()) { + return el + } + return el.multipliedBy(this.lastPrice.mGNOXDAI) } - private getCurrentyAsUnit(currency) { - const unitStored: Unit = MAPPING.get(currency) - return unitStored ? unitStored : Unit.ETHER + public convertCLtoEL(cl: BigNumber) { + if (this.hasSameCLAndELCurrency()) { + return cl + } + return cl.dividedBy(this.pref.Cons.unit.value).dividedBy(this.lastPrice.mGNOXDAI) } - setRPLPrice(price: BigNumber) { - const unitStored: Unit = MAPPING.get('RPL') + public getFiatCurrency(type: RewardType) { + if (type == 'cons') { + if (this.isDefaultCurrency(this.pref.Cons)) { + return UnitconvService.currencyPipe.Cons + } else { + return this.pref.Cons.value + } + } else if (type == 'exec') { + if (this.isDefaultCurrency(this.pref.Exec)) { + return UnitconvService.currencyPipe.Exec + } else { + return this.pref.Exec.value + } + } else if (type == 'rpl') { + if (this.isDefaultCurrency(this.pref.RPL)) { + return UnitconvService.currencyPipe.RPL + } else { + return this.pref.RPL.value + } + } + } + + private async getLastStoredPrice(currency: Currency) { + const lastUpdatedConsPrice = (await this.storage.getObject(this.getLastPriceKey(currency))) as LastPrice + if (lastUpdatedConsPrice && lastUpdatedConsPrice.lastPrice) { + const price = new BigNumber(lastUpdatedConsPrice.lastPrice) + return price + } else { + return currency.unit.value + } + } + + private getLastPriceKey(currency: Currency): string { + return 'last_price_' + currency.type + '_' + currency.value + } + + public setRPLPrice(price: BigNumber) { + const unitStored: Unit = this.pref.RPL.unit if (!unitStored) return unitStored.value = convertEthUnits(price, MAPPING.get('WEI'), Unit.ETHER) + this.rplETHPrice = unitStored.value + this.pref.RPL = this.createCurrency(this.pref.Cons.value, 'rpl') + } + + private getUpdatedRPLPrice() { + return this.rplETHPrice.multipliedBy(this.isDefaultCurrency(this.pref.Cons) ? new BigNumber(1) : this.lastPrice.Cons) } - setRETHPrice(price: BigNumber) { - const unitStored: Unit = MAPPING.get('RETH') + public getRPLPrice() { + const unitStored: Unit = this.pref.RPL.unit if (!unitStored) return - unitStored.value = convertEthUnits(price, Unit.WEI, Unit.ETHER) + return unitStored.value } - async updatePriceData() { - const unit: Unit = this.getCurrentPrefAsUnit() - if (unit.coinbaseSpot) { - const exchangeRate = await this.getExchangeRate(unit.coinbaseSpot) - const bigNumAmount = exchangeRate ? new BigNumber(exchangeRate.amount) : null - if (bigNumAmount && bigNumAmount.isGreaterThan(0)) { - unit.value = bigNumAmount - this.lastPrice = bigNumAmount - this.triggerPropertyChange() - this.storage.setObject('last_price_' + this.pref, { lastPrice: bigNumAmount } as LastPrice) + private triggerPropertyChange() { + if (this.isDefaultCurrency(this.pref.Cons) && UnitconvService.currencyPipe.Cons == null) { + this.pref.Cons = this.createCurrency(this.getNetworkDefaultCurrency(this.pref.Cons), 'cons') + if (this.hasSameCLAndELCurrency()) { + this.pref.Exec = this.createCurrency(this.getNetworkDefaultCurrency(this.pref.Cons), 'exec') } else { - // Handles the case if we get no price data atm - // Currently we fall back to ether being the default unit (since price is 1:1) - // (TODO: we could do this for all eth subunits fe. finney) - this.lastPrice = unit.value - this.pref = 'ETHER' + this.pref.Exec = this.createCurrency(this.getNetworkDefaultCurrency(this.pref.Exec), 'exec') } + if (this.api.isGnosis()) { + UnitconvService.currencyPipe.Exec = this.getNetworkDefaultCurrency(this.pref.Cons) + this.lastPrice.Exec = this.lastPrice.mGNOXDAI.dividedBy(32) + } + + this.pref.RPL = this.createCurrency(this.getNetworkDefaultCurrency(this.pref.RPL), 'rpl') + return } + + this.pref.Cons = this.createCurrency(this.pref.Cons.value, 'cons') + this.pref.Exec = this.createCurrency(this.pref.Exec.value, 'exec') + this.pref.RPL = this.createCurrency(this.pref.RPL.value, 'rpl') } - private triggeredChange = false - private triggerPropertyChange() { - this.triggeredChange = true + public convertToPref(value: BigNumber, from, type: RewardType) { + if (type == 'cons') { + return this.convert(value, from, this.pref.Cons) + } + if (type == 'exec') { + return this.convert(value, from, this.pref.Exec) + } + throw new Error('Unsupported reward type') + } + + private switchConsPipe() { + if (this.isDefaultCurrency(this.pref.Cons)) { + if (UnitconvService.currencyPipe.Cons == null) return + this.pref.Cons = this.createCurrency(UnitconvService.currencyPipe.Cons, 'cons') + } else { + UnitconvService.currencyPipe.Cons = this.pref.Cons.value + this.pref.Cons = this.createCurrency(this.getNetworkDefaultCurrency('cons'), 'cons') + } + } - const temp = this.pref - this.pref = 'ETHER' - this.pref = 'FINNEY' + private switchExecPipe() { + if (this.isDefaultCurrency(this.pref.Exec)) { + if (UnitconvService.currencyPipe.Exec == null) return + this.pref.Exec = this.createCurrency(UnitconvService.currencyPipe.Exec, 'exec') + } else { + UnitconvService.currencyPipe.Exec = this.pref.Exec.value + this.pref.Exec = this.createCurrency(this.getNetworkDefaultCurrency('exec'), 'exec') + } + } - const temp2 = this.prefRpl - this.prefRpl = 'ETHER' - this.prefRpl = 'RPL' + public switchCurrencyPipe() { + this.switchConsPipe() + this.switchExecPipe() - setTimeout(() => { - this.pref = temp - this.prefRpl = temp2 - this.triggeredChange = false - }, 450) + this.pref.RPL = this.createCurrency(this.pref.Cons.value, 'rpl') } - private getPref(unitPref, defaultva = 'ETHER') { - if (unitPref) { - return unitPref.prefered - } - return defaultva + public convert(value: BigNumber | number | string, from: string, to: Currency, displayable = true) { + return this.convertBase(value, from, to.unit, displayable) } - convertToPref(value: BigNumber, from) { - return this.convert(value, from, this.pref) + /** + * Does not support fiat currencies since exchange rate is not applied to units anymore + * (cons and exec could have different conversions so they cant use the same reference to unit) + */ + public convertNonFiat(value: BigNumber | number | string, from: string, to: string, displayable = true) { + if (MAPPING.get(to).coinbaseSpot != null && to != 'ETH' && to != 'ETHER' && to != this.getNetworkDefaultCurrency('cons') && from != to) { + console.warn('convertNonFiat does not support fiat currencies. Use convert instead', value.toString(), from, to, displayable) + } + return this.convertBase(value, from, MAPPING.get(to), displayable) } - convert(value: BigNumber | number | string, from: string, to: string, displayable = true) { + private convertBase(value: BigNumber | number | string, from: string, to: Unit, displayable = true) { if (!value || !from || !to) return value const tempValue = value instanceof BigNumber ? value : new BigNumber(value) if (displayable) { - return convertDisplayable(tempValue, MAPPING.get(from), MAPPING.get(to)) + return convertDisplayable(tempValue, MAPPING.get(from), to) + } else { + return convertEthUnits(tempValue, MAPPING.get(from), to, false) + } + } + + public save() { + this.storage.setObject(STORAGE_KEY_CONS, { + prefered: this.getNetworkDefaultCurrency('cons') == this.pref.Cons.value ? null : this.pref.Cons.value, + coinbaseSpot: this.pref.Cons.unit.coinbaseSpot, + symbol: this.pref.Cons.unit.display, + rounding: this.pref.Cons.unit.rounding, + } as StoredPref) + + this.storage.setObject(STORAGE_KEY_EXEC, { + prefered: this.getNetworkDefaultCurrency('exec') == this.pref.Exec.value ? null : this.pref.Exec.value, + coinbaseSpot: this.pref.Exec.unit.coinbaseSpot, + symbol: this.pref.Exec.unit.display, + rounding: this.pref.Exec.unit.rounding, + } as StoredPref) + + this.storage.setObject(STORAGE_KEY_RPL, { + prefered: this.getNetworkDefaultCurrency('rpl') == this.pref.RPL.value ? null : this.pref.RPL.value, + coinbaseSpot: this.pref.RPL.unit.coinbaseSpot, + symbol: this.pref.RPL.unit.display, + rounding: this.pref.RPL.unit.rounding, + } as StoredPref) + } + + private async migrateToGnosisEra() { + const migratedToGnosis = await this.storage.getBooleanSetting('migrated_gnosis', false) + if (!migratedToGnosis) { + const oldCons = await this.loadStored(STORAGE_KEY_CONS) + try { + if (oldCons && oldCons.prefered == 'Ether') { + oldCons.prefered = null + await this.storage.setObject(STORAGE_KEY_CONS, oldCons) + } else { + await this.storage.setObject(STORAGE_KEY_EXEC, oldCons) + await this.storage.setObject(STORAGE_KEY_RPL, oldCons) + } + this.storage.setBooleanSetting('migrated_gnosis', true) + } catch (e) { + console.warn('could not migrate to gnosis') + } + } + } + + public isCurrency(obj: unknown): obj is Currency { + return typeof obj == 'object' && obj != null && 'value' in obj && 'type' in obj + } + + async updatePriceData() { + let skipFetchingMGNOtoDAIPrice = true + + if (!this.isDefaultCurrency(this.pref.Cons)) { + const consPrice = await this.getPriceData(this.pref.Cons.unit) + if (consPrice) { + this.lastPrice.Cons = consPrice.multipliedBy(MAPPING.get(this.getNetworkDefaultCurrency(this.pref.Cons)).value) + this.storage.setObject(this.getLastPriceKey(this.pref.Cons), { lastPrice: this.lastPrice.Cons } as LastPrice) + } else { + skipFetchingMGNOtoDAIPrice = false + this.lastPrice.Cons = this.pref.Cons.unit.value + if (this.pref.Cons.value != 'mGNO') { + this.pref.Cons.value = this.getNetworkDefaultCurrency(this.pref.Cons) + } + } + + const execPrice = await this.getPriceData(this.pref.Exec.unit) + if (execPrice) { + this.lastPrice.Exec = execPrice.multipliedBy(MAPPING.get(this.getNetworkDefaultCurrency(this.pref.Exec)).value) + this.storage.setObject(this.getLastPriceKey(this.pref.Exec), { lastPrice: this.lastPrice.Exec } as LastPrice) + } else { + skipFetchingMGNOtoDAIPrice = false + this.lastPrice.Exec = this.pref.Exec.unit.value + this.pref.Exec.value = this.getNetworkDefaultCurrency(this.pref.Exec) + } + } else { + skipFetchingMGNOtoDAIPrice = false + } + + // Two ways to get the DAI <-> mGNO price + // First, simply ask coinbase for the price of mGNO in DAI. + // Alternatively to save API calls, we can also calculate the price of mGNO in DAI by dividing the price of mGNO in Fiat by the price of DAI in Fiat. + // For the second approach we need both fiat values though + if (!skipFetchingMGNOtoDAIPrice && this.api.isGnosis()) { + const daiToGno = await this.getPriceData(this.mGNOXDAI.unit) + if (daiToGno) { + this.lastPrice.mGNOXDAI = daiToGno.dividedBy(MAPPING.get(this.getNetworkDefaultCurrency(this.pref.Cons)).value) + this.storage.setObject(this.getLastPriceKey(this.mGNOXDAI), { lastPrice: this.lastPrice.mGNOXDAI } as LastPrice) + } else { + this.lastPrice.mGNOXDAI = this.mGNOXDAI.unit.value + } } else { - return convertEthUnits(tempValue, MAPPING.get(from), MAPPING.get(to), false) + // Keep in mind that both those values already contain any base unit convertion (fe mGNO to GNO) + this.lastPrice.mGNOXDAI = this.lastPrice.Exec.dividedBy(this.lastPrice.Cons) } + + console.log('skipFetchingMGNOtoDAIPrice', skipFetchingMGNOtoDAIPrice, this.lastPrice.mGNOXDAI.toString()) + + this.triggerPropertyChange() } - save() { - if (this.triggeredChange) return - const unit = this.getCurrentPrefAsUnit() - this.storage.setObject(STORAGE_KEY, { prefered: this.pref, coinbaseSpot: unit.coinbaseSpot, symbol: unit.display, rounding: unit.rounding }) + private async getPriceData(unit: Unit) { + if (unit.coinbaseSpot) { + const splitted = unit.coinbaseSpot.split('-') + if (splitted.length == 2 && splitted[0] == splitted[1]) { + return null + } + const exchangeRate = await this.getExchangeRate(unit.coinbaseSpot) + const bigNumAmount = exchangeRate ? new BigNumber(exchangeRate.amount) : null + if (bigNumAmount && bigNumAmount.isGreaterThan(0)) { + unit.value = bigNumAmount + return bigNumAmount + } else { + // Handles the case if we get no price data atm + // Currently we fall back to the current networks default unit (since price is 1:1) + // (TODO: we could do this for all eth subunits fe. finney) + return null + } + } + return null + } - const rplUnit = this.getCurrentyAsUnit(this.prefRpl) - this.storage.setObject(STORAGE_KEY_ROCKETPOOL, { - prefered: this.prefRpl, - coinbaseSpot: rplUnit.coinbaseSpot, - symbol: rplUnit.display, - rounding: rplUnit.rounding, + private async getExchangeRate(unitPair: string): Promise { + const req = new CoinbaseExchangeRequest(unitPair) + const response = await this.api.execute(req).catch((e) => { + console.warn('error in response getExchangeRate', e) + return null }) + const temp = req.parse(response) + if (temp.length <= 0) return null + console.log('requested exchange rate for ', unitPair, 'got', temp[0].amount, 'as response') + return temp[0] } } interface LastPrice { lastPrice: BigNumber } + +interface LastPrice { + Cons: BigNumber + Exec: BigNumber + mGNOXDAI: BigNumber +} + +interface PreferredCurrency { + Cons: Currency + Exec: Currency + RPL: Currency +} + +export class Currency { + value: string + type: RewardType + unit: Unit + constructor(value: string, type: RewardType) { + this.value = value + this.type = type + } + + public toString(): string { + return this.value + } + public getCurrencyName(): string { + return this.value.charAt(0) + this.value.toLocaleLowerCase().slice(1) + } +} +interface CurrencyPipe { + Cons: string + Exec: string + RPL: string +} + +interface StoredPref { + prefered: string | null + coinbaseSpot: string + symbol: string + rounding: number +} diff --git a/src/app/tab-blocks/tab-blocks.page.html b/src/app/tab-blocks/tab-blocks.page.html index fdce75c4..d6d55b13 100644 --- a/src/app/tab-blocks/tab-blocks.page.html +++ b/src/app/tab-blocks/tab-blocks.page.html @@ -1,7 +1,9 @@ - + diff --git a/src/app/tab-blocks/tab-blocks.page.ts b/src/app/tab-blocks/tab-blocks.page.ts index 577687c2..54df0f0a 100644 --- a/src/app/tab-blocks/tab-blocks.page.ts +++ b/src/app/tab-blocks/tab-blocks.page.ts @@ -44,14 +44,16 @@ export class TabBlocksPage implements OnInit { }) this.validatorUtils.getAllValidatorsLocal().then((validators) => { this.dataSource = new InfiniteScrollDataSource(this.blockUtils.getLimit(validators.length), async (offset: number) => { - let sleepTime = 1000 - if (offset >= 50) { - sleepTime = 3500 // 20 req per minute => wait at least 3 seconds. Buffer for dashboard and sync stuff - } else if (offset >= 120) { - sleepTime = 4500 + if (offset > 0) { + let sleepTime = 1000 + if (offset >= 50) { + sleepTime = 3500 // 20 req per minute => wait at least 3 seconds. Buffer for dashboard and sync stuff + } else if (offset >= 120) { + sleepTime = 4500 + } + this.loadMore = true + await sleep(sleepTime) // handling rate limit of some sorts } - if (offset > 0) this.loadMore = true - await sleep(sleepTime) // handling rate limit of some sorts const result = await this.blockUtils.getMyBlocks(offset) this.loadMore = false return result @@ -92,7 +94,7 @@ export class TabBlocksPage implements OnInit { return await modal.present() } - async doRefresh(event) { + doRefresh(event) { setTimeout(async () => { this.virtualScroll.scrollToIndex(0) await this.dataSource.reset() @@ -100,16 +102,6 @@ export class TabBlocksPage implements OnInit { }, 1500) } - switchCurrencyPipe() { - if (this.unit.pref == 'ETHER') { - if (UnitconvService.currencyPipe == null) return - this.unit.pref = UnitconvService.currencyPipe - } else { - UnitconvService.currencyPipe = this.unit.pref - this.unit.pref = 'ETHER' - } - } - luckHelp() { if (!this.luck) { this.alertService.showInfo( diff --git a/src/app/tab-dashboard/tab-dashboard.page.ts b/src/app/tab-dashboard/tab-dashboard.page.ts index cbd32cc9..c47ff596 100644 --- a/src/app/tab-dashboard/tab-dashboard.page.ts +++ b/src/app/tab-dashboard/tab-dashboard.page.ts @@ -92,14 +92,14 @@ export class Tab1Page { this.scrolling = false } - private async removeTooltips() { + private removeTooltips() { const inputs = Array.from(document.getElementsByTagName('tooltip') as HTMLCollectionOf) for (let i = 0; i < inputs.length; i++) { inputs[i].style.display = 'none' } } - async ionViewWillEnter() { + ionViewWillEnter() { if (this.lastRefreshTs + 6 * 60 > this.getUnixSeconds()) return this.refresh() @@ -120,13 +120,23 @@ export class Tab1Page { console.warn('error getRemoteCurrentEpoch', error) return null }) - const overviewController = new OverviewController(() => { - if (this.lastRefreshTs + 60 > this.getUnixSeconds()) return - this.api.invalidateCache() - this.refresh() - }, await this.merchant.getCurrentPlanMaxValidator()) - - this.overallData = overviewController.processDashboard(validators, epoch, this.validatorUtils.syncCommitteesStatsResponse) + const overviewController = new OverviewController( + () => { + if (this.lastRefreshTs + 60 > this.getUnixSeconds()) return + this.api.invalidateCache() + this.refresh() + }, + await this.merchant.getCurrentPlanMaxValidator(), + this.unitConv + ) + + this.overallData = overviewController.processDashboard( + validators, + epoch, + this.validatorUtils.syncCommitteesStatsResponse, + this.validatorUtils.proposalLuckResponse, + this.api.getNetwork() + ) this.lastRefreshTs = this.getUnixSeconds() } diff --git a/src/app/tab-preferences/notification-base.ts b/src/app/tab-preferences/notification-base.ts index 7a4706b6..c92c3de8 100644 --- a/src/app/tab-preferences/notification-base.ts +++ b/src/app/tab-preferences/notification-base.ts @@ -3,7 +3,7 @@ import { Platform } from '@ionic/angular' import { ApiService } from 'src/app/services/api.service' import { CPU_THRESHOLD, HDD_THRESHOLD, RAM_THRESHOLD, SETTING_NOTIFY, StorageService } from 'src/app/services/storage.service' import { GetMobileSettingsRequest, MobileSettingsResponse, NotificationGetRequest } from '../requests/requests' -import { AlertService, SETTINGS_PAGE } from '../services/alert.service' +import { AlertService } from '../services/alert.service' import { SyncService } from '../services/sync.service' import ClientUpdateUtils, { Clients } from '../utils/ClientUpdateUtils' import FirebaseUtils from '../utils/FirebaseUtils' @@ -80,13 +80,6 @@ export class NotificationBase implements OnInit { if (this.platform.is('android')) { const hasToken = await this.firebaseUtils.hasNotificationToken() if (!hasToken) { - this.alerts.showError( - 'Play Service', - 'We could not enable notifications for your device which might be due to missing Google Play Services. Please note that notifications do not work without Google Play Services.', - SETTINGS_PAGE + 2 - ) - this.notify = false - return false } } @@ -97,7 +90,7 @@ export class NotificationBase implements OnInit { async loadAllToggles() { if (!(await this.storage.isLoggedIn())) return - const net = (await this.api.networkConfig).net + const net = this.api.networkConfig.net const request = new NotificationGetRequest() const response = await this.api.execute(request) @@ -105,7 +98,7 @@ export class NotificationBase implements OnInit { const isNotifyClientUpdatesEnabled = await this.storage.isNotifyClientUpdatesEnabled() - let network = await this.api.getNetworkName() + let network = this.api.getNetworkName() if (network == 'main') { network = 'mainnet' } else if (network == 'local dev') { @@ -116,7 +109,6 @@ export class NotificationBase implements OnInit { ) //network = 'prater' // use me, dear developer } - console.log('result', results, network) const clientsToActivate = [] @@ -166,7 +158,7 @@ export class NotificationBase implements OnInit { // locking toggle so we dont execute onChange when setting initial values const preferences = await this.storage.loadPreferencesToggles(net) - if (await this.api.isNotMainnet()) { + if (this.api.isNotEthereumMainnet()) { this.notify = preferences this.notifyInitialized = true this.disableToggleLock() @@ -193,7 +185,14 @@ export class NotificationBase implements OnInit { } async notifyToggle() { - if (!(await this.isSupportedOnAndroid())) return + if (!(await this.isSupportedOnAndroid())) { + this.alerts.showInfo( + 'Play Service', + 'Your device can not receive push notifications. Please note that notifications do not work without Google Play Services. As an alternative you can configure webhook notifications on the ' + + this.api.getHostName() + + ' website, otherwise changing these settings will have no effect.' + ) + } if (this.platform.is('ios') && (await this.firebaseUtils.hasSeenConsentScreenAndNotConsented())) { this.notify = false @@ -208,10 +207,13 @@ export class NotificationBase implements OnInit { } } - const net = (await this.api.networkConfig).net + const net = this.api.networkConfig.net this.storage.setBooleanSetting(net + SETTING_NOTIFY, this.notify) this.settingsChanged = true - if (!(await this.api.isNotMainnet())) { + // TODO: instead of using this.api.isEthereumMainnet(), check each app supported network and if + // one single network has notifications enabled, also sync general als true. If all are disabled, + // set general notify as false + if (this.api.isEthereumMainnet() || this.notify == true) { this.sync.changeGeneralNotify(this.notify) } @@ -289,7 +291,7 @@ export class NotificationBase implements OnInit { return count ? count : 0 } - async notifyEventToggle(eventName, filter = null, threshold = null) { + notifyEventToggle(eventName, filter = null, threshold = null) { this.settingsChanged = true this.sync.changeNotifyEvent(eventName, eventName, this.getNotifyToggleFromEvent(eventName), filter, threshold) this.api.clearSpecificCache(new NotificationGetRequest()) @@ -301,11 +303,12 @@ export class NotificationBase implements OnInit { this.sync.changeClient(clientKey, clientKey) } else { this.sync.changeClient(clientKey, 'null') + this.api.deleteAllHardStorageCacheKeyContains(clientKey) } } // include filter in key (fe used by machine toggles) - async notifyEventFilterToggle(eventName, filter = null, threshold = null) { + notifyEventFilterToggle(eventName, filter = null, threshold = null) { const key = eventName + filter const value = this.getNotifyToggleFromEvent(eventName) this.settingsChanged = true diff --git a/src/app/tab-preferences/tab-preferences.page.html b/src/app/tab-preferences/tab-preferences.page.html index 95232011..39c7575f 100644 --- a/src/app/tab-preferences/tab-preferences.page.html +++ b/src/app/tab-preferences/tab-preferences.page.html @@ -55,8 +55,7 @@ @@ -85,6 +84,7 @@ Default Ethpool Rocketpool + Gnosis Teal Green Cyan @@ -211,6 +211,7 @@ Mainnet + Gnosis {{ item[0] }} @@ -249,7 +250,7 @@ Login - + Delete Account @@ -259,18 +260,18 @@ Legal - + Privacy Policy - + Terms of Service Open Source Licences - + Imprint diff --git a/src/app/tab-preferences/tab-preferences.page.ts b/src/app/tab-preferences/tab-preferences.page.ts index 6931ee67..47450e48 100644 --- a/src/app/tab-preferences/tab-preferences.page.ts +++ b/src/app/tab-preferences/tab-preferences.page.ts @@ -81,6 +81,7 @@ export class Tab3Page { premiumLabel = '' protected package = '' + protected currentFiatCurrency constructor( protected api: ApiService, @@ -103,6 +104,9 @@ export class Tab3Page { ) {} ngOnInit() { + this.unit.getCurrentConsFiat().then((result) => { + this.currentFiatCurrency = result + }) this.theme.isDarkThemed().then((result) => (this.darkMode = result)) this.theme.getThemeColor().then((result) => (this.themeColor = result)) @@ -153,8 +157,13 @@ export class Tab3Page { this.updateUtils.convertOldToNewClientSettings() } + private loadingNotificationPage = false async goToNotificationPage() { + if (this.loadingNotificationPage) return + this.loadingNotificationPage = true + await this.sync.syncAllSettings(true) + this.loadingNotificationPage = false this.router.navigate(['/notifications']) } @@ -211,9 +220,7 @@ export class Tab3Page { ionViewWillEnter() { this.storage.getAuthUser().then((result) => (this.authUser = result)) this.debug = this.api.debug - this.api.getNetworkName().then((result) => { - this.network = result - }) + this.network = this.api.getNetworkName() } private getAllCurrencies() { @@ -232,13 +239,14 @@ export class Tab3Page { overrideDisplayCurrency = null private changeCurrencyLocked = false - changeCurrency() { + async changeCurrency() { if (this.changeCurrencyLocked) return this.changeCurrencyLocked = true + await this.api.deleteAllHardStorageCacheKeyContains('coinbase') this.overrideDisplayCurrency = this.unit.pref - this.unit.updatePriceData() + await this.unit.changeCurrency(this.currentFiatCurrency) this.unit.save() setTimeout(() => { @@ -276,7 +284,7 @@ export class Tab3Page { } } - async logout() { + logout() { this.alerts.confirmDialog('Confirm logout', 'Notifications will stop working if you sign out. Continue?', 'Logout', () => { this.confirmLogout() }) @@ -310,15 +318,19 @@ export class Tab3Page { } async changeNetwork() { - const newConfig = findConfigForKey(this.network) - await this.storage.clearCache() - await this.api.clearCache() - await this.validatorUtils.clearCache() - - await this.storage.setNetworkPreferences(newConfig) - await this.api.updateNetworkConfig() - await this.notificationBase.loadAllToggles() - this.validatorUtils.notifyListeners() + await changeNetwork( + this.network, + this.storage, + this.api, + this.validatorUtils, + this.unit, + this.notificationBase, + this.theme, + this.alerts, + this.merchant, + false + ) + this.currentFiatCurrency = await this.unit.getCurrentConsFiat() } async openIconCredit() { @@ -433,3 +445,49 @@ export class Tab3Page { await alert.present() } } + +export async function changeNetwork( + network: string, + storage: StorageService, + api: ApiService, + validatorUtils: ValidatorUtils, + unit: UnitconvService, + notificationBase: NotificationBase, + theme: ThemeUtils, + alertService: AlertService, + merchant: MerchantUtils, + forceThemeSwitch: boolean +) { + const darkTheme = await theme.isDarkThemed() + + const newConfig = findConfigForKey(network) + await storage.clearCache() + //await api.clearNetworkCache() + + await storage.setNetworkPreferences(newConfig) + await api.initialize() + await notificationBase.loadAllToggles() + await unit.networkSwitchReload() + //await this.unit.changeCurrency(this.currentFiatCurrency) + validatorUtils.notifyListeners() + + const currentTheme = theme.currentThemeColor + + if (forceThemeSwitch && (currentTheme == '' || currentTheme == 'gnosis')) { + theme.undoColor() + setTimeout(() => { + theme.toggle(darkTheme, true, api.isGnosis() ? 'gnosis' : ''), 50 + }) + } else { + const hasTheming = await merchant.hasPremiumTheming() + if (hasTheming) return + if (currentTheme == '' && !api.isGnosis()) return + if (currentTheme == 'gnosis' && api.isGnosis()) return + alertService.confirmDialog('Switch App Theme', 'Do you want to switch to the free ' + api.getNetwork().name + ' App theme?', 'Sure', () => { + theme.undoColor() + setTimeout(() => { + theme.toggle(darkTheme, true, api.isGnosis() ? 'gnosis' : ''), 50 + }) + }) + } +} diff --git a/src/app/tab-validators/tab-validators.page.html b/src/app/tab-validators/tab-validators.page.html index ee80ca2c..cfd171ec 100644 --- a/src/app/tab-validators/tab-validators.page.html +++ b/src/app/tab-validators/tab-validators.page.html @@ -26,7 +26,9 @@ - + @@ -36,13 +38,14 @@ @@ -99,13 +102,18 @@

Nothing found

- We couldn't find the validators you are looking for. Try searching by Index, Public Key or ETH address. + We couldn't find the validators you are looking for. Try searching by index, public key, deposit / withdrawal address or withdrawal + credential.

Add Validators

- You can add your validators by searching for a public key, validator index or your ETH address. + + You can add your validators by searching for a public key, validator index, your deposit / withdrawal address or withdrawal credential. +
diff --git a/src/app/tab-validators/tab-validators.page.scss b/src/app/tab-validators/tab-validators.page.scss index f59a2834..62aecf8c 100644 --- a/src/app/tab-validators/tab-validators.page.scss +++ b/src/app/tab-validators/tab-validators.page.scss @@ -93,3 +93,10 @@ cdk-virtual-scroll-viewport { margin-top: 55px; margin-bottom: 10px; } + +.hidden { + height: 0; + width: 0; + overflow: hidden; + visibility: hidden; +} diff --git a/src/app/tab-validators/tab-validators.page.ts b/src/app/tab-validators/tab-validators.page.ts index 38b9dd20..b14223b2 100644 --- a/src/app/tab-validators/tab-validators.page.ts +++ b/src/app/tab-validators/tab-validators.page.ts @@ -18,9 +18,9 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { Component } from '@angular/core' +import { Component, ViewChild } from '@angular/core' import { ValidatorUtils, Validator, ValidatorState } from '../utils/ValidatorUtils' -import { ModalController, Platform } from '@ionic/angular' +import { IonSearchbar, ModalController, Platform } from '@ionic/angular' import { ValidatordetailPage } from '../pages/validatordetail/validatordetail.page' import { ApiService } from '../services/api.service' import { AlertController } from '@ionic/angular' @@ -67,6 +67,8 @@ export class Tab2Page { selected = new Map() + @ViewChild('searchbarRef', { static: true }) searchbarRef: IonSearchbar + constructor( private validatorUtils: ValidatorUtils, public modalController: ModalController, @@ -81,6 +83,10 @@ export class Tab2Page { public unit: UnitconvService ) { this.validatorUtils.registerListener(() => { + if (this.searchResultMode && this.searchbarRef) { + this.searchResultMode = false + this.searchbarRef.value = null + } this.refresh() }) this.merchant.getCurrentPlanMaxValidator().then((result) => { @@ -101,7 +107,7 @@ export class Tab2Page { if (!this.searchResultMode) { this.dataSource.setLoadFrom(this.getDefaultDataRetriever()) } - this.dataSource.reset() + await this.dataSource.reset() } async syncRemote() { @@ -189,13 +195,13 @@ export class Tab2Page { } } - async removeAllDialog() { + removeAllDialog() { this.showDialog('Remove all', 'Do you want to remove {AMOUNT} validators from your dashboard?', () => { this.confirmRemoveAll() }) } - async addAllDialog() { + addAllDialog() { this.showDialog('Add all', 'Do you want to add {AMOUNT} validators to your dashboard?', () => { this.confirmAddAll() }) @@ -223,18 +229,18 @@ export class Tab2Page { } async confirmRemoveAll() { - this.validatorUtils.deleteAll() - this.dataSource.reset() + await this.validatorUtils.deleteAll() + await this.dataSource.reset() } async confirmAddAll() { const responses: ValidatorResponse[] = [] - this.dataSource.getItems().forEach(async (item) => { + this.dataSource.getItems().forEach((item) => { responses.push(item.data) }) - this.validatorUtils.convertToValidatorModelsAndSaveLocal(false, responses) - this.dataSource.reset() + await this.validatorUtils.convertToValidatorModelsAndSaveLocal(false, responses) + await this.dataSource.reset() } searchEvent(event) { @@ -250,8 +256,9 @@ export class Tab2Page { this.searchResultMode = true this.loading = true const isETH1Address = searchString.startsWith('0x') && searchString.length == 42 + const isWithdrawalCredential = searchString.startsWith('0x') && searchString.length == 66 - if (isETH1Address) await this.searchETH1(searchString) + if (isETH1Address || isWithdrawalCredential) await this.searchETH1(searchString) else await this.searchByPubKeyOrIndex(searchString) // this.loading = false would be preferable here but somehow the first time it is called the promises resolve instantly without waiting @@ -278,12 +285,12 @@ export class Tab2Page { private async searchETH1(target) { this.dataSource.setLoadFrom(() => { return this.validatorUtils - .searchValidatorsViaETH1(target) + .searchValidatorsViaETHAddress(target) .catch(async (error) => { if (error && error.message && error.message.indexOf('only a maximum of') > 0) { console.log('SET reachedMaxValidators to true') this.reachedMaxValidators = true - return this.validatorUtils.searchValidatorsViaETH1(target, this.currentPackageMaxValidators - 1) + return this.validatorUtils.searchValidatorsViaETHAddress(target, this.currentPackageMaxValidators - 1) } return [] }) @@ -478,7 +485,7 @@ export class Tab2Page { buttons: [ { text: 'Remove', - handler: async () => { + handler: () => { this.validatorUtils.saveRocketpoolCollateralShare(onlyOneNodeAddress.rocketpool.node_address, null) this.cancelSelect() this.validatorUtils.notifyListeners() @@ -486,7 +493,7 @@ export class Tab2Page { }, { text: 'Save', - handler: async (alertData) => { + handler: (alertData) => { const shares = alertData.share if (shares < minShareStake) { Toast.show({ @@ -546,7 +553,9 @@ export class Tab2Page { cssClass: 'my-custom-class', header: 'Define stake share', message: - 'If you own partial amounts of these validators, specify the amount of ether for a custom dashboard. First value defines your consensus share, second value your execution share.', + 'If you own partial amounts of these validators, specify the amount of ' + + this.api.getCurrenciesFormatted() + + ' for a custom dashboard. First value defines your consensus share, second value your execution share.', inputs: [ { name: 'share', @@ -564,7 +573,7 @@ export class Tab2Page { buttons: [ { text: 'Remove', - handler: async () => { + handler: () => { for (let i = 0; i < validatorSubArray.length; i++) { validatorSubArray[i].share = null validatorSubArray[i].execshare = null @@ -576,7 +585,7 @@ export class Tab2Page { }, { text: 'Save', - handler: async (alertData) => { + handler: (alertData) => { const shares = alertData.share const sharesEL = alertData.execshare if ((shares && shares < minShareStake) || (sharesEL && sharesEL < minShareStake)) { @@ -616,14 +625,4 @@ export class Tab2Page { await alert.present() } - - switchCurrencyPipe() { - if (this.unit.pref == 'ETHER') { - if (UnitconvService.currencyPipe == null) return - this.unit.pref = UnitconvService.currencyPipe - } else { - UnitconvService.currencyPipe = this.unit.pref - this.unit.pref = 'ETHER' - } - } } diff --git a/src/app/tabs/tabs.page.ts b/src/app/tabs/tabs.page.ts index 6364815f..9dd73da5 100644 --- a/src/app/tabs/tabs.page.ts +++ b/src/app/tabs/tabs.page.ts @@ -80,7 +80,7 @@ export class TabsPage { } const hasTheming = await this.merchant.hasPremiumTheming() - if (!hasTheming) { + if (!hasTheming && this.theme.currentThemeColor != 'gnosis') { this.theme.resetTheming() } } diff --git a/src/app/utils/BlockUtils.ts b/src/app/utils/BlockUtils.ts index e577e73d..00ea5d0b 100644 --- a/src/app/utils/BlockUtils.ts +++ b/src/app/utils/BlockUtils.ts @@ -21,7 +21,6 @@ import { ApiService } from '../services/api.service' import { Injectable } from '@angular/core' import { BlockProducedByRequest, BlockResponse, DashboardRequest } from '../requests/requests' -import { CacheModule } from './CacheModule' import BigNumber from 'bignumber.js' import { ValidatorUtils } from './ValidatorUtils' @@ -33,10 +32,8 @@ const MONTH = 60 * 60 * 24 * 30 @Injectable({ providedIn: 'root', }) -export class BlockUtils extends CacheModule { - constructor(public api: ApiService, public validatorUtils: ValidatorUtils) { - super() - } +export class BlockUtils { + constructor(public api: ApiService, public validatorUtils: ValidatorUtils) {} async getBlockRewardWithShare(block: BlockResponse): Promise { const proposer = block.posConsensus.proposerIndex @@ -85,7 +82,7 @@ export class BlockUtils extends CacheModule { luckPercentage: proposalLuckStats.proposal_luck, timeFrameName: proposalLuckStats.time_frame_name, userValidators: valis.length, - expectedBlocksPerMonth: MONTH / (proposalLuckStats.average_proposal_interval * 12), + expectedBlocksPerMonth: MONTH / (proposalLuckStats.average_proposal_interval * this.api.getNetwork().slotsTime), nextBlockEstimate: proposalLuckStats.next_proposal_estimate_ts * 1000, } as Luck } diff --git a/src/app/utils/CacheModule.ts b/src/app/utils/CacheModule.ts index cdc16266..bdb96c32 100644 --- a/src/app/utils/CacheModule.ts +++ b/src/app/utils/CacheModule.ts @@ -18,9 +18,11 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { StorageService } from '../services/storage.service' +import { StorageService, replacer } from '../services/storage.service' import { Validator } from './ValidatorUtils' +const LOGTAG = '[CacheModule]' + interface CachedData { maxStaleTime: number time: number @@ -32,42 +34,93 @@ export class CacheModule { private keyPrefix = '' private hardStorage: StorageService = null - initialized: Promise> + protected initialized: Promise + + private cache: Map = new Map() + private hotOnly: Map = new Map() + + private hardStorageSizeLimit: number - constructor(keyPrefix = '', staleTime = 6 * 60 * 1000, hardStorage: StorageService = null) { + constructor( + keyPrefix = '', + staleTime = 6 * 60 * 1000, + hardStorage: StorageService = null, + hardStorageSizeLimit: number = 1000, + callInit: boolean = true + ) { this.keyPrefix = keyPrefix this.staleTime = staleTime this.hardStorage = hardStorage - this.init() + this.hardStorageSizeLimit = hardStorageSizeLimit + if (callInit) this.init() } - private async init() { + protected async init() { if (this.hardStorage) { - this.hardStorage.setObject('cachemodule_' + this.keyPrefix, null) - this.initialized = this.hardStorage.getObject('cachemodule2_' + this.keyPrefix) as Promise> - const result = await this.initialized + this.hardStorage.setObject('cachemodule_' + this.keyPrefix, null, false) + await this.initHardCache() + this.initialized = Promise.resolve() + } else { + this.initialized = Promise.resolve() + } + } + + private async initHardCache() { + // dont load hardStorage if last time it was written too is more than 6 hours ago + const lastWrite = (await this.hardStorage.getObject('cachemodule2_' + this.keyPrefix + '_lastWrite')) as number + if (lastWrite && lastWrite + 6 * 60 * 60 * 1000 < this.getTimestamp()) { + console.log(LOGTAG + ' hardStorage too old, ignoring') + } else { + const result = (await this.hardStorage.getObject('cachemodule2_' + this.keyPrefix)) as Map if (result) { this.cache = result } - console.log('[CacheModule] initialized with ', this.cache) - } else { - this.initialized = new Promise>((resolve) => { - resolve(new Map()) - }) - await this.initialized + this.checkHardCacheSize() } } - private cache: Map = new Map() - private hotOnly: Map = new Map() + private async checkHardCacheSize() { + try { + let kiloBytes = null + if (this.hardStorage) { + const size = new TextEncoder().encode(JSON.stringify(this.cache, replacer)).length + kiloBytes = Math.round((size * 100) / 1024) / 100 + } + console.log(LOGTAG + ' initialized with ', kiloBytes == null ? '(unknown size)' : '(' + kiloBytes + ' KiB)', this.cache) + if (kiloBytes && kiloBytes > this.hardStorageSizeLimit) { + console.warn(LOGTAG + ' storage cap exceeded (1 MB), clearing cache') + await this.clearHardCache() + } + } catch (e) { + console.warn('could not calculate cache size') + } + } private getStoreForCacheKey(cacheKey: string): Map { // rationale: don't store big data objects in hardStorage due to severe performance impacts - const storeHard = cacheKey.indexOf('app/dashboard') >= 0 + const storeHard = + cacheKey.indexOf('app/dashboard') >= 0 || + cacheKey.indexOf('produced?offset=0') >= 0 || // first page of blocks page + (cacheKey.indexOf('beaconcha.in') < 0 && cacheKey.indexOf('gnosischa.in') < 0 && cacheKey.indexOf('ads.bitfly') < 0) return storeHard ? this.cache : this.hotOnly } - protected putCache(key: string, data: unknown, staleTime = this.staleTime) { + public async deleteAllHardStorageCacheKeyContains(search: string) { + if (!this.hardStorage) { + return + } + search = search.toLocaleLowerCase() + const keys = Array.from(this.cache.keys()) + for (let i = 0; i < keys.length; i++) { + if (keys[i].toLocaleLowerCase().indexOf(search) >= 0) { + this.cache.delete(keys[i]) + } + } + + await this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, this.cache, false) + } + + protected async putCache(key: string, data: unknown, staleTime = this.staleTime) { const cacheKey = this.getKey(key) const store = this.getStoreForCacheKey(cacheKey) @@ -79,24 +132,41 @@ export class CacheModule { try { if (this.hardStorage) { - this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, this.cache) + await this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, this.cache, false) + this.setLastHardCacheWrite() } } catch (e) { if (isQuotaExceededError(e)) { - this.clearHardCache() + await this.clearHardCache() } } } - clearCache() { - this.clearHardCache() - this.cache.clear() + private setLastHardCacheWrite() { + if (this.hardStorage) { + this.hardStorage.setObject('cachemodule2_' + this.keyPrefix + '_lastWrite', this.getTimestamp(), false) + } + } + + public async clearCache() { + await this.clearHardCache() + this.hotOnly.clear() + } + + public async clearNetworkCache() { + if (this.hardStorage) { + const network = await this.hardStorage.getNetworkPreferences() + this.deleteAllHardStorageCacheKeyContains(network.key == 'main' ? '//beaconcha.in' : '//' + network.key) + } + this.hotOnly.clear() } - clearHardCache() { + public async clearHardCache() { if (this.hardStorage) { - this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, null) + await this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, null, false) + this.setLastHardCacheWrite() + this.cache.clear() } } @@ -119,7 +189,7 @@ export class CacheModule { protected cacheMultiple(prefix: string, data: Validator[]) { if (!data || data.length <= 0) { - console.log('[CacheModule] ignore cache attempt of empty data set', data) + console.log(LOGTAG + ' ignore cache attempt of empty data set', data) return } @@ -148,10 +218,11 @@ export class CacheModule { this.cache[this.getKey(key)] = null } - invalidateAllCache() { + public invalidateAllCache() { this.cache = new Map() if (this.hardStorage) { - this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, null) + this.hardStorage.setObject('cachemodule2_' + this.keyPrefix, null, false) + this.setLastHardCacheWrite() } } diff --git a/src/app/utils/ClientUpdateUtils.ts b/src/app/utils/ClientUpdateUtils.ts index 5edc25b8..7b11b20b 100644 --- a/src/app/utils/ClientUpdateUtils.ts +++ b/src/app/utils/ClientUpdateUtils.ts @@ -119,6 +119,7 @@ export default class ClientUpdateUtils { private oldClientInfoConverted = false updates: Release[] = null lastTry = 0 + private locked = false constructor(private api: ApiService, private storage: StorageService) {} @@ -127,7 +128,7 @@ export default class ClientUpdateUtils { return null } - const client = Clients.find((client) => client.key == clientKey) + const client = Clients.find((c) => c.key == clientKey) if (client == undefined) { console.log('ClientInfo for', clientKey, 'not found') return null @@ -137,18 +138,48 @@ export default class ClientUpdateUtils { async checkAllUpdates() { if (this.lastTry + 10 * 60 * 1000 > Date.now()) return - this.updates = null + if (this.locked) return + + this.locked = true + const promiseArray: Promise[] = [] for (let i = 0; i < Clients.length; i++) { - this.append(this.checkUpdateFor(await this.storage.getItem(Clients[i].storageKey))) + promiseArray.push(this.checkUpdateFor(await this.storage.getItem(Clients[i].storageKey))) + } + + try { + const results = await Promise.all(promiseArray) + let changeFound = false + for (let i = 0; i < results.length; i++) { + if (results[i] && !this.contains(results[i])) { + changeFound = true + break + } + } + + if (changeFound) { + this.updates = null + for (let i = 0; i < results.length; i++) { + if (results[i]) { + this.append(results[i]) + } + } + } + } catch (error) { + console.error('An error occurred while checking for all updates:', error) } + this.locked = false } async checkClientUpdate(clientKey: string) { - this.remove(clientKey) - const client = this.getClientInfo(clientKey) - if (client != null) { - this.append(this.checkUpdateFor(await this.storage.getItem(client.storageKey))) + if (client == null) { + return + } + const update = await this.checkUpdateFor(await this.storage.getItem(client.storageKey)) + + if (update && !this.contains(update)) { + this.remove(clientKey) + this.append(update) } } @@ -156,8 +187,8 @@ export default class ClientUpdateUtils { return (await this.getUpdateChannel()) == 'PRERELEASE' } - private async append(info: Promise) { - const data = await info + private append(info: Release) { + const data = info if (!data) return if (this.updates == null) this.updates = [data] @@ -255,7 +286,7 @@ export default class ClientUpdateUtils { } async dismissRelease(clientKey: string, id: string) { - this.storage.setObject(LOCAL_UPDATED_KEY + clientKey, { clientKey: clientKey, version: id }) + await this.storage.setObject(LOCAL_UPDATED_KEY + clientKey, { clientKey: clientKey, version: id }) } private async getLastClosedVersion(clientKey: string): Promise { diff --git a/src/app/utils/EthereumUnits.ts b/src/app/utils/EthereumUnits.ts index 6c109ea3..bc0092ac 100644 --- a/src/app/utils/EthereumUnits.ts +++ b/src/app/utils/EthereumUnits.ts @@ -32,27 +32,32 @@ export default class Unit { public static SZABO = new Unit('Szabo', new BigNumber('1000000')) public static FINNEY = new Unit('Finney', new BigNumber('1000'), 2, null, 'Finney') - public static ETHER = new Unit('ETH', new BigNumber('1'), 5, null, 'Ether') + public static ETHER = new Unit('ETH', new BigNumber('1'), 5, 'XXX-ETH', 'Ether') public static KETHER = new Unit('KETH', new BigNumber('0.001')) public static RPL = new Unit('RPL', new BigNumber('1'), 1) // RPL TO ETH public static RPL_NAKED = new Unit('RPL', new BigNumber('1'), 2) public static NO_CURRENCY = new Unit('', new BigNumber('1'), 0) public static RETH = new Unit('RETH', new BigNumber('1'), 2) + public static DAI_GNO_HELPER = new Unit('dummy', new BigNumber(1), 5, 'DAI-GNO') - public static USDETH = new Unit('$', new BigNumber('388.43'), 2, 'ETH-USD', 'Dollar') - public static EURETH = new Unit('€', new BigNumber('329.22'), 2, 'ETH-EUR', 'Euro') - public static RUBETH = new Unit('₽', new BigNumber('38093'), 2, 'ETH-RUB', 'Rubel') - public static JPYETH = new Unit('¥', new BigNumber('38093'), 0, 'ETH-JPY', 'Yen') - public static GBPETH = new Unit('£', new BigNumber('368.5'), 2, 'ETH-GBP', 'Pound') - public static AUDETH = new Unit('A$', new BigNumber('683.65'), 2, 'ETH-AUD', 'Australian Dollar') - public static CADETH = new Unit('C$', new BigNumber('651.02'), 2, 'ETH-CAD', 'Canadian Dollar') - public static CHFETH = new Unit('CHF', new BigNumber('455.55'), 2, 'ETH-CHF', 'Swiss Franc') - public static MXNETH = new Unit('MXN', new BigNumber('455.55'), 2, 'ETH-MXN', 'Mexican Peso') - public static ZARETH = new Unit('R', new BigNumber('455.55'), 2, 'ETH-ZAR', 'South African Rand') - public static CNYETH = new Unit('元', new BigNumber('455.55'), 2, 'ETH-CNY', 'Renminbi') - public static HKDETH = new Unit('HK$', new BigNumber('455.55'), 2, 'ETH-HKD', 'Hong Kong Dollar') - public static NZDETH = new Unit('NZ$', new BigNumber('455.55'), 2, 'ETH-NZD', 'New Zealand Dollar') - public static BTCETH = new Unit('₿', new BigNumber('455.55'), 6, 'ETH-BTC', 'Bitcoin') // coinbase endpoit: invalid currency :/ workaroung in unit converter + public static XDAI = new Unit('DAI', new BigNumber('1'), 4, 'XXX-DAI', 'xDAI') + public static GNO = new Unit('GNO', new BigNumber('0.03125'), 5, 'XXX-GNO', 'GNO') + public static MGNO = new Unit('mGNO', new BigNumber('1'), 5, 'XXX-GNO', 'mGNO') + + public static USDETH = new Unit('$', new BigNumber('1'), 2, 'XXX-USD', 'Dollar') + public static EURETH = new Unit('€', new BigNumber('1'), 2, 'XXX-EUR', 'Euro') + public static RUBETH = new Unit('₽', new BigNumber('1'), 2, 'XXX-RUB', 'Rubel') + public static JPYETH = new Unit('¥', new BigNumber('1'), 0, 'XXX-JPY', 'Yen') + public static GBPETH = new Unit('£', new BigNumber('1'), 2, 'XXX-GBP', 'Pound') + public static AUDETH = new Unit('A$', new BigNumber('1'), 2, 'XXX-AUD', 'Australian Dollar') + public static CADETH = new Unit('C$', new BigNumber('1'), 2, 'XXX-CAD', 'Canadian Dollar') + public static CHFETH = new Unit('CHF', new BigNumber('1'), 2, 'XXX-CHF', 'Swiss Franc') + public static MXNETH = new Unit('MXN', new BigNumber('1'), 2, 'XXX-MXN', 'Mexican Peso') + public static ZARETH = new Unit('R', new BigNumber('1'), 2, 'XXX-ZAR', 'South African Rand') + public static CNYETH = new Unit('元', new BigNumber('1'), 2, 'XXX-CNY', 'Renminbi') + public static HKDETH = new Unit('HK$', new BigNumber('1'), 2, 'XXX-HKD', 'Hong Kong Dollar') + public static NZDETH = new Unit('NZ$', new BigNumber('1'), 2, 'XXX-NZD', 'New Zealand Dollar') + public static BTCETH = new Unit('₿', new BigNumber('1'), 6, 'XXX-BTC', 'Bitcoin') private constructor(symbol: string, value: BigNumber, rounding = 2, coinbaseSpot = null, settingsName = null) { this.display = symbol @@ -62,10 +67,25 @@ export default class Unit { this.settingName = settingsName } + public toString(): string { + return ( + this.value.toString() + + ' ' + + this.display + + ' (rounding: ' + + this.rounding + + ', coinbaseSpot: ' + + this.coinbaseSpot + + ', settingName: ' + + this.settingName + + ')' + ) + } + readonly display: string value: BigNumber readonly rounding: number - readonly coinbaseSpot: string + coinbaseSpot: string readonly settingName: string } @@ -84,6 +104,9 @@ export const MAPPING = new Map([ ['RPL_NAKED', Unit.RPL_NAKED], ['NO_CURRENCY', Unit.NO_CURRENCY], ['RETH', Unit.RETH], + ['GNO', Unit.GNO], + ['mGNO', Unit.MGNO], + ['xDAI', Unit.XDAI], ['RUBLE', Unit.RUBETH], ['YEN', Unit.JPYETH], diff --git a/src/app/utils/HighchartOptions.ts b/src/app/utils/HighchartOptions.ts index d3a9cd65..7482d61e 100644 --- a/src/app/utils/HighchartOptions.ts +++ b/src/app/utils/HighchartOptions.ts @@ -18,14 +18,14 @@ * // along with Beaconchain Dashboard. If not, see . */ -export function highChartOptions(where) { +export function highChartOptions(where, hostName) { where.setOptions({ time: { useUTC: false, }, credits: { enabled: true, - href: 'https://beaconcha.in', + href: 'https://' + hostName, text: '', style: { color: 'var(--body-color)', diff --git a/src/app/utils/MachineUtils.ts b/src/app/utils/MachineUtils.ts index e875d30f..54e5900b 100644 --- a/src/app/utils/MachineUtils.ts +++ b/src/app/utils/MachineUtils.ts @@ -104,8 +104,8 @@ export default class MachineUtils extends CacheModule { const machineNames = this.getAllMachineNamesFrom(result) console.log(LOGTAG + ' machine names', machineNames) - this.registerNewRemotesForSync(machineNames).then((result) => { - console.log(LOGTAG + ' registerNewRemotesForSync', result) + this.registerNewRemotesForSync(machineNames).then((remoteResult) => { + console.log(LOGTAG + ' registerNewRemotesForSync', remoteResult) }) // Storing all machine names if diff --git a/src/app/utils/MerchantUtils.ts b/src/app/utils/MerchantUtils.ts index bdd85860..e60257ae 100644 --- a/src/app/utils/MerchantUtils.ts +++ b/src/app/utils/MerchantUtils.ts @@ -98,12 +98,16 @@ export class MerchantUtils { constructor(private alertService: AlertService, private api: ApiService, private platform: Platform, private storage: StorageService) { if (!this.platform.is('ios') && !this.platform.is('android')) { - console.log('merchant is not supported on this platform') + console.info('merchant is not supported on this platform') return } + this.init() + } + + private async init() { try { - this.initProducts() + await this.initProducts() this.initCustomValidator() this.setupListeners() } catch (e) { @@ -144,13 +148,13 @@ export class MerchantUtils { } async refreshToken() { - const refreshSuccess = (await this.api.refreshToken()) != null + let refreshSuccess = (await this.api.refreshToken()) != null if (!refreshSuccess) { console.log('refreshing token after purchase failed, scheduling retry') const loading = await this.alertService.presentLoading('This can take a minute') loading.present() await this.sleep(35000) - const refreshSuccess = (await this.api.refreshToken()) != null + refreshSuccess = (await this.api.refreshToken()) != null if (!refreshSuccess) { this.alertService.showError( 'Purchase Error', @@ -174,7 +178,7 @@ export class MerchantUtils { return result } - private initProducts() { + private async initProducts() { let platform = CdvPurchase.Platform.GOOGLE_PLAY if (this.platform.is('ios')) { platform = CdvPurchase.Platform.APPLE_APPSTORE @@ -189,7 +193,16 @@ export class MerchantUtils { } } - CdvPurchase.store.initialize() + await CdvPurchase.store.initialize() + for (let i = 0; i < CdvPurchase.store.products.length; i++) { + const lastIndex = CdvPurchase.store.products[i].offers[0].pricingPhases.length - 1 + if (lastIndex < 0) { + console.warn('no pricingphases found', CdvPurchase.store.products[i]) + continue + } + + this.updatePrice(CdvPurchase.store.products[i].id, CdvPurchase.store.products[i].offers[0].pricingPhases[lastIndex].price) + } } private updatePrice(id, price) { @@ -204,9 +217,6 @@ export class MerchantUtils { // General query to all products CdvPurchase.store .when() - .productUpdated((p: CdvPurchase.Product) => { - this.updatePrice(p.id, p.pricing.price) - }) .approved((p: CdvPurchase.Transaction) => { // Handle the product deliverable this.currentPlan = p.products[0].id @@ -230,7 +240,7 @@ export class MerchantUtils { async restore() { this.restorePurchase = true - CdvPurchase.store.restorePurchases() + await CdvPurchase.store.restorePurchases() } async purchase(product: string) { @@ -238,7 +248,7 @@ export class MerchantUtils { const loading = await this.alertService.presentLoading('') loading.present() CdvPurchase.store.order(offer).then( - async () => { + () => { this.restorePurchase = true setTimeout(() => { @@ -276,12 +286,12 @@ export class MerchantUtils { const loading = await this.alertService.presentLoading('Confirming, this might take a couple seconds') loading.present() - const result = await this.registerPurchaseOnRemote(purchaseData) + let result = await this.registerPurchaseOnRemote(purchaseData) if (!result) { console.log('registering receipt at remote failed, scheduling retry') await this.sleep(35000) - const result = await this.registerPurchaseOnRemote(purchaseData) + result = await this.registerPurchaseOnRemote(purchaseData) if (!result) { this.alertService.showError( 'Purchase Error', @@ -386,16 +396,16 @@ export class MerchantUtils { const currentProduct = this.findProduct(currentPlan) if (currentProduct == null) return 100 - const notMainnet = await this.api.isNotMainnet() + const notMainnet = this.api.isNotEthereumMainnet() if (notMainnet) return currentProduct.maxTestnetValidators return currentProduct.maxValidators } - async getHighestPackageValidator(): Promise { + getHighestPackageValidator(): number { const currentProduct = this.findProduct(MAX_PRODUCT) if (currentProduct == null) return 100 - const notMainnet = await this.api.isNotMainnet() + const notMainnet = this.api.isNotEthereumMainnet() if (notMainnet) return currentProduct.maxTestnetValidators return currentProduct.maxValidators } diff --git a/src/app/utils/NetworkData.ts b/src/app/utils/NetworkData.ts index a671f291..2376b16b 100644 --- a/src/app/utils/NetworkData.ts +++ b/src/app/utils/NetworkData.ts @@ -18,7 +18,7 @@ * // along with Beaconchain Dashboard. If not, see . */ -import { ApiNetwork } from '../models/StorageTypes' +import { ApiNetwork, NetworkMainCurrency } from '../models/StorageTypes' export const MAP: ApiNetwork[] = [ { @@ -31,6 +31,29 @@ export const MAP: ApiNetwork[] = [ onlyDebug: false, active: true, genesisTs: 1606824023, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', + }, + { + key: 'gnosis', + protocol: 'https', + host: 'gnosischa.in', + net: '', + endpoint: '/api/', + version: 'v1', + onlyDebug: false, + active: true, + genesisTs: 1638993340, + clCurrency: NetworkMainCurrency.GNO, + elCurrency: NetworkMainCurrency.xDAI, + slotPerEpoch: 16, + slotsTime: 5, + epochsPerSyncPeriod: 512, + name: 'Gnosis', }, { key: 'prater', @@ -42,6 +65,12 @@ export const MAP: ApiNetwork[] = [ onlyDebug: false, active: true, genesisTs: 1616508000, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', }, { key: 'sepolia', @@ -53,6 +82,12 @@ export const MAP: ApiNetwork[] = [ onlyDebug: false, active: true, genesisTs: 1655733600, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', }, { key: 'holesky', @@ -64,6 +99,12 @@ export const MAP: ApiNetwork[] = [ onlyDebug: false, active: true, genesisTs: 1695902400, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', }, { key: 'local dev', @@ -75,6 +116,12 @@ export const MAP: ApiNetwork[] = [ onlyDebug: true, active: true, genesisTs: 1606824023, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', }, { key: 'invalid (no connection)', @@ -86,16 +133,21 @@ export const MAP: ApiNetwork[] = [ onlyDebug: true, active: true, genesisTs: 1606824023, + clCurrency: NetworkMainCurrency.ETH, + elCurrency: NetworkMainCurrency.ETH, + slotPerEpoch: 32, + slotsTime: 12, + epochsPerSyncPeriod: 256, + name: 'Ethereum', }, ] export function findConfigForKey(key: string): ApiNetwork { for (const entry of MAP) { if (entry.key == key) { - console.log('found config', key, entry) return entry } } - console.debug('config for ' + key + ' not found, using mainnet instead', key) + console.log('config for ' + key + ' not found, using mainnet instead', key) return MAP[0] } diff --git a/src/app/utils/OAuthUtils.ts b/src/app/utils/OAuthUtils.ts index bb07859f..70d07201 100644 --- a/src/app/utils/OAuthUtils.ts +++ b/src/app/utils/OAuthUtils.ts @@ -75,7 +75,7 @@ export class OAuthUtils { expiresIn: expiresIn, }) - await this.validatorUtils.clearDeletedSet() + this.validatorUtils.clearDeletedSet() await this.firebaseUtils.pushLastTokenUpstream(true) await this.sync.fullSync() @@ -122,7 +122,7 @@ export class OAuthUtils { private async getOAuthOptions() { const api = this.api - const endpointUrl = await api.getResourceUrl('user/token') + const endpointUrl = api.getResourceUrl('user/token') const info = await Device.getId().catch(() => { return { identifier: 'iduno' } @@ -142,7 +142,7 @@ export class OAuthUtils { } return { - authorizationBaseUrl: (await api.getBaseUrl()) + '/user/authorize', + authorizationBaseUrl: api.getBaseUrl() + '/user/authorize', accessTokenEndpoint: endpointUrl, web: { appId: clientID, diff --git a/src/app/utils/ThemeUtils.ts b/src/app/utils/ThemeUtils.ts index 160d5e2f..eb694330 100644 --- a/src/app/utils/ThemeUtils.ts +++ b/src/app/utils/ThemeUtils.ts @@ -54,13 +54,13 @@ export default class ThemeUtils { constructor(private storage: StorageService, private platform: Platform) {} - async init(splashScreenCallback: () => void) { + init(splashScreenCallback: () => void) { this.lock = this.storage.getObject(STORAGE_KEY).then((preferenceDarkMode) => { this.internalInit(preferenceDarkMode as StoredTheme) setTimeout(() => { splashScreenCallback() this.applyColorInitially() - }, 200) + }, 10) return preferenceDarkMode as StoredTheme }) } diff --git a/src/app/utils/ValidatorSyncUtils.ts b/src/app/utils/ValidatorSyncUtils.ts index b05cb195..a3c5870f 100644 --- a/src/app/utils/ValidatorSyncUtils.ts +++ b/src/app/utils/ValidatorSyncUtils.ts @@ -133,7 +133,7 @@ export class ValidatorSyncUtils { } private async deleteUp(): Promise { - const storageKey = await this.validator.getStorageKey() + const storageKey = this.validator.getStorageKey() const deletedSet = await this.validator.getDeletedSet(storageKey) console.log('delete queue', deletedSet) @@ -171,7 +171,7 @@ export class ValidatorSyncUtils { private async syncUp(syncNotificationsForNewValidators: () => void) { console.log('[SYNC] syncing up') - const storageKey = await this.validator.getStorageKey() + const storageKey = this.validator.getStorageKey() const current = await this.validator.getMap(storageKey) const syncUpList: string[] = [] @@ -195,7 +195,7 @@ export class ValidatorSyncUtils { } private async syncRemoteRemovals(myRemotes: MyValidatorResponse[]) { - const storageKey = await this.validator.getStorageKey() + const storageKey = this.validator.getStorageKey() const current = await this.validator.getMap(storageKey) const existsSynced: Set = new Set() diff --git a/src/app/utils/ValidatorUtils.ts b/src/app/utils/ValidatorUtils.ts index 0fcaee89..eac75edb 100644 --- a/src/app/utils/ValidatorUtils.ts +++ b/src/app/utils/ValidatorUtils.ts @@ -27,7 +27,6 @@ import { AttestationPerformanceResponse, ValidatorRequest, ValidatorResponse, - ValidatorETH1Request, GetMyValidatorsRequest, MyValidatorResponse, DashboardRequest, @@ -37,8 +36,10 @@ import { SyncCommitteeResponse, ETH1ValidatorResponse, SyncCommitteesStatisticsResponse, + ProposalLuckResponse, + ValidatorViaDepositAddress, + ValidatorViaWithdrawalAddress, } from '../requests/requests' -import { CacheModule } from './CacheModule' import { MerchantUtils } from './MerchantUtils' import BigNumber from 'bignumber.js' import { UnitconvService } from '../services/unitconv.service' @@ -52,12 +53,6 @@ const KEYPREFIX = 'validators_' export const LAST_TIME_ADDED_KEY = 'last_time_added' export const LAST_TIME_REMOVED_KEY = 'last_time_removed' -const cachePerformanceKeyBare = 'performance' -const cacheAttestationKeyBare = 'attestationperformance' -const epochCachedKeyBare = 'epochcached' -const allMyKeyBare = 'allmy' -const cacheValidatorsKeyBare = 'validators_' - export enum ValidatorState { ACTIVE, OFFLINE, @@ -90,22 +85,21 @@ export interface Validator { @Injectable({ providedIn: 'root', }) -export class ValidatorUtils extends CacheModule { +export class ValidatorUtils { private listeners: (() => void)[] = [] private currentEpoch: EpochResponse private olderEpoch: EpochResponse rocketpoolStats: RocketPoolNetworkStats syncCommitteesStatsResponse: SyncCommitteesStatisticsResponse + proposalLuckResponse: ProposalLuckResponse constructor( private api: ApiService, private storage: StorageService, private merchantUtils: MerchantUtils, private unitConversion: UnitconvService - ) { - super('vu_') // initialize cache module with vu prefix - } + ) {} notifyListeners() { this.listeners.forEach((callback) => callback()) @@ -116,24 +110,28 @@ export class ValidatorUtils extends CacheModule { } async hasLocalValdiators() { - return (await this.getMap(await this.getStorageKey())).size > 0 + return (await this.getMap(this.getStorageKey())).size > 0 } async localValidatorCount() { - return (await this.getMap(await this.getStorageKey())).size + return (await this.getMap(this.getStorageKey())).size } - public async getStorageKey(): Promise { - return KEYPREFIX + (await this.api.getNetworkName()) + public getStorageKey(): string { + return KEYPREFIX + this.api.getNetworkName() } async migrateTo3Dot2() { const share = await this.storage.getStakingShare() const valis = await this.getAllValidatorsLocal() - for (let i = 0; i < valis.length; i++) { - valis[i].share = share.toNumber() + if (valis) { + for (let i = 0; i < valis.length; i++) { + if (share) { + valis[i].share = share.toNumber() + } + } + this.saveValidatorsLocal(valis) } - this.saveValidatorsLocal(valis) } public async getMap(storageKey: string): Promise> { @@ -164,13 +162,13 @@ export class ValidatorUtils extends CacheModule { this.storage.setObject(storageKey + '_deleted', [...list]) } - async clearDeletedSet() { - const storageKey = await this.getStorageKey() + clearDeletedSet() { + const storageKey = this.getStorageKey() this.setDeleteSet(storageKey, new Set()) } async deleteAll() { - const storageKey = await this.getStorageKey() + const storageKey = this.getStorageKey() const current = await this.getMap(storageKey) this.storage.setObject(storageKey, new Map()) @@ -188,7 +186,7 @@ export class ValidatorUtils extends CacheModule { private deletedWithoutNotifying: boolean = false async deleteValidatorLocal(validator: ValidatorResponse, notifyListeners: boolean = true) { - const storageKey = await this.getStorageKey() + const storageKey = this.getStorageKey() const current = await this.getMap(storageKey) current.delete(validator.pubkey) this.storage.setObject(storageKey, current) @@ -211,7 +209,7 @@ export class ValidatorUtils extends CacheModule { } async saveValidatorsLocal(validators: Validator[]) { - const storageKey = await this.getStorageKey() + const storageKey = this.getStorageKey() const current = await this.getMap(storageKey) const newMap = new Map() @@ -229,7 +227,7 @@ export class ValidatorUtils extends CacheModule { } async saveRocketpoolCollateralShare(nodeAddress: string, sharePercent: number) { - this.storage.setObject('rpl_share_' + nodeAddress, { share: sharePercent } as StoredShare) + await this.storage.setObject('rpl_share_' + nodeAddress, { share: sharePercent } as StoredShare) } async getRocketpoolCollateralShare(nodeAddress: string): Promise { @@ -239,12 +237,12 @@ export class ValidatorUtils extends CacheModule { } async getValidatorLocal(pubkey: string): Promise { - const current = await this.getMapWithoutDeleted(await this.getStorageKey()) + const current = await this.getMapWithoutDeleted(this.getStorageKey()) return current.get(pubkey) } async getAllValidatorsLocal(): Promise { - const current = await this.getMap(await this.getStorageKey()) + const current = await this.getMap(this.getStorageKey()) const erg: Validator[] = [...current.values()] return erg } @@ -260,18 +258,12 @@ export class ValidatorUtils extends CacheModule { } async getAllMyValidators(): Promise { - const storageKey = await this.getStorageKey() + const storageKey = this.getStorageKey() const local = await this.getMapWithoutDeleted(storageKey) + if (local.size == 0) return [] const validatorString = getValidatorQueryString([...local.values()], 2000, (await this.merchantUtils.getCurrentPlanMaxValidator()) - 1) - // TODO:: - /* const cached = await this.getMultipleCached(allMyKeyBare, validatorString.split(",")) - if (cached != null) { - console.log("return my validators from cache") - return cached - }*/ - const remoteUpdatesPromise = this.getDashboardDataValidators(SAVED, validatorString).catch((err) => { console.warn('error getAllMyValidators getDashboardDataValidators', err) return [] @@ -289,8 +281,6 @@ export class ValidatorUtils extends CacheModule { const result = [...local.values()] - this.cacheMultiple(allMyKeyBare, result) - return result } @@ -305,17 +295,16 @@ export class ValidatorUtils extends CacheModule { return false } - async updateValidatorStates(validators: Validator[]) { - const epoch = await this.getRemoteCurrentEpoch() + updateValidatorStates(validators: Validator[]) { validators.forEach((item) => { - item.state = this.getValidatorState(item, epoch) + item.state = this.getValidatorState(item) }) } // checks if remote validators are already known locally. // If not, return all indizes of non locally known validators public async getAllNewIndicesOnly(myRemotes: MyValidatorResponse[]): Promise { - const storageKey = await this.getStorageKey() + const storageKey = this.getStorageKey() const current = await this.getMap(storageKey) const result: number[] = [] @@ -328,22 +317,15 @@ export class ValidatorUtils extends CacheModule { return result } - getValidatorState(item: Validator, currentEpoch: EpochResponse): ValidatorState { - if ( - item.data.slashed == false && - item.data.lastattestationslot >= (currentEpoch.epoch - 2) * 32 && // online since last two epochs - item.data.exitepoch >= currentEpoch.epoch && - item.data.activationepoch <= currentEpoch.epoch - ) { + getValidatorState(item: Validator): ValidatorState { + if (item.data.slashed) return ValidatorState.SLASHED + if (item.data.status == 'exited') return ValidatorState.EXITED + if (item.data.status == 'deposited') return ValidatorState.ELIGABLE + if (item.data.status == 'pending') return ValidatorState.WAITING + if (item.data.slashed == false && item.data.status.indexOf('online') > 0) { return ValidatorState.ACTIVE } - if (item.data.slashed) return ValidatorState.SLASHED - if (item.data.exitepoch < currentEpoch.epoch) return ValidatorState.EXITED - if (item.data.activationeligibilityepoch > currentEpoch.epoch) return ValidatorState.ELIGABLE - if (item.data.activationepoch > currentEpoch.epoch) return ValidatorState.WAITING - if (item.data.lastattestationslot < (currentEpoch.epoch - 2) * 32) return ValidatorState.OFFLINE - // default case return ValidatorState.OFFLINE } @@ -366,7 +348,7 @@ export class ValidatorUtils extends CacheModule { return result } - async getDashboardDataValidators(storage: 0 | 1, ...validators): Promise { + private async getDashboardDataValidators(storage: 0 | 1, ...validators): Promise { const request = new DashboardRequest(...validators) const response = await this.api.execute(request) if (!request.wasSuccessful(response)) { @@ -378,13 +360,30 @@ export class ValidatorUtils extends CacheModule { } const result = request.parse(response)[0] - this.currentEpoch = result.currentEpoch[0] - this.olderEpoch = result.olderEpoch[0] - this.rocketpoolStats = result.rocketpool_network_stats[0] + if (!result) { + console.warn('error getDashboardDataValidators', response, result) + return [] + } + + if (result.currentEpoch && result.currentEpoch.length > 0) { + this.currentEpoch = result.currentEpoch[0] + } else { + console.warn('no current epoch information!', result) + } + if (result.olderEpoch && result.olderEpoch.length > 0) { + this.olderEpoch = result.olderEpoch[0] + } else { + console.warn('no older epoch information!', result) + } + + if (result.rocketpool_network_stats && result.rocketpool_network_stats.length > 0) { + this.rocketpoolStats = result.rocketpool_network_stats[0] + } const validatorEffectivenessResponse = result.effectiveness const validatorsResponse = result.validators this.syncCommitteesStatsResponse = result.sync_committees_stats + this.proposalLuckResponse = result.proposal_luck_stats this.updateRplAndRethPrice() @@ -392,16 +391,18 @@ export class ValidatorUtils extends CacheModule { await this.storage.setLastEpochRequestTime(Date.now()) } else { const lastCachedTime = await this.storage.getLastEpochRequestTime() - this.currentEpoch.lastCachedTimestamp = lastCachedTime + if (this.currentEpoch) { + this.currentEpoch.lastCachedTimestamp = lastCachedTime + } } let local = null if (storage == SAVED) { - local = await this.getMapWithoutDeleted(await this.getStorageKey()) + local = await this.getMapWithoutDeleted(this.getStorageKey()) } const temp = this.convertToValidatorModel({ synced: false, storage: storage, validatorResponse: validatorsResponse }) - await this.updateValidatorStates(temp) + this.updateValidatorStates(temp) for (const vali of temp) { vali.attrEffectiveness = this.findAttributionEffectiveness(validatorEffectivenessResponse, vali.index) vali.rocketpool = this.findRocketpoolResponse(result.rocketpool_validators, vali.index) @@ -440,7 +441,7 @@ export class ValidatorUtils extends CacheModule { private updateRplAndRethPrice() { if (!this.rocketpoolStats) return this.unitConversion.setRPLPrice(new BigNumber(this.rocketpoolStats.rpl_price.toString())) - this.unitConversion.setRETHPrice(new BigNumber(this.rocketpoolStats.reth_exchange_rate.toString())) + //this.unitConversion.setRETHPrice(new BigNumber(this.rocketpoolStats.reth_exchange_rate.toString())) } private findExecutionResponse(list: ExecutionResponse[], index: number): ExecutionResponse { @@ -471,7 +472,7 @@ export class ValidatorUtils extends CacheModule { return -1 } - async getOlderEpoch(): Promise { + getOlderEpoch(): EpochResponse { return this.olderEpoch } @@ -496,20 +497,37 @@ export class ValidatorUtils extends CacheModule { } } - async getRemoteValidatorViaETH1(arg: string, enforceMax = -1): Promise { + async getRemoteValidatorViaETHAddress(arg: string, enforceMax = -1): Promise { if (!arg) return [] - const request = new ValidatorETH1Request(arg) - const response = await this.api.execute(request) - - if (request.wasSuccessful(response)) { - const eth1ValidatorList = request.parse(response) - const queryString = getValidatorQueryString(eth1ValidatorList, 2000, enforceMax) - - return await this.getDashboardDataValidators(MEMORY, queryString) - } else { - return this.apiStatusHandler(response) + const viaDeposit = new ValidatorViaDepositAddress(arg) + const viaDepositPromise = this.api.execute(viaDeposit) + + const viaWithdrawal = new ValidatorViaWithdrawalAddress(arg) + const viaWithdrawalPromise = this.api.execute(viaWithdrawal) + + const response = await Promise.all([viaDepositPromise, viaWithdrawalPromise]) + let result: ETH1ValidatorResponse[] = [] + + for (const resp of response) { + if (viaDeposit.wasSuccessful(resp)) { + // since both results are identical we can use any of the requests for parsing + const temp = viaDeposit.parse(resp) + if (temp) { + if (result.length > 0) { + result = Array.from(new Map([...result, ...temp].map((item) => [item.validatorindex, item])).values()) + } else { + result = temp + } + } + } else { + return this.apiStatusHandler(response) + } } + + const queryString = getValidatorQueryString(result, 2000, enforceMax) + return await this.getDashboardDataValidators(MEMORY, queryString) } + private async apiStatusHandler(response) { if (response && response.data && response.data.status) { return Promise.reject(new Error(response.data.status)) @@ -517,33 +535,16 @@ export class ValidatorUtils extends CacheModule { return Promise.reject(new Error('Response is invalid')) } - async getCachedAttestationKey() { - return cacheAttestationKeyBare + (await this.api.getNetworkName()) - } - - async getCachedPerformanceKey() { - return cachePerformanceKeyBare + (await this.api.getNetworkName()) - } - - async getCachedValidatorKey() { - return cacheValidatorsKeyBare + (await this.api.getNetworkName()) - } - - async getCachedEpochKey() { - return epochCachedKeyBare + (await this.api.getNetworkName()) - } - // single async convertToValidatorModelAndSaveValidatorLocal(synced: boolean, validator: ValidatorResponse) { - this.convertToValidatorModelsAndSaveLocal(synced, [validator]) + await this.convertToValidatorModelsAndSaveLocal(synced, [validator]) } // multiple async convertToValidatorModelsAndSaveLocal(synced: boolean, validator: ValidatorResponse[]) { - this.saveValidatorsLocal(this.convertToValidatorModel({ synced, storage: SAVED, validatorResponse: validator })) + await this.saveValidatorsLocal(this.convertToValidatorModel({ synced, storage: SAVED, validatorResponse: validator })) if (!synced) { - this.storage.setObject(LAST_TIME_ADDED_KEY, { timestamp: Date.now() } as StoredTimestamp) - this.clearCache() + await this.storage.setObject(LAST_TIME_ADDED_KEY, { timestamp: Date.now() } as StoredTimestamp) } } @@ -585,8 +586,8 @@ export class ValidatorUtils extends CacheModule { return result } - async searchValidatorsViaETH1(search: string, enforceMax = -1): Promise { - const result = await this.getRemoteValidatorViaETH1(search, enforceMax) + async searchValidatorsViaETHAddress(search: string, enforceMax = -1): Promise { + const result = await this.getRemoteValidatorViaETHAddress(search, enforceMax) if (result == null) return [] return result } diff --git a/src/theme/variables.scss b/src/theme/variables.scss index b462eea3..7408b2c2 100644 --- a/src/theme/variables.scss +++ b/src/theme/variables.scss @@ -714,3 +714,24 @@ body.rocketpool.dark { --x-toolbar-title-color: #fff; } + +body.gnosis { + --ion-color-primary: #3e6957; + --ion-color-primary-rgb: 62, 105, 87; + --ion-color-primary-contrast: #ffffff; + --ion-color-primary-contrast-rgb: 255, 255, 255; + --ion-color-primary-shade: #e97112; + --ion-color-primary-tint: #0e0b0a; + + --ion-toolbar-background: #3e6957; //2f2e42 + --x-alert-primary: #3e6957; + --ion-color-success: #3e6957; + --chart-default: #3e6957; + --x-update-messages-background: #4c816b; + --x-update-messages-background-selected: #4c816b; + + --status-ok: white; + --status-warn: white; + --status-danger: white; + --status-secondary: white; +}