diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 1a9e3af..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,61 +0,0 @@ -# .circleci/config.yml - -version: 2.1 - -orbs: - node: circleci/node@4.0.0 - -jobs: - build_testnet: - macos: - xcode: "12.1" # Specify the Xcode version to use - working_directory: /Users/distiller/project - environment: - FL_OUTPUT_DIR: output - FASTLANE_LANE: beta - steps: - - checkout - - - node/install: - node-version: "10.16.3" - install-yarn: true - # Download and cache dependencies - # - restore_cache: - # keys: - # - v1-dependencies-{{ checksum "package.json" }} - # # fallback to using the latest cache if no exact match is found - # - v1-dependencies- - - - run: cp env/default.json.example env/default.json - - - run: yarn cache clean && yarn install --network-concurrency 1 - - # - save_cache: - # paths: - # - node_modules - # key: v1-dependencies-{{ checksum "package.json" }} - - - run: cd ios && bundle install - - - run: cd ios && bundle exec pod install - - - run: - name: Fastlane - command: cd ios && export BUILD_NUMBER=${CIRCLE_BUILD_NUM} && export VERSION_NUMBER=4.2 && bundle exec fastlane $FASTLANE_LANE - - - store_artifacts: - path: ios/output - - store_test_results: - path: ios/output/scan - -workflows: - version: 2 - build_testnet_and_mainnet: - jobs: - - build_testnet: - filters: - branches: - only: - - main - - dev - - feature_postlaunch_optin diff --git a/CHANGELOG.md b/CHANGELOG.md index 51a8c0f..1dd6295 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,21 @@ All notable changes to this project will be documented in this file. The changelog format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). +## [v1.3.0][v1.3.0] - 1-Nov-2021 + +### Fixed +- Fix invalid accounts generated when importing private key instead mnemonics [#232](https://github.com/symbol/mobile-wallet/issues/232) +- Fix node list should not be hardcoded [#235](https://github.com/symbol/mobile-wallet/issues/235) +- Fix not able to send transaction with encrypted message [#236](https://github.com/symbol/mobile-wallet/issues/236) +- Hide news feed [#242](https://github.com/symbol/mobile-wallet/issues/242) +- Update explorer and faucet url. [#252](https://github.com/symbol/mobile-wallet/pull/252) + +### Added +- Add Multisig multi-level support [#223](https://github.com/symbol/mobile-wallet/issues/223) +- Add harvesting verification if account importance is above zero [#234](https://github.com/symbol/mobile-wallet/issues/234) +- Display current account importance in Harvesting Page [#239](https://github.com/symbol/mobile-wallet/issues/239) +- Add Jenkins pipeline setup [#247](https://github.com/symbol/mobile-wallet/pull/247) + ## [v1.2][v1.2] - 6-Oct-2021 ### Fixed @@ -11,4 +26,5 @@ The changelog format is based on [Keep a Changelog](https://keepachangelog.com/e - Fix harvesting status [#134](https://github.com/symbol/mobile-wallet/issues/134) - Minor translations update. -[v1.2]: https://github.com/symbol/mobile-wallet/releases/tag/1.2 \ No newline at end of file +[v1.2]: https://github.com/symbol/mobile-wallet/releases/tag/1.2 +[v1.3.0]: https://github.com/symbol/mobile-wallet/releases/tag/1.3.0 diff --git a/Jenkinsfile b/Jenkinsfile new file mode 100644 index 0000000..1fef9fd --- /dev/null +++ b/Jenkinsfile @@ -0,0 +1,113 @@ +pipeline { + agent { label 'macos' } + + options { + skipStagesAfterUnstable() + } + + parameters { + string(name: 'VERSION_NUMBER', defaultValue: '4.4.2', description: 'Version Number') + string(name: 'BUILD_NUMBER', defaultValue: '59', description: 'Build Number') + string(name: 'DEPLOY_BETA_BRANCH', defaultValue: 'dev', description: 'Deploy Beta Branch Name') + choice( + name: 'TARGET_OS', + choices: ['All', 'IOS', 'Android'], + description: 'Target Environment' + ) + } + + environment { + RUNNING_ON_CI = 'true' + VERSION_NUMBER = "${params.VERSION_NUMBER}" + BUILD_NUMBER = "${params.BUILD_NUMBER}" + DEPLOY_BETA_BRANCH = "${params.DEPLOY_BETA_BRANCH}" + TARGET_OS = "${params.TARGET_OS}" + MATCH_GIT_BASIC_AUTHORIZATION = credentials('GHUB_CREDS_SECRET') + MATCH_PASSWORD = credentials('MATCH_PASSWORD') + FASTLANE_KEYCHAIN = 'fastlane.keychain' + FASTLANE_KEYCHAIN_PASSWORD = credentials("FASTLANE_KEYCHAIN_PASSWORD") + APP_STORE_CONNECT_API_KEY_KEY_ID = credentials("APP_STORE_CONNECT_API_KEY_KEY_ID") + APP_STORE_CONNECT_API_KEY_ISSUER_ID = credentials("APP_STORE_CONNECT_API_KEY_ISSUER_ID") + APP_STORE_CONNECT_API_KEY_KEY = credentials("APP_STORE_CONNECT_API_KEY_KEY") + } + + stages { + stage('Install') { + steps { + withCredentials([file(credentialsId: 'ENV_FILE_ZIP', variable: 'default_env_zip')]) { + sh "cp $default_env_zip default_whitelist_zip.zip" + } + script { + unzip zipFile: 'default_whitelist_zip.zip', quiet: true + def jsonWhitelist = readJSON file: 'default_whitelist.json' + def json = readJSON file: 'env/default.json.example' + json.optInWhiteList = jsonWhitelist.optInWhiteList + writeJSON file: 'env/default.json', json: json + } + sh "rm -f default_whitelist_zip.zip && rm -f default_whitelist.json" + // Run yarn install for the dependencies + sh "yarn cache clean && yarn install --network-concurrency 1" + sh "npx jetify" // for android + } + } + + stage('Build') { + parallel { + stage('Build - IOS') { + when { + expression { + BRANCH_NAME == DEPLOY_BETA_BRANCH && (TARGET_OS == 'All' || TARGET_OS == 'IOS') + } + } + steps { + sh "cd ios && bundle install" + sh "cd ios && bundle exec pod install" + } + } + stage('Build - Android') { + when { + expression { + TARGET_OS == 'All' || TARGET_OS == 'Android' + } + } + steps { + // copy key.properties and copy keystore file + withCredentials([file(credentialsId: 'ANDROID_KEY_PROPERTIES', variable: 'android_key_properties'), + file(credentialsId: 'ANDROID_KEY_PROPERTIES_STORE_FILE', variable: 'release_keystore')]) { + sh 'cp $android_key_properties ./android/app/key.properties' + sh 'cp $release_keystore ./android/app/release.keystore' + sh "cd android && ./gradlew assembleProdRelease -PBUILD_NUMBER=${BUILD_NUMBER}" + sh "echo 'Build completed and .apk file is generated. Version:${VERSION_NUMBER}, Build:${BUILD_NUMBER}'" + } + } + } + } + } + + stage ('Test') { + steps { + sh "echo 'Currently there are no tests to run!'" + } + } + + // deploy to testnet + stage ('Deploy IOS - Alpha version') { + when { + expression { + BRANCH_NAME == DEPLOY_BETA_BRANCH && (TARGET_OS == 'All' || TARGET_OS == 'IOS') + } + } + steps { + sh 'cd ios && export APP_STORE_CONNECT_API_KEY_KEY_ID=${APP_STORE_CONNECT_API_KEY_KEY_ID} && export APP_STORE_CONNECT_API_KEY_ISSUER_ID=${APP_STORE_CONNECT_API_KEY_ISSUER_ID} && export APP_STORE_CONNECT_API_KEY_KEY=${APP_STORE_CONNECT_API_KEY_KEY} && export FASTLANE_KEYCHAIN=${FASTLANE_KEYCHAIN} && export FASTLANE_KEYCHAIN_PASSWORD=${FASTLANE_KEYCHAIN_PASSWORD} && export RUNNING_ON_CI=${RUNNING_ON_CI} && export BUILD_NUMBER=${BUILD_NUMBER} && export VERSION_NUMBER=${VERSION_NUMBER} && bundle exec fastlane beta' + sh "echo 'Deployed to TestFlight. Version:${VERSION_NUMBER}, Build:${BUILD_NUMBER}'" + } + } + } + post { + always { + archiveArtifacts artifacts: 'android/app/build/outputs/apk/prod/release/*.apk', allowEmptyArchive: true + archiveArtifacts artifacts: 'ios/output/*.ipa', allowEmptyArchive: true + cleanWs() + } + } +} diff --git a/README.md b/README.md index e1dc8b0..74f18c4 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,6 @@ Official Android and iOS applications for NEM2(a.k.a Catapult). -[![CircleCI](https://circleci.com/gh/hatioin/nem-catapult-wallet.svg?style=svg&circle-token=8a16be5d725e06b1380904c8024e6622b6193e86)](https://circleci.com/gh/hatioin/nem-catapult-wallet) ## Getting Started diff --git a/android/app/build.gradle b/android/app/build.gradle index 33fcc9a..04d1565 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -102,14 +102,14 @@ def enableProguardInReleaseBuilds = false * The preferred build flavor of JavaScriptCore. * * For example, to use the international variant, you can use: - * `def jscFlavor = 'org.webkit:android-jsc-intl:+'` + * `def jscFlavor = 'org.webkit:android-jsc:+'' * * The international variant includes ICU i18n library and necessary data * allowing to use e.g. `Date.toLocaleString` and `String.localeCompare` that * give correct results when using with locales other than en-US. Note that * this variant is about 6MiB larger per architecture than default. */ -def jscFlavor = 'org.webkit:android-jsc:+' +def jscFlavor = 'org.webkit:android-jsc-intl:+' /** * Whether to enable the Hermes VM. @@ -119,6 +119,16 @@ def jscFlavor = 'org.webkit:android-jsc:+' * and the benefits of using Hermes will therefore be sharply reduced. */ def enableHermes = project.ext.react.get("enableHermes", false); +def keystoreProperties = new Properties() +def keyPropertiesFilePath = 'key.properties' +def keystorePropertiesFile = file(keyPropertiesFilePath) +if (keystorePropertiesFile.exists()) { + println "${keyPropertiesFilePath} file exists." + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) + println "storeFile is ${keystoreProperties['storeFile']}" +} else { + println "${keyPropertiesFilePath} file not found!" +} android { compileSdkVersion rootProject.ext.compileSdkVersion @@ -144,12 +154,12 @@ android { } } signingConfigs { - debug { - storeFile file('debug.keystore') - storePassword 'android' - keyAlias 'androiddebugkey' - keyPassword 'android' - } + config { + storeFile keystoreProperties['storeFile'] ? file(keystoreProperties['storeFile']) : null + storePassword keystoreProperties['storePassword'] + keyAlias keystoreProperties['keyAlias'] + keyPassword keystoreProperties['keyPassword'] + } } flavorDimensions "version" productFlavors { @@ -166,12 +176,12 @@ android { } buildTypes { debug { - signingConfig signingConfigs.debug + signingConfig signingConfigs.config } release { // Caution! In production, you need to generate your own keystore file. // see https://facebook.github.io/react-native/docs/signed-apk-android. - signingConfig signingConfigs.debug + signingConfig signingConfigs.config minifyEnabled enableProguardInReleaseBuilds proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" } diff --git a/android/app/key.properties b/android/app/key.properties new file mode 100644 index 0000000..e41c5ee --- /dev/null +++ b/android/app/key.properties @@ -0,0 +1,4 @@ +storeFile=debug.keystore +storePassword=android +keyAlias=androiddebugkey +keyPassword=android \ No newline at end of file diff --git a/android/gradle.properties b/android/gradle.properties index 027ef9d..cbae841 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -19,3 +19,4 @@ android.useAndroidX=true android.enableJetifier=true +org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 \ No newline at end of file diff --git a/env/default.json.example b/env/default.json.example index 0aafdcb..caeb433 100644 --- a/env/default.json.example +++ b/env/default.json.example @@ -1,12 +1,15 @@ { "sessionTimeoutInSeconds": 10, "marketCurrencyName": "nem", - "newsURL": "https://nemflash.io/feed", "explorerURL": { - "testnet": "http://explorer.testnet.symboldev.network/", - "mainnet": "http://explorer.symbolblockchain.io/" + "testnet": "https://testnet.symbol.fyi/", + "mainnet": "https://symbol.fyi/" }, - "faucetURL": "http://faucet.testnet.symboldev.network/", + "statisticsServiceURL": { + "testnet": "https://testnet.symbol.services/", + "mainnet": "https://symbol.services/" + }, + "faucetURL": "https://testnet.symbol.tools/", "aboutURL": "https://nem.io", "currencies": { "USD": "usd", @@ -29,15 +32,6 @@ "http://hugealice2.nem.ninja", "http://hachi.nem.ninja", "http://alice2.nem.ninja" - ], - "nodes": [ - "http://ngl-api-501.symbolblockchain.io:3000", - "http://ngl-api-601.symbolblockchain.io:3000", - "http://ngl-api-401.symbolblockchain.io:3000", - "http://ngl-api-001.symbolblockchain.io:3000", - "http://ngl-api-101.symbolblockchain.io:3000", - "http://ngl-api-301.symbolblockchain.io:3000", - "http://ngl-api-201.symbolblockchain.io:3000" ] }, "testnet": { @@ -45,15 +39,6 @@ "http://hugetestalice2.nem.ninja", "http://hugetestalice.nem.ninja", "http://medalice2.nem.ninja" - ], - "nodes": [ - "http://ngl-dual-001.testnet.symboldev.network:3000", - "http://ngl-dual-101.testnet.symboldev.network:3000", - "http://ngl-dual-201.testnet.symboldev.network:3000", - "http://ngl-dual-301.testnet.symboldev.network:3000", - "http://ngl-dual-401.testnet.symboldev.network:3000", - "http://ngl-dual-501.testnet.symboldev.network:3000", - "http://ngl-dual-601.testnet.symboldev.network:3000" ] } }, diff --git a/ios/Gemfile.lock b/ios/Gemfile.lock index 5553d4d..a343385 100644 --- a/ios/Gemfile.lock +++ b/ios/Gemfile.lock @@ -12,6 +12,7 @@ GEM algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) + artifactory (3.0.15) atomos (0.1.3) aws-eventstream (1.1.0) aws-partitions (1.358.0) @@ -93,9 +94,10 @@ GEM faraday_middleware (1.0.0) faraday (~> 1.0) fastimage (2.2.0) - fastlane (2.156.1) + fastlane (2.181.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.3, < 3.0.0) + artifactory (~> 3.0) aws-sdk-s3 (~> 1.0) babosa (>= 1.0.3, < 2.0.0) bundler (>= 1.12.0, < 3.0.0) @@ -116,6 +118,7 @@ GEM jwt (>= 2.1.0, < 3) mini_magick (>= 4.9.4, < 5.0.0) multipart-post (~> 2.0.0) + naturally (~> 2.2) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -240,4 +243,4 @@ DEPENDENCIES fastlane BUNDLED WITH - 2.1.2 + 2.2.28 diff --git a/ios/NEMCatapultWallet/Info.plist b/ios/NEMCatapultWallet/Info.plist index 7d6072e..1e505b8 100644 --- a/ios/NEMCatapultWallet/Info.plist +++ b/ios/NEMCatapultWallet/Info.plist @@ -21,11 +21,11 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1 + 4.4.2 CFBundleSignature ???? CFBundleVersion - 1 + 58 LSRequiresIPhoneOS NSAppTransportSecurity diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3f023e8..1305a0f 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -498,7 +498,7 @@ SPEC CHECKSUMS: React-RCTVibration: a49a1f42bf8f5acf1c3e297097517c6b3af377ad ReactCommon: 198c7c8d3591f975e5431bec1b0b3b581aa1c5dd ReactNativeNavigation: aefc8debafb4a374575adafb44a3352b9d5b618a - RealmJS: 0f2a5db56ff20f2feab9543f57e348575e40b508 + RealmJS: d1afbd12be13d7b455327adceaf2a2aea5f22e1a rn-fetch-blob: f525a73a78df9ed5d35e67ea65e79d53c15255bc RNBackgroundFetch: 8dbb63141792f1473e863a0797ffbd5d987af2fc RNCAsyncStorage: b03032fdbdb725bea0bd9e5ec5a7272865ae7398 diff --git a/ios/SymbolWallet.xcodeproj/project.pbxproj b/ios/SymbolWallet.xcodeproj/project.pbxproj index b9e0a07..84e5df8 100644 --- a/ios/SymbolWallet.xcodeproj/project.pbxproj +++ b/ios/SymbolWallet.xcodeproj/project.pbxproj @@ -371,7 +371,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 8; + CURRENT_PROJECT_VERSION = 58; DEAD_CODE_STRIPPING = NO; DEVELOPMENT_TEAM = W594P93ZB8; ENABLE_TESTABILITY = NO; @@ -418,7 +418,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CODE_SIGN_IDENTITY = "iPhone Distribution"; CODE_SIGN_STYLE = Manual; - CURRENT_PROJECT_VERSION = 16; + CURRENT_PROJECT_VERSION = 58; DEVELOPMENT_TEAM = W594P93ZB8; GCC_NO_COMMON_BLOCKS = NO; HEADER_SEARCH_PATHS = ( @@ -450,7 +450,7 @@ ); PRODUCT_BUNDLE_IDENTIFIER = com.nemgrouplimited.symbolwallet; PRODUCT_NAME = Symbol; - PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.nemgrouplimited.symbolwallet"; + PROVISIONING_PROFILE_SPECIFIER = "match AppStore com.nemgrouplimited.symbolwallet 1634392395"; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; diff --git a/ios/fastlane/Appfile b/ios/fastlane/Appfile index c89465b..6b46927 100644 --- a/ios/fastlane/Appfile +++ b/ios/fastlane/Appfile @@ -1,5 +1,4 @@ app_identifier("com.nemgrouplimited.symbolwallet") # The bundle identifier of your app -apple_id("acarrera@peersyst.com") # Your Apple email address itc_team_id("122014636") # App Store Connect Team ID team_id("W594P93ZB8") # Developer Portal Team ID diff --git a/ios/fastlane/Fastfile b/ios/fastlane/Fastfile index 2f3e0f8..0bc2a69 100644 --- a/ios/fastlane/Fastfile +++ b/ios/fastlane/Fastfile @@ -15,10 +15,18 @@ default_platform(:ios) +def ensure_temp_keychain(name, password) + delete_keychain( + name: name + ) if File.exist? File.expand_path("~/Library/Keychains/#{name}-db") + create_keychain( + name: name, + password: password, + unlock: true, + timeout: false + ) +end platform :ios do - before_all do - setup_circle_ci - end desc "Push a new beta build to TestFlight" lane :beta do |options| increment_build_number( @@ -29,8 +37,17 @@ platform :ios do version_number: ENV["VERSION_NUMBER"], xcodeproj: "SymbolWallet.xcodeproj" ) + ensure_temp_keychain(ENV["FASTLANE_KEYCHAIN"], ENV["FASTLANE_KEYCHAIN_PASSWORD"]) + setup_jenkins(force: true, keychain_path: ENV["FASTLANE_KEYCHAIN"], keychain_password: ENV["FASTLANE_KEYCHAIN_PASSWORD"], add_keychain_to_search_list: true) + #, set_default_keychain: true + app_store_connect_api_key( + key_id: ENV['APP_STORE_CONNECT_API_KEY_KEY_ID'], + issuer_id: ENV['APP_STORE_CONNECT_API_KEY_ISSUER_ID'], + key_content: ENV['APP_STORE_CONNECT_API_KEY_KEY'], + is_key_content_base64: true + ) match(type: "appstore") build_app(workspace: "SymbolWallet.xcworkspace", scheme: "Symbol") upload_to_testflight end -end +end \ No newline at end of file diff --git a/ios/fastlane/Matchfile b/ios/fastlane/Matchfile index ea3f118..95e7043 100644 --- a/ios/fastlane/Matchfile +++ b/ios/fastlane/Matchfile @@ -1,11 +1,11 @@ -git_url("git@github.com:Peersyst/symbol-wallet-credentials.git") +git_url("https://github.com/symbol/mobile-wallet-credentials.git") +git_branch("master") storage_mode("git") type("appstore") # The default type, can be: appstore, adhoc, enterprise or development app_identifier("com.nemgrouplimited.symbolwallet") # The bundle identifier of your app -username("acarrera@peersyst.com") # Your Apple email address # app_identifier(["tools.fastlane.app", "tools.fastlane.app2"]) # username("user@fastlane.tools") # Your Apple Developer Portal username diff --git a/package-lock.json b/package-lock.json index 0dbb7f3..f7f8961 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "symbol-mobile-wallet", - "version": "1.2", + "version": "1.3.0", "lockfileVersion": 1, "requires": true, "dependencies": { diff --git a/package.json b/package.json index 865b664..cfb475a 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,11 @@ { "name": "symbol-mobile-wallet", - "version": "1.2", + "version": "1.3.0", "private": true, "scripts": { "git-info": "echo export default \"{ version: '$(git describe --tags `git rev-list --tags --max-count=1`)' };\" > src/shared/_git_commit.js", "android": "react-native run-android", - "ios": "react-native run-ios", + "ios": "react-native run-ios --scheme Symbol", "start": "yarn git-info; react-native start", "dev": "react-native start", "reconnect": "adb reverse tcp:8081 tcp:8081", @@ -110,7 +110,7 @@ "symbol-paper-wallets": "^1.0.2", "symbol-post-launch-optin-module": "1.0.1", "symbol-qr-library": "0.14.1-alpha-202103081047", - "symbol-sdk": "1.0.0", + "symbol-sdk": "1.0.2-alpha-202108061451", "timers-browserify": "^1.0.1", "tls-browserify": "^0.2.2", "tty-browserify": "0.0.0", diff --git a/src/App.js b/src/App.js index 41c7a53..eae2d9e 100644 --- a/src/App.js +++ b/src/App.js @@ -48,7 +48,6 @@ const initStore = async () => { await store.dispatchAction({ type: 'network/initState' }); } catch {} // store.dispatchAction({ type: 'market/loadMarketData' }); - store.dispatchAction({ type: 'news/loadNews' }); store.dispatchAction({ type: 'addressBook/loadAddressBook' }); }; diff --git a/src/components/atoms/Warning/index.js b/src/components/atoms/Warning/index.js index 0ee6dd6..76f6184 100644 --- a/src/components/atoms/Warning/index.js +++ b/src/components/atoms/Warning/index.js @@ -9,23 +9,17 @@ import { View, Alert } from 'react-native'; import translate from "@src/locales/i18n"; type Props = { - onIgnore: () => void, hideWarning: () => void, message: string, - okButtonText: string, }; const Warning = (props: Props) => { - const { onIgnore, message, okButtonText, hideWarning } = props; + const { message, hideWarning } = props; const invalidAlert = () => { Alert.alert( translate('Shared.Warning.warning'), message, [ - { - text: okButtonText, - onPress: () => onIgnore(), - }, { text: translate('Shared.Warning.cancelModalButton'), onPress: () => hideWarning(), diff --git a/src/components/controls/ReadMoreLink.js b/src/components/controls/ReadMoreLink.js new file mode 100644 index 0000000..4ba9a97 --- /dev/null +++ b/src/components/controls/ReadMoreLink.js @@ -0,0 +1,56 @@ +import React, { Component } from 'react'; +import { TouchableOpacity, StyleSheet, Linking } from 'react-native'; +import { Text, Section, Col, Row } from '@src/components'; +import GlobalStyles from '../../styles/GlobalStyles'; +import translate from '@src/locales/i18n'; + +const styles = StyleSheet.create({ + link: { + fontSize: 12, + color: GlobalStyles.color.BLUE, + textDecorationLine: 'underline', + }, +}); +interface Props { + url: string; + title: string; +} +export default class ReadMoreLink extends Component { + onPress(url: string) { + Linking.openURL(url); + } + + render = () => { + const title = this.props.title; + if (title) { + return ( +
+ + {title} + + + + this.onPress(this.props.url)}> + + {translate('news.readMore')} + + + + +
+ ); + } else { + return ( + + + this.onPress(this.props.url)}> + + {translate('news.readMore')} + + + + + ); + } + }; +} diff --git a/src/components/molecules/BasicAlert.js b/src/components/molecules/BasicAlert.js new file mode 100644 index 0000000..0455974 --- /dev/null +++ b/src/components/molecules/BasicAlert.js @@ -0,0 +1,25 @@ + import React from 'react'; + + import { View, Alert } from 'react-native'; + + type Action = { + text: string, + onPress: () => void + } + + type Props = { + title: string, + message: string, + actions: Action[] + }; + + export default BasicAlert = (props: Props) => { + const { title, message, actions } = props; + return ( + + { + Alert.alert(title,message,actions,{ cancelable: true }) + } + + ) + }; \ No newline at end of file diff --git a/src/components/molecules/MultisigFilter.js b/src/components/molecules/MultisigFilter.js index 22b43cf..53db80a 100644 --- a/src/components/molecules/MultisigFilter.js +++ b/src/components/molecules/MultisigFilter.js @@ -10,6 +10,9 @@ type Props = { }; class MultisigFilter extends Component { + formatAddress = address => { + return address.substring(0, 6) + '...' + address.substring(address.length - 3, address.length); + }; render() { const { cosignatoryOf, selectedAccountAddress, selected, onSelect, ...rest } = this.props; const allMultisigAccounts = [ @@ -20,7 +23,7 @@ class MultisigFilter extends Component { })), ]; - return ; + return ; } } diff --git a/src/components/molecules/index.js b/src/components/molecules/index.js index e82197c..cd10467 100644 --- a/src/components/molecules/index.js +++ b/src/components/molecules/index.js @@ -1,3 +1,4 @@ export TitleBar from './TitleBar'; export OptionsMenu from './OptionsMenu'; -export EmptyListMessage from './EmptyListMessage'; \ No newline at end of file +export EmptyListMessage from './EmptyListMessage'; +export BasicAlert from './BasicAlert'; \ No newline at end of file diff --git a/src/components/organisms/New.js b/src/components/organisms/New.js deleted file mode 100644 index bca3c9a..0000000 --- a/src/components/organisms/New.js +++ /dev/null @@ -1,100 +0,0 @@ -import React, { Component } from 'react'; -import { View, Image, StyleSheet, Linking, TouchableOpacity } from 'react-native'; -import { Text, Row, Col, } from '@src/components'; -import GlobalStyles from '@src/styles/GlobalStyles'; -import TextLink from '@src/components/atoms/TextLink'; -import Card from '@src/components/atoms/Card'; -import translate from "@src/locales/i18n"; - -const styles = StyleSheet.create({ - root: { - width: '100%', - borderRadius: 6, - backgroundColor: GlobalStyles.color.WHITE - }, - content: { - marginTop:0 - }, - title: { - fontFamily: 'NotoSans-SemiBold', - fontWeight: '600', - fontSize: 14, - lineHeight: 20, - marginBottom: 6 - }, - date: { - fontSize: 12, - }, - source: { - fontSize: 12, - fontStyle: 'italic' - }, - body: { - fontFamily: 'NotoSans-Light', - fontSize: 12, - }, - link: { - fontSize: 12, - color: GlobalStyles.color.BLUE, - textDecorationLine: 'underline' - }, -}); - -type Props = { - title: string, - body: string, - contentSnippet: string, - url: string, - publicationDate: string, - creator: string, -}; - -export default class New extends Component { - onPress() { - Linking.openURL(this.props.url); - } - - render() { - return ( - this.onPress(this.props.url)} style={styles.root}> - - - - {this.props.title} - - - - - {this.props.publicationDate} - - - blog.nem.io - - - - - - - {this.props.body} - - - - - - this.onPress()}> - - {translate('news.readMore')} - - - - - {/* - {this.props.title} - - - {this.props.publicationDate} - */} - - ); - } -} diff --git a/src/components/organisms/transaction/AggregateTransaction.js b/src/components/organisms/transaction/AggregateTransaction.js index aee9100..41da9cb 100644 --- a/src/components/organisms/transaction/AggregateTransaction.js +++ b/src/components/organisms/transaction/AggregateTransaction.js @@ -11,19 +11,21 @@ import { showPasscode } from '@src/utils/passcode'; import translate from '@src/locales/i18n'; import { getFinanceBotPublicKeys } from '@src/config/environment'; import Icon from '@src/components/controls/Icon'; -import { TransactionType, UInt64, AggregateTransaction as SdkAggregateTransaction, TransactionHttp } from 'symbol-sdk'; +import { TransactionType, UInt64, AggregateTransaction as SdkAggregateTransaction } from 'symbol-sdk'; import TransactionService from '@src/services/TransactionService'; +import _ from 'lodash'; +import { Router } from '@src/Router'; type Props = { transaction: AggregateTransactionModel, }; class AggregateTransaction extends BaseTransactionItem { - state: { + state = { fullTransaction: null, }; - componentDidMount() { + async componentDidMount() { const { selectedNode, transaction } = this.props; TransactionService.getTransaction(transaction.hash, selectedNode).then(async tx => { this.setState({ fullTransaction: tx }); @@ -65,18 +67,39 @@ class AggregateTransaction extends BaseTransactionItem { showPasscode(this.props.componentId, () => { const { transaction } = this.props; store.dispatchAction({ type: 'transfer/signAggregateBonded', payload: transaction }).then(_ => { + this.showCosignatureMessage(translate('notification.newCosignatureAdded')); store.dispatchAction({ type: 'transaction/changeFilters', payload: {} }); }); }); } + // check if the transaction needs to be Signed from current signer needsSignature = () => { if (this.isPostLaunchOptIn()) return false; - const { transaction, selectedAccount, isMultisig } = this.props; + const { transaction, selectedAccount, isMultisig, cosignatoryOf } = this.props; const accountPubKey = getPublicKeyFromPrivateKey(selectedAccount.privateKey); - return !isMultisig && transaction.cosignaturePublicKeys.indexOf(accountPubKey) === -1 && transaction.status !== 'confirmed'; + const cosignerAddresses = transaction.innerTransactions.map((t) => t.signerAddress); + const cosignRequired = cosignerAddresses.find((c) => { + if (c) { + return ( + (cosignatoryOf && cosignatoryOf.some((address) => address === c)) + ); + } + return false; + }); + return !isMultisig && (((transaction.cosignaturePublicKeys.indexOf(accountPubKey) === -1 && transaction.status !== 'confirmed'))|| (this.hasMissSignatures() && cosignRequired!==undefined)); }; + // check if the transaction misses cosignatories + hasMissSignatures=()=> { + const {transaction}= this.props; + return ( + transaction?.transactionInfo != null && + transaction?.transactionInfo.merkleComponentHash !== undefined && + transaction?.transactionInfo.merkleComponentHash.startsWith('000000000000') + ); + } + renderAction = () => { if (this.needsSignature()) { return ( @@ -87,6 +110,14 @@ class AggregateTransaction extends BaseTransactionItem { } }; + // show notification for transaction signing + showCosignatureMessage = (message: string) => { + Router.showMessage({ + message: message, + type: 'success', + }); + }; + renderDetails = () => { const { transaction, isLoading, isMultisig } = this.props; const table = { innerTxs: transaction.innerTransactions.length }; @@ -123,4 +154,5 @@ export default connect(state => ({ isMultisig: state.account.isMultisig, network: state.network.selectedNetwork.type, address: state.account.selectedAccountAddress, + cosignatoryOf: state.account.cosignatoryOf, }))(AggregateTransaction); diff --git a/src/components/settings/SettingsNodeSelector.js b/src/components/settings/SettingsNodeSelector.js index cfd815c..3ce86d2 100644 --- a/src/components/settings/SettingsNodeSelector.js +++ b/src/components/settings/SettingsNodeSelector.js @@ -6,7 +6,6 @@ import PopupModal from '@src/components/molecules/PopupModal'; import {Dropdown, Text, Section, Row, ManagerHandler, Input, Button} from '@src/components'; import { connect } from 'react-redux'; import store from '@src/store'; -import { getNodes } from '@src/config/environment'; import GlobalStyles from '@src/styles/GlobalStyles'; import ConfirmModal from "@src/components/molecules/ConfirmModal"; @@ -147,16 +146,15 @@ class SettingsNodeSelector extends Component { } render = () => { + const { isModalOpen, error, loading, selectedTab, isConfirmModalOpen } = this.state; + const { selectedNode, selectedNetwork, testnetNodes, mainnetNodes } = this.props; + const nodes = { - mainnet: getNodes('mainnet').map(node => ({ value: node, label: node })), - testnet: getNodes('testnet').map(node => ({ value: node, label: node })), + mainnet: mainnetNodes.map(node => ({ value: node, label: node })), + testnet: testnetNodes.map(node => ({ value: node, label: node })), custom: [], }; - const { isModalOpen, error, loading, selectedTab, isConfirmModalOpen } = this.state; - const { selectedNode, selectedNetwork } = this.props; - const list = nodes[selectedTab]; - return ( '' + index + 'nodes'} /> @@ -270,4 +268,6 @@ class SettingsNodeSelector extends Component { export default connect(state => ({ selectedNode: state.network.selectedNetwork ? state.network.selectedNetwork.node : '', selectedNetwork: state.network.selectedNetwork ? state.network.selectedNetwork.type : '', + testnetNodes: state.network.testnetNodes || [], + mainnetNodes: state.network.mainnetNodes || [], }))(SettingsNodeSelector); diff --git a/src/config/environment.js b/src/config/environment.js index 966d4a3..c2ccb77 100644 --- a/src/config/environment.js +++ b/src/config/environment.js @@ -5,8 +5,8 @@ import { sessionTimeoutInSeconds, marketCurrencyName, - newsURL, explorerURL, + statisticsServiceURL, faucetURL, aboutURL, currencies, @@ -22,9 +22,9 @@ import { optInWhiteList, nglFinanceBot, } from 'react-native-env-json'; -import { NetworkType } from "symbol-sdk"; +import { NetworkType } from 'symbol-sdk'; import { languageNames } from '@src/locales/i18n'; -import type {AppNetworkType} from "@src/storage/models/NetworkModel"; +import type { AppNetworkType } from '@src/storage/models/NetworkModel'; // Session timeout const getSessionTimeoutInMillis = (): number => { @@ -36,16 +36,16 @@ const getMarketCurrencyLabel = (): string => { return marketCurrencyName; }; -// News URL -const getNewsURL = (): string => { - return newsURL; -}; - // Explorer URL const getExplorerURL = (network: AppNetworkType): string => { return explorerURL[network]; }; +// Statistics Service URL +const getStatisticsServiceURL = (network: AppNetworkType): string => { + return statisticsServiceURL[network]; +}; + // Explorer URL const getFaucetUrl = (): string => { return faucetURL; @@ -115,14 +115,13 @@ const isCustomNode = (nodeType: string): boolean => { // Network info const getNetworkInfo = (nodeType: string): Object => { - console.log(networks) const networkConfig = networks[nodeType]; // use mainnet as fallback config return networkConfig !== undefined ? networkConfig : networks.MAINNET; }; const getOptinEnv = (): string => { - return optinEnv + return optinEnv; }; const getNISNodes = (network: 'mainnet' | 'testnet' = 'testnet'): string[] => { @@ -133,10 +132,6 @@ const getDefaultNetworkType = (): NetworkType => { return defaultNetworkType; }; -const getNodes = (network: 'mainnet' | 'testnet' = 'testnet'): string[] => { - return networks[network].nodes; -}; - const getNativeMosaicId = (): string[] => { return nativeMosaicId; }; @@ -149,11 +144,16 @@ const getFinanceBotPublicKeys = (network: 'mainnnet' | 'testnet' = 'testnet'): s return nglFinanceBot[network]; }; +const getHarvestingPrerequisitesUrl = (): string => { + const prerequisitesURL = 'https://docs.symbolplatform.com/guides/harvesting/activating-delegated-harvesting-wallet.html#prerequisites'; + return prerequisitesURL; +}; + export { getSessionTimeoutInMillis, getMarketCurrencyLabel, - getNewsURL, getExplorerURL, + getStatisticsServiceURL, getFaucetUrl, getAboutURL, getCurrencyList, @@ -173,8 +173,8 @@ export { getOptinEnv, getNISNodes, getDefaultNetworkType, - getNodes, getNativeMosaicId, getWhitelistedPublicKeys, getFinanceBotPublicKeys, + getHarvestingPrerequisitesUrl, }; diff --git a/src/config/index.js b/src/config/index.js index 8a20294..ad6bd22 100644 --- a/src/config/index.js +++ b/src/config/index.js @@ -1,16 +1,6 @@ export * as environment from './environment'; export const menuItems = [ - { - text: 'menu.news', - iconName: 'news', - name: 'news', - }, - { - text: 'menu.mosaics', - iconName: 'mosaics', - name: 'mosaics', - }, { text: 'menu.home', iconName: 'home', @@ -21,6 +11,11 @@ export const menuItems = [ iconName: 'history', name: 'history', }, + { + text: 'menu.mosaics', + iconName: 'mosaics', + name: 'mosaics', + }, { text: 'menu.harvest', iconName: 'harvest', diff --git a/src/locales/translations/en.json b/src/locales/translations/en.json index ba04e15..cf36cb7 100644 --- a/src/locales/translations/en.json +++ b/src/locales/translations/en.json @@ -35,7 +35,6 @@ "title": "Import Wallet", "content": "Import your Wallet by entering your mnemonics in order.", "button": "Import", - "ignoreWarning": "Ignore Warning", "cancelModalButton": "Cancel", "error": { "invalidMnemonic": "Invalid Mnemonic", @@ -480,7 +479,11 @@ "optInTitle": "Post-launch Opt-in", "optInDescription": "Press here to access the dashboard", "pendingSignatureTitle": "Pending signature", - "pendingSignatureDescription": "There is a transaction pending awaiting signature" + "pendingSignatureDescription": "There is a transaction pending awaiting signature", + "alertTitle":"Error", + "alertActionCreateWallet": "Create New", + "alertActionCancel": "Cancel", + "alertInvalidMnemonicMessage": "This profile does not have a valid mnemonic phrase. Please create a new profile and transfer your funds as soon as possible." }, "mosaics": { "title": "Mosaics" @@ -524,9 +527,11 @@ "INPROGRESS_DEACTIVATION": "Deactivation in progress", "harvetingIntroTitle": "Start harvesting to earn a block rewards", "minBalanceRequirement": "Insufficient balance, the account needs to hold at least %{balance} to start delegated harvesting.", + "nonZeroImportanceRequirement": "Your account cannot start delegated harvesting while importance is equal to 0. Importance recalculation might take up to 12h.", "KEYS_LINKED": "Keys linked", "viewLinkedKeys": "View Linked Keys", - "linkedNode": "Linked Node" + "linkedNode": "Linked Node", + "Importance": "Importance score" }, "sidebar": { "seed": "Seed account", @@ -579,6 +584,7 @@ "newUnconfirmed": "New unconfirmed transaction!", "newConfirmed": "New confirmed transaction!", "newAggregate": "New aggregate transaction!", + "newCosignatureAdded":"Cosignature received!", "noMosaicPresent": "There is no mosaic '%{mosaicName}' present in this account" }, "addressBook": { diff --git a/src/locales/translations/es.json b/src/locales/translations/es.json index a0eaf15..c7c9420 100644 --- a/src/locales/translations/es.json +++ b/src/locales/translations/es.json @@ -35,7 +35,6 @@ "title": "Importar Wallet", "content": "Importa tu Wallet Symbol al ingresar las palabras nemónicas en el orden correcto.", "button": "Importar", - "ignoreWarning": "Ignorar la advertencia", "cancelModalButton": "Cancelar", "error": { "invalidMnemonic": "Nemónica inválida", @@ -471,7 +470,11 @@ "optInTitle": "Opt-in", "optInDescription": "El proceso de opt-in post lanzamiento se abirá pronto. Por favor comprueba nuevas versiones después del lanzamiento de Symbol.", "pendingSignatureTitle": "Firma pendiente", - "pendingSignatureDescription": "Hay una transacción pendiente a la espera de la firma" + "pendingSignatureDescription": "Hay una transacción pendiente a la espera de la firma", + "alertTitle":"Error", + "alertActionCreateWallet": "Crear nuevo", + "alertActionCancel": "Cancelar", + "alertInvalidMnemonicMessage": "Este perfil no está asociado a una frase nemónica válida. Por favor crea un nuevo perfil y transfiere tus fondos lo antes posible." }, "mosaics": { "title": "Mosaics" @@ -514,9 +517,11 @@ "INPROGRESS_DEACTIVATION": "Desactivación en curso", "harvetingIntroTitle": "Comienza a cosechar para ganar recompensas en bloque", "minBalanceRequirement": "Para cosechar, la cuenta necesita un saldo de por lo menos %{balance}", + "nonZeroImportanceRequirement": "Su cuenta no puede iniciar la recolección delegada mientras la importancia sea igual a 0. El recálculo de la importancia puede demorar hasta 12 horas.", "KEYS_LINKED": "Llaves enlazadas", "viewLinkedKeys": "Ver las llaves enlazadas", - "linkedNode": "Nodo enlazado" + "linkedNode": "Nodo enlazado", + "Importance": "Puntuación de importancia" }, "sidebar": { "seed": "Cuenta semillero", @@ -568,6 +573,7 @@ "newUnconfirmed": "Nueva transacción sin confirmar!", "newConfirmed": "Nueva transacción confirmada!", "newAggregate": "Nueva transacción agregada!", + "newCosignatureAdded":"Firma recibida!", "noMosaicPresent": "El mosaico '%{mosaicName}' no esta presente en esta cuenta" }, diff --git a/src/locales/translations/it.json b/src/locales/translations/it.json index d86e255..4e29340 100644 --- a/src/locales/translations/it.json +++ b/src/locales/translations/it.json @@ -35,7 +35,6 @@ "title": "Importa portafoglio", "content": "Importa il tuo portafoglio inserendo il tuo mnemonico.", "button": "Importare", - "ignoreWarning": "Ignora l'avviso", "cancelModalButton": "Annulla", "error": { "invalidMnemonic": "Mnemonico non valido", @@ -470,7 +469,11 @@ "optInTitle": "Opt-In", "optInDescription": "Il processo di attivazione (Opt-in) si aprirà dopo il lancio della blockchain di Symbol. Attendi la nuova versione del portafoglio.", "pendingSignatureTitle": "In attesa di firma", - "pendingSignatureDescription": "C'è una transazione in attesa di firma" + "pendingSignatureDescription": "C'è una transazione in attesa di firma", + "alertTitle":"Errore", + "alertActionCreateWallet": "Creare un nuovo account", + "alertActionCancel": "Annulla", + "alertInvalidMnemonicMessage": "Questo profilo non ha una frase mnemonica valida. Trasferisci il tuo XYM su un altro account il prima possibile." }, "mosaics": { "title": "Mosaico" @@ -513,9 +516,11 @@ "INPROGRESS_DEACTIVATION": "Disattivazione in corso ", "harvetingIntroTitle": "Inizia a Harvesting per guadagnare ricompense di blocchi", "minBalanceRequirement": "Saldo insufficiente sull'account, poiché deve essere presente almeno %{balance} per avviare la delegated harvesting.", + "nonZeroImportanceRequirement": "Il tuo account non può avviare la raccolta delegata mentre l'importanza è uguale a 0. Il ricalcolo dell'importanza potrebbe richiedere fino a 12 ore.", "viewLinkedKeys": "Visualizzazione delle chiavi correlate", "linkedNode": "Nodo correlato (noda)", - "KEYS_LINKED": "Chiavi correlate" + "KEYS_LINKED": "Chiavi correlate", + "Importance": "Punteggio di importanza" }, "sidebar": { "seed": "Account seed frase", diff --git a/src/locales/translations/ja.json b/src/locales/translations/ja.json index 084be47..9e09602 100644 --- a/src/locales/translations/ja.json +++ b/src/locales/translations/ja.json @@ -34,7 +34,6 @@ "title": "ウォレットのインポート", "content": "正しい順序でニーモニックを入力して、ウォレットをインポートします。", "button": "インポート", - "ignoreWarning": "警告を無視する", "cancelModalButton": "キャンセル", "error": { "invalidMnemonic": "ニーモニックが無効です", @@ -475,7 +474,11 @@ "optInTitle": "オプトイン", "optInDescription": "Symbolローンチ後のオプトインはこちらから", "pendingSignatureTitle": "ペンディング署名", - "pendingSignatureDescription": "署名待ちのペンディングトランザクションがあります" + "pendingSignatureDescription": "署名待ちのペンディングトランザクションがありま", + "alertTitle":"エラーが発生しました", + "alertActionCreateWallet": "新規作成", + "alertActionCancel": "キャンセル", + "alertInvalidMnemonicMessage": "このプロファイルには、有効なニーモニックフレーズがありません。新しいプロファイルを作成し、XYMを出来るだけ早く転送してください。" }, "mosaics": { "title": "モザイク" @@ -519,9 +522,11 @@ "INPROGRESS_DEACTIVATION": "無効化(進行中)", "harvetingIntroTitle": "ブロック報酬を獲得するためハーベストを開始", "minBalanceRequirement": "残高が不十分です。委任ハーベスティングを開始するには、アカウントに少なくとも%{balance}を保有している必要があります。", + "nonZeroImportanceRequirement": "重要度が0の場合、アカウントは委任された収穫を開始できません。重要度の再計算には最大12時間かかる場合があります.", "KEYS_LINKED": "リンクされた鍵", "viewLinkedKeys": "リンクされたキーを表示", - "linkedNode": "リンクされたノード" + "linkedNode": "リンクされたノード", + "Importance": "重要度スコア" }, "sidebar": { "seed": "シードアカウント", @@ -573,6 +578,7 @@ "newUnconfirmed": "新しい未承認トランザクションがあります!", "newConfirmed": "New confirmed transaction!", "newAggregate": "New aggregate transaction!", + "newCosignatureAdded":"連署名を受信!", "noMosaicPresent": "このアカウントにはモザイク「%{mosaicName}」はありません。" }, "addressBook": { diff --git a/src/locales/translations/pl.json b/src/locales/translations/pl.json index 8471b78..0d9cd4a 100644 --- a/src/locales/translations/pl.json +++ b/src/locales/translations/pl.json @@ -33,7 +33,6 @@ "title": "Zaimportuj Portfel", "content": "Zaimportuj konto, wprowadzając mnemoniki w odpowiedniej kolejności.", "button": "Zaimportuj", - "ignoreWarning": "Zignoruj Ostrzeżenie", "cancelModalButton": "Anuluj", "error": { "invalidMnemonic": "Niepoprawny Mnemonik", @@ -380,6 +379,8 @@ "registerButton": "Zarejestruj Konto Delegowane", "startHarvestingButton": "Rozpocznij Zbieranie", "stopHarvestingButton": "Zatrzymaj Zbieranie", + "minBalanceRequirement": "Niewystarczające saldo, konto musi zawierać co najmniej %{balance}, aby rozpocząć delegowane zbieranie.", + "nonZeroImportanceRequirement": "Twoje konto nie może rozpocząć delegowanych zbiorów, gdy ważność jest równa 0. Ponowne obliczenie ważności może potrwać do 12 godzin.", "urlError": "Nie można otworzyć linku" }, "RegisterAccount": { diff --git a/src/locales/translations/ru.json b/src/locales/translations/ru.json index c0bfa52..b55ef80 100644 --- a/src/locales/translations/ru.json +++ b/src/locales/translations/ru.json @@ -35,7 +35,6 @@ "title": "Импортировать кошелек", "content": "Импортируйте свой кошелек, введя кодовую фразу (мнемонику).", "button": "Импортировать", - "ignoreWarning": "Игнорировать предупреждение", "cancelModalButton": "Отмена", "error": { "invalidMnemonic": "Недействительная мнемоника", @@ -434,14 +433,6 @@ "name": "Имя", "seedIndex": "Индекс Сид аккаунта ", - "namespaceId": "Идентификатор Пространств имен", - "namespace": "Пространство имен", - "phone": "Телефон", - "email": "Email", - "label": "Лейбл", - "notes": "Заметка", - "seedIndex": "Сид Индекс Аккаунт ", - "vrfPublicKey": "Связанный публичный Vrf ключ", "vrfPrivateKey": "Связанный приватный Vrf ключ", "remotePublicKey": "Связанный удаленный публичный ключ", @@ -488,7 +479,11 @@ "optInTitle": "Пост Лаунч Опт-Ин", "optInDescription": "Нажмите здесь, чтобы получить доступ к дашборд", "pendingSignatureTitle": "Ожидает подписания", - "pendingSignatureDescription": "Имеется транзакция, которая ожидает подписи" + "pendingSignatureDescription": "Имеется транзакция, которая ожидает подписи", + "alertTitle":"Ошибка", + "alertActionCreateWallet": "Создать новый аккаунт", + "alertActionCancel": "Отмена", + "alertInvalidMnemonicMessage": "В этом профиле нет действующей мнемонической фразы. Пожалуйста, как можно скорее переведите свои XYM на другой аккаунт." }, "mosaics": { "title": "Мозаика" @@ -532,9 +527,11 @@ "INPROGRESS_DEACTIVATION": "Деактивация в процессе", "harvetingIntroTitle": "Активируйте харвестинг, чтобы зарабатывать награды за новые блоки", "minBalanceRequirement": "Недостаточный баланс на аккаунте. Должно быть не менее %{balance}, для того чтобы начать делегированный харвестинг.", + "nonZeroImportanceRequirement": "Ваша учетная запись не может начать делегированный сбор урожая, пока важность равна 0. Пересчет важности может занять до 12 часов.", "KEYS_LINKED": "Ключи связанны", "viewLinkedKeys": "Просмотр связанных ключей", - "linkedNode": "Связанная нода" + "linkedNode": "Связанная нода", + "Importance": "оценка важности" }, "sidebar": { "seed": "Cид-фраза аккаунта", @@ -587,6 +584,7 @@ "newUnconfirmed": "Новая неподтвержденная транзакция!", "newConfirmed": "Новая подтвержденная транзакция!", "newAggregate": "Новая агрегированная транзакция!", + "newCosignatureAdded":"Подпись получена!", "noMosaicPresent": "В этом аккаунте '%{mosaicName}' нет мозаики" }, "addressBook": { diff --git a/src/locales/translations/uk.json b/src/locales/translations/uk.json index 331d28b..21888e2 100644 --- a/src/locales/translations/uk.json +++ b/src/locales/translations/uk.json @@ -35,7 +35,6 @@ "title": "Імпортувати гаманець", "content": "Імпортуйте свій гаманець, ввівши кодову фразу (мнемоніку).", "button": "Імпортувати", - "ignoreWarning": "Ігнорувати попередження", "cancelModalButton": "Скасувати", "error": { "invalidMnemonic": "Неправильна мнемоніка", diff --git a/src/locales/translations/zh.json b/src/locales/translations/zh.json index 01a3561..b86adbc 100644 --- a/src/locales/translations/zh.json +++ b/src/locales/translations/zh.json @@ -34,7 +34,6 @@ "title": "导入钱包", "content": "按顺序输入助记词", "button": "导入", - "ignoreWarning": "忽略警告", "cancelModalButton": "取消", "error": { "invalidMnemonic": "助记词无效", @@ -466,7 +465,11 @@ "optInTitle": "选择加入", "optInDescription": "发布后的Opt-in即将开放。 Symbol启动后,请重新检查是否有新版本", "pendingSignatureTitle": "待定签名", - "pendingSignatureDescription": "有一笔交易正在等待签名" + "pendingSignatureDescription": "有一笔交易正在等待签名", + "alertTitle":"错误", + "alertActionCreateWallet": "创建新账户", + "alertActionCancel": "取消", + "alertInvalidMnemonicMessage": "此配置文件没有有效的助记词。请尽快将您的 XYM 转移到另一个帐户。" }, "mosaics": { "title": "马赛克" @@ -509,9 +512,11 @@ "INPROGRESS_DEACTIVATION": "停用正在进行中", "harvetingIntroTitle": "开始收获,赚取区块奖励", "minBalanceRequirement": "余额不足,帐户需要持有至少 %{balance}才能开始进行委派的收获。", + "nonZeroImportanceRequirement": "当重要性等于 0 时,您的帐户无法开始委托收获。重要性重新计算可能需要长达 12 小时.", "KEYS_LINKED": "关联的钥匙", "viewLinkedKeys": "查看已链接的钥匙", - "linkedNode": "已链接的节点" + "linkedNode": "已链接的节点", + "Importance": "重要性得分" }, "sidebar": { "seed": "种子帐户", diff --git a/src/screens/Dashboard.js b/src/screens/Dashboard.js index a07fc40..d0420fe 100644 --- a/src/screens/Dashboard.js +++ b/src/screens/Dashboard.js @@ -2,15 +2,14 @@ import React, { Component } from 'react'; import { StyleSheet, View } from 'react-native'; import { menuItems } from '@src/config'; import { NavigationMenu, GradientBackground } from '@src/components'; -import Home from './Home'; -import History from './History'; -import Harvest from './Harvest'; -import News from '@src/screens/News'; +import NodeDownOverlay from '@src/components/organisms/NodeDownOverlay'; +import Home from '@src/screens/Home'; +import History from '@src/screens/History'; +import Harvest from '@src/screens/Harvest'; import Mosaics from '@src/screens/Mosaics'; import Sidebar from '@src/screens/Sidebar'; import { Router } from '@src/Router'; import store from '@src/store'; -import NodeDownOverlay from '@src/components/organisms/NodeDownOverlay'; import { connect } from 'react-redux'; const styles = StyleSheet.create({ @@ -59,9 +58,6 @@ class Dashboard extends Component { case 'mosaics': Tab = Mosaics; break; - case 'news': - Tab = News; - break; case 'harvest': Tab = Harvest; break; diff --git a/src/screens/EnterMnemonics/index.js b/src/screens/EnterMnemonics/index.js index a0b30b6..7ede63c 100644 --- a/src/screens/EnterMnemonics/index.js +++ b/src/screens/EnterMnemonics/index.js @@ -180,11 +180,6 @@ class EnterMnemonics extends Component { // Router go to pre-dashboard }; - forceCreate = async () => { - this.setState({ showWarning: false }); - this.createWallet(); - }; - hideWarning = () => { this.setState({ showWarning: false }); }; @@ -199,9 +194,7 @@ class EnterMnemonics extends Component { {showWarning && ( )} diff --git a/src/screens/Harvest.js b/src/screens/Harvest.js index 72bbccc..0fc91d7 100644 --- a/src/screens/Harvest.js +++ b/src/screens/Harvest.js @@ -1,15 +1,15 @@ import React, { Component } from 'react'; import { StyleSheet, Text as NativeText, TouchableOpacity, View } from 'react-native'; -import { Section, ImageBackground, GradientBackground, Text, TitleBar, NodeDropdown, Button, Col, Row } from '@src/components'; +import { Section, GradientBackground, Text, TitleBar, NodeDropdown, Button, Row } from '@src/components'; import GlobalStyles from '@src/styles/GlobalStyles'; import { connect } from 'react-redux'; import HarvestingService from '@src/services/HarvestingService'; import store from '@src/store'; import { showPasscode } from '@src/utils/passcode'; import translate from '@src/locales/i18n'; -import Trunc from '@src/components/organisms/Trunc'; -import {Router} from "@src/Router"; - +import { Router } from '@src/Router'; +import ReadMoreLink from '@src/components/controls/ReadMoreLink'; +import { getHarvestingPrerequisitesUrl } from '@src/config/environment'; const styles = StyleSheet.create({ showButton: { textAlign: 'right', @@ -73,7 +73,8 @@ class Harvest extends Component { isLoading: false, }; - componentDidMount() { + + async componentDidMount() { const { selectedAccount, nodes } = this.props; if (selectedAccount.harvestingNode) { for (let node of nodes) { @@ -98,6 +99,7 @@ class Harvest extends Component { label: `http://${node.url}:3000`, })); }; + onSelectHarvestingNode = node => { const url = node; @@ -154,9 +156,24 @@ class Harvest extends Component { }; render() { - const { status, totalBlockCount, totalFeesEarned, onOpenMenu, onOpenSettings, balance, minRequiredBalance, nativeMosaicNamespace, harvestingModel, selectedAccount } = this.props; + const { + status, + totalBlockCount, + totalFeesEarned, + onOpenMenu, + onOpenSettings, + balance, + minRequiredBalance, + nativeMosaicNamespace, + harvestingModel, + selectedAccount, + accountImportance + } = this.props; const { selectedNodeUrl, isLoading } = this.state; const notEnoughBalance = balance < minRequiredBalance; + const notEnoughBalanceTitle = translate('harvest.minBalanceRequirement', { balance: minRequiredBalance + ' ' + nativeMosaicNamespace }) + const zeroImportanceTitle = translate('harvest.nonZeroImportanceRequirement') + const url = getHarvestingPrerequisitesUrl(); let statusStyle; switch (status) { case 'ACTIVE': @@ -212,6 +229,14 @@ class Harvest extends Component { {totalFeesEarned.toString()} + + + {translate('harvest.Importance')}: + + + {accountImportance} + + {status !== 'INACTIVE' && ( this.onViewLinkedKeysClick()} style={{ textAlign: 'right', width: '100%' }}> @@ -239,7 +264,7 @@ class Harvest extends Component { )}
- {!notEnoughBalance && status === 'INACTIVE' && (<> + {!notEnoughBalance && status === 'INACTIVE' && accountImportance !== '0%' && (<>
{
)} + {notEnoughBalance && (
- - {translate('harvest.minBalanceRequirement', { balance: minRequiredBalance + ' ' + nativeMosaicNamespace })} - +
)} + + + {!notEnoughBalance && accountImportance == '0%' && ( + + )} +
@@ -306,4 +336,8 @@ export default connect(state => ({ totalFeesEarned: state.harvesting.harvestedBlockStats.totalFeesEarned, harvestingModel: state.harvesting.harvestingModel, nodes: state.harvesting.nodes, + selectedNode: state.network.selectedNetwork ? state.network.selectedNetwork.node : '', + selectedAccountAddress: state.account.selectedAccountAddress, + networkCurrencyDivisibility: state.network.selectedNetwork.currencyDivisibility, + accountImportance: state.harvesting.accountImportance, }))(Harvest); diff --git a/src/screens/Home.js b/src/screens/Home.js index 623c71b..28b9d35 100644 --- a/src/screens/Home.js +++ b/src/screens/Home.js @@ -1,5 +1,5 @@ import React, { Component } from 'react'; -import {StyleSheet, ScrollView, TouchableOpacity, View, RefreshControl, FlatList} from 'react-native'; +import { StyleSheet, ScrollView, TouchableOpacity, View, RefreshControl, FlatList } from 'react-native'; import { Section, GradientBackground, @@ -10,14 +10,15 @@ import { Row, Icon, TitleBar, - ListItem + ListItem, + ListContainer, + BasicAlert } from '@src/components'; import { Router } from '@src/Router'; import { connect } from 'react-redux'; import store from '@src/store'; -import ListContainer from "@src/components/backgrounds/ListContainer"; -import GlobalStyles from "@src/styles/GlobalStyles"; -import translate from "@src/locales/i18n"; +import GlobalStyles from '@src/styles/GlobalStyles'; +import translate from '@src/locales/i18n'; const styles = StyleSheet.create({ transactionPreview: { @@ -60,7 +61,7 @@ type Props = { type State = {}; class Home extends Component { - state = { isSidebarShown: false }; + state = { isSidebarShown: false, showWarning: false}; reload = () => { store.dispatchAction({ type: 'account/loadAllData' }); @@ -86,6 +87,13 @@ class Home extends Component { ); } + componentDidMount = () => { + // If wallet created by words mnemonic not equal 24, show warning message. + if (store.getState().wallet.mnemonic.split(' ').length !== 24){ + this.setState({showWarning: true}) + } + }; + render = () => { const { pendingSignature, @@ -98,7 +106,8 @@ class Home extends Component { isLoading, isMultisig } = this.props; - const {} = this.state; + + const { showWarning } = this.state; const notifications = []; notifications.push({ title: translate('home.optInTitle'), description: translate('home.optInDescription' ), handler: () => Router.goToOptInWelcome({}, this.props.componentId)}); @@ -114,6 +123,22 @@ class Home extends Component { fade={true} titleBar={ onOpenMenu()} onSettings={() => onOpenSettings()}/>} > + {showWarning && Router.goToCreateOrImport({}), + }, + { + text: translate('home.alertActionCancel'), + onPress: () => { + this.setState({ showWarning: false }); + }, + } + ]} + />}
diff --git a/src/screens/Mosaics.js b/src/screens/Mosaics.js index c8af4e7..a2370b4 100644 --- a/src/screens/Mosaics.js +++ b/src/screens/Mosaics.js @@ -29,8 +29,8 @@ class Mosaics extends Component { ); }; - refresh = () => { - store.dispatchAction({ type: 'account/loadBalance' }); + refresh = async() => { + await store.dispatchAction({ type: 'account/loadBalance' }); }; render() { diff --git a/src/screens/News.js b/src/screens/News.js deleted file mode 100644 index d658bdb..0000000 --- a/src/screens/News.js +++ /dev/null @@ -1,90 +0,0 @@ -import React, { Component } from 'react'; -import { RefreshControl, StyleSheet, View, FlatList } from 'react-native'; -import { GradientBackground, ListContainer, ListItem, TitleBar } from '@src/components'; -import New from '@src/components/organisms/New'; -import GlobalStyles from '@src/styles/GlobalStyles'; -import { connect } from 'react-redux'; -import store from '@src/store'; -import translate from "@src/locales/i18n"; - -const styles = StyleSheet.create({ - list: { - marginBottom: 10, - }, - inner: { - borderRadius: 6, - backgroundColor: GlobalStyles.color.WHITE, - } -}); - -type Props = {}; -type State = {}; - -class News extends Component { - constructor(props: {}) { - super(props); - } - - refresh = () => { - store.dispatchAction({type: 'news/loadNews'}); - } - - renderNewsItem = (item) => { - return - - - } - - render() { - const { news, onOpenMenu, onOpenSettings, isLoading } = this.props; - const dataManager = {isLoading}; - - return ( - onOpenMenu()} - onSettings={() => onOpenSettings()} - />} - > - - - '' + index + 'news'} - refreshControl={ - this.refresh()} - /> - } - /> - - {/* - {news.map(this.renderNewsItem)} - */} - - - ); - } -} - -export default connect(state => ({ - news: state.news.news, - isLoading: state.news.isLoading -}))(News); diff --git a/src/screens/Send.js b/src/screens/Send.js index 613a199..1152f3c 100644 --- a/src/screens/Send.js +++ b/src/screens/Send.js @@ -68,7 +68,6 @@ class Send extends Component { if(message) this.onMessageChange(message); - this.updateMaxFee(); }; verify = () => { @@ -197,10 +196,11 @@ class Send extends Component { ); }; - onAddressChange = recipientAddress => { + onAddressChange = async recipientAddress => { const { network } = this.props; const showAddressError = !isAddressValid(recipientAddress, network); this.setState({ recipientAddress, showAddressError, isEncrypted: false }); + await this.updateMaxFee(); }; onAmountChange = async val => { @@ -229,7 +229,7 @@ class Send extends Component { } - this.updateMaxFee(); + await this.updateMaxFee(); this.verifyAmount(); }; @@ -265,7 +265,7 @@ class Send extends Component { message.slice(0, 1024); } await this.setState({ message, isEncrypted }); - this.updateMaxFee(); + await this.updateMaxFee(); }; onMessageEncryptedChange = async isEncrypted => { @@ -291,12 +291,12 @@ class Send extends Component { this.setState({isEncrypted: false}); } - this.updateMaxFee(); + await this.updateMaxFee(); }; - onFeeChange(fee) { + onFeeChange = async fee => { this.setState({ fee }); - this.updateMaxFee(); + await this.updateMaxFee(); }; render = () => { diff --git a/src/screens/WalletLoading/index.js b/src/screens/WalletLoading/index.js index 58bceec..222f73f 100644 --- a/src/screens/WalletLoading/index.js +++ b/src/screens/WalletLoading/index.js @@ -22,7 +22,10 @@ class WalletLoading extends Component { } else { store.dispatchAction({type: 'wallet/saveWallet'}).then(_ => { Router.goToDashboard({}) - }); + }) + .finally(() => { + Router.goToDashboard({}) + }) } }; diff --git a/src/services/AccountService.js b/src/services/AccountService.js index 658054e..5cdd4cf 100644 --- a/src/services/AccountService.js +++ b/src/services/AccountService.js @@ -1,13 +1,24 @@ -import { AccountHttp, Account, Address, NetworkType, Mosaic, MosaicHttp, NamespaceHttp, MultisigHttp, MosaicId, UInt64 } from 'symbol-sdk'; -import {ExtendedKey, MnemonicPassPhrase, Network, Wallet} from 'symbol-hd-wallets'; -import type { AccountModel, AccountOriginType } from '@src/storage/models/AccountModel'; +import { + AccountHttp, + Account, + Address, + NetworkType, + Mosaic, + MosaicHttp, + NamespaceHttp, + MultisigHttp, + MosaicId, + UInt64, +} from 'symbol-sdk'; +import { ExtendedKey, MnemonicPassPhrase, Network, Wallet } from 'symbol-hd-wallets'; +import type { AccountModel, AccountOriginType, MultisigAccountInfo } from '@src/storage/models/AccountModel'; import type { MnemonicModel } from '@src/storage/models/MnemonicModel'; import type { AppNetworkType, NetworkModel } from '@src/storage/models/NetworkModel'; import type { MosaicModel } from '@src/storage/models/MosaicModel'; import { AccountSecureStorage } from '@src/storage/persistence/AccountSecureStorage'; import MosaicService from '@src/services/MosaicService'; import { SymbolPaperWallet } from 'symbol-paper-wallets'; -import {getAccountIndexFromDerivationPath} from "@src/utils/format"; +import { getAccountIndexFromDerivationPath } from '@src/utils/format'; export default class AccountService { /** @@ -272,4 +283,55 @@ export default class AccountService { return btoa(Uint8ToString(bytes)); } + + // get signers for current account + static getSigners( + currentAccountAddress: Address, + multisigAccountGraph?: Map, + level?: number, + childMinApproval?: number, + childMinRemoval?: number + ) { + let currentMultisigAccountInfo: MultisigAccountInfo; + if (level === undefined) { + for (const [l, levelAccounts] of multisigAccountGraph) { + for (const levelAccount of levelAccounts) { + if (levelAccount.accountAddress.equals(currentAccountAddress)) { + currentMultisigAccountInfo = levelAccount; + level = l; + break; + } + } + } + } else { + for (const levelAccount of multisigAccountGraph.get(level)) { + if (levelAccount.accountAddress.equals(currentAccountAddress)) { + currentMultisigAccountInfo = levelAccount; + } + } + } + const currentSigner = { + address: currentAccountAddress, + multisig: currentMultisigAccountInfo?.isMultisig() || false, + requiredCosigApproval: Math.max(childMinApproval || 0, currentMultisigAccountInfo?.minApproval || 0), + requiredCosigRemoval: Math.max(childMinRemoval || 0, currentMultisigAccountInfo?.minRemoval || 0), + }; + + const parentSigners = []; + if (currentMultisigAccountInfo?.multisigAddresses) { + for (const parentSignerAddress of currentMultisigAccountInfo.multisigAddresses) { + parentSigners.push( + ...this.getSigners( + parentSignerAddress, + multisigAccountGraph, + level - 1, + currentSigner.requiredCosigApproval, + currentSigner.requiredCosigRemoval + ) + ); + } + currentSigner.parentSigners = parentSigners.filter(ps => currentMultisigAccountInfo.multisigAddresses.some(msa => msa.equals(ps.address))); + } + return [currentSigner, ...parentSigners]; + } } diff --git a/src/services/FetchTransactionService.js b/src/services/FetchTransactionService.js index 40c43a9..78780b3 100644 --- a/src/services/FetchTransactionService.js +++ b/src/services/FetchTransactionService.js @@ -79,7 +79,8 @@ export default class FetchTransactionService { rawAddress: string, page: number, directionFilter: DirectionFilter, - network: NetworkModel + network: NetworkModel, + cosignatoryOf: [] ): Promise { const transactionHttp = new TransactionHttp(network.node); const address = Address.createFromRawAddress(rawAddress); @@ -110,7 +111,19 @@ export default class FetchTransactionService { transactionHttp.search(partialSearchCriteria).toPromise(), transactionHttp.search(unconfirmedSearchCriteria).toPromise(), ]); - allTransactions = [...unconfirmedTransactions.data, ...partialTransactions.data, ...confirmedTransactions.data]; + + let multisigPartialSearchCriteria = partialSearchCriteria; + for(const cosigner of cosignatoryOf){ + multisigPartialSearchCriteria.address = Address.createFromRawAddress(cosigner); + const multisigTransactions = await transactionHttp.search(multisigPartialSearchCriteria).toPromise(); + for(const transaction of multisigTransactions.data){ + if(!partialTransactions.data.some((tx)=>transaction.transactionInfo.hash === tx.transactionInfo.hash)){ + partialTransactions.data.push(transaction); + } + } + }; + + allTransactions = [...partialTransactions.data, ...unconfirmedTransactions.data, ...confirmedTransactions.data]; } else { const confirmedTxs = await transactionHttp.search(confirmedSearchCriteria).toPromise(); allTransactions = confirmedTxs.data; diff --git a/src/services/HarvestingService.js b/src/services/HarvestingService.js index c6a8101..e7e483b 100644 --- a/src/services/HarvestingService.js +++ b/src/services/HarvestingService.js @@ -218,6 +218,23 @@ export default class HarvestingService { }, ]; } + static async getAccountImportance(node: string, accountAddress: string, networkCurrencyDivisibility, selectedNode){ + try{ + const accountHttp = new AccountHttp(node); + const accountInfo = await accountHttp.getAccountInfo(Address.createFromRawAddress(accountAddress)).toPromise(); + if(!accountInfo){ + return '0%' + } + const networkInfo = await NetworkService.getNetworkModelFromNode(selectedNode); + if (!networkCurrencyDivisibility || !networkInfo.totalChainImportance) { + return 'N/A'; + } + return {networkInfo: networkInfo, importance: accountInfo.importance.compact()}; + }catch(err){ + console.log(err); + return; + } + } /** * Creates and links the keys diff --git a/src/services/ListenerService.js b/src/services/ListenerService.js index afd2b8c..c5fa7b4 100644 --- a/src/services/ListenerService.js +++ b/src/services/ListenerService.js @@ -1,9 +1,9 @@ -import {Address, IListener, Listener, RepositoryFactoryHttp} from 'symbol-sdk'; +import { Address, Listener, RepositoryFactoryHttp } from 'symbol-sdk'; import type { NetworkModel } from '@src/storage/models/NetworkModel'; import { Router } from '@src/Router'; import store from '@src/store'; import translate from '@src/locales/i18n'; -import {CommonHelpers} from "@src/utils/commonHelpers"; +import { CommonHelpers } from '@src/utils/commonHelpers'; export default class ListenerService { network: NetworkModel; @@ -60,11 +60,11 @@ export default class ListenerService { }); }; - addConfirmed = (rawAddress: string) => { + addConfirmed = (rawAddress: string, isMultisig: boolean) => { console.log('Adding confirmed listener: ' + rawAddress); const address = Address.createFromRawAddress(rawAddress); this.listener - .confirmed(address) + .confirmed(address, undefined, isMultisig) //.pipe(filter(transaction => transaction.transactionInfo !== undefined)) .subscribe(() => { this.showMessage(translate('notification.newConfirmed'), 'success'); @@ -72,11 +72,11 @@ export default class ListenerService { }); }; - addUnconfirmed = (rawAddress: string) => { + addUnconfirmed = (rawAddress: string, isMultisig: boolean) => { console.log('Adding unconfirmed listener: ' + rawAddress); const address = Address.createFromRawAddress(rawAddress); this.listener - .unconfirmedAdded(address) + .unconfirmedAdded(address, undefined, isMultisig) //.pipe(filteser(transaction => transaction.transactionInfo !== undefined)) .subscribe(() => { this.showMessage(translate('notification.newUnconfirmed'), 'warning'); @@ -84,6 +84,15 @@ export default class ListenerService { }); }; + addPartial = (rawAddress: string, isMultisig: boolean) => { + console.log('Adding unconfirmed listener: ' + rawAddress); + const address = Address.createFromRawAddress(rawAddress); + this.listener.aggregateBondedAdded(address, undefined, isMultisig).subscribe(() => { + this.showMessage(translate('notification.newAggregate'), 'warning'); + store.dispatchAction({ type: 'account/loadAllData' }); + }); + }; + showMessage = (message: string, type: 'danger' | 'warning' | 'success' = 'success') => { Router.showMessage({ message: message, diff --git a/src/services/NetworkService.js b/src/services/NetworkService.js index 34725d6..aab7f3b 100644 --- a/src/services/NetworkService.js +++ b/src/services/NetworkService.js @@ -1,10 +1,19 @@ import {ChainHttp, NetworkConfiguration, NetworkHttp, NetworkType, NodeHttp, RepositoryFactoryHttp, TransactionFees} from 'symbol-sdk'; -import type { NetworkModel } from '@src/storage/models/NetworkModel'; +import type { NetworkModel, AppNetworkType } from '@src/storage/models/NetworkModel'; import { durationStringToSeconds } from '@src/utils/format'; import { timeout } from 'rxjs/operators'; +import { getStatisticsServiceURL } from '@src/config/environment' const REQUEST_TIMEOUT = 5000; +// Statistics service nodes endpoint filters +export type NodeFilters = 'preferred' | 'suggested'; +export interface NodeSearchCriteria{ + nodeFilter: NodeFilters, + limit: 30 +} + + export default class NetworkService { /** * Get network model from node @@ -55,6 +64,7 @@ export default class NetworkService { epochAdjustment: parseInt(networkProps.network.epochAdjustment), transactionFees: transactionFees, defaultDynamicFeeMultiplier: networkProps.chain.defaultDynamicFeeMultiplier || 1000, + totalChainImportance: networkProps.chain.totalChainImportance }; } @@ -103,4 +113,46 @@ export default class NetworkService { return false; } } + + /** + * Gets node list from statistics service + * @param networkType 'mainnet | testnet' + * @param nodeSearchCriteria NodeSearchCriteria + */ + static async getNodeList(networkType: AppNetworkType, {limit, nodeFilter}: NodeSearchCriteria) { + return new Promise(resolve => { + fetch(`${getStatisticsServiceURL(networkType)}nodes?filter=${nodeFilter}&limit=${limit}`) + .then(response => response.json()) + .then((responseData)=> { + resolve(responseData) + }) + .catch(e => { + resolve([]) + console.log(e) + }); + }); + } + + /** + * Gets nodes urls + * @param networkType 'mainnet | testnet' + */ + static async getSelectorNodeList(networkType: AppNetworkType) { + const nodeSearchCriteria = { + nodeFilter: 'suggested', + limit: 30 + } + + const nodes = await NetworkService.getNodeList(networkType, nodeSearchCriteria) || []; + let nodeUrls = []; + + for (const node of nodes) { + const { apiStatus } = node; + if (apiStatus) { + nodeUrls.push(apiStatus.restGatewayUrl) + } + } + + return nodeUrls; + } } diff --git a/src/services/TransactionService.js b/src/services/TransactionService.js index 9a5dfc2..3cdceea 100644 --- a/src/services/TransactionService.js +++ b/src/services/TransactionService.js @@ -72,8 +72,8 @@ export default class TransactionService { signer: AccountModel, networkModel: NetworkModel ): TransferTransaction { - const recipientAddress = Address.createFromRawAddress(transaction.recipientAddress); const networkType = networkModel.type === 'testnet' ? NetworkType.TEST_NET : NetworkType.MAIN_NET; + const recipientAddress = Address.createFromRawAddress(transaction.recipientAddress); const mosaics = [new Mosaic(new MosaicId(transaction.mosaics[0].mosaicId), UInt64.fromUint(transaction.mosaics[0].amount))]; if (!transaction.messageEncrypted) { @@ -88,7 +88,7 @@ export default class TransactionService { ); } else { const signerAccount = Account.createFromPrivateKey(signer.privateKey, networkType); - const repositoryFactory = await new RepositoryFactoryHttp(networkModel.node); + const repositoryFactory = new RepositoryFactoryHttp(networkModel.node); const accountHttp = repositoryFactory.createAccountRepository(); try { const accountInfo = await accountHttp.getAccountInfo(recipientAddress).toPromise(); diff --git a/src/storage/models/NetworkModel.js b/src/storage/models/NetworkModel.js index 574eff3..9291ebf 100644 --- a/src/storage/models/NetworkModel.js +++ b/src/storage/models/NetworkModel.js @@ -15,4 +15,5 @@ export interface NetworkModel { epochAdjustment: number; transactionFees: TransactionFees; defaultDynamicFeeMultiplier: number; + totalChainImportance: number } diff --git a/src/store/account.js b/src/store/account.js index a0f135f..376bccf 100644 --- a/src/store/account.js +++ b/src/store/account.js @@ -1,6 +1,9 @@ import AccountService from '@src/services/AccountService'; -import { from } from 'rxjs'; +import { of } from 'rxjs'; import { GlobalListener } from '@src/store/index'; +import { RepositoryFactoryHttp, Address } from 'symbol-sdk'; +import { map, catchError } from 'rxjs/operators'; +import _ from 'lodash'; export default { namespace: 'account', @@ -15,6 +18,7 @@ export default { accounts: [], cosignatoryOf: [], pendingSignature: false, + multisigGraphInfo: [], }, mutations: { setRefreshingObs(state, payload) { @@ -45,6 +49,10 @@ export default { state.account.cosignatoryOf = payload; return state; }, + setMultisigGraphInfo(state, payload) { + state.account.multisigGraphInfo = payload; + return state; + }, }, actions: { loadAllData: async ({ commit, dispatchAction, state }, reset) => { @@ -52,17 +60,18 @@ export default { commit({ type: 'account/setLoading', payload: true }); const address = AccountService.getAddressByAccountModelAndNetwork(state.wallet.selectedAccount, state.network.network); commit({ type: 'account/setSelectedAccountAddress', payload: address }); - dispatchAction({ type: 'transaction/init' }); + await dispatchAction({ type: 'account/loadMultisigTree' }); if (reset) { + await dispatchAction({ type: 'account/loadCosignatoryOf' }); commit({ type: 'account/setBalance', payload: 0 }); commit({ type: 'account/setOwnedMosaics', payload: [] }); } + await dispatchAction({ type: 'account/loadBalance' }); + await dispatchAction({ type: 'transaction/init' }); if (state.account.refreshingObs) { state.account.refreshingObs.unsubscribe(); } - await dispatchAction({type: 'account/loadBalance'}); - dispatchAction({type: 'harvesting/init'}); - dispatchAction({type: 'account/loadCosignatoryOf'}); + dispatchAction({ type: 'harvesting/init' }); commit({ type: 'account/setLoading', payload: false }); } catch (e) { commit({ type: 'account/setLoading', payload: false }); @@ -78,13 +87,54 @@ export default { loadCosignatoryOf: async ({ commit, state }) => { const address = AccountService.getAddressByAccountModelAndNetwork(state.wallet.selectedAccount, state.network.network); const msigInfo = await AccountService.getCosignatoryOfByAddress(address, state.network.selectedNetwork); + let multisigAccountGraph = state.account.multisigGraphInfo; + // find account signers + if (multisigAccountGraph !== undefined) { + const accountSigners = AccountService.getSigners(Address.createFromRawAddress(address), multisigAccountGraph); + let allSigners = []; + accountSigners.forEach(signer => { + allSigners.push(signer.address.pretty()); + signer.parentSigners.forEach(parent => { + allSigners.push(parent.address.pretty()); + parent.parentSigners.forEach(topLevel => { + allSigners.push(topLevel.address.pretty()); + }); + }); + }); + allSigners = _.uniq(allSigners); + allSigners.forEach(signer => { + if (!msigInfo.cosignatoryOf.some(cosignatory => cosignatory == signer) && signer !== address) { + msigInfo.cosignatoryOf.push(signer); + } + }); + } for (let cosignatoryOf of msigInfo.cosignatoryOf) { - GlobalListener.addConfirmed(cosignatoryOf); - GlobalListener.addUnconfirmed(cosignatoryOf); + GlobalListener.addConfirmed(cosignatoryOf, state.account.cosignatoryOf.length > 0); + GlobalListener.addUnconfirmed(cosignatoryOf, state.account.cosignatoryOf.length > 0); + GlobalListener.addPartial(cosignatoryOf, state.account.cosignatoryOf.length > 0); } commit({ type: 'account/setCosignatoryOf', payload: msigInfo.cosignatoryOf }); commit({ type: 'account/setIsMultisig', payload: msigInfo.isMultisig }); }, + // load multisig tree data + loadMultisigTree: async ({ commit, state }) => { + const address = AccountService.getAddressByAccountModelAndNetwork(state.wallet.selectedAccount, state.network.network); + const repositoryFactory = new RepositoryFactoryHttp(state.network.selectedNode); + const multisigRepo = repositoryFactory.createMultisigRepository(); + await multisigRepo + .getMultisigAccountGraphInfo(Address.createFromRawAddress(address)) + .pipe( + map(g => { + commit({ type: 'account/setMultisigGraphInfo', payload: g.multisigEntries }); + return of(g); + }), + catchError(() => { + commit({ type: 'account/setMultisigGraphInfo', payload: undefined }); + return of([]); + }) + ) + .toPromise(); + }, }, }; diff --git a/src/store/harvesting.js b/src/store/harvesting.js index 06759d9..fe25b7d 100644 --- a/src/store/harvesting.js +++ b/src/store/harvesting.js @@ -1,10 +1,9 @@ import { UInt64 } from 'symbol-sdk'; import HarvestingService from '@src/services/HarvestingService'; -import {HarvestingSecureStorage} from "@src/storage/persistence/HarvestingSecureStorage"; +import { HarvestingSecureStorage } from '@src/storage/persistence/HarvestingSecureStorage'; const MIN_REQUIRED_BALANCE = 10000; - export type HarvestingStatus = 'ACTIVE' | 'INACTIVE' | 'INPROGRESS_ACTIVATION' | 'INPROGRESS_DEACTIVATION' | 'KEYS_LINKED'; export type HarvestedBlock = { @@ -31,7 +30,8 @@ const initialState = { minRequiredBalance: MIN_REQUIRED_BALANCE, harvestingModel: null, nodes: HarvestingService.getHarvestingNodeList(), -} + accountImportance: 0, +}; export default { namespace: 'harvesting', @@ -77,33 +77,54 @@ export default { state.harvesting.nodes = payload; return state; }, + setAccountImportance(state, payload) { + state.harvesting.accountImportance = payload; + return state; + }, }, actions: { init: async ({ dispatchAction, commit }) => { - Promise.all([ + // commit({ type: 'harvesting/setAccountImportance', payload: 0 }); + await Promise.all([ dispatchAction({ type: 'harvesting/loadState' }), dispatchAction({ type: 'harvesting/loadHarvestedBlocks' }), + dispatchAction({ type: 'harvesting/loadAccountImportance' }), dispatchAction({ type: 'harvesting/loadHarvestedBlocksStats' }), dispatchAction({ type: 'harvesting/loadHarvestingModel' }), dispatchAction({ type: 'harvesting/loadHarvestingNodes' }), ]).catch(e => { commit({ type: 'harvesting/resetState' }); - }) + }); }, loadState: async ({ commit, state }) => { try { const status = await HarvestingService.getAccountStatus(state.wallet.selectedAccount, state.network.selectedNetwork); commit({ type: 'harvesting/setStatus', payload: status }); - } catch(e) { + } catch (e) { console.log(e); commit({ type: 'harvesting/setStatus', payload: 'INACTIVE' }); } }, + loadAccountImportance: async ({ commit, state }) => { + try { + const {importance, networkInfo} = await HarvestingService.getAccountImportance(state.network.selectedNetwork.node, state.account.selectedAccountAddress, state.network.selectedNetwork.currencyDivisibility, state.network.selectedNetwork.node || ''); + const totalChainImportance = parseInt(networkInfo.totalChainImportance.toString().replace(/'/g, '')) || 0; + const relativeImportance = importance > 0 ? importance / totalChainImportance : importance; + const formatOptions: Intl.NumberFormatOptions = { + maximumFractionDigits: state.network.selectedNetwork.currencyDivisibility, + style: 'percent', + }; + commit({ type: 'harvesting/setAccountImportance', payload: relativeImportance.toLocaleString(undefined, formatOptions).toString() }); + } catch (e) { + commit({ type: 'harvesting/setAccountImportance', payload: '0%' }); + console.log(e); + } + }, loadHarvestingNodes: async ({ commit, state }) => { try { const nodes = await HarvestingService.getPeerNodes(state.network.selectedNetwork); commit({ type: 'harvesting/setNodes', payload: nodes }); - } catch(e) { + } catch (e) { console.log(e); commit({ type: 'harvesting/setNodes', payload: HarvestingService.getHarvestingNodeList() }); } @@ -144,7 +165,7 @@ export default { harvestingNode: null, }, }); - } catch(e) { + } catch (e) { console.log(e); } dispatchAction({ type: 'harvesting/init' }); diff --git a/src/store/index.js b/src/store/index.js index 7605c6e..6eb6805 100644 --- a/src/store/index.js +++ b/src/store/index.js @@ -6,7 +6,6 @@ import network from '@src/store/network'; import transfer from '@src/store/transfer'; import mosaic from '@src/store/mosaic'; import account from '@src/store/account'; -import news from '@src/store/news'; import harvesting from '@src/store/harvesting'; import addressBook from '@src/store/addressBook'; import ListenerService from '@src/services/ListenerService'; @@ -21,7 +20,6 @@ const modules = { mosaic, transfer, account, - news, harvesting, addressBook, transaction, diff --git a/src/store/network.js b/src/store/network.js index 953fef6..eb475f1 100644 --- a/src/store/network.js +++ b/src/store/network.js @@ -1,5 +1,5 @@ import { AsyncCache } from '@src/utils/storage/AsyncCache'; -import {getDefaultNetworkType, getNISNodes, getNodes} from '@src/config/environment'; +import { getDefaultNetworkType, getNISNodes } from '@src/config/environment'; import NetworkService from '@src/services/NetworkService'; import { GlobalListener } from '@src/store/index'; @@ -28,6 +28,8 @@ export default { transactionFees: {}, defaultDynamicFeeMultiplier: 0, }, + mainnetNodes: [], + testnetNodes: [], }, mutations: { setIsLoaded(state, payload) { @@ -62,30 +64,65 @@ export default { state.network.nodeFailedAttempts = payload; return state; }, + setMainnetNodes(state, payload) { + state.network.mainnetNodes = payload; + return state; + }, + setTestnetNodes(state, payload) { + state.network.testnetNodes = payload; + return state; + }, }, actions: { - initState: async ({ commit, dispatchAction }) => { + initState: async ({ state, commit, dispatchAction }) => { let selectedNode = await AsyncCache.getSelectedNode(); - if (!selectedNode) { - const network = getDefaultNetworkType(); - selectedNode = getNodes(network)[0]; + + // load nodes list from statistic service + await dispatchAction({ type: 'network/loadNodeList' }); + + const networkType = getDefaultNetworkType(); + const nodeList = networkType === 'mainnet' ? state.network.mainnetNodes : state.network.testnetNodes; + + // assign node, if node list available and selectedNode is not set + if (nodeList.length && !selectedNode) { + const randomIndex = Math.floor(Math.random() * nodeList.length); //NOSONAR + selectedNode = nodeList[randomIndex]; + } + + // If selectedNode exists, set network + if (selectedNode) { + const network = await NetworkService.getNetworkModelFromNode(selectedNode); + try { + const nisNodes = getNISNodes(network.type); + await dispatchAction({type: 'settings/saveSetSelectedNISNode', payload: nisNodes[0]}); + } catch {} + commit({ type: 'network/setGenerationHash', payload: network.generationHash }); + commit({ type: 'network/setNetwork', payload: network.type }); + commit({ type: 'network/setSelectedNode', payload: selectedNode }); + commit({ type: 'network/setIsLoaded', payload: true }); + commit({ + type: 'network/setSelectedNetwork', + payload: network, + }); + GlobalListener.setNetwork(network); } - const network = await NetworkService.getNetworkModelFromNode(selectedNode); - try { - const nisNodes = getNISNodes(network.type); - await dispatchAction({type: 'settings/saveSetSelectedNISNode', payload: nisNodes[0]}); - } catch {} - commit({ type: 'network/setGenerationHash', payload: network.generationHash }); - commit({ type: 'network/setNetwork', payload: network.type }); - commit({ type: 'network/setSelectedNode', payload: selectedNode }); - commit({ type: 'network/setIsLoaded', payload: true }); - commit({ - type: 'network/setSelectedNetwork', - payload: network, - }); - GlobalListener.setNetwork(network); await dispatchAction({ type: 'wallet/initState' }); }, + loadNodeList: async ({ commit }) => { + try { + // load nodes list from statistic service + const [testnetNodes, mainnetNodes] = await Promise.all([ + NetworkService.getSelectorNodeList('testnet'), + NetworkService.getSelectorNodeList('mainnet') + ]) + + // Assign nodes on the state + commit({ type: 'network/setTestnetNodes', payload: testnetNodes }); + commit({ type: 'network/setMainnetNodes', payload: mainnetNodes }); + } catch(e) { + console.log(e); + } + }, changeNode: async ({ commit, state, dispatchAction }, payload) => { const network = await NetworkService.getNetworkModelFromNode(payload); try { diff --git a/src/store/news.js b/src/store/news.js deleted file mode 100644 index ef8e642..0000000 --- a/src/store/news.js +++ /dev/null @@ -1,41 +0,0 @@ -import RSSParser from 'rss-parser'; -import { htmlToPlainString, removeRSSContentEnd } from '@src/utils'; -import { formatDate } from '@src/utils/format'; - -export default { - namespace: 'news', - state: { - isLoading: false, - news: [], - }, - mutations: { - setLoading(state, payload) { - state.news.isLoading = payload; - return state; - }, - setNews(state, payload) { - state.news.news = payload; - return state; - }, - }, - actions: { - loadNews: async ({ commit }) => { - commit({ type: 'news/setLoading', payload: true }); - try { - const response = await fetch('http://rssmix.com/u/11801188/rss.xml'); - const responseText = await response.text(); - const rss = await new RSSParser().parseString(responseText); - const news = rss.items.map(el => ({ - ...el, - content: removeRSSContentEnd(htmlToPlainString(el.content)), - pubDate: formatDate(el.pubDate) - })); - commit({ type: 'news/setNews', payload: news}); - } catch (e) { - console.log('Error loading news'); - commit({ type: 'news/setNews', payload: [] }); - } - commit({ type: 'news/setLoading', payload: false }); - }, - }, -}; diff --git a/src/store/transaction.js b/src/store/transaction.js index fb38e9c..b22bc58 100644 --- a/src/store/transaction.js +++ b/src/store/transaction.js @@ -81,13 +81,17 @@ export default { setTimeout(() => { commit({ type: 'transaction/setLoadingNext', payload: true }); }); + if((!state.account.cosignatoryOf || !state.account.cosignatoryOf.length) && state.account.multisigGraphInfo !== undefined ){ + await dispatchAction({ type: 'account/loadCosignatoryOf' }); + } const nextPage = state.transaction.page + 1; const subscription = from( FetchTransactionService.getTransactionsFromAddress( state.transaction.addressFilter, nextPage, state.transaction.directionFilter, - state.network.selectedNetwork + state.network.selectedNetwork, + state.account.cosignatoryOf ) ).subscribe( transactions => { diff --git a/src/store/transfer.js b/src/store/transfer.js index 8d1b1fb..cfb43a0 100644 --- a/src/store/transfer.js +++ b/src/store/transfer.js @@ -60,7 +60,7 @@ export default { const dummyAccount = Account.generateNewAccount(networkType); const transactionModel = { type: 'transfer', - recipientAddress: dummyAccount.address.plain(), + recipientAddress: payload.recipientAddress, messageText: payload.message, messageEncrypted: payload.messageEncrypted, mosaics: payload.mosaics, diff --git a/src/store/wallet.js b/src/store/wallet.js index e7818bc..826e452 100644 --- a/src/store/wallet.js +++ b/src/store/wallet.js @@ -85,12 +85,12 @@ export default { await AccountSecureStorage.createNewAccount(account); } await dispatchAction({ type: 'wallet/reloadAccounts' }); - dispatchAction({ type: 'wallet/loadAccount', payload: state.network.network === 'testnet' ? testnetAccountModel.id : mainnetAccountModel.id }); + await dispatchAction({ type: 'wallet/loadAccount', payload: state.network.network === 'testnet' ? testnetAccountModel.id : mainnetAccountModel.id }); }, reloadAccounts: async ({ commit, state, dispatchAction }) => { const accounts = await AccountSecureStorage.getAllAccountsByNetwork(state.network.network); commit({ type: 'wallet/setAccounts', payload: accounts }); - dispatchAction({ type: 'wallet/loadAccountsBalances' }); + await dispatchAction({ type: 'wallet/loadAccountsBalances' }); }, loadAccountsBalances: async ({ commit, state }) => { try { diff --git a/yarn.lock b/yarn.lock index fe47199..8230170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10233,7 +10233,7 @@ rxjs@^5.4.3: dependencies: symbol-observable "1.0.1" -rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.3: +rxjs@^6.5.2, rxjs@^6.6.0, rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -10999,6 +10999,11 @@ symbol-openapi-typescript-fetch-client@0.11.2: resolved "https://registry.yarnpkg.com/symbol-openapi-typescript-fetch-client/-/symbol-openapi-typescript-fetch-client-0.11.2.tgz#c34b1a2345b567645b802b79b6e8431281a4c938" integrity sha512-A1MAN8/UWlaCEibBf6zxkduZwDNSvWwLPp6JB0GeYI/FAOrw/9nLyuS/NJQ3siGAUclnuejH1wG7KdUg0/4RSw== +symbol-openapi-typescript-fetch-client@1.0.1-SNAPSHOT.202106160954: + version "1.0.1-SNAPSHOT.202106160954" + resolved "https://registry.yarnpkg.com/symbol-openapi-typescript-fetch-client/-/symbol-openapi-typescript-fetch-client-1.0.1-SNAPSHOT.202106160954.tgz#ed0382e54c3c9e72d19f270781949e2c4864a50d" + integrity sha512-Aj/lGcQOpBdGSYt5z+0H3o5E75w6llOi+RGS+CRQXF8FqPOIdTuf6KqAoKxCWctGFct3Fii1dDbJPsQrhMFDJg== + symbol-paper-wallets@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/symbol-paper-wallets/-/symbol-paper-wallets-1.0.2.tgz#4419834b5e12c9ead0409e1e0a49770917fdce9b" @@ -11065,6 +11070,32 @@ symbol-sdk@1.0.0: tweetnacl "^1.0.3" ws "^7.3.1" +symbol-sdk@1.0.2-alpha-202108061451: + version "1.0.2-alpha-202108061451" + resolved "https://registry.yarnpkg.com/symbol-sdk/-/symbol-sdk-1.0.2-alpha-202108061451.tgz#1c2e6f53066fa2667ddaeca326a2a30e0019a2f5" + integrity sha512-HH45iZCX8cDDTz4CMSe2PdIAIrTLj5I43AlBYr2k4+SYHS6ZYD2vihQ7RoKjLex+eGz70/JFYmos3RixRQLryw== + dependencies: + "@js-joda/core" "^3.2.0" + bluebird "^3.7.2" + catbuffer-typescript "0.1.1" + crypto-js "^4.0.0" + diff "^4.0.2" + futoin-hkdf "^1.3.2" + js-sha256 "^0.9.0" + js-sha3 "^0.8.0" + js-sha512 "^0.8.0" + long "^4.0.0" + merkletreejs "^0.2.9" + minimist "^1.2.5" + node-fetch "^2.6.0" + ripemd160 "^2.0.2" + rxjs "^6.6.7" + rxjs-compat "^6.6.3" + symbol-openapi-typescript-fetch-client "1.0.1-SNAPSHOT.202106160954" + tweetnacl "^1.0.3" + utf8 "^2.1.2" + ws "^7.3.1" + symbol-tree@^3.2.2: version "3.2.4" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" @@ -11650,7 +11681,7 @@ utf-8-validate@^5.0.2: dependencies: node-gyp-build "^4.2.0" -utf8@2.1.2: +utf8@2.1.2, utf8@^2.1.2: version "2.1.2" resolved "https://registry.yarnpkg.com/utf8/-/utf8-2.1.2.tgz#1fa0d9270e9be850d9b05027f63519bf46457d96" integrity sha1-H6DZJw6b6FDZsFAn9jUZv0ZFfZY=