diff --git a/.gitignore b/.gitignore index db43e964a..0580d006c 100644 --- a/.gitignore +++ b/.gitignore @@ -96,3 +96,5 @@ vendor/ android/app/.cxx/** ios/Flutter/.last_build_id /lib/generated/** +#**# +/**/#**# \ No newline at end of file diff --git a/analysis_options.yaml b/analysis_options.yaml index 52f1e3224..d74761d26 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -25,7 +25,6 @@ linter: - empty_constructor_bodies - empty_statements - hash_and_equals - - implementation_imports - invariant_booleans - iterable_contains_unrelated_type - library_names diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 7efe1606c..074ce247f 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -1,10 +1,19 @@ package com.cakewallet.cake_wallet; import androidx.annotation.NonNull; + import io.flutter.embedding.android.FlutterFragmentActivity; import io.flutter.embedding.engine.FlutterEngine; import io.flutter.plugins.GeneratedPluginRegistrant; +import io.flutter.plugin.common.MethodCall; +import io.flutter.plugin.common.MethodChannel; + +import android.os.AsyncTask; +import android.os.Handler; +import android.os.Looper; + +import java.security.SecureRandom; import com.unstoppabledomains.resolution.DomainResolution; import com.unstoppabledomains.resolution.Resolution; @@ -16,6 +25,8 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; public class MainActivity extends FlutterFragmentActivity { + final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; + final String UNSTOPPABLE_DOMAIN_CHANNEL = "com.cakewallet.cake_wallet/unstoppable-domain"; @Override @@ -55,5 +66,29 @@ public class MainActivity extends FlutterFragmentActivity { handler.post(() -> result.error("INVALID DOMAIN", e.getMessage(), null)); } }); + + MethodChannel utilsChannel = + new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), + UTILS_CHANNEL); + + utilsChannel.setMethodCallHandler(this::handle); + } + + private void handle(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + Handler handler = new Handler(Looper.getMainLooper()); + + try { + if (call.method.equals("sec_random")) { + int count = call.argument("count"); + SecureRandom random = new SecureRandom(); + byte bytes[] = new byte[count]; + random.nextBytes(bytes); + handler.post(() -> result.success(bytes)); + } else { + handler.post(() -> result.notImplemented()); + } + } catch (Exception e) { + handler.post(() -> result.error("UNCAUGHT_ERROR", e.getMessage(), null)); + } } } diff --git a/android/app/src/main/kotlin/com/cakewallet/cake_wallet/MainActivity.kt b/android/app/src/main/kotlin/com/cakewallet/cake_wallet/MainActivity.kt deleted file mode 100644 index b5c933268..000000000 --- a/android/app/src/main/kotlin/com/cakewallet/cake_wallet/MainActivity.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.cakewallet.cake_wallet - -import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.android.FlutterFragmentActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine){ - GeneratedPluginRegistrant.registerWith(flutterEngine); - } -} diff --git a/android/app/src/main/res/drawable/ic_launcher.png b/android/app/src/main/res/drawable/ic_launcher.png new file mode 100755 index 000000000..a96724f1c Binary files /dev/null and b/android/app/src/main/res/drawable/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 000000000..b2c4ad887 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index 5d9200d0a..b11806d56 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 83a98b56d..b76508e83 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index c8cbe84dd..1d32c2836 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index c4352f579..d5b7374a7 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 73fe04234..af40ea947 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/electrum_server_list.yml b/assets/bitcoin_electrum_server_list.yml similarity index 100% rename from assets/electrum_server_list.yml rename to assets/bitcoin_electrum_server_list.yml diff --git a/assets/images/2.0x/cake_logo.png b/assets/images/2.0x/cake_logo.png index c464cdcd4..277816813 100644 Binary files a/assets/images/2.0x/cake_logo.png and b/assets/images/2.0x/cake_logo.png differ diff --git a/assets/images/3.0x/cake_logo.png b/assets/images/3.0x/cake_logo.png index 5da47a539..87dd05ce2 100644 Binary files a/assets/images/3.0x/cake_logo.png and b/assets/images/3.0x/cake_logo.png differ diff --git a/assets/images/app_logo.png b/assets/images/app_logo.png index 4540659cf..bf6896ad2 100644 Binary files a/assets/images/app_logo.png and b/assets/images/app_logo.png differ diff --git a/assets/images/cake_logo.png b/assets/images/cake_logo.png index fb5267a79..8a85bf225 100644 Binary files a/assets/images/cake_logo.png and b/assets/images/cake_logo.png differ diff --git a/assets/images/litecoin_icon.png b/assets/images/litecoin_icon.png new file mode 100644 index 000000000..9cc47b6fb Binary files /dev/null and b/assets/images/litecoin_icon.png differ diff --git a/assets/images/litecoin_menu.png b/assets/images/litecoin_menu.png new file mode 100644 index 000000000..d39aff717 Binary files /dev/null and b/assets/images/litecoin_menu.png differ diff --git a/assets/images/monero_menu.png b/assets/images/monero_menu.png index 2bb420a80..51b1e2240 100644 Binary files a/assets/images/monero_menu.png and b/assets/images/monero_menu.png differ diff --git a/assets/litecoin_electrum_server_list.yml b/assets/litecoin_electrum_server_list.yml new file mode 100644 index 000000000..e61d0996c --- /dev/null +++ b/assets/litecoin_electrum_server_list.yml @@ -0,0 +1,2 @@ +- + uri: ltc-electrum.cakewallet.com:50002 \ No newline at end of file diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index d0313a194..dd1ab3cbb 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -179,8 +179,6 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; - std::mutex store_mutex; - void change_current_wallet(Monero::Wallet *wallet) { @@ -451,9 +449,7 @@ extern "C" void store(char *path) { - store_mutex.lock(); get_current_wallet()->store(std::string(path)); - store_mutex.unlock(); } bool transaction_create(char *address, char *payment_id, char *amount, diff --git a/cw_monero/ios/cw_monero.podspec b/cw_monero/ios/cw_monero.podspec index 78416a9e7..ad8a94d04 100644 --- a/cw_monero/ios/cw_monero.podspec +++ b/cw_monero/ios/cw_monero.podspec @@ -5,20 +5,18 @@ Pod::Spec.new do |s| s.name = 'cw_monero' s.version = '0.0.2' - s.summary = 'A new flutter plugin project.' - s.description = <<-DESC -A new flutter plugin project. - DESC - s.homepage = 'http://example.com' + s.summary = 'CW Monero' + s.description = 'Cake Wallet wrapper over Monero project.' + s.homepage = 'http://cakewallet.com' s.license = { :file => '../LICENSE' } - s.author = { 'Your Company' => 'email@example.com' } + s.author = { 'CakeWallet' => 'support@cakewallet.com' } s.source = { :path => '.' } s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h, Classes/*.h, External/ios/libs/monero/include/src/**/*.h, External/ios/libs/monero/include/contrib/**/*.h, External/ios/libs/monero/include/External/ios/**/*.h' s.dependency 'Flutter' s.platform = :ios, '9.0' s.swift_version = '4.0' - s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64' } + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS' => 'arm64', 'ENABLE_BITCODE' => 'NO' } s.xcconfig = { 'HEADER_SEARCH_PATHS' => "${PODS_ROOT}/#{s.name}/Classes/*.h" } s.subspec 'OpenSSL' do |openssl| @@ -53,4 +51,4 @@ A new flutter plugin project. lmdb.vendored_libraries = 'External/ios/libs/lmdb/liblmdb.a' lmdb.libraries = 'lmdb' end -end \ No newline at end of file +end diff --git a/cw_monero/lib/wallet_manager.dart b/cw_monero/lib/wallet_manager.dart index e48055cf9..2400c5f3f 100644 --- a/cw_monero/lib/wallet_manager.dart +++ b/cw_monero/lib/wallet_manager.dart @@ -1,12 +1,12 @@ import 'dart:ffi'; -import 'package:cw_monero/exceptions/wallet_opening_exception.dart'; -import 'package:cw_monero/wallet.dart'; import 'package:ffi/ffi.dart'; import 'package:flutter/foundation.dart'; import 'package:cw_monero/convert_utf8_to_string.dart'; import 'package:cw_monero/signatures.dart'; import 'package:cw_monero/types.dart'; import 'package:cw_monero/monero_api.dart'; +import 'package:cw_monero/wallet.dart'; +import 'package:cw_monero/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/exceptions/wallet_creation_exception.dart'; import 'package:cw_monero/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/exceptions/wallet_restore_from_seed_exception.dart'; diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 648dce288..d44aacc6b 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -7,49 +7,49 @@ packages: name: async url: "https://pub.dartlang.org" source: hosted - version: "2.4.2" + version: "2.5.0" boolean_selector: dependency: transitive description: name: boolean_selector url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" characters: dependency: transitive description: name: characters url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.1.0" charcode: dependency: transitive description: name: charcode url: "https://pub.dartlang.org" source: hosted - version: "1.1.3" + version: "1.2.0" clock: dependency: transitive description: name: clock url: "https://pub.dartlang.org" source: hosted - version: "1.0.1" + version: "1.1.0" collection: dependency: transitive description: name: collection url: "https://pub.dartlang.org" source: hosted - version: "1.14.13" + version: "1.15.0" fake_async: dependency: transitive description: name: fake_async url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" ffi: dependency: "direct main" description: @@ -73,21 +73,21 @@ packages: name: matcher url: "https://pub.dartlang.org" source: hosted - version: "0.12.8" + version: "0.12.10" meta: dependency: transitive description: name: meta url: "https://pub.dartlang.org" source: hosted - version: "1.1.8" + version: "1.3.0" path: dependency: transitive description: name: path url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" path_provider: dependency: "direct main" description: @@ -113,56 +113,56 @@ packages: name: source_span url: "https://pub.dartlang.org" source: hosted - version: "1.7.0" + version: "1.8.0" stack_trace: dependency: transitive description: name: stack_trace url: "https://pub.dartlang.org" source: hosted - version: "1.9.5" + version: "1.10.0" stream_channel: dependency: transitive description: name: stream_channel url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner url: "https://pub.dartlang.org" source: hosted - version: "1.0.5" + version: "1.1.0" term_glyph: dependency: transitive description: name: term_glyph url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "1.2.0" test_api: dependency: transitive description: name: test_api url: "https://pub.dartlang.org" source: hosted - version: "0.2.17" + version: "0.2.19" typed_data: dependency: transitive description: name: typed_data url: "https://pub.dartlang.org" source: hosted - version: "1.2.0" + version: "1.3.0" vector_math: dependency: transitive description: name: vector_math url: "https://pub.dartlang.org" source: hosted - version: "2.0.8" + version: "2.1.0" sdks: - dart: ">=2.9.0-14.0.dev <3.0.0" - flutter: ">=0.1.4 <2.0.0" + dart: ">=2.12.0-0.0 <3.0.0" + flutter: ">=0.1.4" diff --git a/ios/CakeWallet/secRandom.swift b/ios/CakeWallet/secRandom.swift new file mode 100644 index 000000000..c9b2e3593 --- /dev/null +++ b/ios/CakeWallet/secRandom.swift @@ -0,0 +1,12 @@ +import Foundation + +func secRandom(count: Int) -> Data? { + var bytes = [Int8](repeating: 0, count: count) + let status = SecRandomCopyBytes(kSecRandomDefault, bytes.count, &bytes) + + if status == errSecSuccess { + return Data(bytes: bytes, count: bytes.count) + } + + return nil +} diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f627ed23f..95fa31cc4 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -169,7 +169,7 @@ SPEC CHECKSUMS: BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 - cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649 + cw_monero: 78f369253cc913efc23db9cf6be81a11eaf40fe1 devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6e3df44f7..79c69b9ed 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -8,6 +8,7 @@ /* Begin PBXBuildFile section */ 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C44A7192518EF8000B570ED /* decrypt.swift */; }; + 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C9D68C8264854B60011B691 /* secRandom.swift */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 20ED0868E1BD7E12278C0CB3 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = B26E3F56D69167FBB1DC160A /* Pods_Runner.framework */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; @@ -21,6 +22,7 @@ 0C400E0F25B21ABB0025E469 /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = ""; }; 0C9986A3251A932F00D566FD /* CryptoSwift.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CryptoSwift.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 0C9D68C8264854B60011B691 /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = secRandom.swift; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 20F67A1B2C2FCB2A3BB048C1 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; @@ -65,6 +67,7 @@ isa = PBXGroup; children = ( 0C44A7192518EF8000B570ED /* decrypt.swift */, + 0C9D68C8264854B60011B691 /* secRandom.swift */, ); path = CakeWallet; sourceTree = ""; @@ -274,6 +277,7 @@ files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 0C9D68C9264854B60011B691 /* secRandom.swift in Sources */, 0C44A71A2518EF8000B570ED /* decrypt.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -358,7 +362,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -375,7 +379,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.5; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -501,7 +505,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -518,7 +522,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.5; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -536,7 +540,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 34; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -553,7 +557,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.5; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index a28140cfd..fb2dffc49 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -27,8 +27,6 @@ selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB" selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB" shouldUseLaunchSchemeArgsEnv = "YES"> - - - - + + - - Bool { let controller : FlutterViewController = window?.rootViewController as! FlutterViewController + let legacyMigrationChannel = FlutterMethodChannel( + name: "com.cakewallet.cakewallet/legacy_wallet_migration", + binaryMessenger: controller.binaryMessenger) + legacyMigrationChannel.setMethodCallHandler({ let batteryChannel = FlutterMethodChannel(name: "com.cakewallet.cakewallet/legacy_wallet_migration", binaryMessenger: controller.binaryMessenger) let unstoppableDomainChannel = FlutterMethodChannel(name: "com.cakewallet.cake_wallet/unstoppable-domain", binaryMessenger: controller.binaryMessenger) - + batteryChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in @@ -58,7 +62,25 @@ import UnstoppableDomainsResolution result(FlutterMethodNotImplemented) } }) - + + let utilsChannel = FlutterMethodChannel( + name: "com.cake_wallet/native_utils", + binaryMessenger: controller.binaryMessenger) + utilsChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + switch call.method { + case "sec_random": + guard let args = call.arguments as? Dictionary, + let count = args["count"] as? Int else { + result(nil) + return + } + + result(secRandom(count: count)) + default: + result(FlutterMethodNotImplemented) + } + }) + unstoppableDomainChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { @@ -69,29 +91,29 @@ import UnstoppableDomainsResolution result(nil) return } - + guard let resolution = self?.resolution else { result(nil) return } - + resolution.addr(domain: domain, ticker: ticker) { addrResult in var address : String = "" - + switch addrResult { case .success(let returnValue): address = returnValue case .failure(let error): print("Expected Address, but got \(error)") } - + result(address) } default: result(FlutterMethodNotImplemented) } }) - + GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 6613d9583..6d834d4ee 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -2,100 +2,100 @@ "images" : [ { "idiom" : "iphone", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "20x20", - "scale" : "3x" + "scale" : "3x", + "size" : "20x20" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "29x29", - "scale" : "3x" + "scale" : "3x", + "size" : "29x29" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "iphone", - "size" : "40x40", - "scale" : "3x" + "scale" : "3x", + "size" : "40x40" }, { - "size" : "60x60", + "filename" : "app_icon_120.png", "idiom" : "iphone", - "filename" : "cake_xmr_120.png", - "scale" : "2x" + "scale" : "2x", + "size" : "60x60" }, { - "size" : "60x60", + "filename" : "app_icon_180.png", "idiom" : "iphone", - "filename" : "cake_xmr_180.png", - "scale" : "3x" + "scale" : "3x", + "size" : "60x60" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "1x" + "scale" : "1x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "20x20", - "scale" : "2x" + "scale" : "2x", + "size" : "20x20" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "1x" + "scale" : "1x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "29x29", - "scale" : "2x" + "scale" : "2x", + "size" : "29x29" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "1x" + "scale" : "1x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "40x40", - "scale" : "2x" + "scale" : "2x", + "size" : "40x40" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "1x" + "scale" : "1x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "76x76", - "scale" : "2x" + "scale" : "2x", + "size" : "76x76" }, { "idiom" : "ipad", - "size" : "83.5x83.5", - "scale" : "2x" + "scale" : "2x", + "size" : "83.5x83.5" }, { - "size" : "1024x1024", + "filename" : "app_icon_1024.png", "idiom" : "ios-marketing", - "filename" : "cake_xmr_1024.png", - "scale" : "1x" + "scale" : "1x", + "size" : "1024x1024" } ], "info" : { - "version" : 1, - "author" : "xcode" + "author" : "xcode", + "version" : 1 } -} \ No newline at end of file +} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png new file mode 100644 index 000000000..268157627 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png new file mode 100644 index 000000000..cc95df4ed Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png new file mode 100644 index 000000000..71e2cadc1 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_1024.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_1024.png deleted file mode 100644 index 4540659cf..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_1024.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_120.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_120.png deleted file mode 100644 index 303625681..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_120.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_180.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_180.png deleted file mode 100644 index a22c1f36d..000000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/cake_xmr_180.png and /dev/null differ diff --git a/lib/bitcoin/address_to_output_script.dart b/lib/bitcoin/address_to_output_script.dart index 2f6698ff0..01c7b67a5 100644 --- a/lib/bitcoin/address_to_output_script.dart +++ b/lib/bitcoin/address_to_output_script.dart @@ -1,25 +1,29 @@ import 'dart:typed_data'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bs58check/bs58check.dart' as bs58check; import 'package:bitcoin_flutter/src/utils/constants/op.dart'; import 'package:bitcoin_flutter/src/utils/script.dart' as bscript; import 'package:bitcoin_flutter/src/address.dart'; - Uint8List p2shAddressToOutputScript(String address) { final decodeBase58 = bs58check.decode(address); final hash = decodeBase58.sublist(1); return bscript.compile([OPS['OP_HASH160'], hash, OPS['OP_EQUAL']]); } -Uint8List addressToOutputScript(String address) { +Uint8List addressToOutputScript( + String address, bitcoin.NetworkType networkType) { try { // FIXME: improve validation for p2sh addresses - if (address.startsWith('3')) { + // 3 for bitcoin + // m for litecoin + if (address.startsWith('3') || address.toLowerCase().startsWith('m')) { return p2shAddressToOutputScript(address); } - return Address.addressToOutputScript(address); - } catch (_) { + return Address.addressToOutputScript(address, networkType); + } catch (err) { + print(err); return Uint8List(0); } -} \ No newline at end of file +} diff --git a/lib/bitcoin/bitcoin_address_record.dart b/lib/bitcoin/bitcoin_address_record.dart index af492de2d..5e3967308 100644 --- a/lib/bitcoin/bitcoin_address_record.dart +++ b/lib/bitcoin/bitcoin_address_record.dart @@ -1,14 +1,14 @@ import 'dart:convert'; -import 'package:quiver/core.dart'; class BitcoinAddressRecord { - BitcoinAddressRecord(this.address, {this.index}); + BitcoinAddressRecord(this.address, {this.index, bool isHidden}) + : _isHidden = isHidden; factory BitcoinAddressRecord.fromJSON(String jsonSource) { final decoded = json.decode(jsonSource) as Map; return BitcoinAddressRecord(decoded['address'] as String, - index: decoded['index'] as int); + index: decoded['index'] as int, isHidden: decoded['isHidden'] as bool); } @override @@ -16,10 +16,13 @@ class BitcoinAddressRecord { o is BitcoinAddressRecord && address == o.address; final String address; + bool get isHidden => _isHidden ?? false; int index; + final bool _isHidden; @override int get hashCode => address.hashCode; - String toJSON() => json.encode({'address': address, 'index': index}); + String toJSON() => + json.encode({'address': address, 'index': index, 'isHidden': isHidden}); } diff --git a/lib/bitcoin/bitcoin_mnemonic.dart b/lib/bitcoin/bitcoin_mnemonic.dart index 7f4bcd13c..4f56161e0 100644 --- a/lib/bitcoin/bitcoin_mnemonic.dart +++ b/lib/bitcoin/bitcoin_mnemonic.dart @@ -4,23 +4,11 @@ import 'dart:typed_data'; import 'package:crypto/crypto.dart'; import 'package:unorm_dart/unorm_dart.dart' as unorm; import 'package:cryptography/cryptography.dart' as cryptography; +import 'package:cake_wallet/core/sec_random_native.dart'; const segwit = '100'; final wordlist = englishWordlist; -Uint8List randomBytes(int length, {bool secure = false}) { - assert(length > 0); - - final random = secure ? Random.secure() : Random(); - final ret = Uint8List(length); - - for (var i = 0; i < length; i++) { - ret[i] = random.nextInt(256); - } - - return ret; -} - double logBase(num x, num base) => log(x) / log(base); String mnemonicEncode(int i) { @@ -102,14 +90,15 @@ List prefixMatches(String source, List prefixes) { return prefixes.map((prefix) => hx.startsWith(prefix.toLowerCase())).toList(); } -String generateMnemonic({int strength = 132, String prefix = segwit}) { +Future generateMnemonic( + {int strength = 264, String prefix = segwit}) async { final wordBitlen = logBase(wordlist.length, 2).ceil(); final wordCount = strength / wordBitlen; final byteCount = ((wordCount * wordBitlen).ceil() / 8).ceil(); var result = ''; do { - final bytes = randomBytes(byteCount); + final bytes = await secRandom(byteCount); maskBytes(bytes, strength); result = encode(bytes); } while (!prefixMatches(result, [prefix]).first); @@ -134,7 +123,7 @@ bool matchesAnyPrefix(String mnemonic) => bool validateMnemonic(String mnemonic, {String prefix = segwit}) { try { return matchesAnyPrefix(mnemonic); - } catch(e) { + } catch (e) { return false; } } diff --git a/lib/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart b/lib/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart index 761b02601..5171a4f05 100644 --- a/lib/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart +++ b/lib/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart @@ -1,5 +1,5 @@ class BitcoinMnemonicIsIncorrectException implements Exception { @override String toString() => - 'Bitcoin mnemonic has incorrect format. Mnemonic should contain 12 words separated by space.'; + 'Bitcoin mnemonic has incorrect format. Mnemonic should contain 24 words separated by space.'; } diff --git a/lib/bitcoin/bitcoin_transaction_history.dart b/lib/bitcoin/bitcoin_transaction_history.dart deleted file mode 100644 index 16171267c..000000000 --- a/lib/bitcoin/bitcoin_transaction_history.dart +++ /dev/null @@ -1,192 +0,0 @@ -import 'dart:convert'; -import 'package:flutter/foundation.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cake_wallet/core/transaction_history.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/bitcoin/electrum.dart'; - -part 'bitcoin_transaction_history.g.dart'; - -const _transactionsHistoryFileName = 'transactions.json'; - -class BitcoinTransactionHistory = BitcoinTransactionHistoryBase - with _$BitcoinTransactionHistory; - -abstract class BitcoinTransactionHistoryBase - extends TransactionHistoryBase with Store { - BitcoinTransactionHistoryBase( - {this.eclient, String dirPath, @required String password}) - : path = '$dirPath/$_transactionsHistoryFileName', - _password = password, - _height = 0, - _isUpdating = false { - transactions = ObservableMap(); - } - - BitcoinWalletBase wallet; - final ElectrumClient eclient; - final String path; - final String _password; - int _height; - bool _isUpdating; - - Future init() async { - await _load(); - } - - @override - Future update() async { - if (_isUpdating) { - return; - } - - try { - _isUpdating = true; - final txs = await fetchTransactions(); - await add(txs); - _isUpdating = false; - } catch (_) { - _isUpdating = false; - rethrow; - } - } - - @override - Future> fetchTransactions() async { - final histories = - wallet.scriptHashes.map((scriptHash) => eclient.getHistory(scriptHash)); - final _historiesWithDetails = await Future.wait(histories) - .then((histories) => histories.expand((i) => i).toList()) - .then((histories) => histories.map((tx) => fetchTransactionInfo( - hash: tx['tx_hash'] as String, height: tx['height'] as int))); - final historiesWithDetails = await Future.wait(_historiesWithDetails); - - return historiesWithDetails.fold>( - {}, (acc, tx) { - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); - } - - Future fetchTransactionInfo( - {@required String hash, @required int height}) async { - final tx = await eclient.getTransactionExpanded(hash: hash); - return BitcoinTransactionInfo.fromElectrumVerbose(tx, - height: height, addresses: wallet.addresses); - } - - Future add(Map transactionsList) async { - transactionsList.entries.forEach((entry) { - _updateOrInsert(entry.value); - - if (entry.value.height > _height) { - _height = entry.value.height; - } - }); - - await save(); - } - - Future addOne(BitcoinTransactionInfo tx) async { - _updateOrInsert(tx); - - if (tx.height > _height) { - _height = tx.height; - } - - await save(); - } - - BitcoinTransactionInfo get(String id) => transactions[id]; - - Future save() async { - try { - final data = json.encode({'height': _height, 'transactions': transactions}); - await writeData(path: path, password: _password, data: data); - } catch(e) { - print('Error while save bitcoin transaction history: ${e.toString()}'); - } - } - - @override - void updateAsync({void Function() onFinished}) { - fetchTransactionsAsync((transaction) => _updateOrInsert(transaction), - onFinished: onFinished); - } - - @override - void fetchTransactionsAsync( - void Function(BitcoinTransactionInfo transaction) onTransactionLoaded, - {void Function() onFinished}) async { - final histories = await Future.wait(wallet.scriptHashes - .map((scriptHash) async => await eclient.getHistory(scriptHash))); - final transactionsCount = - histories.fold(0, (acc, m) => acc + m.length); - var counter = 0; - - final batches = histories.map((metaList) => - _fetchBatchOfTransactions(metaList, onTransactionLoaded: (transaction) { - onTransactionLoaded(transaction); - counter += 1; - - if (counter == transactionsCount) { - onFinished?.call(); - } - })); - - await Future.wait(batches); - } - - Future _fetchBatchOfTransactions( - Iterable> metaList, - {void Function(BitcoinTransactionInfo tranasaction) - onTransactionLoaded}) async => - metaList.forEach((txMeta) => fetchTransactionInfo( - hash: txMeta['tx_hash'] as String, - height: txMeta['height'] as int) - .then((transaction) => onTransactionLoaded(transaction))); - - Future> _read() async { - final content = await read(path: path, password: _password); - return json.decode(content) as Map; - } - - Future _load() async { - try { - final content = await _read(); - final txs = content['transactions'] as Map ?? {}; - - txs.entries.forEach((entry) { - final val = entry.value; - - if (val is Map) { - final tx = BitcoinTransactionInfo.fromJson(val); - _updateOrInsert(tx); - } - }); - - _height = content['height'] as int; - } catch (e) { - print(e); - } - } - - void _updateOrInsert(BitcoinTransactionInfo transaction) { - if (transaction.id == null) { - return; - } - - if (transactions[transaction.id] == null) { - transactions[transaction.id] = transaction; - } else { - final originalTx = transactions[transaction.id]; - originalTx.confirmations = transaction.confirmations; - originalTx.amount = transaction.amount; - originalTx.height = transaction.height; - originalTx.date ??= transaction.date; - originalTx.isPending = transaction.isPending; - } - } -} diff --git a/lib/bitcoin/bitcoin_transaction_priority.dart b/lib/bitcoin/bitcoin_transaction_priority.dart index 71002819b..455c4a5ad 100644 --- a/lib/bitcoin/bitcoin_transaction_priority.dart +++ b/lib/bitcoin/bitcoin_transaction_priority.dart @@ -26,13 +26,15 @@ class BitcoinTransactionPriority extends TransactionPriority { } } + String get units => 'sat'; + @override String toString() { var label = ''; switch (this) { case BitcoinTransactionPriority.slow: - label = S.current.transaction_priority_slow; + label = '${S.current.transaction_priority_slow} ~24hrs'; break; case BitcoinTransactionPriority.medium: label = S.current.transaction_priority_medium; @@ -46,4 +48,56 @@ class BitcoinTransactionPriority extends TransactionPriority { return label; } + + String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; +} + +class LitecoinTransactionPriority extends BitcoinTransactionPriority { + const LitecoinTransactionPriority({String title, int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const LitecoinTransactionPriority slow = + LitecoinTransactionPriority(title: 'Slow', raw: 0); + static const LitecoinTransactionPriority medium = + LitecoinTransactionPriority(title: 'Medium', raw: 1); + static const LitecoinTransactionPriority fast = + LitecoinTransactionPriority(title: 'Fast', raw: 2); + + static LitecoinTransactionPriority deserialize({int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + return null; + } + } + + @override + String get units => 'Latoshi'; + + @override + String toString() { + var label = ''; + + switch (this) { + case LitecoinTransactionPriority.slow: + label = S.current.transaction_priority_slow; + break; + case LitecoinTransactionPriority.medium: + label = S.current.transaction_priority_medium; + break; + case LitecoinTransactionPriority.fast: + label = S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } } diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart index bacc03dd4..846eb8c7d 100644 --- a/lib/bitcoin/bitcoin_unspent.dart +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -13,5 +13,6 @@ class BitcoinUnspent { final int value; final int vout; - bool get isP2wpkh => address.address.startsWith('bc1'); + bool get isP2wpkh => + address.address.startsWith('bc') || address.address.startsWith('ltc'); } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index 6cd113904..fd8402887 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,472 +1,51 @@ -import 'dart:async'; -import 'dart:convert'; -import 'package:cake_wallet/bitcoin/address_to_output_script.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; -import 'package:rxdart/rxdart.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; -import 'package:cake_wallet/bitcoin/electrum.dart'; -import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart'; -import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:cake_wallet/bitcoin/utils.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; -import 'package:cake_wallet/entities/sync_status.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_history.dart'; import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_balance.dart'; -import 'package:cake_wallet/entities/node.dart'; -import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; part 'bitcoin_wallet.g.dart'; class BitcoinWallet = BitcoinWalletBase with _$BitcoinWallet; -abstract class BitcoinWalletBase extends WalletBase with Store { - BitcoinWalletBase._internal( - {@required this.eclient, - @required this.path, - @required String password, - @required WalletInfo walletInfo, - @required List initialAddresses, - int accountIndex = 0, - this.transactionHistory, - this.mnemonic, - BitcoinBalance initialBalance}) - : balance = - initialBalance ?? BitcoinBalance(confirmed: 0, unconfirmed: 0), - hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), - network: bitcoin.bitcoin) - .derivePath("m/0'/0"), - addresses = initialAddresses != null - ? ObservableList.of(initialAddresses.toSet()) - : ObservableList(), - syncStatus = NotConnectedSyncStatus(), - _password = password, - _accountIndex = accountIndex, - _feeRates = [], - super(walletInfo) { - _unspent = []; - _scripthashesUpdateSubject = {}; - } - - static BitcoinWallet fromJSON( - {@required String password, - @required String name, - @required String dirPath, - @required WalletInfo walletInfo, - String jsonSource}) { - final data = json.decode(jsonSource) as Map; - final mnemonic = data['mnemonic'] as String; - final accountIndex = - (data['account_index'] == 'null' || data['account_index'] == null) - ? 0 - : int.parse(data['account_index'] as String); - final _addresses = data['addresses'] as List ?? []; - final addresses = []; - final balance = BitcoinBalance.fromJSON(data['balance'] as String) ?? - BitcoinBalance(confirmed: 0, unconfirmed: 0); - - _addresses.forEach((Object el) { - if (el is String) { - addresses.add(BitcoinAddressRecord.fromJSON(el)); - } - }); - - return BitcoinWalletBase.build( - dirPath: dirPath, - mnemonic: mnemonic, - password: password, - name: name, - accountIndex: accountIndex, - initialAddresses: addresses, - initialBalance: balance, - walletInfo: walletInfo); - } - - static BitcoinWallet build( +abstract class BitcoinWalletBase extends ElectrumWallet with Store { + BitcoinWalletBase( {@required String mnemonic, @required String password, - @required String name, - @required String dirPath, @required WalletInfo walletInfo, List initialAddresses, - BitcoinBalance initialBalance, - int accountIndex = 0}) { - final walletPath = '$dirPath/$name'; - final eclient = ElectrumClient(); - final history = BitcoinTransactionHistory( - eclient: eclient, dirPath: dirPath, password: password); + ElectrumBalance initialBalance, + int accountIndex = 0}) + : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + networkType: bitcoin.bitcoin, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + accountIndex: accountIndex); - return BitcoinWallet._internal( - eclient: eclient, - path: walletPath, - mnemonic: mnemonic, + static Future open({ + @required String name, + @required WalletInfo walletInfo, + @required String password, + }) async { + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); + await snp.load(); + return BitcoinWallet( + mnemonic: snp.mnemonic, password: password, - accountIndex: accountIndex, - initialAddresses: initialAddresses, - initialBalance: initialBalance, - transactionHistory: history, - walletInfo: walletInfo); - } - - static int estimatedTransactionSize(int inputsCount, int outputsCounts) => - inputsCount * 146 + outputsCounts * 33 + 8; - - @override - final BitcoinTransactionHistory transactionHistory; - final String path; - final bitcoin.HDWallet hd; - final ElectrumClient eclient; - final String mnemonic; - - List _unspent; - - @override - @observable - String address; - - @override - @observable - BitcoinBalance balance; - - @override - @observable - SyncStatus syncStatus; - - ObservableList addresses; - - List get scriptHashes => - addresses.map((addr) => scriptHash(addr.address)).toList(); - - String get xpub => hd.base58; - - @override - String get seed => mnemonic; - - @override - BitcoinWalletKeys get keys => BitcoinWalletKeys( - wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); - - final String _password; - List _feeRates; - int _accountIndex; - Map> _scripthashesUpdateSubject; - - Future init() async { - if (addresses.isEmpty || addresses.length < 33) { - final addressesCount = 33 - addresses.length; - await generateNewAddresses(addressesCount, startIndex: addresses.length); - } - - address = addresses[_accountIndex].address; - transactionHistory.wallet = this; - await transactionHistory.init(); - } - - @action - Future nextAddress() async { - _accountIndex += 1; - - if (_accountIndex >= addresses.length) { - _accountIndex = 0; - } - - address = addresses[_accountIndex].address; - - await save(); - } - - Future generateNewAddress() async { - _accountIndex += 1; - final address = BitcoinAddressRecord(_getAddress(index: _accountIndex), - index: _accountIndex); - addresses.add(address); - - await save(); - - return address; - } - - Future> generateNewAddresses(int count, - {int startIndex = 0}) async { - final list = []; - - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord(_getAddress(index: i), index: i); - list.add(address); - } - - addresses.addAll(list); - await save(); - - return list; - } - - Future updateAddress(String address) async { - for (final addr in addresses) { - if (addr.address == address) { - await save(); - break; - } - } - } - - @action - @override - Future startSync() async { - try { - syncStatus = StartingSyncStatus(); - transactionHistory.updateAsync(onFinished: () { - print('transactionHistory update finished!'); - transactionHistory.save(); - }); - _subscribeForUpdates(); - await _updateBalance(); - await _updateUnspent(); - _feeRates = await eclient.feeRates(); - - Timer.periodic(const Duration(minutes: 1), - (timer) async => _feeRates = await eclient.feeRates()); - - syncStatus = SyncedSyncStatus(); - } catch (e) { - print(e.toString()); - syncStatus = FailedSyncStatus(); - } - } - - @action - @override - Future connectToNode({@required Node node}) async { - try { - syncStatus = ConnectingSyncStatus(); - await eclient.connectToUri(node.uri); - eclient.onConnectionStatusChange = (bool isConnected) { - if (!isConnected) { - syncStatus = LostConnectionSyncStatus(); - } - }; - syncStatus = ConnectedSyncStatus(); - } catch (e) { - print(e.toString()); - syncStatus = FailedSyncStatus(); - } + walletInfo: walletInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + accountIndex: snp.accountIndex); } @override - Future createTransaction( - Object credentials) async { - const minAmount = 546; - final transactionCredentials = credentials as BitcoinTransactionCredentials; - final inputs = []; - final allAmountFee = - calculateEstimatedFee(transactionCredentials.priority, null); - final allAmount = balance.confirmed - allAmountFee; - var fee = 0; - final credentialsAmount = transactionCredentials.amount != null - ? stringDoubleToBitcoinAmount(transactionCredentials.amount) - : 0; - final amount = transactionCredentials.amount == null || - allAmount - credentialsAmount < minAmount - ? allAmount - : credentialsAmount; - final txb = bitcoin.TransactionBuilder(network: bitcoin.bitcoin); - final changeAddress = address; - var leftAmount = amount; - var totalInputAmount = 0; - - if (_unspent.isEmpty) { - await _updateUnspent(); - } - - for (final utx in _unspent) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); - - if (leftAmount <= 0) { - break; - } - } - - if (inputs.isEmpty) { - throw BitcoinTransactionNoInputsException(); - } - - final totalAmount = amount + fee; - fee = transactionCredentials.amount != null - ? feeAmountForPriority(transactionCredentials.priority, inputs.length, - amount == allAmount ? 1 : 2) - : allAmountFee; - - if (totalAmount > balance.confirmed) { - throw BitcoinTransactionWrongBalanceException(); - } - - if (amount <= 0 || totalInputAmount < amount) { - throw BitcoinTransactionWrongBalanceException(); - } - - txb.setVersion(1); - - inputs.forEach((input) { - if (input.isP2wpkh) { - final p2wpkh = bitcoin - .P2WPKH( - data: generatePaymentData(hd: hd, index: input.address.index), - network: bitcoin.bitcoin) - .data; - - txb.addInput(input.hash, input.vout, null, p2wpkh.output); - } else { - txb.addInput(input.hash, input.vout); - } - }); - - txb.addOutput( - addressToOutputScript(transactionCredentials.address), amount); - - final estimatedSize = estimatedTransactionSize(inputs.length, 2); - final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize; - final changeValue = totalInputAmount - amount - feeAmount; - - if (changeValue > minAmount) { - txb.addOutput(changeAddress, changeValue); - } - - for (var i = 0; i < inputs.length; i++) { - final input = inputs[i]; - final keyPair = generateKeyPair(hd: hd, index: input.address.index); - final witnessValue = input.isP2wpkh ? input.value : null; - - txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); - } - - return PendingBitcoinTransaction(txb.build(), - eclient: eclient, amount: amount, fee: fee) - ..addListener((transaction) async { - transactionHistory.addOne(transaction); - await _updateBalance(); - }); - } - - String toJSON() => json.encode({ - 'mnemonic': mnemonic, - 'account_index': _accountIndex.toString(), - 'addresses': addresses.map((addr) => addr.toJSON()).toList(), - 'balance': balance?.toJSON() - }); - - int feeRate(TransactionPriority priority) { - if (priority is BitcoinTransactionPriority) { - return _feeRates[priority.raw]; - } - - return 0; - } - - int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, - int outputsCount) => - feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); - - @override - int calculateEstimatedFee(TransactionPriority priority, int amount) { - if (priority is BitcoinTransactionPriority) { - int inputsCount = 0; - - if (amount != null) { - int totalValue = 0; - - for (final input in _unspent) { - if (totalValue >= amount) { - break; - } - - totalValue += input.value; - inputsCount += 1; - } - } else { - inputsCount = _unspent.length; - } - // If send all, then we have no change value - return feeAmountForPriority( - priority, inputsCount, amount != null ? 2 : 1); - } - - return 0; - } - - @override - Future save() async { - await write(path: path, password: _password, data: toJSON()); - await transactionHistory.save(); - } - - bitcoin.ECPair keyPairFor({@required int index}) => - generateKeyPair(hd: hd, index: index); - - @override - Future rescan({int height}) async { - // FIXME: Unimplemented - } - - @override - void close() async { - await eclient.close(); - } - - Future _updateUnspent() async { - final unspent = await Future.wait(addresses.map((address) => eclient - .getListUnspentWithAddress(address.address) - .then((unspent) => unspent - .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); - _unspent = unspent.expand((e) => e).toList(); - } - - void _subscribeForUpdates() { - scriptHashes.forEach((sh) async { - await _scripthashesUpdateSubject[sh]?.close(); - _scripthashesUpdateSubject[sh] = eclient.scripthashUpdate(sh); - _scripthashesUpdateSubject[sh].listen((event) async { - try { - await _updateBalance(); - await _updateUnspent(); - transactionHistory.updateAsync(); - } catch (e) { - print(e.toString()); - } - }); - }); - } - - Future _fetchBalances() async { - final balances = await Future.wait( - scriptHashes.map((sHash) => eclient.getBalance(sHash))); - final balance = balances.fold( - BitcoinBalance(confirmed: 0, unconfirmed: 0), - (BitcoinBalance acc, val) => BitcoinBalance( - confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), - unconfirmed: - (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); - - return balance; - } - - Future _updateBalance() async { - balance = await _fetchBalances(); - await save(); - } - - String _getAddress({@required int index}) => - generateAddress(hd: hd, index: index); + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); } diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index 8e3035a2c..aefe0fadf 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -1,7 +1,6 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; -import 'package:cake_wallet/bitcoin/file.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; @@ -19,44 +18,32 @@ class BitcoinWalletService extends WalletService< final Box walletInfoSource; + @override + WalletType getType() => WalletType.bitcoin; + @override Future create(BitcoinNewWalletCredentials credentials) async { - final dirPath = await pathForWalletDir( - type: WalletType.bitcoin, name: credentials.name); - final wallet = BitcoinWalletBase.build( - dirPath: dirPath, - mnemonic: generateMnemonic(), + final wallet = BitcoinWallet( + mnemonic: await generateMnemonic(), password: credentials.password, - name: credentials.name, walletInfo: credentials.walletInfo); await wallet.save(); await wallet.init(); - return wallet; } @override Future isWalletExit(String name) async => - File(await pathForWallet(name: name, type: WalletType.bitcoin)) - .existsSync(); + File(await pathForWallet(name: name, type: getType())).existsSync(); @override Future openWallet(String name, String password) async { - final walletDirPath = - await pathForWalletDir(name: name, type: WalletType.bitcoin); - final walletPath = '$walletDirPath/$name'; - final walletJSONRaw = await read(path: walletPath, password: password); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, WalletType.bitcoin), + (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); - final wallet = BitcoinWalletBase.fromJSON( - password: password, - name: name, - dirPath: walletDirPath, - jsonSource: walletJSONRaw, - walletInfo: walletInfo); + final wallet = await BitcoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo); await wallet.init(); - return wallet; } @@ -67,10 +54,8 @@ class BitcoinWalletService extends WalletService< @override Future restoreFromKeys( - BitcoinRestoreWalletFromWIFCredentials credentials) async { - // TODO: implement restoreFromKeys - throw UnimplementedError(); - } + BitcoinRestoreWalletFromWIFCredentials credentials) async => + throw UnimplementedError(); @override Future restoreFromSeed( @@ -79,17 +64,12 @@ class BitcoinWalletService extends WalletService< throw BitcoinMnemonicIsIncorrectException(); } - final dirPath = await pathForWalletDir( - type: WalletType.bitcoin, name: credentials.name); - final wallet = BitcoinWalletBase.build( - dirPath: dirPath, - name: credentials.name, + final wallet = BitcoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo); await wallet.save(); await wallet.init(); - return wallet; } } diff --git a/lib/bitcoin/electrum.dart b/lib/bitcoin/electrum.dart index 29e85afa5..71813ac3f 100644 --- a/lib/bitcoin/electrum.dart +++ b/lib/bitcoin/electrum.dart @@ -2,21 +2,12 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/script_hash.dart'; import 'package:flutter/foundation.dart'; import 'package:rxdart/rxdart.dart'; -class UriParseException implements Exception { - UriParseException(this.uri); - - final String uri; - - @override - String toString() => - 'Cannot parse host and port from uri. Invalid uri format. Uri: $uri'; -} - String jsonrpcparams(List params) { final _params = params?.map((val) => '"${val.toString()}"')?.join(','); return '[$_params]'; @@ -53,17 +44,8 @@ class ElectrumClient { Timer _aliveTimer; String unterminatedString; - Future connectToUri(String uri) async { - final splittedUri = uri.split(':'); - - if (splittedUri.length != 2) { - throw UriParseException(uri); - } - - final host = splittedUri.first; - final port = int.parse(splittedUri.last); - await connect(host: host, port: port); - } + Future connectToUri(Uri uri) async => + await connect(host: uri.host, port: uri.port); Future connect({@required String host, @required int port}) async { try { @@ -173,10 +155,11 @@ class ElectrumClient { }); Future>> getListUnspentWithAddress( - String address) => + String address, NetworkType networkType) => call( - method: 'blockchain.scripthash.listunspent', - params: [scriptHash(address)]).then((dynamic result) { + method: 'blockchain.scripthash.listunspent', + params: [scriptHash(address, networkType: networkType)]) + .then((dynamic result) { if (result is List) { return result.map((dynamic val) { if (val is Map) { @@ -259,7 +242,7 @@ class ElectrumClient { if (result is String) { return result; } - print(result); + return ''; }); @@ -303,14 +286,24 @@ class ElectrumClient { }); Future> feeRates() async { - final topDoubleString = await estimatefee(p: 1); - final middleDoubleString = await estimatefee(p: 20); - final bottomDoubleString = await estimatefee(p: 150); - final top = (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000).round(); - final middle = (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000).round(); - final bottom = (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000).round(); + try { + final topDoubleString = await estimatefee(p: 1); + final middleDoubleString = await estimatefee(p: 20); + final bottomDoubleString = await estimatefee(p: 100); + final top = + (stringDoubleToBitcoinAmount(topDoubleString.toString()) / 1000) + .round(); + final middle = + (stringDoubleToBitcoinAmount(middleDoubleString.toString()) / 1000) + .round(); + final bottom = + (stringDoubleToBitcoinAmount(bottomDoubleString.toString()) / 1000) + .round(); - return [bottom, middle, top]; + return [bottom, middle, top]; + } catch (_) { + return []; + } } BehaviorSubject scripthashUpdate(String scripthash) { diff --git a/lib/bitcoin/bitcoin_balance.dart b/lib/bitcoin/electrum_balance.dart similarity index 70% rename from lib/bitcoin/bitcoin_balance.dart rename to lib/bitcoin/electrum_balance.dart index 7d8441250..66e53f921 100644 --- a/lib/bitcoin/bitcoin_balance.dart +++ b/lib/bitcoin/electrum_balance.dart @@ -1,21 +1,20 @@ import 'dart:convert'; - import 'package:flutter/foundation.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/entities/balance.dart'; -class BitcoinBalance extends Balance { - const BitcoinBalance({@required this.confirmed, @required this.unconfirmed}) +class ElectrumBalance extends Balance { + const ElectrumBalance({@required this.confirmed, @required this.unconfirmed}) : super(confirmed, unconfirmed); - factory BitcoinBalance.fromJSON(String jsonSource) { + factory ElectrumBalance.fromJSON(String jsonSource) { if (jsonSource == null) { return null; } final decoded = json.decode(jsonSource) as Map; - return BitcoinBalance( + return ElectrumBalance( confirmed: decoded['confirmed'] as int ?? 0, unconfirmed: decoded['unconfirmed'] as int ?? 0); } @@ -24,7 +23,8 @@ class BitcoinBalance extends Balance { final int unconfirmed; @override - String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed); + String get formattedAvailableBalance => + bitcoinAmountToString(amount: confirmed); @override String get formattedAdditionalBalance => diff --git a/lib/bitcoin/electrum_transaction_history.dart b/lib/bitcoin/electrum_transaction_history.dart new file mode 100644 index 000000000..1d5e894c0 --- /dev/null +++ b/lib/bitcoin/electrum_transaction_history.dart @@ -0,0 +1,98 @@ +import 'dart:convert'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; + +part 'electrum_transaction_history.g.dart'; + +const _transactionsHistoryFileName = 'transactions.json'; + +class ElectrumTransactionHistory = ElectrumTransactionHistoryBase + with _$ElectrumTransactionHistory; + +abstract class ElectrumTransactionHistoryBase + extends TransactionHistoryBase with Store { + ElectrumTransactionHistoryBase( + {@required this.walletInfo, @required String password}) + : _password = password, + _height = 0 { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + final String _password; + int _height; + + Future init() async => await _load(); + + @override + void addOne(ElectrumTransactionInfo transaction) => + transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + @override + Future save() async { + try { + final dirPath = + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$_transactionsHistoryFileName'; + final data = + json.encode({'height': _height, 'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e) { + print('Error while save bitcoin transaction history: ${e.toString()}'); + } + } + + Future> _read() async { + final dirPath = + await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$_transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + return json.decode(content) as Map; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = ElectrumTransactionInfo.fromJson(val, walletInfo.type); + _updateOrInsert(tx); + } + }); + + _height = content['height'] as int; + } catch (e) { + print(e); + } + } + + void _updateOrInsert(ElectrumTransactionInfo transaction) { + if (transaction.id == null) { + return; + } + + if (transactions[transaction.id] == null) { + transactions[transaction.id] = transaction; + } else { + final originalTx = transactions[transaction.id]; + originalTx.confirmations = transaction.confirmations; + originalTx.amount = transaction.amount; + originalTx.height = transaction.height; + originalTx.date ??= transaction.date; + originalTx.isPending = transaction.isPending; + } + } +} diff --git a/lib/bitcoin/bitcoin_transaction_info.dart b/lib/bitcoin/electrum_transaction_info.dart similarity index 81% rename from lib/bitcoin/bitcoin_transaction_info.dart rename to lib/bitcoin/electrum_transaction_info.dart index fb1400c5e..47005edc7 100644 --- a/lib/bitcoin/bitcoin_transaction_info.dart +++ b/lib/bitcoin/electrum_transaction_info.dart @@ -6,9 +6,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/entities/transaction_direction.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/format_amount.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; -class BitcoinTransactionInfo extends TransactionInfo { - BitcoinTransactionInfo( +class ElectrumTransactionInfo extends TransactionInfo { + ElectrumTransactionInfo(this.type, {@required String id, @required int height, @required int amount, @@ -27,7 +28,8 @@ class BitcoinTransactionInfo extends TransactionInfo { this.confirmations = confirmations; } - factory BitcoinTransactionInfo.fromElectrumVerbose(Map obj, + factory ElectrumTransactionInfo.fromElectrumVerbose( + Map obj, WalletType type, {@required List addresses, @required int height}) { final addressesSet = addresses.map((addr) => addr.address).toSet(); final id = obj['txid'] as String; @@ -47,7 +49,8 @@ class BitcoinTransactionInfo extends TransactionInfo { final out = vin['tx']['vout'][vout] as Map; final outAddresses = (out['scriptPubKey']['addresses'] as List)?.toSet(); - inputsAmount += stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString()); + inputsAmount += + stringDoubleToBitcoinAmount((out['value'] as double ?? 0).toString()); if (outAddresses?.intersection(addressesSet)?.isNotEmpty ?? false) { direction = TransactionDirection.outgoing; @@ -58,7 +61,8 @@ class BitcoinTransactionInfo extends TransactionInfo { final outAddresses = out['scriptPubKey']['addresses'] as List ?? []; final ntrs = outAddresses.toSet().intersection(addressesSet); - final value = stringDoubleToBitcoinAmount((out['value'] as double ?? 0.0).toString()); + final value = stringDoubleToBitcoinAmount( + (out['value'] as double ?? 0.0).toString()); totalOutAmount += value; if ((direction == TransactionDirection.incoming && ntrs.isNotEmpty) || @@ -69,7 +73,7 @@ class BitcoinTransactionInfo extends TransactionInfo { final fee = inputsAmount - totalOutAmount; - return BitcoinTransactionInfo( + return ElectrumTransactionInfo(type, id: id, height: height, isPending: false, @@ -80,7 +84,7 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory BitcoinTransactionInfo.fromHexAndHeader(String hex, + factory ElectrumTransactionInfo.fromHexAndHeader(WalletType type, String hex, {List addresses, int height, int timestamp, int confirmations}) { final tx = bitcoin.Transaction.fromHex(hex); var exist = false; @@ -104,7 +108,7 @@ class BitcoinTransactionInfo extends TransactionInfo { ? DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) : DateTime.now(); - return BitcoinTransactionInfo( + return ElectrumTransactionInfo(type, id: tx.getId(), height: height, isPending: false, @@ -115,8 +119,9 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: confirmations); } - factory BitcoinTransactionInfo.fromJson(Map data) { - return BitcoinTransactionInfo( + factory ElectrumTransactionInfo.fromJson( + Map data, WalletType type) { + return ElectrumTransactionInfo(type, id: data['id'] as String, height: data['height'] as int, amount: data['amount'] as int, @@ -127,15 +132,17 @@ class BitcoinTransactionInfo extends TransactionInfo { confirmations: data['confirmations'] as int); } + final WalletType type; + String _fiatAmount; @override String amountFormatted() => - '${formatAmount(bitcoinAmountToString(amount: amount))} BTC'; + '${formatAmount(bitcoinAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(type).title}'; @override String feeFormatted() => fee != null - ? '${formatAmount(bitcoinAmountToString(amount: fee))} BTC' + ? '${formatAmount(bitcoinAmountToString(amount: fee))} ${walletTypeToCryptoCurrency(type).title}' : ''; @override @@ -144,8 +151,8 @@ class BitcoinTransactionInfo extends TransactionInfo { @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - BitcoinTransactionInfo updated(BitcoinTransactionInfo info) { - return BitcoinTransactionInfo( + ElectrumTransactionInfo updated(ElectrumTransactionInfo info) { + return ElectrumTransactionInfo(info.type, id: id, height: info.height, amount: info.amount, diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart new file mode 100644 index 000000000..bf3dfd542 --- /dev/null +++ b/lib/bitcoin/electrum_wallet.dart @@ -0,0 +1,467 @@ +import 'dart:async'; +import 'dart:convert'; +import 'package:mobx/mobx.dart'; +import 'package:rxdart/subjects.dart'; +import 'package:flutter/foundation.dart'; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/bitcoin/address_to_output_script.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_credentials.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_history.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_no_inputs_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_wrong_balance_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_unspent.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_keys.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/bitcoin/pending_bitcoin_transaction.dart'; +import 'package:cake_wallet/bitcoin/script_hash.dart'; +import 'package:cake_wallet/bitcoin/utils.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/node.dart'; +import 'package:cake_wallet/entities/sync_status.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/bitcoin/electrum.dart'; + +part 'electrum_wallet.g.dart'; + +class ElectrumWallet = ElectrumWalletBase with _$ElectrumWallet; + +abstract class ElectrumWalletBase extends WalletBase with Store { + ElectrumWalletBase( + {@required String password, + @required WalletInfo walletInfo, + @required List initialAddresses, + @required this.networkType, + @required this.mnemonic, + ElectrumClient electrumClient, + int accountIndex = 0, + ElectrumBalance initialBalance}) + : balance = initialBalance ?? + const ElectrumBalance(confirmed: 0, unconfirmed: 0), + hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), + network: networkType) + .derivePath("m/0'/0"), + addresses = ObservableList.of( + (initialAddresses ?? []).toSet()), + syncStatus = NotConnectedSyncStatus(), + _password = password, + _accountIndex = accountIndex, + _feeRates = [], + _isTransactionUpdating = false, + super(walletInfo) { + this.electrumClient = electrumClient ?? ElectrumClient(); + this.walletInfo = walletInfo; + transactionHistory = + ElectrumTransactionHistory(walletInfo: walletInfo, password: password); + _unspent = []; + _scripthashesUpdateSubject = {}; + } + + static int estimatedTransactionSize(int inputsCount, int outputsCounts) => + inputsCount * 146 + outputsCounts * 33 + 8; + + final bitcoin.HDWallet hd; + final String mnemonic; + + ElectrumClient electrumClient; + + @override + @observable + String address; + + @override + @observable + ElectrumBalance balance; + + @override + @observable + SyncStatus syncStatus; + + ObservableList addresses; + + List get scriptHashes => addresses + .map((addr) => scriptHash(addr.address, networkType: networkType)) + .toList(); + + String get xpub => hd.base58; + + @override + String get seed => mnemonic; + + bitcoin.NetworkType networkType; + + @override + BitcoinWalletKeys get keys => BitcoinWalletKeys( + wif: hd.wif, privateKey: hd.privKey, publicKey: hd.pubKey); + + final String _password; + List _unspent; + List _feeRates; + int _accountIndex; + Map> _scripthashesUpdateSubject; + bool _isTransactionUpdating; + + Future init() async { + await generateAddresses(); + address = addresses[_accountIndex].address; + await transactionHistory.init(); + } + + @action + Future nextAddress() async { + _accountIndex += 1; + + if (_accountIndex >= addresses.length) { + _accountIndex = 0; + } + + address = addresses[_accountIndex].address; + + await save(); + } + + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 33 - addresses.length; + await generateNewAddresses(addressesCount, + startIndex: addresses.length, hd: hd); + } + } + + Future generateNewAddress( + {bool isHidden = false, bitcoin.HDWallet hd}) async { + _accountIndex += 1; + final _hd = hd ?? this.hd; + final address = BitcoinAddressRecord( + getAddress(index: _accountIndex, hd: _hd), + index: _accountIndex, + isHidden: isHidden); + addresses.add(address); + await save(); + return address; + } + + Future> generateNewAddresses(int count, + {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { + final list = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord(getAddress(index: i, hd: hd), + index: i, isHidden: isHidden); + list.add(address); + } + + addresses.addAll(list); + await save(); + return list; + } + + Future updateAddress(String address) async { + for (final addr in addresses) { + if (addr.address == address) { + await save(); + break; + } + } + } + + @action + @override + Future startSync() async { + try { + syncStatus = StartingSyncStatus(); + updateTransactions(); + _subscribeForUpdates(); + await _updateBalance(); + await _updateUnspent(); + _feeRates = await electrumClient.feeRates(); + + Timer.periodic(const Duration(minutes: 1), + (timer) async => _feeRates = await electrumClient.feeRates()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @action + @override + Future connectToNode({@required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + await electrumClient.connectToUri(node.uri); + electrumClient.onConnectionStatusChange = (bool isConnected) { + if (!isConnected) { + syncStatus = LostConnectionSyncStatus(); + } + }; + syncStatus = ConnectedSyncStatus(); + } catch (e) { + print(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction( + Object credentials) async { + const minAmount = 546; + final transactionCredentials = credentials as BitcoinTransactionCredentials; + final inputs = []; + final allAmountFee = + calculateEstimatedFee(transactionCredentials.priority, null); + final allAmount = balance.confirmed - allAmountFee; + var fee = 0; + final credentialsAmount = transactionCredentials.amount != null + ? stringDoubleToBitcoinAmount(transactionCredentials.amount) + : 0; + final amount = transactionCredentials.amount == null || + allAmount - credentialsAmount < minAmount + ? allAmount + : credentialsAmount; + final txb = bitcoin.TransactionBuilder(network: networkType); + final changeAddress = address; + var leftAmount = amount; + var totalInputAmount = 0; + + if (_unspent.isEmpty) { + await _updateUnspent(); + } + + for (final utx in _unspent) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); + + if (leftAmount <= 0) { + break; + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + + final totalAmount = amount + fee; + fee = transactionCredentials.amount != null + ? feeAmountForPriority(transactionCredentials.priority, inputs.length, + amount == allAmount ? 1 : 2) + : allAmountFee; + + if (totalAmount > balance.confirmed) { + throw BitcoinTransactionWrongBalanceException(); + } + + if (amount <= 0 || totalInputAmount < amount) { + throw BitcoinTransactionWrongBalanceException(); + } + + txb.setVersion(1); + + inputs.forEach((input) { + if (input.isP2wpkh) { + final p2wpkh = bitcoin + .P2WPKH( + data: generatePaymentData(hd: hd, index: input.address.index), + network: networkType) + .data; + + txb.addInput(input.hash, input.vout, null, p2wpkh.output); + } else { + txb.addInput(input.hash, input.vout); + } + }); + + txb.addOutput( + addressToOutputScript(transactionCredentials.address, networkType), + amount); + + final estimatedSize = estimatedTransactionSize(inputs.length, 2); + final feeAmount = feeRate(transactionCredentials.priority) * estimatedSize; + final changeValue = totalInputAmount - amount - feeAmount; + + if (changeValue > minAmount) { + txb.addOutput(changeAddress, changeValue); + } + + for (var i = 0; i < inputs.length; i++) { + final input = inputs[i]; + final keyPair = generateKeyPair( + hd: hd, index: input.address.index, network: networkType); + final witnessValue = input.isP2wpkh ? input.value : null; + + txb.sign(vin: i, keyPair: keyPair, witnessValue: witnessValue); + } + + return PendingBitcoinTransaction(txb.build(), type, + electrumClient: electrumClient, amount: amount, fee: fee) + ..addListener((transaction) async { + transactionHistory.addOne(transaction); + await _updateBalance(); + }); + } + + String toJSON() => json.encode({ + 'mnemonic': mnemonic, + 'account_index': _accountIndex.toString(), + 'addresses': addresses.map((addr) => addr.toJSON()).toList(), + 'balance': balance?.toJSON() + }); + + int feeRate(TransactionPriority priority) { + if (priority is BitcoinTransactionPriority) { + return _feeRates[priority.raw]; + } + + return 0; + } + + int feeAmountForPriority(BitcoinTransactionPriority priority, int inputsCount, + int outputsCount) => + feeRate(priority) * estimatedTransactionSize(inputsCount, outputsCount); + + @override + int calculateEstimatedFee(TransactionPriority priority, int amount) { + if (priority is BitcoinTransactionPriority) { + int inputsCount = 0; + + if (amount != null) { + int totalValue = 0; + + for (final input in _unspent) { + if (totalValue >= amount) { + break; + } + + totalValue += input.value; + inputsCount += 1; + } + } else { + inputsCount = _unspent.length; + } + // If send all, then we have no change value + return feeAmountForPriority( + priority, inputsCount, amount != null ? 2 : 1); + } + + return 0; + } + + @override + Future save() async { + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + bitcoin.ECPair keyPairFor({@required int index}) => + generateKeyPair(hd: hd, index: index, network: networkType); + + @override + Future rescan({int height}) async => throw UnimplementedError(); + + @override + Future close() async { + try { + await electrumClient?.close(); + } catch (_) {} + } + + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => ''; + + Future makePath() async => + pathForWallet(name: walletInfo.name, type: walletInfo.type); + + Future _updateUnspent() async { + final unspent = await Future.wait(addresses.map((address) => electrumClient + .getListUnspentWithAddress(address.address, networkType) + .then((unspent) => unspent + .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); + _unspent = unspent.expand((e) => e).toList(); + } + + Future fetchTransactionInfo( + {@required String hash, @required int height}) async { + final tx = await electrumClient.getTransactionExpanded(hash: hash); + return ElectrumTransactionInfo.fromElectrumVerbose(tx, walletInfo.type, + height: height, addresses: addresses); + } + + @override + Future> fetchTransactions() async { + final histories = + scriptHashes.map((scriptHash) => electrumClient.getHistory(scriptHash)); + final _historiesWithDetails = await Future.wait(histories) + .then((histories) => histories.expand((i) => i).toList()) + .then((histories) => histories.map((tx) => fetchTransactionInfo( + hash: tx['tx_hash'] as String, height: tx['height'] as int))); + final historiesWithDetails = await Future.wait(_historiesWithDetails); + + return historiesWithDetails.fold>( + {}, (acc, tx) { + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + void _subscribeForUpdates() { + scriptHashes.forEach((sh) async { + await _scripthashesUpdateSubject[sh]?.close(); + _scripthashesUpdateSubject[sh] = electrumClient.scripthashUpdate(sh); + _scripthashesUpdateSubject[sh].listen((event) async { + try { + await _updateBalance(); + await _updateUnspent(); + await updateTransactions(); + } catch (e) { + print(e.toString()); + } + }); + }); + } + + Future _fetchBalances() async { + final balances = await Future.wait( + scriptHashes.map((sh) => electrumClient.getBalance(sh))); + final balance = balances.fold( + ElectrumBalance(confirmed: 0, unconfirmed: 0), + (ElectrumBalance acc, val) => ElectrumBalance( + confirmed: (val['confirmed'] as int ?? 0) + (acc.confirmed ?? 0), + unconfirmed: + (val['unconfirmed'] as int ?? 0) + (acc.unconfirmed ?? 0))); + + return balance; + } + + Future _updateBalance() async { + balance = await _fetchBalances(); + await save(); + } +} diff --git a/lib/bitcoin/electrum_wallet_snapshot.dart b/lib/bitcoin/electrum_wallet_snapshot.dart new file mode 100644 index 000000000..9347157ad --- /dev/null +++ b/lib/bitcoin/electrum_wallet_snapshot.dart @@ -0,0 +1,42 @@ +import 'dart:convert'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/file.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; + +class ElectrumWallletSnapshot { + ElectrumWallletSnapshot(this.name, this.type, this.password); + + final String name; + final String password; + final WalletType type; + + String mnemonic; + List addresses; + ElectrumBalance balance; + int accountIndex; + + Future load() async { + try { + final path = await pathForWallet(name: name, type: type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final addressesTmp = data['addresses'] as List ?? []; + mnemonic = data['mnemonic'] as String; + addresses = addressesTmp + .whereType() + .map((addr) => BitcoinAddressRecord.fromJSON(addr)) + .toList(); + balance = ElectrumBalance.fromJSON(data['balance'] as String) ?? + ElectrumBalance(confirmed: 0, unconfirmed: 0); + accountIndex = 0; + + try { + accountIndex = int.parse(data['account_index'] as String); + } catch (_) {} + } catch (e) { + print(e); + } + } +} diff --git a/lib/bitcoin/litecoin_network.dart b/lib/bitcoin/litecoin_network.dart new file mode 100644 index 000000000..d7ad2f837 --- /dev/null +++ b/lib/bitcoin/litecoin_network.dart @@ -0,0 +1,9 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart'; + +final litecoinNetwork = NetworkType( + messagePrefix: '\x19Litecoin Signed Message:\n', + bech32: 'ltc', + bip32: Bip32Type(public: 0x0488b21e, private: 0x0488ade4), + pubKeyHash: 0x30, + scriptHash: 0x32, + wif: 0xb0); diff --git a/lib/bitcoin/litecoin_wallet.dart b/lib/bitcoin/litecoin_wallet.dart new file mode 100644 index 000000000..209d1904e --- /dev/null +++ b/lib/bitcoin/litecoin_wallet.dart @@ -0,0 +1,88 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/litecoin_network.dart'; +import 'package:cake_wallet/bitcoin/utils.dart'; + +part 'litecoin_wallet.g.dart'; + +class LitecoinWallet = LitecoinWalletBase with _$LitecoinWallet; + +abstract class LitecoinWalletBase extends ElectrumWallet with Store { + LitecoinWalletBase( + {@required String mnemonic, + @required String password, + @required WalletInfo walletInfo, + List initialAddresses, + ElectrumBalance initialBalance, + int accountIndex = 0}) + : super( + mnemonic: mnemonic, + password: password, + walletInfo: walletInfo, + networkType: litecoinNetwork, + initialAddresses: initialAddresses, + initialBalance: initialBalance, + accountIndex: accountIndex); + + static Future open({ + @required String name, + @required WalletInfo walletInfo, + @required String password, + }) async { + final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); + await snp.load(); + return LitecoinWallet( + mnemonic: snp.mnemonic, + password: password, + walletInfo: walletInfo, + initialAddresses: snp.addresses, + initialBalance: snp.balance, + accountIndex: snp.accountIndex); + } + + @override + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + + @override + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 22 - addresses.length; + await generateNewAddresses(addressesCount, + hd: hd, startIndex: addresses.length); + + final changeRoot = bitcoin.HDWallet.fromSeed( + mnemonicToSeedBytes(mnemonic), + network: networkType) + .derivePath("m/0'/1"); + + await generateNewAddresses(11, + startIndex: 0, hd: changeRoot, isHidden: true); + } + } + + @override + int feeRate(TransactionPriority priority) { + if (priority is LitecoinTransactionPriority) { + switch (priority) { + case LitecoinTransactionPriority.slow: + return 1; + case LitecoinTransactionPriority.medium: + return 2; + case LitecoinTransactionPriority.fast: + return 3; + } + } + + return 0; + } +} diff --git a/lib/bitcoin/litecoin_wallet_service.dart b/lib/bitcoin/litecoin_wallet_service.dart new file mode 100644 index 000000000..053fd785f --- /dev/null +++ b/lib/bitcoin/litecoin_wallet_service.dart @@ -0,0 +1,76 @@ +import 'dart:io'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/bitcoin/litecoin_wallet.dart'; +import 'package:cake_wallet/core/wallet_service.dart'; +import 'package:cake_wallet/entities/pathForWallet.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:cake_wallet/core/wallet_base.dart'; + +class LitecoinWalletService extends WalletService< + BitcoinNewWalletCredentials, + BitcoinRestoreWalletFromSeedCredentials, + BitcoinRestoreWalletFromWIFCredentials> { + LitecoinWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + WalletType getType() => WalletType.litecoin; + + @override + Future create(BitcoinNewWalletCredentials credentials) async { + final wallet = LitecoinWallet( + mnemonic: await generateMnemonic(), + password: credentials.password, + walletInfo: credentials.walletInfo); + await wallet.save(); + await wallet.init(); + + return wallet; + } + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values.firstWhere( + (info) => info.id == WalletBase.idFor(name, getType()), + orElse: () => null); + final wallet = await LitecoinWalletBase.open( + password: password, name: name, walletInfo: walletInfo); + await wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async => + File(await pathForWalletDir(name: wallet, type: getType())) + .delete(recursive: true); + + @override + Future restoreFromKeys( + BitcoinRestoreWalletFromWIFCredentials credentials) async => + throw UnimplementedError(); + + @override + Future restoreFromSeed( + BitcoinRestoreWalletFromSeedCredentials credentials) async { + if (!validateMnemonic(credentials.mnemonic)) { + throw BitcoinMnemonicIsIncorrectException(); + } + + final wallet = LitecoinWallet( + password: credentials.password, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo); + await wallet.save(); + await wallet.init(); + return wallet; + } +} diff --git a/lib/bitcoin/pending_bitcoin_transaction.dart b/lib/bitcoin/pending_bitcoin_transaction.dart index edd5a0450..ec3e3a985 100644 --- a/lib/bitcoin/pending_bitcoin_transaction.dart +++ b/lib/bitcoin/pending_bitcoin_transaction.dart @@ -1,18 +1,22 @@ -import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/entities/transaction_direction.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/core/pending_transaction.dart'; import 'package:cake_wallet/bitcoin/electrum.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; +import 'package:cake_wallet/entities/transaction_direction.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; class PendingBitcoinTransaction with PendingTransaction { - PendingBitcoinTransaction(this._tx, - {@required this.eclient, @required this.amount, @required this.fee}) - : _listeners = []; + PendingBitcoinTransaction(this._tx, this.type, + {@required this.electrumClient, + @required this.amount, + @required this.fee}) + : _listeners = []; + final WalletType type; final bitcoin.Transaction _tx; - final ElectrumClient eclient; + final ElectrumClient electrumClient; final int amount; final int fee; @@ -25,24 +29,25 @@ class PendingBitcoinTransaction with PendingTransaction { @override String get feeFormatted => bitcoinAmountToString(amount: fee); - final List _listeners; + final List _listeners; @override Future commit() async { - await eclient.broadcastTransaction(transactionRaw: _tx.toHex()); + await electrumClient.broadcastTransaction(transactionRaw: _tx.toHex()); _listeners?.forEach((listener) => listener(transactionInfo())); } void addListener( - void Function(BitcoinTransactionInfo transaction) listener) => + void Function(ElectrumTransactionInfo transaction) listener) => _listeners.add(listener); - BitcoinTransactionInfo transactionInfo() => BitcoinTransactionInfo( + ElectrumTransactionInfo transactionInfo() => ElectrumTransactionInfo(type, id: id, height: 0, amount: amount, direction: TransactionDirection.outgoing, date: DateTime.now(), isPending: true, - confirmations: 0); + confirmations: 0, + fee: fee); } diff --git a/lib/bitcoin/script_hash.dart b/lib/bitcoin/script_hash.dart index b252a0700..b1025f66b 100644 --- a/lib/bitcoin/script_hash.dart +++ b/lib/bitcoin/script_hash.dart @@ -1,18 +1,20 @@ +import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:crypto/crypto.dart'; -String scriptHash(String address) { - final outputScript = bitcoin.Address.addressToOutputScript(address); - final splitted = sha256.convert(outputScript).toString().split(''); +String scriptHash(String address, {@required bitcoin.NetworkType networkType}) { + final outputScript = + bitcoin.Address.addressToOutputScript(address, networkType); + final parts = sha256.convert(outputScript).toString().split(''); var res = ''; - for (var i = splitted.length - 1; i >= 0; i--) { - final char = splitted[i]; + for (var i = parts.length - 1; i >= 0; i--) { + final char = parts[i]; i--; - final nextChar = splitted[i]; + final nextChar = parts[i]; res += nextChar; res += char; } return res; -} \ No newline at end of file +} diff --git a/lib/bitcoin/utils.dart b/lib/bitcoin/utils.dart index 257c8b176..3a638555a 100644 --- a/lib/bitcoin/utils.dart +++ b/lib/bitcoin/utils.dart @@ -13,14 +13,43 @@ bitcoin.ECPair generateKeyPair( {@required bitcoin.HDWallet hd, @required int index, bitcoin.NetworkType network}) => - bitcoin.ECPair.fromWIF(hd.derive(index).wif, - network: network ?? bitcoin.bitcoin); + bitcoin.ECPair.fromWIF(hd.derive(index).wif, network: network); -String generateAddress({@required bitcoin.HDWallet hd, @required int index}) => +String generateP2WPKHAddress( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType networkType}) => bitcoin .P2WPKH( data: PaymentData( pubkey: - Uint8List.fromList(HEX.decode(hd.derive(index).pubKey)))) + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + network: networkType) + .data + .address; + +String generateP2WPKHAddressByPath( + {@required bitcoin.HDWallet hd, + @required String path, + bitcoin.NetworkType networkType}) => + bitcoin + .P2WPKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derivePath(path).pubKey))), + network: networkType) + .data + .address; + +String generateP2PKHAddress( + {@required bitcoin.HDWallet hd, + @required int index, + bitcoin.NetworkType networkType}) => + bitcoin + .P2PKH( + data: PaymentData( + pubkey: + Uint8List.fromList(HEX.decode(hd.derive(index).pubKey))), + network: networkType) .data .address; diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 273a6ca14..f481d0b1d 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -72,7 +72,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.eth: return [42]; case CryptoCurrency.ltc: - return [34]; + return [34, 43]; case CryptoCurrency.nano: return [64, 65]; case CryptoCurrency.trx: diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index fa984f3dd..09edc6c1f 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -8,7 +8,6 @@ import 'package:path_provider/path_provider.dart'; import 'package:cryptography/cryptography.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:archive/archive_io.dart'; -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/key_service.dart'; import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 1744f3df8..9bda4e7ad 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -3,7 +3,6 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'dart:convert'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; -import 'package:cake_wallet/entities/currency_formatter.dart'; const fiatApiAuthority = 'fiat-api.cakewallet.com'; const fiatApiPath = '/v1/rates'; @@ -15,9 +14,8 @@ Future _fetchPrice(Map args) async { try { final fiatStringified = fiat.toString(); - final uri = - Uri.https(fiatApiAuthority, fiatApiPath, - {'convert': fiatStringified}); + final uri = Uri.https(fiatApiAuthority, fiatApiPath, + {'convert': fiatStringified}); final response = await get(uri.toString()); if (response.statusCode != 200) { @@ -28,7 +26,7 @@ Future _fetchPrice(Map args) async { final data = responseJSON['data'] as List; for (final item in data) { - if (item['symbol'] == cryptoToString(crypto)) { + if (item['symbol'] == crypto.title) { price = item['quote'][fiatStringified]['price'] as double; break; } @@ -45,6 +43,7 @@ Future _fetchPriceAsync( compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto}); class FiatConversionService { - static Future fetchPrice(CryptoCurrency crypto, FiatCurrency fiat) async => + static Future fetchPrice( + CryptoCurrency crypto, FiatCurrency fiat) async => await _fetchPriceAsync(crypto, fiat); } diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart index abf2e6dac..9f126d8c2 100644 --- a/lib/core/generate_wallet_password.dart +++ b/lib/core/generate_wallet_password.dart @@ -4,9 +4,9 @@ import 'package:cake_wallet/entities/wallet_type.dart'; String generateWalletPassword(WalletType type) { switch (type) { - case WalletType.bitcoin: - return generateKey(); - default: + case WalletType.monero: return Uuid().v4(); + default: + return generateKey(); } } diff --git a/lib/core/node_port_validator.dart b/lib/core/node_port_validator.dart index 16be00dde..89d4ec1da 100644 --- a/lib/core/node_port_validator.dart +++ b/lib/core/node_port_validator.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; diff --git a/lib/core/sec_random_native.dart b/lib/core/sec_random_native.dart new file mode 100644 index 000000000..b9800fd71 --- /dev/null +++ b/lib/core/sec_random_native.dart @@ -0,0 +1,13 @@ +import 'dart:typed_data'; + +import 'package:flutter/services.dart'; + +const utils = const MethodChannel('com.cake_wallet/native_utils'); + +Future secRandom(int count) async { + try { + return await utils.invokeMethod('sec_random', {'count': count}); + } on PlatformException catch (_) { + return Uint8List.fromList([]); + } +} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 94ada7481..fd2e4851b 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -24,6 +24,8 @@ class SeedValidator extends Validator { switch (type) { case WalletType.bitcoin: return getBitcoinWordList(language); + case WalletType.litecoin: + return getBitcoinWordList(language); case WalletType.monero: return getMoneroWordList(language); default: diff --git a/lib/core/transaction_history.dart b/lib/core/transaction_history.dart index dd91fb203..ee386e392 100644 --- a/lib/core/transaction_history.dart +++ b/lib/core/transaction_history.dart @@ -3,43 +3,50 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; abstract class TransactionHistoryBase { - TransactionHistoryBase() : _isUpdating = false; + TransactionHistoryBase(); + // : _isUpdating = false; @observable ObservableMap transactions; - bool _isUpdating; + Future save(); - @action - Future update() async { - if (_isUpdating) { - return; - } + void addOne(TransactionType transaction); - try { - _isUpdating = true; - final _transactions = await fetchTransactions(); - transactions.keys - .toSet() - .difference(_transactions.keys.toSet()) - .forEach((k) => transactions.remove(k)); - _transactions.forEach((key, value) => transactions[key] = value); - _isUpdating = false; - } catch (e) { - _isUpdating = false; - rethrow; - } - } + void addMany(Map transactions); - void updateAsync({void Function() onFinished}) { - fetchTransactionsAsync( - (transaction) => transactions[transaction.id] = transaction, - onFinished: onFinished); - } + // bool _isUpdating; - void fetchTransactionsAsync( - void Function(TransactionType transaction) onTransactionLoaded, - {void Function() onFinished}); + // @action + // Future update() async { + // if (_isUpdating) { + // return; + // } - Future> fetchTransactions(); + // try { + // _isUpdating = true; + // final _transactions = await fetchTransactions(); + // transactions.keys + // .toSet() + // .difference(_transactions.keys.toSet()) + // .forEach((k) => transactions.remove(k)); + // _transactions.forEach((key, value) => transactions[key] = value); + // _isUpdating = false; + // } catch (e) { + // _isUpdating = false; + // rethrow; + // } + // } + + // void updateAsync({void Function() onFinished}) { + // fetchTransactionsAsync( + // (transaction) => transactions[transaction.id] = transaction, + // onFinished: onFinished); + // } + + // void fetchTransactionsAsync( + // void Function(TransactionType transaction) onTransactionLoaded, + // {void Function() onFinished}); + + // Future> fetchTransactions(); } diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index 8369ed113..ced918342 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; @@ -11,7 +12,10 @@ import 'package:cake_wallet/entities/sync_status.dart'; import 'package:cake_wallet/entities/node.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; -abstract class WalletBase { +abstract class WalletBase< + BalanceType extends Balance, + HistoryType extends TransactionHistoryBase, + TransactionType extends TransactionInfo> { WalletBase(this.walletInfo); static String idFor(String name, WalletType type) => @@ -41,7 +45,7 @@ abstract class WalletBase { Object get keys; - TransactionHistoryBase transactionHistory; + HistoryType transactionHistory; Future connectToNode({@required Node node}); @@ -51,6 +55,12 @@ abstract class WalletBase { int calculateEstimatedFee(TransactionPriority priority, int amount); + // void fetchTransactionsAsync( + // void Function(TransactionType transaction) onTransactionLoaded, + // {void Function() onFinished}); + + Future> fetchTransactions(); + Future save(); Future rescan({int height}); diff --git a/lib/core/wallet_service.dart b/lib/core/wallet_service.dart index f26fc10c2..2d207c31e 100644 --- a/lib/core/wallet_service.dart +++ b/lib/core/wallet_service.dart @@ -1,8 +1,11 @@ import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_credentials.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; abstract class WalletService { + WalletType getType(); + Future create(N credentials); Future restoreFromSeed(RFS credentials); diff --git a/lib/di.dart b/lib/di.dart index 66ac22231..cae3a068f 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; @@ -443,6 +444,8 @@ Future setup( getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource)); + getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource)); + getIt.registerFactoryParam( (WalletType param1, __) { switch (param1) { @@ -450,6 +453,8 @@ Future setup( return getIt.get(); case WalletType.bitcoin: return getIt.get(); + case WalletType.litecoin: + return getIt.get(); default: return null; } @@ -533,8 +538,7 @@ Future setup( TradeDetailsPage(getIt.get(param1: trade))); getIt.registerFactory(() { - final wallet = getIt.get().wallet; - return WyreService(wallet: wallet); + return WyreService(appStore: getIt.get()); }); getIt.registerFactory(() { @@ -546,10 +550,9 @@ Future setup( WyrePage(getIt.get(), ordersStore: getIt.get(), url: url)); - getIt.registerFactoryParam( - (order, _) => OrderDetailsViewModel( - wyreViewModel: getIt.get(), - orderForDetails: order)); + getIt.registerFactoryParam((order, _) => + OrderDetailsViewModel( + wyreViewModel: getIt.get(), orderForDetails: order)); getIt.registerFactoryParam((Order order, _) => OrderDetailsPage(getIt.get(param1: order))); diff --git a/lib/entities/calculate_fiat_amount.dart b/lib/entities/calculate_fiat_amount.dart index 6302c23a0..407030d7f 100644 --- a/lib/entities/calculate_fiat_amount.dart +++ b/lib/entities/calculate_fiat_amount.dart @@ -4,11 +4,12 @@ String calculateFiatAmount({double price, String cryptoAmount}) { } final _amount = double.parse(cryptoAmount); - final result = price * _amount; + final _result = price * _amount; + final result = _result < 0 ? _result * -1 : _result; if (result == 0.0) { return '0.00'; } return result > 0.01 ? result.toStringAsFixed(2) : '< 0.01'; -} \ No newline at end of file +} diff --git a/lib/entities/crypto_currency.dart b/lib/entities/crypto_currency.dart index d5f23a0fa..7f27da7d6 100644 --- a/lib/entities/crypto_currency.dart +++ b/lib/entities/crypto_currency.dart @@ -19,7 +19,6 @@ class CryptoCurrency extends EnumerableItem with Serializable { CryptoCurrency.eos, CryptoCurrency.eth, CryptoCurrency.ltc, - CryptoCurrency.nano, CryptoCurrency.trx, CryptoCurrency.usdt, CryptoCurrency.usdterc20, @@ -29,7 +28,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { static const xmr = CryptoCurrency(title: 'XMR', raw: 0); static const ada = CryptoCurrency(title: 'ADA', raw: 1); static const bch = CryptoCurrency(title: 'BCH', raw: 2); - static const bnb = CryptoCurrency(title: 'BNB', raw: 3); + static const bnb = CryptoCurrency(title: 'BNB BEP2', raw: 3); static const btc = CryptoCurrency(title: 'BTC', raw: 4); static const dai = CryptoCurrency(title: 'DAI', raw: 5); static const dash = CryptoCurrency(title: 'DASH', raw: 6); @@ -90,7 +89,7 @@ class CryptoCurrency extends EnumerableItem with Serializable { return CryptoCurrency.ada; case 'bch': return CryptoCurrency.bch; - case 'bnb': + case 'bnbmainnet': return CryptoCurrency.bnb; case 'btc': return CryptoCurrency.btc; diff --git a/lib/entities/currency_for_wallet_type.dart b/lib/entities/currency_for_wallet_type.dart index 840e75dd1..7f13d12b0 100644 --- a/lib/entities/currency_for_wallet_type.dart +++ b/lib/entities/currency_for_wallet_type.dart @@ -7,7 +7,9 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.btc; case WalletType.monero: return CryptoCurrency.xmr; + case WalletType.litecoin: + return CryptoCurrency.ltc; default: return null; } -} \ No newline at end of file +} diff --git a/lib/entities/currency_formatter.dart b/lib/entities/currency_formatter.dart deleted file mode 100644 index 40326a7e6..000000000 --- a/lib/entities/currency_formatter.dart +++ /dev/null @@ -1,12 +0,0 @@ -import 'package:cake_wallet/entities/crypto_currency.dart'; - -String cryptoToString(CryptoCurrency crypto) { - switch (crypto) { - case CryptoCurrency.xmr: - return 'XMR'; - case CryptoCurrency.btc: - return 'BTC'; - default: - return ''; - } -} \ No newline at end of file diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 1d8dd37f9..51aa5b49a 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,11 +1,7 @@ import 'dart:io' show File, Platform; import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/core/generate_wallet_password.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; -import 'package:cake_wallet/monero/monero_wallet_service.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; @@ -24,7 +20,8 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:encrypt/encrypt.dart' as encrypt; const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; -const cakeWalletElectrumUri = 'electrum.cakewallet.com:50002'; +const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; +const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; Future defaultSettingsMigration( {@required int version, @@ -68,6 +65,8 @@ Future defaultSettingsMigration( sharedPreferences: sharedPreferences, nodes: nodes); await changeBitcoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); + await changeLitecoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); break; case 2: @@ -97,6 +96,7 @@ Future defaultSettingsMigration( case 9: await generateBackupPassword(secureStorage); break; + case 10: await changeTransactionPriorityAndFeeRateKeys(sharedPreferences); break; @@ -110,7 +110,14 @@ Future defaultSettingsMigration( break; case 13: - await resetElectrumServer(nodes, sharedPreferences); + await resetBitcoinElectrumServer(nodes, sharedPreferences); + break; + + case 15: + await addLitecoinElectrumServerList(nodes: nodes); + await changeLitecoinCurrentElectrumServerToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + await checkCurrentNodes(nodes, sharedPreferences); break; default: @@ -142,7 +149,7 @@ Future replaceNodesMigration({@required Box nodes}) async { final nodeToReplace = replaceNodes[node.uri]; if (nodeToReplace != null) { - node.uri = nodeToReplace.uri; + node.uriRaw = nodeToReplace.uriRaw; node.login = nodeToReplace.login; node.password = nodeToReplace.password; await node.save(); @@ -160,12 +167,21 @@ Future changeMoneroCurrentNodeToDefault( } Node getBitcoinDefaultElectrumServer({@required Box nodes}) { - return nodes.values - .firstWhere((Node node) => node.uri == cakeWalletElectrumUri, orElse: () => null) ?? + return nodes.values.firstWhere( + (Node node) => node.uri == cakeWalletBitcoinElectrumUri, + orElse: () => null) ?? nodes.values.firstWhere((node) => node.type == WalletType.bitcoin, orElse: () => null); } +Node getLitecoinDefaultElectrumServer({@required Box nodes}) { + return nodes.values.firstWhere( + (Node node) => node.uri == cakeWalletLitecoinElectrumUri, + orElse: () => null) ?? + nodes.values.firstWhere((node) => node.type == WalletType.litecoin, + orElse: () => null); +} + Node getMoneroDefaultNode({@required Box nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -192,6 +208,15 @@ Future changeBitcoinCurrentElectrumServerToDefault( await sharedPreferences.setInt('current_node_id_btc', serverId); } +Future changeLitecoinCurrentElectrumServerToDefault( + {@required SharedPreferences sharedPreferences, + @required Box nodes}) async { + final server = getLitecoinDefaultElectrumServer(nodes: nodes); + final serverId = server?.key as int ?? 0; + + await sharedPreferences.setInt('current_node_id_ltc', serverId); +} + Future replaceDefaultNode( {@required SharedPreferences sharedPreferences, @required Box nodes}) async { @@ -224,7 +249,12 @@ Future updateNodeTypes({@required Box nodes}) async { } Future addBitcoinElectrumServerList({@required Box nodes}) async { - final serverList = await loadElectrumServerList(); + final serverList = await loadBitcoinElectrumServerList(); + await nodes.addAll(serverList); +} + +Future addLitecoinElectrumServerList({@required Box nodes}) async { + final serverList = await loadLitecoinElectrumServerList(); await nodes.addAll(serverList); } @@ -284,57 +314,97 @@ Future changeTransactionPriorityAndFeeRateKeys( Future changeDefaultMoneroNode( Box nodeSource, SharedPreferences sharedPreferences) async { const cakeWalletMoneroNodeUriPattern = '.cakewallet.com'; - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = currentMoneroNode.uri.contains(cakeWalletMoneroNodeUriPattern); + final currentMoneroNodeId = + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentMoneroNode = + nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); + final needToReplaceCurrentMoneroNode = + currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && node.uri.contains(cakeWalletMoneroNodeUriPattern)) { + if (node.type == WalletType.monero && + node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { await node.delete(); } }); - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); if (needToReplaceCurrentMoneroNode) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); } } -Future checkCurrentNodes(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentElectrumSeverId = await sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId, orElse: () => null); - final currentElectrumServer = nodeSource.values.firstWhere((node) => node.key == currentElectrumSeverId, orElse: () => null); +Future checkCurrentNodes( + Box nodeSource, SharedPreferences sharedPreferences) async { + final currentMoneroNodeId = + sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); + final currentBitcoinElectrumSeverId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final currentLitecoinElectrumSeverId = sharedPreferences + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final currentMoneroNode = nodeSource.values.firstWhere( + (node) => node.key == currentMoneroNodeId, + orElse: () => null); + final currentBitcoinElectrumServer = nodeSource.values.firstWhere( + (node) => node.key == currentBitcoinElectrumSeverId, + orElse: () => null); + final currentLitecoinElectrumServer = nodeSource.values.firstWhere( + (node) => node.key == currentLitecoinElectrumSeverId, + orElse: () => null); if (currentMoneroNode == null) { - final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); + final newCakeWalletNode = + Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); } - if (currentElectrumServer == null) { - final cakeWalletElectrum = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin); + if (currentBitcoinElectrumServer == null) { + final cakeWalletElectrum = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); await nodeSource.add(cakeWalletElectrum); - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletElectrum.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + cakeWalletElectrum.key as int); + } + + if (currentLitecoinElectrumServer == null) { + final cakeWalletElectrum = + Node(uri: cakeWalletLitecoinElectrumUri, type: WalletType.litecoin); + await nodeSource.add(cakeWalletElectrum); + await sharedPreferences.setInt( + PreferencesKey.currentLitecoinElectrumSererIdKey, + cakeWalletElectrum.key as int); } } - -Future resetElectrumServer(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final oldElectrumServer = nodeSource.values.firstWhere((node) => node.uri.contains('electrumx.cakewallet.com'), orElse: () => null); - var cakeWalletNode = nodeSource.values.firstWhere((node) => node.uri == cakeWalletElectrumUri, orElse: () => null); +Future resetBitcoinElectrumServer( + Box nodeSource, SharedPreferences sharedPreferences) async { + final currentElectrumSeverId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final oldElectrumServer = nodeSource.values.firstWhere( + (node) => node.uri.toString().contains('electrumx.cakewallet.com'), + orElse: () => null); + var cakeWalletNode = nodeSource.values.firstWhere( + (node) => node.uri.toString() == cakeWalletBitcoinElectrumUri, + orElse: () => null); if (cakeWalletNode == null) { - cakeWalletNode = Node(uri: cakeWalletElectrumUri, type: WalletType.bitcoin); + cakeWalletNode = + Node(uri: cakeWalletBitcoinElectrumUri, type: WalletType.bitcoin); await nodeSource.add(cakeWalletNode); } if (currentElectrumSeverId == oldElectrumServer?.key) { - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, cakeWalletNode.key as int); + await sharedPreferences.setInt( + PreferencesKey.currentBitcoinElectrumSererIdKey, + cakeWalletNode.key as int); } await oldElectrumServer?.delete(); diff --git a/lib/entities/fs_migration.dart b/lib/entities/fs_migration.dart index 3e7513117..efaefdd64 100644 --- a/lib/entities/fs_migration.dart +++ b/lib/entities/fs_migration.dart @@ -1,7 +1,10 @@ import 'dart:io'; import 'dart:convert'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:hive/hive.dart'; +import 'package:path_provider/path_provider.dart'; import 'package:cake_wallet/core/key_service.dart'; -import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/contact.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; import 'package:cake_wallet/entities/encrypt.dart'; @@ -14,11 +17,6 @@ import 'package:cake_wallet/entities/wallet_info.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; import 'package:cake_wallet/exchange/trade.dart'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:flutter/foundation.dart'; -import 'package:hive/hive.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; const reservedNames = ["flutter_assets", "wallets", "db"]; @@ -407,7 +405,7 @@ Future ios_migrate_address_book(Box contactSource) async { } final List addresses = - json.decode(addressBookJSON.readAsStringSync()) as List; + json.decode(addressBookJSON.readAsStringSync()) as List; final contacts = addresses.map((dynamic item) { final _item = item as Map; final type = _item["type"] as String; @@ -420,7 +418,7 @@ Future ios_migrate_address_book(Box contactSource) async { await contactSource.addAll(contacts); await prefs.setBool('ios_migration_address_book_completed', true); - } catch(e) { + } catch (e) { print(e.toString()); } } diff --git a/lib/entities/language_service.dart b/lib/entities/language_service.dart index e405522c0..33e5ca5be 100644 --- a/lib/entities/language_service.dart +++ b/lib/entities/language_service.dart @@ -15,7 +15,9 @@ class LanguageService { 'pt': 'Português (Portuguese)', 'ru': 'Русский (Russian)', 'uk': 'Українська (Ukrainian)', - 'zh': '中文 (Chinese)' + 'zh': '中文 (Chinese)', + 'hr': 'Hrvatski (Croatian)', + 'it': 'Italiano (Italian)' }; static final list = {}; diff --git a/lib/entities/node.dart b/lib/entities/node.dart index 2636f9601..c1aa19ed3 100644 --- a/lib/entities/node.dart +++ b/lib/entities/node.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:cake_wallet/utils/mobx.dart'; import 'package:flutter/foundation.dart'; import 'dart:convert'; @@ -8,19 +10,23 @@ import 'package:cake_wallet/entities/digest_request.dart'; part 'node.g.dart'; +Uri createUriFromElectrumAddress(String address) => + Uri.tryParse('tcp://$address'); + @HiveType(typeId: Node.typeId) class Node extends HiveObject with Keyable { Node( - {@required this.uri, + {@required String uri, @required WalletType type, this.login, this.password, this.useSSL}) { + uriRaw = uri; this.type = type; } Node.fromMap(Map map) - : uri = map['uri'] as String ?? '', + : uriRaw = map['uri'] as String ?? '', login = map['login'] as String, password = map['password'] as String, typeRaw = map['typeRaw'] as int, @@ -30,7 +36,7 @@ class Node extends HiveObject with Keyable { static const boxName = 'Nodes'; @HiveField(0) - String uri; + String uriRaw; @HiveField(1) String login; @@ -46,6 +52,19 @@ class Node extends HiveObject with Keyable { bool get isSSL => useSSL ?? false; + Uri get uri { + switch (type) { + case WalletType.monero: + return Uri.http(uriRaw, ''); + case WalletType.bitcoin: + return createUriFromElectrumAddress(uriRaw); + case WalletType.litecoin: + return createUriFromElectrumAddress(uriRaw); + default: + return null; + } + } + @override dynamic get keyIndex { _keyIndex ??= key; @@ -64,7 +83,9 @@ class Node extends HiveObject with Keyable { case WalletType.monero: return requestMoneroNode(); case WalletType.bitcoin: - return requestBitcoinElectrumServer(); + return requestElectrumServer(); + case WalletType.litecoin: + return requestElectrumServer(); default: return false; } @@ -80,15 +101,15 @@ class Node extends HiveObject with Keyable { if (login != null && password != null) { final digestRequest = DigestRequest(); final response = await digestRequest.request( - uri: uri, login: login, password: password); + uri: uri.toString(), login: login, password: password); resBody = response.data as Map; } else { - final url = Uri.http(uri, '/json_rpc'); + final rpcUri = Uri.http(uri.authority, '/json_rpc'); final headers = {'Content-type': 'application/json'}; final body = json.encode({'jsonrpc': '2.0', 'id': '0', 'method': 'get_info'}); final response = - await http.post(url.toString(), headers: headers, body: body); + await http.post(rpcUri.toString(), headers: headers, body: body); resBody = json.decode(response.body) as Map; } @@ -98,8 +119,13 @@ class Node extends HiveObject with Keyable { } } - Future requestBitcoinElectrumServer() async { - // FIXME: IMPLEMENT ME - return true; + Future requestElectrumServer() async { + try { + await SecureSocket.connect(uri.host, uri.port, + timeout: Duration(seconds: 5), onBadCertificate: (_) => true); + return true; + } catch (_) { + return false; + } } } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 118394e6f..a35ed47d4 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -20,9 +20,9 @@ Future> loadDefaultNodes() async { }).toList(); } -Future> loadElectrumServerList() async { +Future> loadBitcoinElectrumServerList() async { final serverListRaw = - await rootBundle.loadString('assets/electrum_server_list.yml'); + await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml'); final serverList = loadYaml(serverListRaw) as YamlList; return serverList.map((dynamic raw) { @@ -37,10 +37,29 @@ Future> loadElectrumServerList() async { }).toList(); } +Future> loadLitecoinElectrumServerList() async { + final serverListRaw = + await rootBundle.loadString('assets/litecoin_electrum_server_list.yml'); + final serverList = loadYaml(serverListRaw) as YamlList; + + return serverList.map((dynamic raw) { + if (raw is Map) { + final node = Node.fromMap(raw); + node?.type = WalletType.litecoin; + + return node; + } + + return null; + }).toList(); +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); - final bitcoinElectrumServerList = await loadElectrumServerList(); - final nodes = moneroNodes + bitcoinElectrumServerList; + final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); + final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); + final nodes = + moneroNodes + bitcoinElectrumServerList + litecoinElectrumServerList; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 15dbc2fb8..6d55748cc 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -1,8 +1,9 @@ class PreferencesKey { - static const currentWalletType ='current_wallet_type'; - static const currentWalletName ='current_wallet_name'; + static const currentWalletType = 'current_wallet_type'; + static const currentWalletName = 'current_wallet_name'; static const currentNodeIdKey = 'current_node_id'; static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; + static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -14,7 +15,8 @@ class PreferencesKey { static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; - static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version'; + static const currentDefaultSettingsMigrationVersion = + 'current_default_settings_migration_version'; static const moneroTransactionPriority = 'current_fee_priority_monero'; static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; -} \ No newline at end of file +} diff --git a/lib/entities/wallet_type.dart b/lib/entities/wallet_type.dart index f4375e43e..d59f336c0 100644 --- a/lib/entities/wallet_type.dart +++ b/lib/entities/wallet_type.dart @@ -3,7 +3,11 @@ import 'package:hive/hive.dart'; part 'wallet_type.g.dart'; -const walletTypes = [WalletType.monero, WalletType.bitcoin]; +const walletTypes = [ + WalletType.monero, + WalletType.bitcoin, + WalletType.litecoin +]; const walletTypeTypeId = 5; @HiveType(typeId: walletTypeTypeId) @@ -15,7 +19,10 @@ enum WalletType { none, @HiveField(2) - bitcoin + bitcoin, + + @HiveField(3) + litecoin } int serializeToInt(WalletType type) { @@ -24,6 +31,8 @@ int serializeToInt(WalletType type) { return 0; case WalletType.bitcoin: return 1; + case WalletType.litecoin: + return 2; default: return -1; } @@ -35,6 +44,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.monero; case 1: return WalletType.bitcoin; + case 2: + return WalletType.litecoin; default: return null; } @@ -46,6 +57,8 @@ String walletTypeToString(WalletType type) { return 'Monero'; case WalletType.bitcoin: return 'Bitcoin'; + case WalletType.litecoin: + return 'Litecoin'; default: return ''; } @@ -57,6 +70,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Monero'; case WalletType.bitcoin: return 'Bitcoin (Electrum)'; + case WalletType.litecoin: + return 'Litecoin'; default: return ''; } @@ -68,6 +83,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.xmr; case WalletType.bitcoin: return CryptoCurrency.btc; + case WalletType.litecoin: + return CryptoCurrency.ltc; default: return null; } diff --git a/lib/entities/wyre_service.dart b/lib/entities/wyre_service.dart index b83bfce68..63346f0e4 100644 --- a/lib/entities/wyre_service.dart +++ b/lib/entities/wyre_service.dart @@ -1,7 +1,7 @@ import 'dart:convert'; -import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/wyre_exception.dart'; import 'package:cake_wallet/exchange/trade_state.dart'; +import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -9,15 +9,9 @@ import 'package:cake_wallet/entities/order.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; class WyreService { - WyreService({ - @required this.wallet, - this.isTestEnvironment = false}) { - baseApiUrl = isTestEnvironment - ? _baseTestApiUrl - : _baseProductApiUrl; - trackUrl = isTestEnvironment - ? _trackTestUrl - : _trackProductUrl; + WyreService({@required this.appStore, this.isTestEnvironment = false}) { + baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl; + trackUrl = isTestEnvironment ? _trackTestUrl : _trackProductUrl; } static const _baseTestApiUrl = 'https://api.testwyre.com'; @@ -31,24 +25,28 @@ class WyreService { static const _trackSuffix = '/track'; final bool isTestEnvironment; - final WalletBase wallet; + final AppStore appStore; - WalletType get walletType => wallet.type; - String get walletAddress => wallet.address; - String get walletId => wallet.id; + WalletType get walletType => appStore.wallet.type; + String get walletAddress => appStore.wallet.address; + String get walletId => appStore.wallet.id; String baseApiUrl; String trackUrl; Future getWyreUrl() async { final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final url = baseApiUrl + _ordersSuffix + _reserveSuffix + - _timeStampSuffix + timestamp; + final url = baseApiUrl + + _ordersSuffix + + _reserveSuffix + + _timeStampSuffix + + timestamp; final secretKey = secrets.wyreSecretKey; final accountId = secrets.wyreAccountId; final body = { 'destCurrency': walletTypeToCryptoCurrency(walletType).title, - 'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, + 'dest': + walletTypeToString(walletType).toLowerCase() + ':' + walletAddress, 'referrerAccountId': accountId, 'lockFields': ['destCurrency', 'dest'] }; @@ -79,7 +77,7 @@ class WyreService { } final orderResponseJSON = - json.decode(orderResponse.body) as Map; + json.decode(orderResponse.body) as Map; final transferId = orderResponseJSON['transferId'] as String; final from = orderResponseJSON['sourceCurrency'] as String; final to = orderResponseJSON['destCurrency'] as String; @@ -87,7 +85,7 @@ class WyreService { final state = TradeState.deserialize(raw: status.toLowerCase()); final createdAtRaw = orderResponseJSON['createdAt'] as int; final createdAt = - DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); + DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal(); final transferUrl = baseApiUrl + _transferSuffix + transferId + _trackSuffix; @@ -98,7 +96,7 @@ class WyreService { } final transferResponseJSON = - json.decode(transferResponse.body) as Map; + json.decode(transferResponse.body) as Map; final amount = transferResponseJSON['destAmount'] as double; return Order( @@ -110,7 +108,6 @@ class WyreService { createdAt: createdAt, amount: amount.toString(), receiveAddress: walletAddress, - walletId: walletId - ); + walletId: walletId); } -} \ No newline at end of file +} diff --git a/lib/exchange/changenow/changenow_exchange_provider.dart b/lib/exchange/changenow/changenow_exchange_provider.dart index 69674131d..0adaf6c83 100644 --- a/lib/exchange/changenow/changenow_exchange_provider.dart +++ b/lib/exchange/changenow/changenow_exchange_provider.dart @@ -48,7 +48,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { @override Future fetchLimits({CryptoCurrency from, CryptoCurrency to, bool isFixedRateMode}) async { - final symbol = from.toString() + '_' + to.toString(); + final fromTitle = defineCurrencyTitle(from); + final toTitle = defineCurrencyTitle(to); + final symbol = fromTitle + '_' + toTitle; final url = isFixedRateMode ? apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey : apiUri + _minAmountUriSufix + symbol; @@ -61,8 +63,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final elemFrom = elem["from"] as String; final elemTo = elem["to"] as String; - if ((elemFrom == from.toString().toLowerCase()) && - (elemTo == to.toString().toLowerCase())) { + if ((elemFrom == fromTitle) && (elemTo == toTitle)) { final min = elem["min"] as double; final max = elem["max"] as double; @@ -84,9 +85,11 @@ class ChangeNowExchangeProvider extends ExchangeProvider { ? apiUri + _transactionsUriSufix + _fixedRateUriSufix + apiKey : apiUri + _transactionsUriSufix + apiKey; final _request = request as ChangeNowRequest; + final fromTitle = defineCurrencyTitle(_request.from); + final toTitle = defineCurrencyTitle(_request.to); final body = { - 'from': _request.from.toString(), - 'to': _request.to.toString(), + 'from': fromTitle, + 'to': toTitle, 'address': _request.address, 'amount': _request.amount, 'refundAddress': _request.refundAddress @@ -182,6 +185,8 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final url = apiUri + _marketInfoUriSufix + _fixedRateUriSufix + apiKey; final response = await get(url); final responseJSON = json.decode(response.body) as List; + final fromTitle = defineCurrencyTitle(from); + final toTitle = defineCurrencyTitle(to); var rate = 0.0; var fee = 0.0; @@ -189,8 +194,7 @@ class ChangeNowExchangeProvider extends ExchangeProvider { final elemFrom = elem["from"] as String; final elemTo = elem["to"] as String; - if ((elemFrom == to.toString().toLowerCase()) && - (elemTo == from.toString().toLowerCase())) { + if ((elemFrom == toTitle) && (elemTo == fromTitle)) { rate = elem["rate"] as double; fee = elem["minerFee"] as double; break; @@ -216,22 +220,32 @@ class ChangeNowExchangeProvider extends ExchangeProvider { CryptoCurrency to, double amount, bool isFixedRateMode) { + final fromTitle = defineCurrencyTitle(from); + final toTitle = defineCurrencyTitle(to); + return isFixedRateMode ? apiUri + _exchangeAmountUriSufix + _fixedRateUriSufix + amount.toString() + '/' + - from.toString() + + fromTitle + '_' + - to.toString() + + toTitle + '?api_key=' + apiKey : apiUri + _exchangeAmountUriSufix + amount.toString() + '/' + - from.toString() + + fromTitle + '_' + - to.toString(); + toTitle; + } + + static String defineCurrencyTitle(CryptoCurrency currency) { + const bnbTitle = 'bnbmainnet'; + final currencyTitle = currency == CryptoCurrency.bnb + ? bnbTitle : currency.title.toLowerCase(); + return currencyTitle; } } diff --git a/lib/main.dart b/lib/main.dart index 48b54c416..1a1724e3e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -107,7 +107,7 @@ Future main() async { exchangeTemplates: exchangeTemplates, transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, - initialMigrationVersion: 13); + initialMigrationVersion: 15); runApp(App()); } catch (e) { runApp(MaterialApp( @@ -135,7 +135,7 @@ Future initialSetup( @required Box exchangeTemplates, @required Box transactionDescriptions, FlutterSecureStorage secureStorage, - int initialMigrationVersion = 13}) async { + int initialMigrationVersion = 15}) async { LanguageService.loadLocaleList(); await defaultSettingsMigration( secureStorage: secureStorage, diff --git a/lib/monero/monero_account_list.dart b/lib/monero/monero_account_list.dart index 9792d3c1d..9e3b14931 100644 --- a/lib/monero/monero_account_list.dart +++ b/lib/monero/monero_account_list.dart @@ -49,13 +49,13 @@ abstract class MoneroAccountListBase with Store { Future addAccount({String label}) async { await account_list.addAccount(label: label); - await update(); + update(); } Future setLabelAccount({int accountIndex, String label}) async { await account_list.setLabelForAccount( accountIndex: accountIndex, label: label); - await update(); + update(); } void refresh() { diff --git a/lib/monero/monero_transaction_history.dart b/lib/monero/monero_transaction_history.dart index 93c00f376..11eaff4b6 100644 --- a/lib/monero/monero_transaction_history.dart +++ b/lib/monero/monero_transaction_history.dart @@ -1,18 +1,10 @@ import 'dart:core'; import 'package:mobx/mobx.dart'; -import 'package:cw_monero/transaction_history.dart' - as monero_transaction_history; import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; part 'monero_transaction_history.g.dart'; -List _getAllTransactions(dynamic _) => - monero_transaction_history - .getAllTransations() - .map((row) => MoneroTransactionInfo.fromRow(row)) - .toList(); - class MoneroTransactionHistory = MoneroTransactionHistoryBase with _$MoneroTransactionHistory; @@ -23,30 +15,13 @@ abstract class MoneroTransactionHistoryBase } @override - Future> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, MoneroTransactionInfo tx) { - acc[tx.id] = tx; - return acc; - }); - } + Future save() async {} @override - @action - void updateAsync({void Function() onFinished}) { - fetchTransactionsAsync( - (transaction) => transactions[transaction.id] = transaction, - onFinished: onFinished); - } + void addOne(MoneroTransactionInfo transaction) => + transactions[transaction.id] = transaction; @override - void fetchTransactionsAsync( - void Function(MoneroTransactionInfo transaction) onTransactionLoaded, - {void Function() onFinished}) async { - final transactions = await fetchTransactions(); - transactions.values.forEach((tx) => onTransactionLoaded(tx)); - onFinished?.call(); - } + void addMany(Map transactions) => + this.transactions.addAll(transactions); } diff --git a/lib/monero/monero_transaction_info.dart b/lib/monero/monero_transaction_info.dart index 71bc5957a..6f099fbfa 100644 --- a/lib/monero/monero_transaction_info.dart +++ b/lib/monero/monero_transaction_info.dart @@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo { @override void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + @override String feeFormatted() => '${formatAmount(moneroAmountToString(amount: fee))} XMR'; } diff --git a/lib/monero/monero_wallet.dart b/lib/monero/monero_wallet.dart index 0891100c8..92742273c 100644 --- a/lib/monero/monero_wallet.dart +++ b/lib/monero/monero_wallet.dart @@ -1,10 +1,12 @@ import 'dart:async'; - import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:cake_wallet/monero/monero_amount_format.dart'; import 'package:cake_wallet/monero/monero_transaction_creation_exception.dart'; +import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/transaction_history.dart' + as monero_transaction_history; import 'package:cw_monero/wallet.dart'; import 'package:cw_monero/wallet.dart' as monero_wallet; import 'package:cw_monero/transaction_history.dart' as transaction_history; @@ -30,19 +32,21 @@ const moneroBlockSize = 1000; class MoneroWallet = MoneroWalletBase with _$MoneroWallet; -abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({String filename, WalletInfo walletInfo}) - : transactionHistory = MoneroTransactionHistory(), - accountList = MoneroAccountList(), +abstract class MoneroWalletBase extends WalletBase with Store { + MoneroWalletBase({WalletInfo walletInfo}) + : accountList = MoneroAccountList(), subaddressList = MoneroSubaddressList(), super(walletInfo) { - _filename = filename; + transactionHistory = MoneroTransactionHistory(); balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)); _lastAutosaveTimestamp = 0; + _lastSaveTimestamp = 0; _isSavingAfterSync = false; _isSavingAfterNewTransaction = false; + _isTransactionUpdating = false; _onAccountChangeReaction = reaction((_) => account, (Account account) { balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), @@ -56,9 +60,6 @@ abstract class MoneroWalletBase extends WalletBase with Store { static const int _autoAfterSyncSaveInterval = 60000; - @override - final MoneroTransactionHistory transactionHistory; - @observable Account account; @@ -91,12 +92,13 @@ abstract class MoneroWalletBase extends WalletBase with Store { final MoneroAccountList accountList; - String _filename; SyncListener _listener; ReactionDisposer _onAccountChangeReaction; int _lastAutosaveTimestamp; bool _isSavingAfterSync; bool _isSavingAfterNewTransaction; + bool _isTransactionUpdating; + int _lastSaveTimestamp; Future init() async { accountList.update(); @@ -109,7 +111,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { monero_wallet.getUnlockedBalance(accountIndex: account.id)); address = subaddress.address; _setListeners(); - await transactionHistory.update(); + await updateTransactions(); if (walletInfo.isRecovery) { monero_wallet.setRecoveringFromSeed(isRecovery: walletInfo.isRecovery); @@ -150,7 +152,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { try { syncStatus = ConnectingSyncStatus(); await monero_wallet.setupNode( - address: node.uri, + address: node.uri.toString(), login: node.login, password: node.password, useSSL: node.isSSL, @@ -236,6 +238,13 @@ abstract class MoneroWalletBase extends WalletBase with Store { @override Future save() async { + final now = DateTime.now().millisecondsSinceEpoch; + + if (now - _lastSaveTimestamp < Duration(seconds: 10).inMilliseconds) { + return; + } + + _lastSaveTimestamp = now; await monero_wallet.store(); } @@ -262,6 +271,40 @@ abstract class MoneroWalletBase extends WalletBase with Store { await walletInfo.save(); } + @override + Future> fetchTransactions() async { + monero_transaction_history.refreshTransactions(); + return _getAllTransactions(null).fold>( + {}, + (Map acc, MoneroTransactionInfo tx) { + acc[tx.id] = tx; + return acc; + }); + } + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e) { + print(e); + _isTransactionUpdating = false; + } + } + + List _getAllTransactions(dynamic _) => + monero_transaction_history + .getAllTransations() + .map((row) => MoneroTransactionInfo.fromRow(row)) + .toList(); + void _setListeners() { _listener?.stop(); _listener = monero_wallet.setListeners(_onNewBlock, _onNewTransaction); @@ -313,7 +356,7 @@ abstract class MoneroWalletBase extends WalletBase with Store { } Future _askForUpdateTransactionHistory() async => - await transactionHistory.update(); + await updateTransactions(); int _getFullBalance() => monero_wallet.getFullBalance(accountIndex: account.id); @@ -387,11 +430,12 @@ abstract class MoneroWalletBase extends WalletBase with Store { } } - void _onNewTransaction() { + void _onNewTransaction() async { try { - _askForUpdateTransactionHistory(); + await _askForUpdateTransactionHistory(); _askForUpdateBalance(); - Timer(Duration(seconds: 1), () => _afterNewTransactionSave()); + await Future.delayed(Duration(seconds: 1)); + await _afterNewTransactionSave(); } catch (e) { print(e.toString()); } diff --git a/lib/monero/monero_wallet_service.dart b/lib/monero/monero_wallet_service.dart index d964f0232..7795b8700 100644 --- a/lib/monero/monero_wallet_service.dart +++ b/lib/monero/monero_wallet_service.dart @@ -68,18 +68,18 @@ class MoneroWalletService extends WalletService< static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); + @override + WalletType getType() => WalletType.monero; + @override Future create(MoneroNewWalletCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.createWallet( path: path, password: credentials.password, language: credentials.language); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -93,7 +93,7 @@ class MoneroWalletService extends WalletService< @override Future isWalletExit(String name) async { try { - final path = await pathForWallet(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: getType()); return monero_wallet_manager.isWalletExist(path: path); } catch (e) { // TODO: Implement Exception for wallet list service. @@ -105,7 +105,7 @@ class MoneroWalletService extends WalletService< @override Future openWallet(String name, String password) async { try { - final path = await pathForWallet(name: name, type: WalletType.monero); + final path = await pathForWallet(name: name, type: getType()); if (walletFilesExist(path)) { await repairOldAndroidWallet(name); @@ -114,10 +114,9 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( - (info) => info.id == WalletBase.idFor(name, WalletType.monero), + (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), walletInfo: walletInfo); + final wallet = MoneroWallet(walletInfo: walletInfo); final isValid = wallet.validate(); if (!isValid) { @@ -146,7 +145,7 @@ class MoneroWalletService extends WalletService< @override Future remove(String wallet) async { - final path = await pathForWalletDir(name: wallet, type: WalletType.monero); + final path = await pathForWalletDir(name: wallet, type: getType()); final file = Directory(path); final isExist = file.existsSync(); @@ -159,8 +158,7 @@ class MoneroWalletService extends WalletService< Future restoreFromKeys( MoneroRestoreWalletFromKeysCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromKeys( path: path, password: credentials.password, @@ -169,9 +167,7 @@ class MoneroWalletService extends WalletService< address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -186,16 +182,13 @@ class MoneroWalletService extends WalletService< Future restoreFromSeed( MoneroRestoreWalletFromSeedCredentials credentials) async { try { - final path = - await pathForWallet(name: credentials.name, type: WalletType.monero); + final path = await pathForWallet(name: credentials.name, type: getType()); await monero_wallet_manager.restoreFromSeed( path: path, password: credentials.password, seed: credentials.mnemonic, restoreHeight: credentials.height); - final wallet = MoneroWallet( - filename: monero_wallet.getFilename(), - walletInfo: credentials.walletInfo); + final wallet = MoneroWallet(walletInfo: credentials.walletInfo); await wallet.init(); return wallet; @@ -221,14 +214,14 @@ class MoneroWalletService extends WalletService< } final newWalletDirPath = - await pathForWalletDir(name: name, type: WalletType.monero); + await pathForWalletDir(name: name, type: getType()); dir.listSync().forEach((f) { final file = File(f.path); final name = f.path.split('/').last; final newPath = newWalletDirPath + '/$name'; final newFile = File(newPath); - print(file.path); + if (!newFile.existsSync()) { newFile.createSync(); } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index f89943856..b5550fcff 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -7,19 +7,22 @@ import 'package:connectivity/connectivity.dart'; Timer _checkConnectionTimer; -void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore, {int timeInterval = 5}) { +void startCheckConnectionReaction( + WalletBase wallet, SettingsStore settingsStore, + {int timeInterval = 5}) { _checkConnectionTimer?.cancel(); - _checkConnectionTimer = Timer.periodic(Duration(seconds: timeInterval), (_) async { - final connectivityResult = await (Connectivity().checkConnectivity()); + _checkConnectionTimer = + Timer.periodic(Duration(seconds: timeInterval), (_) async { + try { + final connectivityResult = await (Connectivity().checkConnectivity()); - if (connectivityResult == ConnectivityResult.none) { - wallet.syncStatus = FailedSyncStatus(); - return; - } + if (connectivityResult == ConnectivityResult.none) { + wallet.syncStatus = FailedSyncStatus(); + return; + } - if (wallet.syncStatus is LostConnectionSyncStatus || - wallet.syncStatus is FailedSyncStatus) { - try { + if (wallet.syncStatus is LostConnectionSyncStatus || + wallet.syncStatus is FailedSyncStatus) { final alive = await settingsStore.getCurrentNode(wallet.type).requestNode(); @@ -27,9 +30,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore await wallet.connectToNode( node: settingsStore.getCurrentNode(wallet.type)); } - } catch (_) { - // FIXME: empty catch clojure } + } catch (e) { + print(e.toString()); } }); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 17e2ca4f9..428f2703e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,4 +1,6 @@ +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/di.dart'; @@ -18,9 +20,11 @@ ReactionDisposer _onCurrentWalletChangeFiatRateUpdateReaction; void startCurrentWalletChangeReaction(AppStore appStore, SettingsStore settingsStore, FiatConversionStore fiatConversionStore) { _onCurrentWalletChangeReaction?.reaction?.dispose(); + _onCurrentWalletChangeFiatRateUpdateReaction?.reaction?.dispose(); - _onCurrentWalletChangeReaction = - reaction((_) => appStore.wallet, (WalletBase wallet) async { + _onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase< + Balance, TransactionHistoryBase, TransactionInfo> + wallet) async { try { final node = settingsStore.getCurrentNode(wallet.type); startWalletSyncStatusChangeReaction(wallet); @@ -45,7 +49,9 @@ void startCurrentWalletChangeReaction(AppStore appStore, }); _onCurrentWalletChangeFiatRateUpdateReaction = - reaction((_) => appStore.wallet, (WalletBase wallet) async { + reaction((_) => appStore.wallet, (WalletBase, TransactionInfo> + wallet) async { try { fiatConversionStore.prices[wallet.currency] = 0; fiatConversionStore.prices[wallet.currency] = diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 8ad1c2e1b..f02dd0441 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,16 +1,21 @@ -import 'package:cake_wallet/entities/balance.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/sync_status.dart'; ReactionDisposer _onWalletSyncStatusChangeReaction; -void startWalletSyncStatusChangeReaction(WalletBase wallet) { +void startWalletSyncStatusChangeReaction( + WalletBase, + TransactionInfo> + wallet) { _onWalletSyncStatusChangeReaction?.reaction?.dispose(); _onWalletSyncStatusChangeReaction = reaction((_) => wallet.syncStatus, (SyncStatus status) async { - if (status is ConnectedSyncStatus) { - await wallet.startSync(); - } - }); -} \ No newline at end of file + if (status is ConnectedSyncStatus) { + await wallet.startSync(); + } + }); +} diff --git a/lib/router.dart b/lib/router.dart index 0474758e3..19a6b760f 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -237,9 +237,11 @@ Route createRoute(RouteSettings settings) { case Routes.unlock: return MaterialPageRoute( fullscreenDialog: true, - builder: (_) => getIt.get( - param1: settings.arguments as OnAuthenticationFinished, - param2: false)); + builder: (_) => WillPopScope( + child: getIt.get( + param1: settings.arguments as OnAuthenticationFinished, + param2: false), + onWillPop: () async => false)); case Routes.nodeList: return CupertinoPageRoute( diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 172483ed0..46a4185ce 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -14,6 +16,7 @@ import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:mobx/mobx.dart'; import 'package:smooth_page_indicator/smooth_page_indicator.dart'; import 'package:flutter_spinkit/flutter_spinkit.dart'; @@ -24,8 +27,8 @@ class DashboardPage extends BasePage { }); @override - Color get backgroundLightColor => currentTheme.type == ThemeType.bright - ? Colors.transparent : Colors.white; + Color get backgroundLightColor => + currentTheme.type == ThemeType.bright ? Colors.transparent : Colors.white; @override Color get backgroundDarkColor => Colors.transparent; @@ -42,7 +45,7 @@ class DashboardPage extends BasePage { child: scaffold); @override - bool get resizeToAvoidBottomPadding => false; + bool get resizeToAvoidBottomInset => false; @override Widget get endDrawer => MenuWidget(walletViewModel); @@ -54,9 +57,8 @@ class DashboardPage extends BasePage { @override Widget trailing(BuildContext context) { - final menuButton = - Image.asset('assets/images/menu.png', - color: Theme.of(context).accentTextTheme.display3.backgroundColor); + final menuButton = Image.asset('assets/images/menu.png', + color: Theme.of(context).accentTextTheme.display3.backgroundColor); return Container( alignment: Alignment.centerRight, @@ -79,15 +81,18 @@ class DashboardPage extends BasePage { @override Widget body(BuildContext context) { final sendImage = Image.asset('assets/images/upload.png', - height: 22.24, width: 24, + height: 22.24, + width: 24, color: Theme.of(context).accentTextTheme.display3.backgroundColor); final exchangeImage = Image.asset('assets/images/transfer.png', - height: 24.27, width: 22.25, + height: 24.27, + width: 22.25, color: Theme.of(context).accentTextTheme.display3.backgroundColor); final buyImage = Image.asset('assets/images/coins.png', - height: 22.24, width: 24, + height: 22.24, + width: 24, color: Theme.of(context).accentTextTheme.display3.backgroundColor); - _setEffects(); + _setEffects(context); return SafeArea( child: Column( @@ -109,12 +114,14 @@ class DashboardPage extends BasePage { dotWidth: 6.0, dotHeight: 6.0, dotColor: Theme.of(context).indicatorColor, - activeDotColor: Theme.of(context).accentTextTheme.display1 + activeDotColor: Theme.of(context) + .accentTextTheme + .display1 .backgroundColor), )), Container( padding: EdgeInsets.only(left: 45, right: 45, bottom: 24), - child: Observer(builder: (_) => Row( + child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ ActionButton( @@ -125,47 +132,37 @@ class DashboardPage extends BasePage { image: exchangeImage, title: S.of(context).exchange, route: Routes.exchange), - if (walletViewModel.type == WalletType.bitcoin) Observer( + Observer( builder: (_) => Stack( - clipBehavior: Clip.none, - alignment: Alignment.topCenter, - children: [ - if (walletViewModel.isRunningWebView) Positioned( - top: -5, - child: SpinKitRing( - color: Theme.of(context).buttonColor, - lineWidth: 3, - size: 70.0, - ), - ), - ActionButton( - image: buyImage, - title: S.of(context).buy, - onClick: walletViewModel.isRunningWebView - ? null - : () async { - try { - walletViewModel.isRunningWebView = true; - final url = - await walletViewModel.wyreViewModel.wyreUrl; - await Navigator.of(context) - .pushNamed(Routes.wyre, arguments: url); - walletViewModel.isRunningWebView = false; - } catch(e) { - print(e.toString()); - walletViewModel.isRunningWebView = false; - } - }) - ], - )), + clipBehavior: Clip.none, + alignment: Alignment.topCenter, + children: [ + if (walletViewModel.isRunningWebView) + Positioned( + top: -5, + child: SpinKitRing( + color: Theme.of(context).buttonColor, + lineWidth: 3, + size: 70.0, + ), + ), + ActionButton( + image: buyImage, + title: S.of(context).buy, + onClick: walletViewModel.isRunningWebView + ? null + : () async => + await _onClickBuyButton(context)) + ], + )), ], - )), + ), ) ], )); } - void _setEffects() { + void _setEffects(BuildContext context) { if (_isEffectsInstalled) { return; } @@ -174,6 +171,52 @@ class DashboardPage extends BasePage { pages.add(BalancePage(dashboardViewModel: walletViewModel)); pages.add(TransactionsPage(dashboardViewModel: walletViewModel)); + autorun((_) async { + if (!walletViewModel.isOutdatedElectrumWallet) { + return; + } + + await Future.delayed(Duration(seconds: 1)); + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).pre_seed_title, + alertContent: + S.of(context).outdated_electrum_wallet_desceription, + buttonText: S.of(context).understand, + buttonAction: () => Navigator.of(context).pop()); + }); + }); + _isEffectsInstalled = true; } + + Future _onClickBuyButton(BuildContext context) async { + final walletType = walletViewModel.type; + + switch (walletType) { + case WalletType.bitcoin: + try { + walletViewModel.isRunningWebView = true; + final url = await walletViewModel.wyreViewModel.wyreUrl; + await Navigator.of(context).pushNamed(Routes.wyre, arguments: url); + walletViewModel.isRunningWebView = false; + } catch (_) { + walletViewModel.isRunningWebView = false; + } + break; + default: + await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithOneAction( + alertTitle: S.of(context).buy, + alertContent: S.of(context).buy_alert_content, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop()); + }); + break; + } + } } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 5e45152ce..d9e34280a 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -21,6 +21,7 @@ class MenuWidget extends StatefulWidget { class MenuWidgetState extends State { Image moneroIcon; Image bitcoinIcon; + Image litecoinIcon; final largeScreen = 731; double menuWidth; @@ -76,6 +77,7 @@ class MenuWidgetState extends State { color: Theme.of(context).accentTextTheme.overline.decorationColor); bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', color: Theme.of(context).accentTextTheme.overline.decorationColor); + litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); return Row( mainAxisSize: MainAxisSize.max, @@ -238,6 +240,8 @@ class MenuWidgetState extends State { return moneroIcon; case WalletType.bitcoin: return bitcoinIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 3e74c3855..f808a237c 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -59,6 +59,8 @@ class WalletTypeFormState extends State { Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @@ -69,7 +71,7 @@ class WalletTypeFormState extends State { @override void initState() { - types = [WalletType.bitcoin, WalletType.monero]; + types = [WalletType.bitcoin, WalletType.monero, WalletType.litecoin]; super.initState(); } @@ -84,8 +86,7 @@ class WalletTypeFormState extends State { padding: EdgeInsets.only(left: 12, right: 12), child: AspectRatio( aspectRatio: aspectRatioImage, - child: - FittedBox(child: widget.walletImage, fit: BoxFit.fill)), + child: FittedBox(child: widget.walletImage, fit: BoxFit.fill)), ), Padding( padding: EdgeInsets.only(top: 48), @@ -99,13 +100,13 @@ class WalletTypeFormState extends State { ), ), ...types.map((type) => Padding( - padding: EdgeInsets.only(top: 24), - child: SelectButton( - image: _iconFor(type), - text: walletTypeToDisplayName(type), - isSelected: selected == type, - onTap: () => setState(() => selected = type)), - )) + padding: EdgeInsets.only(top: 24), + child: SelectButton( + image: _iconFor(type), + text: walletTypeToDisplayName(type), + isSelected: selected == type, + onTap: () => setState(() => selected = type)), + )) ], ), bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), @@ -125,6 +126,8 @@ class WalletTypeFormState extends State { return moneroIcon; case WalletType.bitcoin: return bitcoinIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/src/screens/nodes/nodes_list_page.dart b/lib/src/screens/nodes/nodes_list_page.dart index 87753e1ce..fa4762068 100644 --- a/lib/src/screens/nodes/nodes_list_page.dart +++ b/lib/src/screens/nodes/nodes_list_page.dart @@ -87,7 +87,7 @@ class NodeListPage extends BasePage { final isSelected = node.keyIndex == nodeListViewModel.currentNode?.keyIndex; final nodeListRow = NodeListRow( - title: node.uri, + title: node.uriRaw, isSelected: isSelected, isAlive: node.requestNode(), onTap: (_) async { @@ -101,8 +101,9 @@ class NodeListPage extends BasePage { return AlertWithTwoActions( alertTitle: S.of(context).change_current_node_title, - alertContent: - S.of(context).change_current_node(node.uri), + alertContent: S + .of(context) + .change_current_node(node.uriRaw), leftButtonText: S.of(context).cancel, rightButtonText: S.of(context).change, actionLeftButton: () => diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 03fc17425..0a12b752a 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -13,7 +13,7 @@ class PreSeedPage extends BasePage { imageDark = Image.asset('assets/images/pre_seed_dark.png'), wordsCount = type == WalletType.monero ? 25 - : 12; // FIXME: Stupid fast implementation + : 24; // FIXME: Stupid fast implementation final Image imageDark; final Image imageLight; @@ -49,7 +49,9 @@ class PreSeedPage extends BasePage { Padding( padding: EdgeInsets.only(top: 70, left: 16, right: 16), child: Text( - S.of(context).pre_seed_description(wordsCount.toString()), + S + .of(context) + .pre_seed_description(wordsCount.toString()), textAlign: TextAlign.center, style: TextStyle( fontSize: 14, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 58ef36797..e7856d685 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -40,6 +40,8 @@ class WalletListBodyState extends State { Image.asset('assets/images/monero_logo.png', height: 24, width: 24); final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); + final litecoinIcon = + Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar _progressBar; @@ -193,6 +195,8 @@ class WalletListBodyState extends State { return bitcoinIcon; case WalletType.monero: return moneroIcon; + case WalletType.litecoin: + return litecoinIcon; default: return null; } diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index 06e0de1b2..aee7610cd 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,6 +1,8 @@ -import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/store/wallet_list_store.dart'; import 'package:cake_wallet/store/authentication_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -20,7 +22,8 @@ abstract class AppStoreBase with Store { AuthenticationStore authenticationStore; @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; WalletListStore walletList; @@ -29,7 +32,10 @@ abstract class AppStoreBase with Store { NodeListStore nodeListStore; @action - void changeCurrentWallet(WalletBase wallet) { + void changeCurrentWallet( + WalletBase, + TransactionInfo> + wallet) { this.wallet?.close(); this.wallet = wallet; } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 6e16e2324..da2410378 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -149,7 +149,7 @@ abstract class SettingsStoreBase with Store { static Future load( {@required Box nodeSource, - @required bool isBitcoinBuyEnabled, + @required bool isBitcoinBuyEnabled, FiatCurrency initialFiatCurrency = FiatCurrency.usd, MoneroTransactionPriority initialMoneroTransactionPriority = MoneroTransactionPriority.slow, @@ -205,15 +205,19 @@ abstract class SettingsStoreBase with Store { final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = sharedPreferences .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); + final litecoinElectrumServerId = sharedPreferences + .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); + final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final packageInfo = await PackageInfo.fromPlatform(); return SettingsStore( sharedPreferences: sharedPreferences, nodes: { WalletType.monero: moneroNode, - WalletType.bitcoin: bitcoinElectrumServer + WalletType.bitcoin: bitcoinElectrumServer, + WalletType.litecoin: litecoinElectrumServer }, appVersion: packageInfo.version, isBitcoinBuyEnabled: isBitcoinBuyEnabled, @@ -263,6 +267,10 @@ abstract class SettingsStoreBase with Store { await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, node.key as int); break; + case WalletType.litecoin: + await _sharedPreferences.setInt( + PreferencesKey.currentLitecoinElectrumSererIdKey, node.key as int); + break; case WalletType.monero: await _sharedPreferences.setInt( PreferencesKey.currentNodeIdKey, node.key as int); diff --git a/lib/view_model/auth_view_model.dart b/lib/view_model/auth_view_model.dart index 5bf5c25a1..76df95283 100644 --- a/lib/view_model/auth_view_model.dart +++ b/lib/view_model/auth_view_model.dart @@ -110,8 +110,6 @@ abstract class AuthViewModelBase with Store { if (isAuthenticated) { state = ExecutedSuccessfullyState(); - } else { - state = FailureState('Failure biometric authentication'); } } } catch(e) { diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index fb9b07a27..a0935a936 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,7 +1,9 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; @@ -23,11 +25,7 @@ abstract class BalanceViewModelBase with Store { @required this.settingsStore, @required this.fiatConvertationStore}) { isReversing = false; - wallet ??= appStore.wallet; - - _reaction = reaction((_) => appStore.wallet, _onWalletChange); - final _wallet = wallet; if (_wallet is MoneroWallet) { @@ -38,6 +36,8 @@ abstract class BalanceViewModelBase with Store { balance = _wallet.balance; } + reaction((_) => appStore.wallet, _onWalletChange); + _onCurrentWalletChangeReaction = reaction((_) => wallet.balance, (dynamic balance) { if (balance is Balance) { @@ -59,7 +59,8 @@ abstract class BalanceViewModelBase with Store { Balance balance; @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; @computed double get price => fiatConvertationStore.prices[appStore.wallet.currency]; @@ -70,8 +71,8 @@ abstract class BalanceViewModelBase with Store { @computed BalanceDisplayMode get displayMode => isReversing ? savedDisplayMode == BalanceDisplayMode.hiddenBalance - ? BalanceDisplayMode.displayableBalance - : savedDisplayMode + ? BalanceDisplayMode.displayableBalance + : savedDisplayMode : savedDisplayMode; @computed @@ -153,14 +154,14 @@ abstract class BalanceViewModelBase with Store { CryptoCurrency get currency => appStore.wallet.currency; ReactionDisposer _onCurrentWalletChangeReaction; - ReactionDisposer _reaction; @action - void _onWalletChange(WalletBase wallet) { + void _onWalletChange( + WalletBase, + TransactionInfo> + wallet) { this.wallet = wallet; - balance = wallet.balance; - _onCurrentWalletChangeReaction?.reaction?.dispose(); _onCurrentWalletChangeReaction = reaction( (_) => wallet.balance, (Balance balance) => this.balance = balance); diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 66fc53269..97ed234e6 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,23 +1,13 @@ -import 'dart:convert'; -import 'dart:io'; - -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/entities/order.dart'; -import 'package:cake_wallet/entities/transaction_history.dart'; -import 'package:cake_wallet/exchange/trade_state.dart'; import 'package:cake_wallet/monero/account.dart'; import 'package:cake_wallet/monero/monero_balance.dart'; -import 'package:cake_wallet/monero/monero_transaction_history.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; -import 'package:cake_wallet/entities/crypto_currency.dart'; -import 'package:cake_wallet/entities/transaction_direction.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/exchange/exchange_provider_description.dart'; -import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; @@ -27,12 +17,8 @@ import 'package:cake_wallet/view_model/dashboard/order_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/trade_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.dart'; import 'package:cake_wallet/view_model/wyre_view_model.dart'; -import 'package:crypto/crypto.dart'; -import 'package:flutter/services.dart'; import 'package:hive/hive.dart'; -import 'package:http/http.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/entities/sync_status.dart'; @@ -43,8 +29,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/trade_filter_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/view_model/dashboard/formatted_item_list.dart'; -import 'package:url_launcher/url_launcher.dart'; -import 'package:convert/convert.dart'; part 'dashboard_view_model.g.dart'; @@ -100,9 +84,8 @@ abstract class DashboardViewModelBase with Store { name = appStore.wallet?.name; wallet ??= appStore.wallet; type = wallet.type; - - _reaction = reaction((_) => appStore.wallet, _onWalletChange); - + isOutdatedElectrumWallet = + wallet.type == WalletType.bitcoin && wallet.seed.split(' ').length < 24; final _wallet = wallet; if (_wallet is MoneroWallet) { @@ -133,6 +116,8 @@ abstract class DashboardViewModelBase with Store { settingsStore: appStore.settingsStore))); } + reaction((_) => appStore.wallet, _onWalletChange); + connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, @@ -215,7 +200,8 @@ abstract class DashboardViewModelBase with Store { } @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; bool get hasRescan => wallet.type == WalletType.monero; @@ -241,8 +227,6 @@ abstract class DashboardViewModelBase with Store { bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - ReactionDisposer _reaction; - ReactionDisposer _onMoneroAccountChangeReaction; ReactionDisposer _onMoneroBalanceChangeReaction; @@ -252,11 +236,19 @@ abstract class DashboardViewModelBase with Store { await wallet.connectToNode(node: node); } + @observable + bool isOutdatedElectrumWallet; + @action - void _onWalletChange(WalletBase wallet) { + void _onWalletChange( + WalletBase, + TransactionInfo> + wallet) { this.wallet = wallet; type = wallet.type; name = wallet.name; + isOutdatedElectrumWallet = + wallet.type == WalletType.bitcoin && wallet.seed.split(' ').length < 24; if (wallet is MoneroWallet) { subname = wallet.account?.label; @@ -286,17 +278,17 @@ abstract class DashboardViewModelBase with Store { connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, - (TransactionInfo val) => TransactionListItem( + (TransactionInfo val) => TransactionListItem( transaction: val, balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore), filter: (TransactionInfo tx) { - if (tx is MoneroTransactionInfo && wallet is MoneroWallet) { - return tx.accountIndex == wallet.account.id; - } + if (tx is MoneroTransactionInfo && wallet is MoneroWallet) { + return tx.accountIndex == wallet.account.id; + } - return true; - }); + return true; + }); } @action @@ -319,6 +311,4 @@ abstract class DashboardViewModelBase with Store { balanceViewModel: balanceViewModel, settingsStore: appStore.settingsStore))); } - - } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 83477544e..9974e3b11 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -4,7 +4,7 @@ import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/monero/monero_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; @@ -42,7 +42,7 @@ class TransactionListItem extends ActionListItem with Keyable { transaction.changeFiatAmount(amount); } - if (transaction is BitcoinTransactionInfo) { + if (transaction is ElectrumTransactionInfo) { final amount = calculateFiatAmountRaw( cryptoAmount: bitcoinAmountToDouble(amount: transaction.amount), price: price); @@ -56,4 +56,4 @@ class TransactionListItem extends ActionListItem with Keyable { @override DateTime get date => transaction.date; -} \ No newline at end of file +} diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 2bcec3565..7f0f601ab 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -34,10 +34,8 @@ class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, this.tradesStore, this._settingsStore) { - providerList = [ - ChangeNowExchangeProvider() - ]; - + const excludeCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, CryptoCurrency.bnb]; + providerList = [ChangeNowExchangeProvider()]; _initialPairBasedOnWallet(); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -58,9 +56,9 @@ abstract class ExchangeViewModelBase with Store { _onPairChange(); } }); - receiveCurrencies = CryptoCurrency.all.where((cryptoCurrency) => - (cryptoCurrency != CryptoCurrency.xlm)&& - (cryptoCurrency != CryptoCurrency.xrp)).toList(); + receiveCurrencies = CryptoCurrency.all + .where((cryptoCurrency) => !excludeCurrencies.contains(cryptoCurrency)) + .toList(); _defineIsReceiveAmountEditable(); isFixedRateMode = false; isReceiveAmountEntered = false; @@ -219,8 +217,10 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsIsLoading(); try { - limits = await provider.fetchLimits(from: depositCurrency, - to: receiveCurrency, isFixedRateMode: isFixedRateMode); + limits = await provider.fetchLimits( + from: depositCurrency, + to: receiveCurrency, + isFixedRateMode: isFixedRateMode); limitsState = LimitsLoadedSuccessfully(limits: limits); } catch (e) { limitsState = LimitsLoadedFailure(error: e.toString()); @@ -284,8 +284,8 @@ abstract class ExchangeViewModelBase with Store { } else { try { tradeState = TradeIsCreating(); - final trade = await provider.createTrade(request: request, - isFixedRateMode: isFixedRateMode); + final trade = await provider.createTrade( + request: request, isFixedRateMode: isFixedRateMode); trade.walletId = wallet.id; tradesStore.setTrade(trade); await trades.add(trade); @@ -321,7 +321,8 @@ abstract class ExchangeViewModelBase with Store { void calculateDepositAllAmount() { if (wallet is BitcoinWallet) { final availableBalance = wallet.balance.available; - final priority = _settingsStore.priority[wallet.type] as BitcoinTransactionPriority; + final priority = + _settingsStore.priority[wallet.type] as BitcoinTransactionPriority; final fee = wallet.calculateEstimatedFee(priority, null); if (availableBalance < fee || availableBalance == 0) { @@ -403,6 +404,10 @@ abstract class ExchangeViewModelBase with Store { depositCurrency = CryptoCurrency.btc; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.litecoin: + depositCurrency = CryptoCurrency.ltc; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 9cf800705..c680cc859 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -39,6 +39,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.monero: node = getMoneroDefaultNode(nodes: _nodeSource); break; + case WalletType.litecoin: + node = getLitecoinDefaultElectrumServer(nodes: _nodeSource); + break; default: break; } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index f36261f73..850ec0b3b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_amount_format.dart'; import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; @@ -91,6 +92,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: _amount = stringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.litecoin: + _amount = stringDoubleToBitcoinAmount(_cryptoAmount); + break; default: break; } @@ -103,7 +107,7 @@ abstract class SendViewModelBase with Store { final fee = _wallet.calculateEstimatedFee( _settingsStore.priority[_wallet.type], amount); - if (_wallet is BitcoinWallet) { + if (_wallet is ElectrumWallet) { return bitcoinAmountToDouble(amount: fee); } @@ -309,6 +313,12 @@ abstract class SendViewModelBase with Store { final amount = !sendAll ? _amount : null; final priority = _settingsStore.priority[_wallet.type]; + return BitcoinTransactionCredentials( + address, amount, priority as BitcoinTransactionPriority); + case WalletType.litecoin: + final amount = !sendAll ? _amount : null; + final priority = _settingsStore.priority[_wallet.type]; + return BitcoinTransactionCredentials( address, amount, priority as BitcoinTransactionPriority); case WalletType.monero: @@ -335,6 +345,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: maximumFractionDigits = 8; break; + case WalletType.litecoin: + maximumFractionDigits = 8; + break; default: break; } @@ -362,9 +375,9 @@ abstract class SendViewModelBase with Store { final _priority = priority as TransactionPriority; final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final rate = wallet.feeRate(_priority); - return '${priority.toString()} ($rate sat/byte)'; + return '${priority.labelWithRate(rate)}'; } return priority.toString(); diff --git a/lib/view_model/settings/settings_view_model.dart b/lib/view_model/settings/settings_view_model.dart index 71088d339..57de6d7d9 100644 --- a/lib/view_model/settings/settings_view_model.dart +++ b/lib/view_model/settings/settings_view_model.dart @@ -1,10 +1,3 @@ -import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; -import 'package:cake_wallet/entities/balance.dart'; -import 'package:cake_wallet/entities/transaction_priority.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; -import 'package:cake_wallet/themes/theme_list.dart'; -import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; import 'package:flutter/cupertino.dart'; import 'package:mobx/mobx.dart'; import 'package:package_info/package_info.dart'; @@ -25,6 +18,15 @@ import 'package:cake_wallet/view_model/settings/regular_list_item.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; import 'package:cake_wallet/view_model/settings/switcher_list_item.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_transaction_priority.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/theme_list.dart'; +import 'package:cake_wallet/src/screens/pin_code/pin_code_widget.dart'; part 'settings_view_model.g.dart'; @@ -36,13 +38,19 @@ List priorityForWalletType(WalletType type) { return MoneroTransactionPriority.all; case WalletType.bitcoin: return BitcoinTransactionPriority.all; + case WalletType.litecoin: + return LitecoinTransactionPriority.all; default: return []; } } abstract class SettingsViewModelBase with Store { - SettingsViewModelBase(this._settingsStore, WalletBase wallet) + SettingsViewModelBase( + this._settingsStore, + WalletBase, + TransactionInfo> + wallet) : itemHeaders = {}, _walletType = wallet.type, _biometricAuth = BiometricAuth() { @@ -77,9 +85,9 @@ abstract class SettingsViewModelBase with Store { displayItem: (dynamic priority) { final _priority = priority as TransactionPriority; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final rate = wallet.feeRate(_priority); - return '${priority.toString()} ($rate sat/byte)'; + return '${priority.labelWithRate(rate)}'; } return priority.toString(); diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 67822e207..47b0957d7 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -1,5 +1,6 @@ -import 'package:cake_wallet/bitcoin/bitcoin_transaction_info.dart'; +import 'package:cake_wallet/bitcoin/electrum_transaction_info.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/entities/wallet_type.dart'; import 'package:cake_wallet/monero/monero_transaction_info.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; @@ -43,12 +44,6 @@ abstract class TransactionDetailsViewModelBase with Store { value: tx.amountFormatted()), StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()), - BlockExplorerListItem( - title: "View in Block Explorer", - value: "View Transaction on XMRChain.net", - onTap: () { - launch("https://xmrchain.net/search?value=${tx.id}"); - }) ]; if (tx.key?.isNotEmpty ?? null) { @@ -59,7 +54,7 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - if (tx is BitcoinTransactionInfo) { + if (tx is ElectrumTransactionInfo) { final _items = [ StandartListItem( title: S.current.transaction_details_transaction_id, value: tx.id), @@ -78,12 +73,6 @@ abstract class TransactionDetailsViewModelBase with Store { StandartListItem( title: S.current.transaction_details_fee, value: tx.feeFormatted()), - BlockExplorerListItem( - title: "View in Block Explorer", - value: "View Transaction on Blockchain.com", - onTap: () { - launch("https://www.blockchain.com/btc/tx/${tx.id}"); - }) ]; items.addAll(_items); @@ -101,6 +90,19 @@ abstract class TransactionDetailsViewModelBase with Store { } } + WalletType type; + + if (tx is MoneroTransactionInfo) { + type = WalletType.monero; + } else if (tx is ElectrumTransactionInfo) { + type = tx.type; + } + + items.add(BlockExplorerListItem( + title: "View in Block Explorer", + value: _explorerDescription(type), + onTap: () => launch(_explorerUrl(type, tx.id)))); + final description = transactionDescriptionBox.values.firstWhere( (val) => val.id == transactionInfo.id, orElse: () => TransactionDescription(id: transactionInfo.id)); @@ -125,4 +127,30 @@ abstract class TransactionDetailsViewModelBase with Store { final List items; bool showRecipientAddress; + + String _explorerUrl(WalletType type, String txId) { + switch (type) { + case WalletType.monero: + return 'https://xmrchain.net/search?value=${txId}'; + case WalletType.bitcoin: + return 'https://www.blockchain.com/btc/tx/${txId}'; + case WalletType.litecoin: + return 'https://blockchair.com/litecoin/transaction/${txId}'; + default: + return ''; + } + } + + String _explorerDescription(WalletType type) { + switch (type) { + case WalletType.monero: + return 'View Transaction on XMRChain.net'; + case WalletType.bitcoin: + return 'View Transaction on Blockchain.com'; + case WalletType.litecoin: + return 'View Transaction on Blockchair.com'; + default: + return ''; + } + } } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index b06a00e99..b018c542d 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/core/wallet_base.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; @@ -62,7 +63,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { await wallet.generateNewAddress(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 9f9531864..b15443222 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,5 +1,3 @@ -import 'package:cake_wallet/entities/balance.dart'; -import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -10,6 +8,11 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_h import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/entities/wallet_type.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; +import 'package:cake_wallet/core/transaction_history.dart'; +import 'package:cake_wallet/entities/balance.dart'; +import 'package:cake_wallet/entities/transaction_info.dart'; +import 'package:cake_wallet/store/app_store.dart'; part 'wallet_address_list_view_model.g.dart'; @@ -56,12 +59,13 @@ class BitcoinURI extends PaymentURI { } abstract class WalletAddressListViewModelBase with Store { - WalletAddressListViewModelBase( - {@required AppStore appStore}) { + WalletAddressListViewModelBase({@required AppStore appStore}) { _appStore = appStore; _wallet = _appStore.wallet; hasAccounts = _wallet?.type == WalletType.monero; - _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase wallet) { + _onWalletChangeReaction = reaction((_) => _appStore.wallet, (WalletBase< + Balance, TransactionHistoryBase, TransactionInfo> + wallet) { _wallet = wallet; hasAccounts = _wallet.type == WalletType.monero; }); @@ -119,9 +123,7 @@ abstract class WalletAddressListViewModelBase with Store { final isPrimary = addr == primaryAddress; return WalletAddressListItem( - isPrimary: isPrimary, - name: null, - address: addr.address); + isPrimary: isPrimary, name: null, address: addr.address); }); addressList.addAll(bitcoinAddresses); } @@ -147,7 +149,8 @@ abstract class WalletAddressListViewModelBase with Store { bool get hasAddressList => _wallet.type == WalletType.monero; @observable - WalletBase _wallet; + WalletBase, TransactionInfo> + _wallet; List _baseItems; @@ -155,7 +158,6 @@ abstract class WalletAddressListViewModelBase with Store { ReactionDisposer _onWalletChangeReaction; - @action void setAddress(WalletAddressListItem address) => _wallet.address = address.address; @@ -174,7 +176,7 @@ abstract class WalletAddressListViewModelBase with Store { void nextAddress() { final wallet = _wallet; - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { wallet.nextAddress(); } } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 4a5315c9a..b56425ee9 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -2,7 +2,7 @@ import 'package:mobx/mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/monero/monero_wallet.dart'; -import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; part 'wallet_keys_view_model.g.dart'; @@ -24,12 +24,11 @@ abstract class WalletKeysViewModelBase with Store { title: S.current.view_key_public, value: keys.publicViewKey), StandartListItem( title: S.current.view_key_private, value: keys.privateViewKey), - StandartListItem( - title: S.current.wallet_seed, value: wallet.seed), + StandartListItem(title: S.current.wallet_seed, value: wallet.seed), ]); } - if (wallet is BitcoinWallet) { + if (wallet is ElectrumWallet) { final keys = wallet.keys; items.addAll([ diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index c21f1b8c1..0611786a8 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -37,6 +37,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { name: name, language: options as String); case WalletType.bitcoin: return BitcoinNewWalletCredentials(name: name); + case WalletType.litecoin: + return BitcoinNewWalletCredentials(name: name); default: return null; } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index b015535ae..4fe81c7d0 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -62,6 +62,9 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.bitcoin: return BitcoinRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password); + case WalletType.litecoin: + return BitcoinRestoreWalletFromSeedCredentials( + name: name, mnemonic: seed, password: password); default: break; } diff --git a/pubspec.lock b/pubspec.lock index 75b67e638..3ff596074 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -67,10 +67,12 @@ packages: bech32: dependency: transitive description: - name: bech32 - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.2" + path: "." + ref: cake + resolved-ref: "02fef082f20af13de00b4e64efb93a2c1e5e1cf2" + url: "git@github.com:cake-tech/bech32.git" + source: git + version: "0.2.0" bip32: dependency: transitive description: @@ -88,9 +90,11 @@ packages: bitcoin_flutter: dependency: "direct main" description: - name: bitcoin_flutter - url: "https://pub.dartlang.org" - source: hosted + path: "." + ref: cake + resolved-ref: b3ab2926c665f0e68b74a4a5f31059f7fcd817b7 + url: "git@github.com:cake-tech/bitcoin_flutter.git" + source: git version: "2.0.2" boolean_selector: dependency: transitive diff --git a/pubspec.yaml b/pubspec.yaml index 388b8a5f2..4d1b63ceb 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: Cake Wallet. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 4.1.5+44 +version: 4.2.0+49 environment: sdk: ">=2.7.0 <3.0.0" @@ -65,7 +65,10 @@ dependencies: crypto: ^2.1.5 password: ^1.0.0 basic_utils: ^2.0.3 - bitcoin_flutter: ^2.0.0 + bitcoin_flutter: + git: + url: https://github.com/cake-tech/bitcoin_flutter.git + ref: cake get_it: ^6.0.0 connectivity: ^3.0.3 keyboard_actions: ^3.3.0 @@ -105,7 +108,8 @@ flutter: assets: - assets/images/ - assets/node_list.yml - - assets/electrum_server_list.yml + - assets/bitcoin_electrum_server_list.yml + - assets/litecoin_electrum_server_list.yml - assets/text/ - assets/faq/ diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 34cef6078..da4d4975b 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Beschreibung zur Wiederherstellung der Brieftasche", "restore_new_seed" : "Neuer Seed", "restore_active_seed" : "Aktives Seed", - "restore_bitcoin_description_from_seed" : "Stellen Sie Ihre Brieftasche aus dem 12-Wort-Kombinationscode wieder her", + "restore_bitcoin_description_from_seed" : "Stellen Sie Ihre Brieftasche aus dem 24-Wort-Kombinationscode wieder her", "restore_bitcoin_description_from_keys" : "Stellen Sie Ihre Brieftasche aus der generierten WIF-Zeichenfolge aus Ihren privaten Schlüsseln wieder her", "restore_bitcoin_title_from_keys" : "Aus WIF wiederherstellen", "restore_from_date_or_blockheight" : "Bitte geben Sie einige Tage vor dem Erstellen dieser Brieftasche ein Datum ein. Oder wenn Sie die Blockhöhe kennen, geben Sie sie stattdessen ein", @@ -471,6 +471,11 @@ "submit_request" : "Einen Antrag stellen", + "buy_alert_content" : "Derzeit unterstützen wir nur den Kauf von Bitcoin. Um Bitcoin zu kaufen, erstellen Sie bitte Ihre Bitcoin-Brieftasche oder wechseln Sie zu dieser", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "Adresse erkannt", "address_from_domain" : "Sie haben die Adresse von der unaufhaltsamen Domain ${domain} erhalten" } \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index a641fabf1..e43ec7318 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Wallet restore description", "restore_new_seed" : "New seed", "restore_active_seed" : "Active seed", - "restore_bitcoin_description_from_seed" : "Restore your wallet from 12 word combination code", + "restore_bitcoin_description_from_seed" : "Restore your wallet from 24 word combination code", "restore_bitcoin_description_from_keys" : "Restore your wallet from generated WIF string from your private keys", "restore_bitcoin_title_from_keys" : "Restore from WIF", "restore_from_date_or_blockheight" : "Please enter a date a few days before you created this wallet. Or if you know the blockheight, please enter it instead", @@ -471,6 +471,11 @@ "submit_request" : "submit a request", + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "Address detected", "address_from_domain" : "You got address from unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 95a34e230..09dae1e32 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Restaurar billetera", "restore_new_seed" : "Nueva semilla", "restore_active_seed" : "Semilla activa", - "restore_bitcoin_description_from_seed" : "Restaure su billetera a partir del código de combinación de 12 palabras", + "restore_bitcoin_description_from_seed" : "Restaure su billetera a partir del código de combinación de 24 palabras", "restore_bitcoin_description_from_keys" : "Restaure su billetera a partir de una cadena WIF generada a partir de sus claves privadas", "restore_bitcoin_title_from_keys" : "Restaurar desde WIF", "restore_from_date_or_blockheight" : "Ingrese una fecha unos días antes de crear esta billetera. O si conoce la altura del bloque, ingréselo en su lugar", @@ -471,6 +471,11 @@ "submit_request" : "presentar una solicitud", + "buy_alert_content" : "Actualmente solo apoyamos la compra de Bitcoin. Para comprar Bitcoin, cree o cambie a su billetera Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "Dirección detectada", "address_from_domain" : "Tienes la dirección de unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 0db4e7216..b9a873265 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "बटुआ विवरण पुनर्स्थापित करें", "restore_new_seed" : "नया बीज", "restore_active_seed" : "सक्रिय बीज", - "restore_bitcoin_description_from_seed" : "12 शब्द संयोजन कोड से अपने वॉलेट को पुनर्स्थापित करें", + "restore_bitcoin_description_from_seed" : "24 शब्द संयोजन कोड से अपने वॉलेट को पुनर्स्थापित करें", "restore_bitcoin_description_from_keys" : "अपने निजी कुंजी से उत्पन्न WIF स्ट्रिंग से अपने वॉलेट को पुनर्स्थापित करें", "restore_bitcoin_title_from_keys" : "WIF से पुनर्स्थापित करें", "restore_from_date_or_blockheight" : "कृपया इस वॉलेट को बनाने से कुछ दिन पहले एक तारीख दर्ज करें। या यदि आप ब्लॉकचेट जानते हैं, तो कृपया इसके बजाय इसे दर्ज करें", @@ -471,6 +471,11 @@ "submit_request" : "एक अनुरोध सबमिट करें", + "buy_alert_content" : "वर्तमान में हम केवल बिटकॉइन की खरीद का समर्थन करते हैं। बिटकॉइन खरीदने के लिए, कृपया अपना बिटकॉइन वॉलेट बनाएं या स्विच करें", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "पता लग गया", "address_from_domain" : "आपको अजेय डोमेन ${domain} से पता मिला है" } \ No newline at end of file diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb new file mode 100644 index 000000000..d449fca1b --- /dev/null +++ b/res/values/strings_hr.arb @@ -0,0 +1,478 @@ +{ + "welcome" : "Dobrodošli na", + "cake_wallet" : "Cake Wallet", + "first_wallet_text" : "Odličan novčanik za Monero i Bitcoin", + "please_make_selection" : "Molimo odaberite opcije niže za izradu novog novčanika ili za oporavak postojećeg.", + "create_new" : "Izradi novi novčanik", + "restore_wallet" : "Oporavi novčanik", + + + "accounts" : "Računi", + "edit" : "Uredi", + "account" : "Račun", + "add" : "Dodaj", + + + "address_book" : "Imenik", + "contact" : "Kontakt", + "please_select" : "Molimo odaberite:", + "cancel" : "Poništi", + "ok" : "OK", + "contact_name" : "Ime kontakta", + "reset" : "Resetiraj", + "save" : "Spremi", + "address_remove_contact" : "Ukloni kontakt", + "address_remove_content" : "Jeste li sigurni da želite ukloniti odabrani kontakt?", + + + "authenticated" : "Autentificiran", + "authentication" : "Autentifikacija", + "failed_authentication" : "Autentifikacija neuspješna. ${state_error}", + + + "wallet_menu" : "Izbornik", + "Blocks_remaining" : "${status} preostalih blokova", + "please_try_to_connect_to_another_node" : "Molimo pokušajte se spojiti na drugi node.", + "xmr_hidden" : "Sakriven", + "xmr_available_balance" : "Raspoloživ iznos", + "xmr_full_balance" : "Ukupan iznos", + "send" : "Pošalji", + "receive" : "Primi", + "transactions" : "Transakcije", + "incoming" : "Dolazno", + "outgoing" : "Odlazno", + "transactions_by_date" : "Transakcije prema datumu", + "trades" : "Razmjene", + "filters" : "Filter", + "today" : "Danas", + "yesterday" : "Jučer", + "received" : "Primljeno", + "sent" : "Poslano", + "pending" : " (u tijeku)", + "rescan" : "Ponovno skeniranje", + "reconnect" : "Ponovno povezivanje", + "wallets" : "Novčanici", + "show_seed" : "Prikaži pristupni izraz", + "show_keys" : "Prikaži pristupni izraz/ključ", + "address_book_menu" : "Imenik", + "reconnection" : "Ponovno povezivanje", + "reconnect_alert_text" : "Jeste li sigurni da se želite ponovno povezati?", + + + "exchange" : "Razmijeni", + "clear" : "Izbriši", + "refund_address" : "Adresa za povrat", + "change_exchange_provider" : "Promjena davatelja usluge razmjene", + "you_will_send" : "Razmijeni iz", + "you_will_get" : "Razmijeni u", + "amount_is_guaranteed" : "Iznos koji ćete primiti je zajamčen", + "amount_is_estimate" : "Iznos koji ćete primiti je okviran", + "powered_by" : "Omogućio ${title}", + "error" : "Greška", + "estimated" : "procijenjen", + "min_value" : "Min.: ${value} ${currency}", + "max_value" : "Maks.: ${value} ${currency}", + "change_currency" : "Promijenite valutu", + + + "copy_id" : "Kopirati ID", + "exchange_result_write_down_trade_id" : "Molimo kopirajte ili zapišite transakcijski ID za nastavak.", + "trade_id" : "Transakcijski ID:", + "copied_to_clipboard" : "Kopirano u međuspremnik", + "saved_the_trade_id" : "Spremio/la sam transakcijski ID", + "fetching" : "Dohvaćanje", + "id" : "ID: ", + "amount" : "Iznos: ", + "payment_id" : "ID plaćanja: ", + "status" : "Status: ", + "offer_expires_in" : "Ponuda istječe za: ", + "trade_is_powered_by" : "Razmjenu je omogućio ${provider}", + "copy_address" : "Kopiraj adresu", + "exchange_result_confirm" : "Pritiskom na potvrdi, poslat ćete ${fetchingLabel} ${from} sa svog novčanika pod nazivom ${walletName} na adresu prikazanu ispod ili iznos možete poslati s vanjskog novčanika na niže navedenu adresu. /QR code.\n\nMolimo potvrdite za nastavak ili se vratite natrag za promjenu iznosa.", + "exchange_result_description" : "Potrebno poslati minimalno ${fetchingLabel} ${from} na adresu prikazanu na sljedećoj stranici. Ukoliko pošaljete iznos manji od ${fetchingLabel} ${from}, postoji mogućnost da razmjena neće biti uspješna i da iznos neće biti vraćen.", + "exchange_result_write_down_ID" : "*Molimo kopirajte ili zapišite svoj ID prikazan ispod.", + "confirm" : "Potvrdi", + "confirm_sending" : "Potvrdi slanje", + "commit_transaction_amount_fee" : "Izvrši transakciju \nAmount: ${amount}\nFee: ${fee}", + "sending" : "Slanje", + "transaction_sent" : "Transakcija provedena!", + "expired" : "Isteklo", + "time" : "${minutes}m ${seconds}s", + "send_xmr" : "Pošalji XMR", + "exchange_new_template" : "Novi predložak", + + "faq" : "FAQ", + + + "enter_your_pin" : "Upišite PIN", + "loading_your_wallet" : "Novčanik se učitava", + + + "new_wallet" : "Novi novčanik", + "wallet_name" : "Ime novčanika", + "continue_text" : "Nastavak", + "choose_wallet_currency" : "Molimo odaberite valutu novčanika:", + + + "node_new" : "Novi node", + "node_address" : "Node adresa", + "node_port" : "Node port", + "login" : "Prijava", + "password" : "Lozinka", + "nodes" : "Nodes", + "node_reset_settings_title" : "Resetiraj postavke", + "nodes_list_reset_to_default_message" : "Jeste li sigurni da se želite vratiti na početne postavke?", + "change_current_node" : "Jeste li sigurni da želite promijeniti trenutni node na ${node}?", + "change" : "Promijeni", + "remove_node" : "Ukloni node", + "remove_node_message" : "Jeste li sigurni da želite ukloniti odabrani node?", + "remove" : "Ukloni", + "delete" : "Izbriši", + "add_new_node" : "Dodaj novi node", + "change_current_node_title" : "Promijeni trenutni node", + "node_test" : "Provjeri", + "node_connection_successful" : "Uspješno spajanje", + "node_connection_failed" : "Neuspješno spajanje", + "new_node_testing" : "Provjera novog nodea", + + + "use" : "Prebaci na", + "digit_pin" : "-znamenkasti PIN", + + + "share_address" : "Podijeli adresu", + "receive_amount" : "Iznos", + "subaddresses" : "Podadrese", + "addresses" : "Adrese", + "scan_qr_code" : "Skeniraj QR kod za dobivanje adrese", + "rename" : "Preimenuj", + "choose_account" : "Odaberi račun", + "create_new_account" : "Izradi novi račun", + "accounts_subaddresses" : "Računi i podadrese", + + + "restore_restore_wallet" : "Oporavi novčanik", + "restore_title_from_seed_keys" : "Oporavi pomoću pristupnog izraza/ključa", + "restore_description_from_seed_keys" : "Oporavi novčanik pomoću pristupnog izraza/ključa spremljenog na sigurno mjesto", + "restore_next" : "Dalje", + "restore_title_from_backup" : "Oporavak pomoću sigurnosne kopije", + "restore_description_from_backup" : "Možete oporaviti cijelu Cake Wallet aplikaciju pomoću vlastite datoteke sa sigurnosnom kopijom", + "restore_seed_keys_restore" : "Oporavak pomoću pristupnog izraza/ključa", + "restore_title_from_seed" : "Oporavi pomoću pristupnog izraza", + "restore_description_from_seed" : "Oporavi novčanik pomoću koda koji sadrži kombinaciju od 25 ili 13 riječi", + "restore_title_from_keys" : "Oporavi pomoću ključa", + "restore_description_from_keys" : "Oporavi novčanik pomoću generiranih pritisaka na tipke spremljenih od vlastitih privatnih ključeva (keys)", + "restore_wallet_name" : "Ime novčanika", + "restore_address" : "Adresa", + "restore_view_key_private" : "View key (privatni)", + "restore_spend_key_private" : "Spend key (privatni)", + "restore_recover" : "Oporavi", + "restore_wallet_restore_description" : "Opis oporavka novčanika", + "restore_new_seed" : "Novi pristupi izraz", + "restore_active_seed" : "Aktivan pristupni izraz", + "restore_bitcoin_description_from_seed" : "Oporavi novčanik pomoću koda od 12 riječi", + "restore_bitcoin_description_from_keys" : "Oporavi novčanik pomoću WIF niza generiranog iz vlastitih privatnih ključeva (keys)", + "restore_bitcoin_title_from_keys" : "Oporavi pomoću formata uvoza novčanika (WIF)", + "restore_from_date_or_blockheight" : "Molimo unesite datum od nekoliko dana prije nego što ste izradili ovaj novčanik ili ako znate visinu bloka, molimo unesite je.", + + + "seed_reminder" : "Molimo zapišite ih u slučaju da izgubite mobitel ili izbrišete podatke", + "seed_title" : "Prisupni izraz", + "seed_share" : "Podijeli pristupni izraz", + "copy" : "Kopiraj", + + + "seed_language_choose" : "Molimo odaberite jezik pristupnog izraza:", + "seed_choose" : "Odaberi jezik pristupnog izraza", + "seed_language_next" : "Dalje", + "seed_language_english" : "Engleski", + "seed_language_chinese" : "Kineski", + "seed_language_dutch" : "Nizozemski", + "seed_language_german" : "Njemački", + "seed_language_japanese" : "Japanski", + "seed_language_portuguese" : "Portugalski", + "seed_language_russian" : "Ruski", + "seed_language_spanish" : "Španjolski", + + + "send_title" : "Pošalji", + "send_your_wallet" : "Tvoj novčanik", + "send_address" : "${cryptoCurrency} adresa", + "send_payment_id" : "ID plaćanja (nije obvezno)", + "all" : "SVE", + "send_error_minimum_value" : "Minimalna vrijednost iznosa je 0.01", + "send_error_currency" : "Iznos smije sadržavati samo brojeve", + "send_estimated_fee" : "Procijenjena naknada:", + "send_priority" : "Trenutno se naknada nalazi na ${transactionPriority} mjestu prioriteta.\nPrioritet transakcije moguće je prilagoditi u postavkama", + "send_creating_transaction" : "Izrada transakcije", + "send_templates" : "Predlošci", + "send_new" : "Novi", + "send_amount" : "Iznos:", + "send_fee" : "Naknada:", + "send_name" : "Ime", + "send_got_it" : "U redu", + "send_sending" : "Slanje...", + "send_success" : "Vaš ${crypto} je uspješno poslan", + + + "settings_title" : "Postavke", + "settings_nodes" : "Nodovi", + "settings_current_node" : "Trenutni node", + "settings_wallets" : "Novčanik", + "settings_display_balance_as" : "Prikaži stanje računa kao", + "settings_currency" : "Valuta", + "settings_fee_priority" : "Prioritet naknade", + "settings_save_recipient_address" : "Spremi primateljevu adresu", + "settings_personal" : "Osobni", + "settings_change_pin" : "Promijeni PIN", + "settings_change_language" : "Promijeni jezik", + "settings_allow_biometrical_authentication" : "Dopusti biometrijsku autentifikaciju", + "settings_dark_mode" : "Tamni način rada", + "settings_transactions" : "Transakcije", + "settings_trades" : "Razmjene", + "settings_display_on_dashboard_list" : "Prikaži na listi kontrolne ploče", + "settings_all" : "SVE", + "settings_only_trades" : "Samo razmjene", + "settings_only_transactions" : "Samo transakcije", + "settings_none" : "Nijedno", + "settings_support" : "Podrška", + "settings_terms_and_conditions" : "Uvjeti i odredbe", + "pin_is_incorrect" : "Netočan PIN", + + + "setup_pin" : "Podesi PIN", + "enter_your_pin_again" : "Ponovno upišite pin", + "setup_successful" : "Vaš je pin uspješno postavljen!", + + + "wallet_keys" : "Pristupni izraz/ključ novčanika", + "wallet_seed" : "Pristupni izraz novčanika", + "private_key" : "Privatni ključ", + "public_key" : "Javni ključ", + "view_key_private" : "View key (privatni)", + "view_key_public" : "View key (javni)", + "spend_key_private" : "Spend key (privatni)", + "spend_key_public" : "Spend key (javni)", + "copied_key_to_clipboard" : "${key} kopiran u međuspremnik", + + + "new_subaddress_title" : "Nova adresa", + "new_subaddress_label_name" : "Oznaka", + "new_subaddress_create" : "Izradi", + + + "subaddress_title" : "Lista podadresa", + + + "trade_details_title" : "Detalji razmjene", + "trade_details_id" : "ID", + "trade_details_state" : "Stanje", + "trade_details_fetching" : "Dohvaćanje", + "trade_details_provider" : "Pružatelj", + "trade_details_created_at" : "Stvoreno u", + "trade_details_pair" : "Upari", + "trade_details_copied" : "${title} kopiran u međuspremnik", + + + "trade_history_title" : "Povijest razmjena", + + + "transaction_details_title" : "Detalji transakcije", + "transaction_details_transaction_id" : "Transakcijski ID", + "transaction_details_date" : "Datum", + "transaction_details_height" : "Visina", + "transaction_details_amount" : "Iznos", + "transaction_details_fee" : "Naknada", + "transaction_details_copied" : "${title} kopiran u međuspremnik", + "transaction_details_recipient_address" : "Primateljeva adresa", + + + "wallet_list_title" : "Monero novčanik", + "wallet_list_create_new_wallet" : "Izradi novi novčanik", + "wallet_list_restore_wallet" : "Oporavi novčanik", + "wallet_list_load_wallet" : "Učitaj novčanik", + "wallet_list_loading_wallet" : "Učitavanje novčanika ${wallet_name}", + "wallet_list_failed_to_load" : "Neuspješno učitavanje novčanika ${wallet_name}. ${error}", + "wallet_list_removing_wallet" : "Uklanjanje novčanika ${wallet_name}", + "wallet_list_failed_to_remove" : "Neuspješno uklanjanje novčanika ${wallet_name}. ${error}", + + + "widgets_address" : "Adresa", + "widgets_restore_from_blockheight" : "Oporavi pomoću visine bloka", + "widgets_restore_from_date" : "Oporavi pomoću datuma", + "widgets_or" : "ili", + "widgets_seed" : "pristupnog izraza", + + + "router_no_route" : "Nije definirana ruta za ${name}", + + + "error_text_account_name" : "Ime računa smije sadržavati samo slova i brojeve\nte mora biti dužine između 1 i 15 znakova", + "error_text_contact_name" : "Ime kontakta ne smije sadržavati znakove ` , ' \" \ni mora biti dužine između 1 i 32 znaka", + "error_text_address" : "Adresa novčanika mora odgovarati\nvrsti kriptovalute", + "error_text_node_address" : "Molimo unesite iPv4 adresu", + "error_text_node_port" : "Node port smije sadržavati samo brojeve između 0 i 65535", + "error_text_payment_id" : "ID plaćanja smije sadržavati samo od 16 do 64 znakova hex vrijednosti", + "error_text_xmr" : "XMR vrijednost ne smije biti veća od raspoloživog iznosa.\nBroj decimala smije biti 12 ili manji.", + "error_text_fiat" : "Vrijednost iznosa ne smije biti veća od raspoloživog iznosa.\nBroj decimala smije biti 2 ili manji.", + "error_text_subaddress_name" : "Ime podadrese ne smije sadržavati znakove ` , ' \" \ni mora biti dužine između 1 i 20 znakova", + "error_text_amount" : "Iznos smije sadržavati samo brojeve", + "error_text_wallet_name" : "Ime novčanika smije sadržavati samo slova i brojeve\nte mora biti dužine između 1 i 15 znakova", + "error_text_keys" : "Novčanik smije sadržavati samo 64 znakova hex vrijednosti", + "error_text_crypto_currency" : "Broj decimala mora\nbiti 12 ili manji", + "error_text_minimal_limit" : "Razmjena za ${provider} nije izrađena. Iznos je manji od minimalnog: ${min} ${currency}", + "error_text_maximum_limit" : "Razmjena za ${provider} nije izrađena. Iznos je veći od maskimalnog: ${max} ${currency}", + "error_text_limits_loading_failed" : "Razmjena za ${provider} nije izrađena. Neuspješno učitavanje limita", + "error_text_template" : "Ime i adresa predloška ne smiju sadržavati znakove ` , ' \" \ni moraju biti dužine između 1 i 106 znakova", + + + "auth_store_ban_timeout" : "ban_timeout", + "auth_store_banned_for" : "Suspendiran na ", + "auth_store_banned_minutes" : " minute", + "auth_store_incorrect_password" : "Pogrešan PIN", + "wallet_store_monero_wallet" : "Monero novčanik", + "wallet_restoration_store_incorrect_seed_length" : "Netočna dužina pristupnog izraza", + + + "full_balance" : "Pun iznos", + "available_balance" : "Raspoloživ iznos", + "hidden_balance" : "Skriven iznos", + + + "sync_status_syncronizing" : "SINKRONIZIRANJE", + "sync_status_syncronized" : "SINKRONIZIRANO", + "sync_status_not_connected" : "NIJE POVEZANO", + "sync_status_starting_sync" : "ZAPOČINJEMO SINKRONIZIRANJE", + "sync_status_failed_connect" : "ISKLJUČENO", + "sync_status_connecting" : "SPAJANJE", + "sync_status_connected" : "SPOJENO", + + + "transaction_priority_slow" : "Sporo", + "transaction_priority_regular" : "Uobičajeno", + "transaction_priority_medium" : "Srednje", + "transaction_priority_fast" : "Brzo", + "transaction_priority_fastest" : "Najbrže", + + + "trade_for_not_created" : "Razmjena za ${title} nije izrađena.", + "trade_not_created" : "Razmjena nije izrađena.", + "trade_id_not_found" : "Razmjena ${tradeId} za ${title} nije pronađena.", + "trade_not_found" : "Razmjena nije pronađena.", + + + "trade_state_pending" : "U tijeku", + "trade_state_confirming" : "Potvrđivanje", + "trade_state_trading" : "Razmjenjivanje", + "trade_state_traded" : "Razmijenjeno", + "trade_state_complete" : "Dovršeno", + "trade_state_to_be_created" : "Stvaranje", + "trade_state_unpaid" : "Neplaćen", + "trade_state_underpaid" : "Nedovoljno plaćen", + "trade_state_paid_unconfirmed" : "Plaćanje nije potrđeno", + "trade_state_paid" : "Plaćen", + "trade_state_btc_sent" : "BTC poslan", + "trade_state_timeout" : "Isteklo", + "trade_state_created" : "Stvoreno", + "trade_state_finished" : "Završeno", + + "change_language" : "Promijeni jezik", + "change_language_to" : "Promijeni jezik u ${language}?", + + "paste" : "Zalijepi", + "restore_from_seed_placeholder" : "Molimo unesite ili zalijepite svoj pristupni izraz ovdje", + "add_new_word" : "Dodaj novu riječ", + "incorrect_seed" : "Uneseni tekst nije valjan.", + + "biometric_auth_reason" : "Skenirajte svoj otisak prsta za autentifikaciju", + "version" : "Verzija ${currentVersion}", + + "openalias_alert_title" : "XMR primatelj otkriven", + "openalias_alert_content" : "Poslat ćete sredstva primatelju\n${recipient_name}", + + "card_address" : "Adresa:", + "buy" : "Kupi", + + "placeholder_transactions" : "Vaše će transakcije biti prikazane ovdje", + "placeholder_contacts" : "Vaši će kontakti biti prikazani ovdje", + + "template" : "Predložak", + "confirm_delete_template" : "Ovom ćete radnjom izbrisati ovaj predložak. Želite li nastaviti?", + "confirm_delete_wallet" : "Ovom ćete radnjom izbrisati ovaj novčanik. Želite li nastaviti?", + + "picker_description" : "Da biste odabrali ChangeNOW ili MorphToken, molimo da prvo odabete dvije valute za trgovanje", + + "change_wallet_alert_title" : "Izmijeni trenutni novčanik", + "change_wallet_alert_content" : "Želite li promijeniti trenutni novčanik u ${wallet_name}?", + + "creating_new_wallet" : "Stvaranje novog novčanika", + "creating_new_wallet_error" : "Greška: ${description}", + + "seed_alert_title" : "Upozorenje", + "seed_alert_content" : "Pristupni izraz jedini je način za oporavak novčanika. Jeste li ga zapisali?", + "seed_alert_back" : "Vrati se natrag", + "seed_alert_yes" : "Jesam", + + "exchange_sync_alert_content" : "Molimo pričekajte dok se Vaš novčanik ne sinkronizira.", + + "pre_seed_title" : "VAŽNO", + "pre_seed_description" : "Na sljedećoj ćete stranici vidjeti niz ${words} riječi. Radi se o Vašem jedinstvenom i tajnom pristupnom izrazu koji je ujedno i JEDINI način na koji možete oporaviti svoj novčanik u slučaju gubitka ili kvara. VAŠA je odgovornost zapisati ga te pohraniti na sigurno mjesto izvan Cake Wallet aplikacije.", + "pre_seed_button_text" : "Razumijem. Prikaži mi moj pristupni izraz", + + "xmr_to_error" : "XMR.TO greška", + "xmr_to_error_description" : "Nevažeći iznos. Dopušteno je najviše 8 znamenki iza točke", + + "provider_error" : "${provider} greška", + + "use_ssl" : "Koristi SSL", + + "color_theme" : "Shema boja", + "light_theme" : "Svijetla", + "bright_theme" : "Jarka", + "dark_theme" : "Tamna", + "enter_your_note" : "Unesite svoju poruku…", + "note_optional" : "Poruka (nije obvezno)", + "note_tap_to_change" : "Poruka (dodirnite za promjenu)", + "transaction_key" : "Transakcijski ključ", + "confirmations" : "Potvrde", + "recipient_address" : "Primateljeva adresa", + + "extra_id" : "Dodatni ID:", + "destination_tag" : "Odredišna oznaka:", + "memo" : "Memo:", + + "backup" : "Sigurnosna kopija", + "change_password" : "Promijeni lozinku", + "backup_password" : "Lozinka za sigurnosnu kopiju", + "write_down_backup_password" : "Molimo zapišite svoju lozinku za sigurnosnu kopiju koja se koristi za uvoz datoteka sigurnosne kopije.", + "export_backup" : "Izvezi sigurnosnu kopiju", + "save_backup_password" : "Molimo pobrinite se da spremite svoju lozinku za sigurnosnu kopiju. Bez nje nećete moći uvesti datoteke sigurnosne kopije.", + "backup_file" : "Sigurnosna kopija datoteke", + + "edit_backup_password" : "Uredi lozinku za sigurnosnu kopiju", + "save_backup_password_alert" : "Spremi lozinku za sigurnosnu kopiju", + "change_backup_password_alert" : "Nećemo moći uvesti Vaše prethodne datoteke sigurnosne kopije s novom lozinkom za sigurnosnu kopiju. Novu lozinku za sigurnosnu kopiju moći ćete koristiti samo za nove datoteke sigurnosne kopije. Jeste li sigurni da želite promijeniti lozinku za sigurnosnu kopiju?", + + "enter_backup_password" : "Unesite svoju lozinku za sigurnosnu kopiju ovdje", + "select_backup_file" : "Odaberite datoteku sigurnosne kopije", + "import" : "Uvezi", + "please_select_backup_file" : "Molimo odaberite datoteku sigurnosne kopije i unesite lozinku za sigurnosnu kopiju.", + + "fixed_rate" : "Fiksna stopa", + "fixed_rate_alert" : "Moći ćete unijeti iznos koji želite primiti nakon što označite način rada fiksne stope. Želite li se prebaciti na način rada fiksne stope?", + + "xlm_extra_info" : "Molimo ne zaboravite navesti memo ID prilikom slanja XLM transakcije na razmjenu", + "xrp_extra_info" : "Molimo ne zaboravite navesti odredišnu oznaku prilikom slanja XRP transakcije na razmjenu", + + "exchange_incorrect_current_wallet_for_xmr" : "Ako želite razmijeniti XMR s vlastitog Monero računa na Cake Wallet novčaniku, molimo prvo se prebacite na svoj Monero novčanik.", + "confirmed" : "Potvrđeno", + "unconfirmed" : "Nepotvrđeno", + "displayable" : "Dostupno za prikaz", + + "submit_request" : "podnesi zahtjev", + + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" +} \ No newline at end of file diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb new file mode 100644 index 000000000..e8c4da26d --- /dev/null +++ b/res/values/strings_it.arb @@ -0,0 +1,478 @@ +{ + "welcome" : "Benvenuto", + "cake_wallet" : "Cake Wallet", + "first_wallet_text" : "Fantastico portafoglio per Monero e Bitcoin", + "please_make_selection" : "Gentilmente seleziona se vuoi generare o recuperare il tuo portafoglio.", + "create_new" : "Genera nuovo Portafoglio", + "restore_wallet" : "Recupera Portafoglio", + + + "accounts" : "Accounts", + "edit" : "Modifica", + "account" : "Account", + "add" : "Aggiungi", + + + "address_book" : "Rubrica indirizzi", + "contact" : "Contatta", + "please_select" : "Gentilmente seleziona:", + "cancel" : "Cancella", + "ok" : "OK", + "contact_name" : "Nome Contatto", + "reset" : "Resetta", + "save" : "Salva", + "address_remove_contact" : "Rimuovi contatto", + "address_remove_content" : "Sei sicuro di voler eliminare il contatto selezionato?", + + + "authenticated" : "Autenticato", + "authentication" : "Autenticazione", + "failed_authentication" : "Autenticazione fallita. ${state_error}", + + + "wallet_menu" : "Menu", + "Blocks_remaining" : "${status} Blocchi Rimanenti", + "please_try_to_connect_to_another_node" : "Gentilmente prova a connetterti ad un altro nodo", + "xmr_hidden" : "Nascosto", + "xmr_available_balance" : "Saldo Disponibile", + "xmr_full_balance" : "Saldo Completo", + "send" : "Invia", + "receive" : "Ricevi", + "transactions" : "Transazioni", + "incoming" : "In arrivo", + "outgoing" : "In uscita", + "transactions_by_date" : "Transazioni per data", + "trades" : "Scambi", + "filters" : "Filtri", + "today" : "Oggi", + "yesterday" : "Ieri", + "received" : "Ricevuto", + "sent" : "Inviato", + "pending" : " (pendente)", + "rescan" : "Scansiona di nuovo", + "reconnect" : "Riconnetti", + "wallets" : "Portafogli", + "show_seed" : "Mostra seme", + "show_keys" : "Mostra seme/chiavi", + "address_book_menu" : "Rubrica indirizzi", + "reconnection" : "Riconnessione", + "reconnect_alert_text" : "Sei sicuro di volerti riconnettere?", + + + "exchange" : "Scambia", + "clear" : "Pulisci", + "refund_address" : "Indirizzo di rimborso", + "change_exchange_provider" : "Cambia Exchange", + "you_will_send" : "Conveti da", + "you_will_get" : "Converti a", + "amount_is_guaranteed" : "L'ammonare da ricevere è fissato", + "amount_is_estimate" : "L'ammontare da ricevere è una stima", + "powered_by" : "Sviluppato da ${title}", + "error" : "Errore", + "estimated" : "Stimato", + "min_value" : "Min: ${value} ${currency}", + "max_value" : "Max: ${value} ${currency}", + "change_currency" : "Cambia Moneta", + + + "copy_id" : "Copia ID", + "exchange_result_write_down_trade_id" : "Gentilmente fai una copia o trascrivi l'ID dello scambio per continuare.", + "trade_id" : "ID Scambio:", + "copied_to_clipboard" : "Copiato negli Appunti", + "saved_the_trade_id" : "Ho salvato l'ID dello scambio", + "fetching" : "Recupero", + "id" : "ID: ", + "amount" : "Ammontare: ", + "payment_id" : "ID Pagamento: ", + "status" : "Stato: ", + "offer_expires_in" : "Offerta termina tra: ", + "trade_is_powered_by" : "Questo scambio è fornito da ${provider}", + "copy_address" : "Copia Indirizzo", + "exchange_result_confirm" : "Cliccando su Conferma, invierai ${fetchingLabel} ${from} dal tuo portafoglio chiamato ${walletName} all'indirizzo mostrato qui in basso. O puoi inviare dal tuo portafoglio esterno all'indirizzo/codice QR mostrato in basso.\n\nGentilmente clicca su Conferma per continuare o torna indietro per cambiare l'ammontare.", + "exchange_result_description" : "Devi inviare un minimo di ${fetchingLabel} ${from} all'indirizzo mostrato nella pagina seguente. Se invii un ammontare inferiore a ${fetchingLabel} ${from} la conversione potrebbe non andare a buon fine e l'indirizzo potrebbe non essere rimborsato.", + "exchange_result_write_down_ID" : "*Gentilmente fai una copia o trascrivi il tuo ID mostrato in alto.", + "confirm" : "Conferma", + "confirm_sending" : "Conferma l'invio", + "commit_transaction_amount_fee" : "Invia transazione\nAmmontare: ${amount}\nCommissione: ${fee}", + "sending" : "Invio", + "transaction_sent" : "Transazione inviata!", + "expired" : "Scaduta", + "time" : "${minutes}m ${seconds}s", + "send_xmr" : "Invia XMR", + "exchange_new_template" : "Nuovo modello", + + "faq" : "Domande Frequenti", + + + "enter_your_pin" : "Inserisci il tuo PIN", + "loading_your_wallet" : "Caricamento portafoglio", + + + "new_wallet" : "Nuovo Portafoglio", + "wallet_name" : "Nome del Portafoglio", + "continue_text" : "Continua", + "choose_wallet_currency" : "Gentilmente scegli la moneta del portafoglio:", + + + "node_new" : "Nuovo Nodo", + "node_address" : "Indirizzo Nodo", + "node_port" : "Porta Nodo", + "login" : "Accedi", + "password" : "Password", + "nodes" : "Nodi", + "node_reset_settings_title" : "Ripristina impostazioni", + "nodes_list_reset_to_default_message" : "Sei sicuro di voler ripristinare le impostazioni predefinite?", + "change_current_node" : "Sei sicuro di voler cambiare il nodo corrente con ${node}?", + "change" : "Cambia", + "remove_node" : "Rimuovi nodo", + "remove_node_message" : "Sei sicuro di voler rimuovere il nodo selezionato?", + "remove" : "Remuovi", + "delete" : "Elimina", + "add_new_node" : "Aggiungi nuovo nodo", + "change_current_node_title" : "Cambia nodo corrente", + "node_test" : "Test", + "node_connection_successful" : "Connessione avvenuta con successo", + "node_connection_failed" : "Connessione fallita", + "new_node_testing" : "Test novo nodo", + + + "use" : "Passa a ", + "digit_pin" : "-cifre PIN", + + + "share_address" : "Condividi indirizzo", + "receive_amount" : "Ammontare", + "subaddresses" : "Sottoindirizzi", + "addresses" : "Indirizzi", + "scan_qr_code" : "Scansiona il codice QR per ottenere l'indirizzo", + "rename" : "Rinomina", + "choose_account" : "Scegli account", + "create_new_account" : "Crea nuovo account", + "accounts_subaddresses" : "Accounts e sottoindirizzi", + + + "restore_restore_wallet" : "Recupera Portafoglio", + "restore_title_from_seed_keys" : "Recupera dal seme/chiavi", + "restore_description_from_seed_keys" : "Recupera il tuo portafoglio dal seme/chiavi che hai salvato in un posto sicuro", + "restore_next" : "Prossimo", + "restore_title_from_backup" : "Recupera da backup", + "restore_description_from_backup" : "Puoi recuperare l'app Cake Wallet per intero dal tuo file di backup", + "restore_seed_keys_restore" : "Recupera Seme/Chiavi", + "restore_title_from_seed" : "Recupera dal seme", + "restore_description_from_seed" : "Recupera il tuo portafoglio da una combinazione di 25 o 13 parole", + "restore_title_from_keys" : "Recupera dalle chiavi", + "restore_description_from_keys" : "Recupera il tuo portafoglio da una sequenza di caratteri generati dalle tue chiavi private", + "restore_wallet_name" : "Nome Portafoglio", + "restore_address" : "Indirizzo", + "restore_view_key_private" : "Chiave di Visualizzazione (privata)", + "restore_spend_key_private" : "Chiave di Spesa (privata)", + "restore_recover" : "Recupera", + "restore_wallet_restore_description" : "Descrizione recupero Portafoglio", + "restore_new_seed" : "Nuovo seme", + "restore_active_seed" : "Seme attivo", + "restore_bitcoin_description_from_seed" : "Recupera il tuo portafoglio da una combinazione di 12 parole", + "restore_bitcoin_description_from_keys" : "Recupera il tuo portafoglio da una stringa WIF generata dalle tue chiavi private", + "restore_bitcoin_title_from_keys" : "Recupera da WIF", + "restore_from_date_or_blockheight" : "Gentilmente inserisci la data di un paio di giorni prima che hai creato questo portafoglio. Oppure inserisci l'altezza del blocco se la conosci", + + + "seed_reminder" : "Gentilmente trascrivi le parole. Ti tornerà utie in caso perdessi o ripristinassi il tuo telefono", + "seed_title" : "Seme", + "seed_share" : "Condividi seme", + "copy" : "Copia", + + + "seed_language_choose" : "Gentilmente scegli la lingua del seme:", + "seed_choose" : "Scegli la lingua del seme", + "seed_language_next" : "Prossimo", + "seed_language_english" : "Inglese", + "seed_language_chinese" : "Cinese", + "seed_language_dutch" : "Olandese", + "seed_language_german" : "Tedesco", + "seed_language_japanese" : "Giapponese", + "seed_language_portuguese" : "Portoghese", + "seed_language_russian" : "Russo", + "seed_language_spanish" : "Spagnolo", + + + "send_title" : "Invia", + "send_your_wallet" : "Il tuo portafoglio", + "send_address" : "${cryptoCurrency} indirizzo", + "send_payment_id" : "ID Pagamento (opzionale)", + "all" : "TUTTO", + "send_error_minimum_value" : "L'ammontare minimo è 0.01", + "send_error_currency" : "L'ammontare può contenere solo numeri", + "send_estimated_fee" : "Commissione stimata:", + "send_priority" : "Attualmente la commissione è impostata a priorità ${transactionPriority} .\nLa priorità della transazione può essere modificata nelle impostazioni", + "send_creating_transaction" : "Creazione della transazione", + "send_templates" : "Modelli", + "send_new" : "Nuovo", + "send_amount" : "Ammontare:", + "send_fee" : "Commissione:", + "send_name" : "Nome", + "send_got_it" : "Ho capito", + "send_sending" : "Invio...", + "send_success" : " ${crypto} inviati con successo", + + + "settings_title" : "Impostazioni", + "settings_nodes" : "Nodi", + "settings_current_node" : "Nodo attuale", + "settings_wallets" : "Portafogli", + "settings_display_balance_as" : "Mostra saldo come", + "settings_currency" : "Moneta", + "settings_fee_priority" : "Priorità commissione", + "settings_save_recipient_address" : "Salva indirizzo di destinazione", + "settings_personal" : "Personali", + "settings_change_pin" : "Cambia PIN", + "settings_change_language" : "Cambia lingua", + "settings_allow_biometrical_authentication" : "Consenti autenticazione biometrica", + "settings_dark_mode" : "Tema scuro", + "settings_transactions" : "Transazioni", + "settings_trades" : "Scambi", + "settings_display_on_dashboard_list" : "Mostra nella lista della pagina principale", + "settings_all" : "TUTTO", + "settings_only_trades" : "Solo scambi", + "settings_only_transactions" : "Solo transazioni", + "settings_none" : "Nessuno", + "settings_support" : "Supporto", + "settings_terms_and_conditions" : "Termini e condizioni", + "pin_is_incorrect" : "Il PIN non è corretto", + + + "setup_pin" : "Imposta PIN", + "enter_your_pin_again" : "Inserisci il tuo pin di nuovo", + "setup_successful" : "Il tuo PIN è stato impostato con successo!", + + + "wallet_keys" : "Seme Portafoglio /chiavi", + "wallet_seed" : "Seme Portafoglio", + "private_key" : "Chiave privata", + "public_key" : "Chiave pubblica", + "view_key_private" : "Chiave di visualizzazione (privata)", + "view_key_public" : "Chiave di visualizzazione (pubblica)", + "spend_key_private" : "Chiave di spesa (privata)", + "spend_key_public" : "Chiave di spesa (pubblica)", + "copied_key_to_clipboard" : " ${key} copiata negli Appunti", + + + "new_subaddress_title" : "Nuovo indirizzo", + "new_subaddress_label_name" : "Nome etichetta", + "new_subaddress_create" : "Crea", + + + "subaddress_title" : "Lista sottoindirizzi", + + + "trade_details_title" : "Dettagli Scambio", + "trade_details_id" : "ID", + "trade_details_state" : "Stato", + "trade_details_fetching" : "Recupero", + "trade_details_provider" : "Fornitore", + "trade_details_created_at" : "Creato alle", + "trade_details_pair" : "Coppia", + "trade_details_copied" : "${title} copiati negli Appunti", + + + "trade_history_title" : "Storico scambi", + + + "transaction_details_title" : "Dettagli Transazione", + "transaction_details_transaction_id" : "ID Transazione", + "transaction_details_date" : "Data", + "transaction_details_height" : "Altezza", + "transaction_details_amount" : "Ammontare", + "transaction_details_fee" : "Commissione", + "transaction_details_copied" : "${title} copiati negli Appunti", + "transaction_details_recipient_address" : "Indirizzo destinatario", + + + "wallet_list_title" : "Portafoglio Monero", + "wallet_list_create_new_wallet" : "Crea Nuovo Portafoglio", + "wallet_list_restore_wallet" : "Recupera Portafoglio", + "wallet_list_load_wallet" : "Caricamento Portafoglio", + "wallet_list_loading_wallet" : "Caricamento portafoglio ${wallet_name}", + "wallet_list_failed_to_load" : "Caricamento portafoglio ${wallet_name} fallito. ${error}", + "wallet_list_removing_wallet" : "Rimozione portafoglio ${wallet_name}", + "wallet_list_failed_to_remove" : "Rimozione portafoglio ${wallet_name} fallita. ${error}", + + + "widgets_address" : "Indirizzo", + "widgets_restore_from_blockheight" : "Recupera da altezza blocco", + "widgets_restore_from_date" : "Recupera da data", + "widgets_or" : "o", + "widgets_seed" : "Seme", + + + "router_no_route" : "Nessun percorso definito per ${name}", + + + "error_text_account_name" : "Il nome dell'Account può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", + "error_text_contact_name" : "Il nome del Contatto non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 32 caratteri", + "error_text_address" : "L'indirizzo del Portafoglio deve corrispondere alla tipologia\ndi criptovaluta", + "error_text_node_address" : "Gentilmente inserisci un indirizzo iPv4", + "error_text_node_port" : "La porta del nodo può contenere solo numeri compresi tra 0 e 65535", + "error_text_payment_id" : "l'ID del pagamento può contenere solo da 16 a 64 caratteri in hex", + "error_text_xmr" : "Il valore XMR non può eccedere il saldo disponibile.\nIl numero delle cifre decimali deve essere inferiore o uguale a 12", + "error_text_fiat" : "L'ammontare non può eccedere il saldo dispoinibile.\nIl numero di cifre decimali deve essere inferiore o uguale a 2", + "error_text_subaddress_name" : "Il nome del sottoindirizzo non può contenere i simboli ` , ' \" \ne deve avere una lunghezza compresa tra 1 e 20 caratteri", + "error_text_amount" : "L'ammontare può contenere solo numeri", + "error_text_wallet_name" : "Il nome del portafoglio può contenere solo lettere, numeri\ne deve avere una lunghezza compresa tra 1 e 15 caratteri", + "error_text_keys" : "Le chiavi del portafoglio possono contenere solo 64 caratteri in hex", + "error_text_crypto_currency" : "Il numero delle cifre decimali\ndeve essere inferiore o uguale a 12", + "error_text_minimal_limit" : "Lo scambio per ${provider} non è stato creato. L'ammontare è inferiore al minimo: ${min} ${currency}", + "error_text_maximum_limit" : "Lo scambio per ${provider} non è stato creato. L'ammontare è superiore al massimo: ${max} ${currency}", + "error_text_limits_loading_failed" : "Lo scambio per ${provider} non è stato creato. Caricamento dei limiti fallito", + "error_text_template" : "Il nome del modello e l'indirizzo non possono contenere i simboli ` , ' \" \ne devono avere una lunghezza compresa tra 1 e 106 caratteri", + + + "auth_store_ban_timeout" : "ban_timeout", + "auth_store_banned_for" : "Bannato per ", + "auth_store_banned_minutes" : " minuti", + "auth_store_incorrect_password" : "PIN non corretto", + "wallet_store_monero_wallet" : "Portafoglio Monero", + "wallet_restoration_store_incorrect_seed_length" : "Lunghezza seme non corretta", + + + "full_balance" : "Saldo Completo", + "available_balance" : "Saldo Disponibile", + "hidden_balance" : "Saldo Nascosto", + + + "sync_status_syncronizing" : "SINCRONIZZAZIONE", + "sync_status_syncronized" : "SINCRONIZZATO", + "sync_status_not_connected" : "NON CONNESSO", + "sync_status_starting_sync" : "INIZIO SINC", + "sync_status_failed_connect" : "DISCONNESSO", + "sync_status_connecting" : "CONNESSIONE", + "sync_status_connected" : "CONNESSO", + + + "transaction_priority_slow" : "Bassa", + "transaction_priority_regular" : "Regolare", + "transaction_priority_medium" : "Media", + "transaction_priority_fast" : "Alta", + "transaction_priority_fastest" : "Massima", + + + "trade_for_not_created" : "Lo scambio per ${title} non è stato creato.", + "trade_not_created" : "Scambio non creato.", + "trade_id_not_found" : "Scambio ${tradeId} di ${title} not trovato.", + "trade_not_found" : "Scambio non trovato.", + + + "trade_state_pending" : "In corso", + "trade_state_confirming" : "Conferma", + "trade_state_trading" : "Scambio", + "trade_state_traded" : "Scambiato", + "trade_state_complete" : "Completato", + "trade_state_to_be_created" : "Da creare", + "trade_state_unpaid" : "Non pagato", + "trade_state_underpaid" : "Sottopagato", + "trade_state_paid_unconfirmed" : "Pagato non confermato", + "trade_state_paid" : "Pagato", + "trade_state_btc_sent" : "Btc inviati", + "trade_state_timeout" : "Timeout", + "trade_state_created" : "Creato", + "trade_state_finished" : "Finito", + + "change_language" : "Cambia lingua", + "change_language_to" : "Cambia lingua in ${language}?", + + "paste" : "Incolla", + "restore_from_seed_placeholder" : "Gentilmente inserisci o incolla il tuo seme qui", + "add_new_word" : "Aggiungi nuova parola", + "incorrect_seed" : "Il testo inserito non è valido.", + + "biometric_auth_reason" : "Scansiona la tua impronta per autenticarti", + "version" : "Versione ${currentVersion}", + + "openalias_alert_title" : "XMR Destinatario Rilevato", + "openalias_alert_content" : "Invierai i tuoi fondi a\n${recipient_name}", + + "card_address" : "Indirizzo:", + "buy" : "Compra", + + "placeholder_transactions" : "Le tue transazioni saranno mostrate qui", + "placeholder_contacts" : "I tuoi contatti saranno mostrati qui", + + "template" : "Modello", + "confirm_delete_template" : "Questa azione cancellerà questo modello. Desideri continuare?", + "confirm_delete_wallet" : "Questa azione cancellerà questo portafoglio. Desideri continuare?", + + "picker_description" : "Per scegliere ChangeNOW o MorphToken, gentilmente cambia prima la tua coppia di valute", + + "change_wallet_alert_title" : "Cambia portafoglio attuale", + "change_wallet_alert_content" : "Sei sicuro di voler cambiare il portafoglio attuale con ${wallet_name}?", + + "creating_new_wallet" : "Creazione nuovo portafoglio", + "creating_new_wallet_error" : "Errore: ${description}", + + "seed_alert_title" : "Attenzione", + "seed_alert_content" : "Il seme è l'unico modo per recuperare il tuo portafoglio. L'hai trascritto?", + "seed_alert_back" : "Torna indietro", + "seed_alert_yes" : "Sì, l'ho fatto", + + "exchange_sync_alert_content" : "Gentilmente aspetta che il tuo portafoglio sia sincronizzato", + + "pre_seed_title" : "IMPORTANTE", + "pre_seed_description" : "Nella pagina seguente ti sarà mostrata una serie di parole ${words}. Questo è il tuo seme unico e privato ed è l'UNICO modo per recuperare il tuo portafoglio in caso di perdita o malfunzionamento. E' TUA responsabilità trascriverlo e conservarlo in un posto sicuro fuori dall'app Cake Wallet.", + "pre_seed_button_text" : "Ho capito. Mostrami il seme", + + "xmr_to_error" : "XMR.TO errore", + "xmr_to_error_description" : "Ammontare invalido. Il limite massimo è 8 cifre dopo il punto decimale", + + "provider_error" : "${provider} errore", + + "use_ssl" : "Usa SSL", + + "color_theme" : "Colore tema", + "light_theme" : "Bianco", + "bright_theme" : "Colorato", + "dark_theme" : "Scuro", + "enter_your_note" : "Inserisci la tua nota…", + "note_optional" : "Nota (opzionale)", + "note_tap_to_change" : "Nota (clicca per cambiare)", + "transaction_key" : "Chiave Transazione", + "confirmations" : "Conferme", + "recipient_address" : "Indirizzo di destinazione", + + "extra_id" : "Extra ID:", + "destination_tag" : "Tag destinazione:", + "memo" : "Memo:", + + "backup" : "Backup", + "change_password" : "Cambia password", + "backup_password" : "Backup password", + "write_down_backup_password" : "Gentilmente trascrivi la password del backup, che è usata per importare i tuoi file di backup.", + "export_backup" : "Esporta backup", + "save_backup_password" : "Gentilmente assicurati di aver salvato la password del tuo backup. Senza questa non sarai in grado di importare i tuoi file di backup.", + "backup_file" : "Backup file", + + "edit_backup_password" : "Modifica Password Backup", + "save_backup_password_alert" : "Salva password Backup", + "change_backup_password_alert" : "I precedenti file di backup non potranno essere importati con la nuova password di backup. La nuova password di backup verrà usata soltanto per i nuovi file di backup. Sei sicuro di voler cambiare la tua password di backup?", + + "enter_backup_password" : "Inserisci la password di backup qui", + "select_backup_file" : "Seleziona file di backup", + "import" : "Importa", + "please_select_backup_file" : "Gentilmente seleziona il file di backup e inserisci la password di backup.", + + "fixed_rate" : "Tasso fisso", + "fixed_rate_alert" : "Potrai inserire l'ammontare da ricevere quando il tasso è fisso. Vuoi cambiare alla modalità tasso fisso?", + + "xlm_extra_info" : "Gentilmente ricorda di indicare il Memo ID quando invii la transazione XLM per lo scambio", + "xrp_extra_info" : "Gentilmente ricorda di indicare il Tag di Destinazione quando invii una transazione XRP per lo scambio", + + "exchange_incorrect_current_wallet_for_xmr" : "Se vuoi scambiare XMR dal tuo saldo Cake Wallet Monero, gentilmente passa al tuo portafoglio Monero.", + "confirmed" : "Confermato", + "unconfirmed" : "Non confermato", + "displayable" : "Visualizzabile", + + "submit_request" : "invia una richiesta", + + "buy_alert_content" : "Currently we only support the purchase of Bitcoin. To buy Bitcoin, please create or switch to your Bitcoin wallet", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" +} \ No newline at end of file diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 89556602a..5fb875578 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "ウォレットの復元", "restore_new_seed" : "新しい種", "restore_active_seed" : "アクティブシード", - "restore_bitcoin_description_from_seed" : "12ワードの組み合わせコードからウォレットを復元する", + "restore_bitcoin_description_from_seed" : "24ワードの組み合わせコードからウォレットを復元する", "restore_bitcoin_description_from_keys" : "秘密鍵から生成されたWIF文字列からウォレットを復元します", "restore_bitcoin_title_from_keys" : "WIFから復元", "restore_from_date_or_blockheight" : "このウォレットを作成する数日前に日付を入力してください。 または、ブロックの高さがわかっている場合は、代わりに入力してください", @@ -471,6 +471,11 @@ "submit_request" : "リクエストを送信する", + "buy_alert_content" : "現在、ビットコインの購入のみをサポートしています。 ビットコインを購入するには、ビットコインウォレットを作成するか切り替えてください", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "アドレスが検出されました", "address_from_domain" : "あなたはからアドレスを得ました unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 9433a4189..0d64ea4fc 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "월렛 복원 설명", "restore_new_seed" : "새로운 씨앗", "restore_active_seed" : "활성 종자", - "restore_bitcoin_description_from_seed" : "12 단어 조합 코드에서 지갑 복원", + "restore_bitcoin_description_from_seed" : "24 단어 조합 코드에서 지갑 복원", "restore_bitcoin_description_from_keys" : "개인 키에서 생성 된 WIF 문자열에서 지갑 복원", "restore_bitcoin_title_from_keys" : "WIF에서 복원", "restore_from_date_or_blockheight" : "이 지갑을 생성하기 며칠 전에 날짜를 입력하십시오. 또는 블록 높이를 알고있는 경우 대신 입력하십시오.", @@ -471,6 +471,11 @@ "submit_request" : "요청을 제출", + "buy_alert_content" : "현재 우리는 비트 코인 구매 만 지원합니다. 비트 코인을 구매하려면 비트 코인 지갑을 생성하거나 전환하십시오", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand" + "address_detected" : "주소 감지", "address_from_domain" : "주소는 unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 8b8bef453..eba15b0ca 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Portemonnee-herstelbeschrijving", "restore_new_seed" : "Nieuw zaad", "restore_active_seed" : "Actief zaad", - "restore_bitcoin_description_from_seed" : "Herstel uw portemonnee met een combinatiecode van 12 woorden", + "restore_bitcoin_description_from_seed" : "Herstel uw portemonnee met een combinatiecode van 24 woorden", "restore_bitcoin_description_from_keys" : "Herstel uw portemonnee van de gegenereerde WIF-string van uw privésleutels", "restore_bitcoin_title_from_keys" : "Herstel van WIF", "restore_from_date_or_blockheight" : "Voer een datum in een paar dagen voordat u deze portemonnee heeft gemaakt. Of als u de blokhoogte kent, voert u deze in", @@ -471,6 +471,11 @@ "submit_request" : "een verzoek indienen", + "buy_alert_content" : "Momenteel ondersteunen we alleen de aankoop van Bitcoin. Om Bitcoin te kopen, moet u uw Bitcoin-portemonnee aanmaken of naar uw Bitcoin-portemonnee overschakelen", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", + "address_detected" : "Adres gedetecteerd", "address_from_domain" : "Je adres is van unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index b5291e227..02a16c17a 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Opis przywracania portfela", "restore_new_seed" : "Nowe nasienie", "restore_active_seed" : "Aktywne nasiona", - "restore_bitcoin_description_from_seed" : "Przywróć swój portfel z kodu złożonego z 12 słów", + "restore_bitcoin_description_from_seed" : "Przywróć swój portfel z kodu złożonego z 24 słów", "restore_bitcoin_description_from_keys" : "Przywróć swój portfel z wygenerowanego ciągu WIF z kluczy prywatnych", "restore_bitcoin_title_from_keys" : "Przywróć z WIF", "restore_from_date_or_blockheight" : "Wprowadź datę na kilka dni przed utworzeniem tego portfela. Lub jeśli znasz wysokość bloku, wprowadź go zamiast tego", @@ -471,6 +471,11 @@ "submit_request" : "złożyć wniosek", + "buy_alert_content" : "Obecnie obsługujemy tylko zakup Bitcoinów. Aby kupić Bitcoin, utwórz lub przełącz się na swój portfel Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", + "address_detected" : "Wykryto adres", "address_from_domain" : "Dostałeś adres od unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8e955310f..9bf5d59ac 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Restauração da carteira", "restore_new_seed" : "Nova semente", "restore_active_seed" : "Semente ativa", - "restore_bitcoin_description_from_seed" : "Restaure sua carteira a partir de um código de combinação de 12 palavras", + "restore_bitcoin_description_from_seed" : "Restaure sua carteira a partir de um código de combinação de 24 palavras", "restore_bitcoin_description_from_keys" : "Restaure sua carteira a partir da string WIF gerada de suas chaves privadas", "restore_bitcoin_title_from_keys" : "Restaurar de WIF", "restore_from_date_or_blockheight" : "Insira uma data alguns dias antes de criar esta carteira. Ou se você souber a altura do bloco, insira-o", @@ -471,6 +471,11 @@ "submit_request" : "enviar um pedido", + "buy_alert_content" : "Atualmente, apoiamos apenas a compra de Bitcoin. Para comprar Bitcoin, crie ou mude para sua carteira Bitcoin", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", + "address_detected" : "Endereço detectado", "address_from_domain" : "Você obteve o endereço de unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 142ae0dd1..5658c96c1 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Описание восстановления кошелька", "restore_new_seed" : "Новая мнемоническая фраза", "restore_active_seed" : "Активная мнемоническая фраза", - "restore_bitcoin_description_from_seed" : "Вы можете восстановить кошелёк используя 12-ти значную мнемоническую фразу", + "restore_bitcoin_description_from_seed" : "Вы можете восстановить кошелёк используя 24-ти значную мнемоническую фразу", "restore_bitcoin_description_from_keys" : "Вы можете восстановить кошелёк с помощью WIF", "restore_bitcoin_title_from_keys" : "Восстановить с помощью WIF", "restore_from_date_or_blockheight" : "Пожалуйста, введите дату за несколько дней до создания этого кошелька. Или, если вы знаете высоту блока, введите ее значение", @@ -471,6 +471,11 @@ "submit_request" : "отправить запрос", + "buy_alert_content" : "В настоящее время мы поддерживаем только покупку Bitcoin. Чтобы купить Bitcoin, создайте или переключитесь на ваш Bitcoin кошелек", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", + "address_detected" : "Обнаружен адрес", "address_from_domain" : "Вы получили адрес из unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 6c659bcea..cfe1f6bee 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -170,7 +170,7 @@ "restore_wallet_restore_description" : "Опис відновлюваного гаманця", "restore_new_seed" : "Нова мнемонічна фраза", "restore_active_seed" : "Активна мнемонічна фраза", - "restore_bitcoin_description_from_seed" : "Ви можете відновити гаманець використовуючи 12-ти слівну мнемонічну фразу", + "restore_bitcoin_description_from_seed" : "Ви можете відновити гаманець використовуючи 24-ти слівну мнемонічну фразу", "restore_bitcoin_description_from_keys" : "Ви можете відновити гаманець за допомогою WIF", "restore_bitcoin_title_from_keys" : "Відновити за допомогою WIF", "restore_from_date_or_blockheight" : "Будь ласка, введіть дату за кілька днів до створення цього гаманця. Або, якщо ви знаєте висоту блоку, введіть її значення", @@ -471,6 +471,11 @@ "submit_request" : "надіслати запит", + "buy_alert_content" : "На даний час ми підтримуємо тільки покупку Bitcoin. Щоб купити Bitcoin, будь ласка, створіть або переключіться на ваш Bitcoin гаманець", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", + "address_detected" : "Виявлено адресу", "address_from_domain" : "Ви отримали адресу від unstoppable domain ${domain}" } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index f1560941e..ce831a4b3 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -1,28 +1,28 @@ { - "welcome" : "歡迎來到", + "welcome" : "欢迎使用", "cake_wallet" : "Cake Wallet", - "first_wallet_text" : "很棒的Monero和比特幣錢包", + "first_wallet_text" : "很棒的门罗币和比特币钱包", "please_make_selection" : "请在下面进行选择 创建或恢复您的钱包.", "create_new" : "创建新钱包", "restore_wallet" : "恢复钱包", - "accounts" : "帐目", + "accounts" : "账户", "edit" : "编辑", "account" : "帐户", - "add" : "加", + "add" : "添加", "address_book" : "地址簿", "contact" : "联系", "please_select" : "请选择:", "cancel" : "取消", - "ok" : "好", + "ok" : "确认", "contact_name" : "联系人姓名", - "reset" : "重启", + "reset" : "重置", "save" : "保存", - "address_remove_contact" : "刪除聯繫人", - "address_remove_content" : "您確定要刪除所選的聯繫人嗎?", + "address_remove_contact" : "刪除联系人", + "address_remove_content" : "您确定要刪除所选的联系人吗?", "authenticated" : "已认证", @@ -32,18 +32,18 @@ "wallet_menu" : "钱包菜单", "Blocks_remaining" : "${status} 剩余的块", - "please_try_to_connect_to_another_node" : "请尝试连接到另一个节点", - "xmr_hidden" : "隱", + "please_try_to_connect_to_another_node" : "请尝试连接到其他节点", + "xmr_hidden" : "隐藏", "xmr_available_balance" : "可用余额 ", "xmr_full_balance" : "全部余额", "send" : "发送", "receive" : "接收", - "transactions" : "交易次数", - "incoming" : "传入", - "outgoing" : "外向", + "transactions" : "交易情况", + "incoming" : "收到", + "outgoing" : "发送", "transactions_by_date" : "按日期交易", "trades" : "交易", - "filters" : "過濾", + "filters" : "过滤", "today" : "今天", "yesterday" : "昨天", "received" : "已收到", @@ -51,67 +51,67 @@ "pending" : " (待定)", "rescan" : "重新扫描", "reconnect" : "重新连接", - "wallets" : "皮夹", + "wallets" : "钱包", "show_seed" : "显示种子", - "show_keys" : "顯示種子/密鑰", + "show_keys" : "显示种子/密钥", "address_book_menu" : "地址簿", - "reconnection" : "重新连线", + "reconnection" : "重新连接", "reconnect_alert_text" : "您确定要重新连接吗?", - "exchange" : "交换", - "clear" : "明确", + "exchange" : "兑换", + "clear" : "清空", "refund_address" : "退款地址", - "change_exchange_provider" : "更改交易所提供商", - "you_will_send" : "從轉換", - "you_will_get" : "轉換成", - "amount_is_guaranteed" : "接收金額有保證", + "change_exchange_provider" : "更改交易所", + "you_will_send" : "转换自", + "you_will_get" : "转换到", + "amount_is_guaranteed" : "保证收到的金额", "amount_is_estimate" : "收款金额为估算值", - "powered_by" : "供电 ${title}", + "powered_by" : "Powered by ${title}", "error" : "错误", - "estimated" : "估计的", - "min_value" : "敏: ${value} ${currency}", + "estimated" : "估计值", + "min_value" : "最低: ${value} ${currency}", "max_value" : "最高: ${value} ${currency}", "change_currency" : "更改币种", - "copy_id" : "复印ID", + "copy_id" : "复制ID", "exchange_result_write_down_trade_id" : "请复制或写下交易编号以继续.", - "trade_id" : "贸易编号:", + "trade_id" : "交易编号:", "copied_to_clipboard" : "复制到剪贴板", - "saved_the_trade_id" : "我已经保存了交易ID", - "fetching" : "正在取得", + "saved_the_trade_id" : "我已经保存了交易编号", + "fetching" : "正在获取", "id" : "ID: ", - "amount" : "量: ", + "amount" : "金额: ", "payment_id" : "付款 ID: ", "status" : "状态: ", "offer_expires_in" : "优惠有效期至 ", "trade_is_powered_by" : "该交易由 ${provider}", "copy_address" : "复制地址", - "exchange_result_confirm" : "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到下面顯示的地址。 或者您可以從外部錢包發送到以下地址/ QR码。\n\n请按确认继续或返回以更改金额", - "exchange_result_description" : "您必須至少發送 ${fetchingLabel} ${from} 到下一頁上顯示的地址。 如果您發送的金額少於 ${fetchingLabel} ${from},則可能無法轉換,因此無法退還。", + "exchange_result_confirm" : "点击确认 您将发送 ${fetchingLabel} ${from} 从你的钱包里 ${walletName} 到下面显示的地址。 或者您可以从外部钱包发送到以下地址/ QR码。\n\n请按确认继续或返回以更改金额", + "exchange_result_description" : "您必須至少发送 ${fetchingLabel} ${from} 到下一页上显示的地址。 如果您发送的金额少于 ${fetchingLabel} ${from},则可能无法转换,因此无法退还。", "exchange_result_write_down_ID" : "*请复制或写下您上面显示的ID.", "confirm" : "确认", "confirm_sending" : "确认发送", - "commit_transaction_amount_fee" : "提交交易\n量: ${amount}\nFee: ${fee}", + "commit_transaction_amount_fee" : "提交交易\n金额: ${amount}\n手续费: ${fee}", "sending" : "正在发送", "transaction_sent" : "交易已发送", "expired" : "已过期", "time" : "${minutes}m ${seconds}s", "send_xmr" : "发送 XMR", - "exchange_new_template" : "新範本", + "exchange_new_template" : "新模板", "faq" : "FAQ", "enter_your_pin" : "输入密码", - "loading_your_wallet" : "装钱包", + "loading_your_wallet" : "加载您的钱包", "new_wallet" : "新钱包", "wallet_name" : "钱包名称", "continue_text" : "继续", - "choose_wallet_currency" : "請選擇錢包貨幣:", + "choose_wallet_currency" : "请选择钱包货币:", "node_new" : "新节点", @@ -128,27 +128,27 @@ "remove_node_message" : "您确定要删除所选节点吗?", "remove" : "去掉", "delete" : "删除", - "add_new_node" : "添加新節點", - "change_current_node_title" : "更改當前節點", - "node_test" : "測試", - "node_connection_successful" : "連接成功", - "node_connection_failed" : "連接失敗", - "new_node_testing" : "新節點測試", + "add_new_node" : "添加新节点", + "change_current_node_title" : "更改当前节点", + "node_test" : "测试", + "node_connection_successful" : "连接成功", + "node_connection_failed" : "连接失敗", + "new_node_testing" : "新节点测试", - "use" : "採用 ", - "digit_pin" : "数字别针", + "use" : "使用 ", + "digit_pin" : "位 PIN", "share_address" : "分享地址", - "receive_amount" : "量", + "receive_amount" : "金额", "subaddresses" : "子地址", "addresses" : "地址", - "scan_qr_code" : "掃描二維碼獲取地址", - "rename" : "改名", - "choose_account" : "選擇帳號", - "create_new_account" : "建立新帳戶", - "accounts_subaddresses" : "帳戶和子地址", + "scan_qr_code" : "扫描二维码获取地址", + "rename" : "重命名", + "choose_account" : "选择账户", + "create_new_account" : "建立新账户", + "accounts_subaddresses" : "账户和子地址", "restore_restore_wallet" : "恢复钱包", @@ -161,41 +161,40 @@ "restore_title_from_seed" : "从种子还原", "restore_description_from_seed" : "从25个字中恢复您的钱包或13个字的组合码", "restore_title_from_keys" : "从密钥还原", - "restore_description_from_keys" : "R从生成的电子钱包从您的私钥中保存的击键", + "restore_description_from_keys" : "使用私钥恢复钱包", "restore_wallet_name" : "钱包名称", "restore_address" : "地址", - "restore_view_key_private" : "查看金钥 (私人的)", - "restore_spend_key_private" : "支出金钥 (私人的)", + "restore_view_key_private" : "View私钥", + "restore_spend_key_private" : "Spend私钥", "restore_recover" : "恢复", "restore_wallet_restore_description" : "钱包还原说明", - "restore_new_seed" : "新種子", - "restore_active_seed" : "活性種子", - "restore_bitcoin_description_from_seed" : "從12個單詞的組合碼恢復您的錢包", - "restore_bitcoin_description_from_keys" : "從私鑰中生成的WIF字符串還原您的錢包", - "restore_bitcoin_title_from_keys" : "從WIF還原", - "restore_from_date_or_blockheight" : "請在創建此錢包之前幾天輸入一個日期。 或者,如果您知道塊高,請改為輸入", + "restore_new_seed" : "新种子", + "restore_active_seed" : "活性种子", + "restore_bitcoin_description_from_seed" : "从24个文字的组成码恢复您的钱包", + "restore_bitcoin_description_from_keys" : "从私钥中生成的WIF字符串恢复您钱包", + "restore_bitcoin_title_from_keys" : "从WIF还原", + "restore_from_date_or_blockheight" : "请输入您创建这个钱包前几天的日期。或者如果您知道区块高度,请输入区块高度", - - "seed_reminder" : "請寫下這些,以防丟失或擦拭手機", + "seed_reminder" : "请记下这些内容,以防丟失或数据损坏", "seed_title" : "种子", "seed_share" : "分享种子", "copy" : "复制", - "seed_language_choose" : "請選擇種子語言:", - "seed_choose" : "選擇種子語言", + "seed_language_choose" : "请选择种子语言:", + "seed_choose" : "选择种子语言", "seed_language_next" : "下一个", - "seed_language_english" : "英語", + "seed_language_english" : "英文", "seed_language_chinese" : "中文", - "seed_language_dutch" : "荷蘭人", - "seed_language_german" : "德語", - "seed_language_japanese" : "日本", - "seed_language_portuguese" : "葡萄牙語", - "seed_language_russian" : "俄語", + "seed_language_dutch" : "荷兰文", + "seed_language_german" : "德文", + "seed_language_japanese" : "日文", + "seed_language_portuguese" : "葡萄牙文", + "seed_language_russian" : "俄文", "seed_language_spanish" : "西班牙文", - "send_title" : "發送", + "send_title" : "发送", "send_your_wallet" : "你的钱包", "send_address" : "${cryptoCurrency} 地址", "send_payment_id" : "付款编号 (可选的)", @@ -205,37 +204,37 @@ "send_estimated_fee" : "预估费用:", "send_priority" : "目前,费用设置为 ${transactionPriority} 优先.\n交易优先级可以在设置中进行调整", "send_creating_transaction" : "创建交易", - "send_templates" : "範本", - "send_new" : "新", - "send_amount" : "量:", - "send_fee" : "費用:", - "send_name" : "名稱", - "send_got_it" : "得到它了", - "send_sending" : "正在發送...", - "send_success" : "你${crypto}已成功發送", + "send_templates" : "模板", + "send_new" : "新建", + "send_amount" : "金额:", + "send_fee" : "手续费:", + "send_name" : "名称", + "send_got_it" : "明白", + "send_sending" : "正在发送...", + "send_success" : "您的${crypto}已成功发送", "settings_title" : "设定值", "settings_nodes" : "节点数", "settings_current_node" : "当前节点", - "settings_wallets" : "皮夹", + "settings_wallets" : "钱包", "settings_display_balance_as" : "将余额显示为", "settings_currency" : "货币", - "settings_fee_priority" : "费用优先", + "settings_fee_priority" : "交易优先级", "settings_save_recipient_address" : "保存收件人地址", "settings_personal" : "个人", "settings_change_pin" : "更改密码", - "settings_change_language" : "改变语言", - "settings_allow_biometrical_authentication" : "允许生物特征认证", - "settings_dark_mode" : "暗模式", - "settings_transactions" : "交易次数", + "settings_change_language" : "修改语言", + "settings_allow_biometrical_authentication" : "允许生物识别认证", + "settings_dark_mode" : "黑暗模式", + "settings_transactions" : "交易情况", "settings_trades" : "交易", "settings_display_on_dashboard_list" : "显示在仪表板上", "settings_all" : "所有", "settings_only_trades" : "只交易", "settings_only_transactions" : "仅交易", "settings_none" : "没有", - "settings_support" : "支持", + "settings_support" : "帮助", "settings_terms_and_conditions" : "条款和条件", "pin_is_incorrect" : "PIN码不正确", @@ -245,14 +244,14 @@ "setup_successful" : "您的PIN码已成功设置!", - "wallet_keys" : "錢包種子/鑰匙", - "wallet_seed" : "錢包種子", - "private_key" : "私鑰", - "public_key" : "公鑰", - "view_key_private" : "查看金钥 (私人的)", - "view_key_public" : "查看金钥 (public)", - "spend_key_private" : "支出金钥 (私人的)", - "spend_key_public" : "支出金钥 (public)", + "wallet_keys" : "钱包种子/密钥", + "wallet_seed" : "钱包种子", + "private_key" : "私钥", + "public_key" : "公钥", + "view_key_private" : "View 密钥(私钥)", + "view_key_public" : "View 密钥(公钥)", + "spend_key_private" : "Spend 密钥 (私钥)", + "spend_key_public" : "Spend 密钥 (公钥)", "copied_key_to_clipboard" : "复制 ${key} 到剪贴板", @@ -261,7 +260,7 @@ "new_subaddress_create" : "创建", - "subaddress_title" : "子地址清单", + "subaddress_title" : "子地址列表", "trade_details_title" : "交易明细", @@ -280,9 +279,9 @@ "transaction_details_title" : "交易明细", "transaction_details_transaction_id" : "交易编号", "transaction_details_date" : "日期", - "transaction_details_height" : "高度", - "transaction_details_amount" : "量", - "transaction_details_fee" : "費用", + "transaction_details_height" : "区块高度", + "transaction_details_amount" : "金额", + "transaction_details_fee" : "手续费", "transaction_details_copied" : "${title} 复制到剪贴板", "transaction_details_recipient_address" : "收件人地址", @@ -290,17 +289,17 @@ "wallet_list_title" : "Monero 钱包", "wallet_list_create_new_wallet" : "创建新钱包", "wallet_list_restore_wallet" : "恢复钱包", - "wallet_list_load_wallet" : "装入钱包", + "wallet_list_load_wallet" : "加载钱包", "wallet_list_loading_wallet" : "载入中 ${wallet_name} 钱包", "wallet_list_failed_to_load" : "加载失败 ${wallet_name} 钱包. ${error}", - "wallet_list_removing_wallet" : "拆下 ${wallet_name} 钱包", + "wallet_list_removing_wallet" : "删除 ${wallet_name} 钱包", "wallet_list_failed_to_remove" : "删除失败 ${wallet_name} 钱包. ${error}", "widgets_address" : "地址", "widgets_restore_from_blockheight" : "从块高还原", "widgets_restore_from_date" : "从日期还原", - "widgets_or" : "要么", + "widgets_or" : "或者", "widgets_seed" : "种子", @@ -310,7 +309,7 @@ "error_text_account_name" : "帐户名称只能包含字母数字\n且必须介于1到15个字符之间", "error_text_contact_name" : "联系人姓名不能包含`,' \" 符号\n并且必须介于1到32个字符之间", "error_text_address" : "钱包地址必须与类型对应\n加密货币", - "error_text_node_address" : "请输入一个iPv4地址", + "error_text_node_address" : "请输入一个IPv4地址", "error_text_node_port" : "节点端口只能包含0到65535之间的数字", "error_text_payment_id" : "付款ID只能包含16到64个字符(十六进制)", "error_text_xmr" : "XMR值不能超过可用余额.\n小数位数必须小于或等于12", @@ -320,10 +319,10 @@ "error_text_wallet_name" : "钱包名称只能包含字母,数字\n且必须介于1到15个字符之间", "error_text_keys" : "钱包密钥只能包含16个字符的十六进制字符", "error_text_crypto_currency" : "小数位数\n必须小于或等于12", - "error_text_minimal_limit" : "未創建 ${provider} 交易。 金額少於最小值:${min} ${currency}", - "error_text_maximum_limit" : "未創建 ${provider} 交易。 金額大於最大值:${max} ${currency}", - "error_text_limits_loading_failed" : "未創建 ${provider} 交易。 限制加載失敗", - "error_text_template" : "模板名稱和地址不能包含`,' \" 符号\n并且必须在1到106个字符之间", + "error_text_minimal_limit" : "未创建 ${provider} 交易。 金额小于最小值:${min} ${currency}", + "error_text_maximum_limit" : "未创建 ${provider} 交易。 金额大于最大值:${max} ${currency}", + "error_text_limits_loading_failed" : "未创建 ${provider} 交易。 限制加载失敗", + "error_text_template" : "模板名称和地址不能包含`,' \" 符号\n并且必须在1到106个字符之间", "auth_store_ban_timeout" : "禁止超时", @@ -339,32 +338,32 @@ "hidden_balance" : "隐藏余额", - "sync_status_syncronizing" : "同步化", + "sync_status_syncronizing" : "正在同步", "sync_status_syncronized" : "已同步", "sync_status_not_connected" : "未连接", "sync_status_starting_sync" : "开始同步", - "sync_status_failed_connect" : "斷線", + "sync_status_failed_connect" : "断线", "sync_status_connecting" : "连接中", - "sync_status_connected" : "连接的", + "sync_status_connected" : "已连接", - "transaction_priority_slow" : "慢", - "transaction_priority_regular" : "定期", - "transaction_priority_medium" : "介质", + "transaction_priority_slow" : "慢速", + "transaction_priority_regular" : "常规", + "transaction_priority_medium" : "中等", "transaction_priority_fast" : "快速", - "transaction_priority_fastest" : "最快的", + "transaction_priority_fastest" : "最快", "trade_for_not_created" : "交易 ${title} 未创建.", "trade_not_created" : "未建立交易.", - "trade_id_not_found" : "贸易方式 ${tradeId} 的 ${title} 未找到.", + "trade_id_not_found" : "交易方式 ${tradeId} 的 ${title} 未找到.", "trade_not_found" : "找不到交易.", "trade_state_pending" : "待定", "trade_state_confirming" : "确认中", - "trade_state_trading" : "贸易", - "trade_state_traded" : "交易", + "trade_state_trading" : "交易", + "trade_state_traded" : "已交易", "trade_state_complete" : "完成", "trade_state_to_be_created" : "待创建", "trade_state_unpaid" : "未付", @@ -376,100 +375,104 @@ "trade_state_created" : "已建立", "trade_state_finished" : "已完成", - "change_language" : "改變語言", - "change_language_to" : "將語言更改為 ${language}?", + "change_language" : "修改语言", + "change_language_to" : "修改语言为 ${language}?", - "paste" : "糊", + "paste" : "粘贴", "restore_from_seed_placeholder" : "请在此处输入或粘贴您的代码短语", "add_new_word" : "添加新词", "incorrect_seed" : "输入的文字无效。", - "biometric_auth_reason" : "掃描指紋以進行身份驗證", - "version" : "版 ${currentVersion}", + "biometric_auth_reason" : "扫描指纹进行身份认证", + "version" : "版本 ${currentVersion}", - "openalias_alert_title" : "檢測到XMR收件人", - "openalias_alert_content" : "您將匯款至\n${recipient_name}", + "openalias_alert_title" : "检测到XMR收件人", + "openalias_alert_content" : "您将汇款至\n${recipient_name}", "card_address" : "地址:", - "buy" : "購買", + "buy" : "购买", - "placeholder_transactions" : "您的交易將顯示在這裡", - "placeholder_contacts" : "您的聯繫人將顯示在這裡", + "placeholder_transactions" : "您的交易将显示在这里", + "placeholder_contacts" : "您的联系人将显示在这里", "template" : "模板", - "confirm_delete_template" : "此操作將刪除此模板。 你想繼續嗎?", - "confirm_delete_wallet" : "此操作將刪除此錢包。 你想繼續嗎?", + "confirm_delete_template" : "此操作将刪除此模板。 确定吗?", + "confirm_delete_wallet" : "此操作将刪除此钱包。确定吗?", - "picker_description" : "要選擇ChangeNOW或MorphToken,請先更改您的交易對", + "picker_description" : "要选择ChangeNOW或MorphToken,请先更改您的交易币", - "change_wallet_alert_title" : "更換當前錢包", - "change_wallet_alert_content" : "您要將當前的錢包更改為 ${wallet_name}?", + "change_wallet_alert_title" : "更换当前钱包", + "change_wallet_alert_content" : "您是否想将当前钱包改为 ${wallet_name}?", - "creating_new_wallet" : "創建新錢包", - "creating_new_wallet_error" : "錯誤: ${description}", + "creating_new_wallet" : "创建新钱包", + "creating_new_wallet_error" : "错误: ${description}", "seed_alert_title" : "注意", - "seed_alert_content" : "種子是恢復錢包的唯一方法。 你寫下來了嗎?", - "seed_alert_back" : "回去", - "seed_alert_yes" : "是的,我有", + "seed_alert_content" : "种子是恢复钱包的唯一方法。记住了吗?", + "seed_alert_back" : "返回", + "seed_alert_yes" : "确定", - "exchange_sync_alert_content" : "請等待,直到您的錢包同步", + "exchange_sync_alert_content" : "请等待,直到您的钱包同步", "pre_seed_title" : "重要", - "pre_seed_description" : "在下一頁上,您將看到一系列${words}個單詞。 這是您獨特的私人種子,是丟失或出現故障時恢復錢包的唯一方法。 您有責任將其寫下並存儲在Cake Wallet應用程序外部的安全地方。", - "pre_seed_button_text" : "我明白。 給我看我的種子", + "pre_seed_description" : "在下一页上,您将看到${words}个文字。 这是您独有的种子,是丟失或出现故障时恢复钱包的唯一方法。 您有必须将其写下并储存在Cake Wallet应用程序以外的安全地方。", + "pre_seed_button_text" : "我明白。 查看种子", - "xmr_to_error" : "XMR.TO錯誤", - "xmr_to_error_description" : "無效的金額。 小數點後最多8位數字", + "xmr_to_error" : "XMR.TO 错误", + "xmr_to_error_description" : "无效的金额。 小数点后最多8位数字", - "provider_error" : "${provider} 錯誤", + "provider_error" : "${provider} 错误", "use_ssl" : "使用SSL", - "color_theme" : "顏色主題", - "light_theme" : "光", - "bright_theme" : "亮", + "color_theme" : "主題", + "light_theme" : "艳丽", + "bright_theme" : "明亮", "dark_theme" : "黑暗", - "enter_your_note" : "輸入您的筆記...", - "note_optional" : "注意(可選)", - "note_tap_to_change" : "注意(輕按即可更改)", - "transaction_key" : "交易密碼", - "confirmations" : "確認書", + "enter_your_note" : "输入您的笔记...", + "note_optional" : "注意(可选)", + "note_tap_to_change" : "注意(轻按即可更改)", + "transaction_key" : "交易密码", + "confirmations" : "确认", "recipient_address" : "收件人地址", - "extra_id" : "額外編號:", - "destination_tag" : "目標標籤:", - "memo" : "備忘錄:", + "extra_id" : "额外ID:", + "destination_tag" : "目标Tag:", + "memo" : "备忘录:", - "backup" : "後備", - "change_password" : "更改密碼", - "backup_password" : "備用密碼", - "write_down_backup_password" : "請寫下您的備份密碼,該密碼用於導入備份文件。", - "export_backup" : "導出備份", - "save_backup_password" : "請確保您已保存備份密碼。 沒有它,您將無法導入備份文件。", - "backup_file" : "備份檔案", + "backup" : "备份", + "change_password" : "更改密码", + "backup_password" : "备份密码", + "write_down_backup_password" : "请写下您的备份密码,该密码用于导入备份文件。", + "export_backup" : "导出备份", + "save_backup_password" : "请确保您已保存备份密码。 沒有它,您将无法导入备份文件。", + "backup_file" : "备份文件", - "edit_backup_password" : "編輯備份密碼", - "save_backup_password_alert" : "保存備份密碼", - "change_backup_password_alert" : "您以前的備份文件將無法使用新的備份密碼導入。 新的備份密碼將僅用於新的備份文件。 您確定要更改備份密碼嗎?", + "edit_backup_password" : "编辑备份密码", + "save_backup_password_alert" : "保存备份密码", + "change_backup_password_alert" : "您以前的备份文件将无法使用新的备份密码導入。 新的备份密码将仅用于新的备份文件。 您确定要更改备份密码吗?", - "enter_backup_password" : "在此處輸入備用密碼", - "select_backup_file" : "選擇備份文件", - "import" : "進口", - "please_select_backup_file" : "請選擇備份文件,然後輸入備份密碼。", + "enter_backup_password" : "在此处输入備用密码", + "select_backup_file" : "选择备份文件", + "import" : "导入", + "please_select_backup_file" : "请选择备份文件,然后输入备份密码。", - "fixed_rate" : "固定利率", - "fixed_rate_alert" : "選中固定費率模式後,您將可以輸入接收金額。 您要切換到固定速率模式嗎?", + "fixed_rate" : "固定汇率", + "fixed_rate_alert" : "选中固定汇率模式后,您将可以输入接收金额。 您要切换到固定汇率模式吗?", - "xlm_extra_info" : "發送用於交換的XLM交易時,請不要忘記指定備忘錄ID", - "xrp_extra_info" : "發送用於交換的XRP交易時,請不要忘記指定目標標記", + "xlm_extra_info" : "发送用于交换的XLM交易时,请不要忘记指定备忘录ID", + "xrp_extra_info" : "发送用于交换的XRP交易时,请不要忘记指定目标Tag", "exchange_incorrect_current_wallet_for_xmr" : "如果要从Cake Wallet Monero余额中兑换XMR,请先切换到Monero钱包。", "confirmed" : "已确认", "unconfirmed" : "未经证实", "displayable" : "可显示", - "submit_request" : "提交請求", + "submit_request" : "提交请求", + "buy_alert_content" : "目前,我們僅支持購買比特幣。 要購買比特幣,請創建或切換到您的比特幣錢包", + + "outdated_electrum_wallet_desceription" : "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", + "understand" : "I understand", "address_detected" : "檢測到地址", "address_from_domain" : "您有以下地址 unstoppable domain ${domain}"