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 cf650375f..26cc56187 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,15 +1,49 @@ 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; public class MainActivity extends FlutterFragmentActivity { + final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine); + + 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/account_list.dart b/cw_monero/lib/account_list.dart index 6e84f5931..79342cf3b 100644 --- a/cw_monero/lib/account_list.dart +++ b/cw_monero/lib/account_list.dart @@ -5,6 +5,7 @@ import 'package:cw_monero/types.dart'; import 'package:cw_monero/monero_api.dart'; import 'package:cw_monero/structs/account_row.dart'; import 'package:flutter/foundation.dart'; +import 'package:cw_monero/wallet.dart'; final accountSizeNative = moneroApi .lookup>('account_size') @@ -70,8 +71,13 @@ void _setLabelForAccount(Map args) { setLabelForAccountSync(label: label, accountIndex: accountIndex); } -Future addAccount({String label}) async => compute(_addAccount, label); +Future addAccount({String label}) async { + await compute(_addAccount, label); + await store(); +} -Future setLabelForAccount({int accountIndex, String label}) async => - compute( - _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); \ No newline at end of file +Future setLabelForAccount({int accountIndex, String label}) async { + await compute( + _setLabelForAccount, {'accountIndex': accountIndex, 'label': label}); + await store(); +} \ No newline at end of file diff --git a/cw_monero/lib/subaddress_list.dart b/cw_monero/lib/subaddress_list.dart index 97a307040..caa0fffd3 100644 --- a/cw_monero/lib/subaddress_list.dart +++ b/cw_monero/lib/subaddress_list.dart @@ -5,6 +5,7 @@ import 'package:cw_monero/signatures.dart'; import 'package:cw_monero/types.dart'; import 'package:cw_monero/monero_api.dart'; import 'package:cw_monero/structs/subaddress_row.dart'; +import 'package:cw_monero/wallet.dart'; final subaddressSizeNative = moneroApi .lookup>('subaddrress_size') @@ -79,14 +80,18 @@ void _setLabelForSubaddress(Map args) { accountIndex: accountIndex, addressIndex: addressIndex, label: label); } -Future addSubaddress({int accountIndex, String label}) async => - compute, void>( +Future addSubaddress({int accountIndex, String label}) async { + await compute, void>( _addSubaddress, {'accountIndex': accountIndex, 'label': label}); + await store(); +} Future setLabelForSubaddress( - {int accountIndex, int addressIndex, String label}) => - compute, void>(_setLabelForSubaddress, { - 'accountIndex': accountIndex, - 'addressIndex': addressIndex, - 'label': label - }); + {int accountIndex, int addressIndex, String label}) async { + await compute, void>(_setLabelForSubaddress, { + 'accountIndex': accountIndex, + 'addressIndex': addressIndex, + 'label': label + }); + await store(); +} 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/howto-build-android.md b/howto-build-android.md index dff95d17c..623aed6b5 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -8,7 +8,7 @@ The following are the system requirements to build CakeWallet for your Android d Ubuntu >= 16.04 Android SDK 28 Android NDK 17c -Flutter 1.22.6 +Flutter 2 or above ``` ## Building CakeWallet on Android @@ -51,9 +51,9 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -CakeWallet requires **EXACTLY** Flutter version `1.22.6` to build properly. +The easiest way to install Flutter is by using the `snap` package manager. Other means of installing Flutter on your system can be found [here](https://flutter.dev/docs/get-started/install/linux). -To install this version of Flutter on your Ubuntu system, please use [these instructions](https://flutter.dev/docs/get-started/install/linux#install-flutter-manually). +`$ sudo snap install flutter --classic` ### 4. Verify Installations @@ -64,7 +64,7 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 1.22.6, on Linux, locale en_US.UTF-8) +[✓] Flutter (Channel stable, 2.0.2, on Linux, locale en_US.UTF-8) [✓] Android toolchain - develop for Android devices (Android SDK version 28) [✓] Android Studio (version 4.0) ``` @@ -87,7 +87,7 @@ $ cd /opt/android ..and download the source code into that directory. -`$ git clone https://github.com/cake-tech/cake_wallet.git --branch deploy` +`$ git clone https://github.com/cake-tech/cake_wallet.git --branch main` Proceed into the source code before proceeding with the next steps: 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 7efcbc302..879041cac 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -72,7 +72,7 @@ PODS: - Flutter - path_provider (0.0.1): - Flutter - - "permission_handler (5.0.1+1)": + - "permission_handler (5.1.0+2)": - Flutter - Reachability (3.2) - SDWebImage (5.9.1): @@ -157,19 +157,19 @@ SPEC CHECKSUMS: barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 - cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649 - devicelocale: feebbe5e7a30adb8c4f83185de1b50ff19b44f00 + cw_monero: 78f369253cc913efc23db9cf6be81a11eaf40fe1 + devicelocale: b22617f40038496deffba44747101255cee005b0 DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 - Flutter: 0e3d915762c693b495b44d77113d4970485de6ec + Flutter: 434fef37c0980e73bb6479ef766c45957d4b510c flutter_secure_storage: 7953c38a04c3fdbb00571bcd87d8e3b5ceb9daec local_auth: 25938960984c3a7f6e3253e3f8d962fdd16852bd MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 path_provider: abfe2b5c733d04e238b0d8691db0cfd63a27a93c - permission_handler: eac8e15b4a1a3fba55b761d19f3f4e6b005d15b6 + permission_handler: ccb20a9fad0ee9b1314a52b70b76b473c5f8dab0 Reachability: 33e18b67625424e47b6cde6d202dce689ad7af96 SDWebImage: a990c053fff71e388a10f3357edb0be17929c9c5 share: 0b2c3e82132f5888bccca3351c504d0003b3b410 @@ -177,7 +177,7 @@ SPEC CHECKSUMS: SwiftProtobuf: 4ef85479c18ca85b5482b343df9c319c62bda699 SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef - webview_flutter: d2b4d6c66968ad042ad94cbb791f5b72b4678a96 + webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b PODFILE CHECKSUM: 5b5f101b119a1b6eb857c967d462832a9062dec4 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index f5cbf5b46..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; @@ -331,6 +335,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -357,7 +362,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -374,7 +379,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.3; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -415,6 +420,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_BITCODE = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -470,6 +476,7 @@ "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; COPY_PHASE_STRIP = NO; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_BITCODE = NO; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; GCC_C_LANGUAGE_STANDARD = gnu99; @@ -498,7 +505,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -515,7 +522,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.3; + MARKETING_VERSION = 4.2.0; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -533,7 +540,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 31; + CURRENT_PROJECT_VERSION = 40; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -550,7 +557,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.1.3; + 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/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a16e..919434a62 100644 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> 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 batteryChannel = FlutterMethodChannel(name: "com.cakewallet.cakewallet/legacy_wallet_migration", - binaryMessenger: controller.binaryMessenger) - batteryChannel.setMethodCallHandler({ + let legacyMigrationChannel = FlutterMethodChannel( + name: "com.cakewallet.cakewallet/legacy_wallet_migration", + binaryMessenger: controller.binaryMessenger) + legacyMigrationChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { @@ -52,6 +53,24 @@ import Flutter } }) + 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) + } + }) + 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/ios/Runner/Info.plist b/ios/Runner/Info.plist index 89cd769e0..970c892cd 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -24,6 +24,11 @@ $(CURRENT_PROJECT_VERSION) LSRequiresIPhoneOS + NSAppTransportSecurity + + NSAllowsArbitraryLoads + + NSCameraUsageDescription Used for scan QR code NSDocumentsFolderUsageDescription 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 e66d8516a..675366209 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'; @@ -441,6 +442,8 @@ Future setup( getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource)); + getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource)); + getIt.registerFactoryParam( (WalletType param1, __) { switch (param1) { @@ -448,6 +451,8 @@ Future setup( return getIt.get(); case WalletType.bitcoin: return getIt.get(); + case WalletType.litecoin: + return getIt.get(); default: return null; } 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/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 80b48f50b..b24943f3d 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 ac7f0cd94..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,19 +92,18 @@ 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(); - accountList.accounts.observe((_) async => await save()); account = accountList.accounts.first; subaddressList.update(accountIndex: account.id ?? 0); - subaddressList.subaddresses.observe((_) async => await save()); subaddress = subaddressList.getAll().first; balance = MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), @@ -111,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); @@ -152,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, @@ -238,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(); } @@ -264,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); @@ -315,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); @@ -389,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 e8650e34e..43e652315 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -239,9 +239,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 9ea094dda..64f04c507 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,21 +132,20 @@ class DashboardPage extends BasePage { image: exchangeImage, title: S.of(context).exchange, route: Routes.exchange), - if (walletViewModel.type == WalletType.bitcoin) ActionButton( - image: buyImage, - title: S.of(context).buy, - onClick: () { - Navigator.of(context).pushNamed(Routes.preOrder); - }, + ActionButton( + image: buyImage, + title: S.of(context).buy, + onClick: () async => + await _onClickBuyButton(context), ), ], - )), + ), ) ], )); } - void _setEffects() { + void _setEffects(BuildContext context) { if (_isEffectsInstalled) { return; } @@ -148,6 +154,45 @@ 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: + Navigator.of(context).pushNamed(Routes.preOrder); + 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/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index cfd115811..82d7dd80e 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -248,7 +248,8 @@ class ExchangePage extends BasePage { exchangeViewModel .isReceiveAddressEnabled, isAmountEstimated: true, - currencies: CryptoCurrency.all, + currencies: + exchangeViewModel.receiveCurrencies, onCurrencySelected: (currency) => exchangeViewModel .changeReceiveCurrency( 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 ff1d2411d..823637ff7 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,23 +1,15 @@ -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/buy/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'; @@ -31,7 +23,6 @@ import 'package:cake_wallet/view_model/dashboard/action_list_display_mode.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'; @@ -42,8 +33,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'; @@ -95,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) { @@ -128,6 +116,8 @@ abstract class DashboardViewModelBase with Store { settingsStore: appStore.settingsStore))); } + reaction((_) => appStore.wallet, _onWalletChange); + connectMapToListWithTransform( appStore.wallet.transactionHistory.transactions, transactions, @@ -207,7 +197,8 @@ abstract class DashboardViewModelBase with Store { } @observable - WalletBase wallet; + WalletBase, TransactionInfo> + wallet; bool get hasRescan => wallet.type == WalletType.monero; @@ -229,8 +220,6 @@ abstract class DashboardViewModelBase with Store { bool get isBuyEnabled => settingsStore.isBitcoinBuyEnabled; - ReactionDisposer _reaction; - ReactionDisposer _onMoneroAccountChangeReaction; ReactionDisposer _onMoneroBalanceChangeReaction; @@ -240,11 +229,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; @@ -274,17 +271,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 @@ -307,6 +304,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 e708e93e9..f3f00cd77 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -33,10 +33,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); @@ -57,6 +55,9 @@ abstract class ExchangeViewModelBase with Store { _onPairChange(); } }); + receiveCurrencies = CryptoCurrency.all + .where((cryptoCurrency) => !excludeCurrencies.contains(cryptoCurrency)) + .toList(); _defineIsReceiveAmountEditable(); isFixedRateMode = false; isReceiveAmountEntered = false; @@ -123,6 +124,8 @@ abstract class ExchangeViewModelBase with Store { bool get hasAllAmount => wallet.type == WalletType.bitcoin && depositCurrency == wallet.currency; + List receiveCurrencies; + Limits limits; NumberFormat _cryptoNumberFormat; @@ -213,8 +216,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()); @@ -278,8 +283,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); @@ -315,7 +320,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) { @@ -369,19 +375,17 @@ abstract class ExchangeViewModelBase with Store { pair.from == depositCurrency && pair.to == receiveCurrency) .isNotEmpty; - if (!isPairExist) { + if (isPairExist) { final provider = _providerForPair(from: depositCurrency, to: receiveCurrency); if (provider != null) { changeProvider(provider: provider); } + } else { + depositAmount = ''; + receiveAmount = ''; } - - _defineIsReceiveAmountEditable(); - depositAmount = ''; - receiveAmount = ''; - loadLimits(); } ExchangeProvider _providerForPair({CryptoCurrency from, CryptoCurrency to}) { @@ -399,6 +403,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 b92672ff8..689032223 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'; @@ -90,6 +91,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: _amount = stringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.litecoin: + _amount = stringDoubleToBitcoinAmount(_cryptoAmount); + break; default: break; } @@ -102,7 +106,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); } @@ -304,6 +308,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: @@ -330,6 +340,9 @@ abstract class SendViewModelBase with Store { case WalletType.bitcoin: maximumFractionDigits = 8; break; + case WalletType.litecoin: + maximumFractionDigits = 8; + break; default: break; } @@ -357,9 +370,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 1b414f267..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.3+42 +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/ @@ -147,4 +151,4 @@ flutter: # weight: 700 # # For details regarding fonts from package dependencies, - # see https://flutter.dev/custom-fonts/#from-packages \ No newline at end of file + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 14c692c0d..6d689104f 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", + "buy_bitcoin" : "Bitcoin kaufen", "buy_with" : "Kaufen mit" } \ No newline at end of file diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index b2cebcca3..2c4c2e28f 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", + "buy_bitcoin" : "Buy Bitcoin", "buy_with" : "Buy with" } \ No newline at end of file diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 20c6e1586..2e07285db 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", + "buy_bitcoin" : "Comprar Bitcoin", "buy_with" : "Compra con" } \ No newline at end of file diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index e93615549..2430f2bce 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", + "buy_bitcoin" : "बिटकॉइन खरीदें", "buy_with" : "के साथ खरीदें" } \ 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 113e60f46..1eeb07c27 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", + "buy_bitcoin" : "ビットコインを購入する", "buy_with" : "で購入" } \ No newline at end of file diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index e225f4746..91fd84b90 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", + "buy_bitcoin" : "비트 코인 구매", "buy_with" : "구매" } \ No newline at end of file diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 04c239ff4..653078810 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", + "buy_bitcoin" : "Koop Bitcoin", "buy_with" : "Koop met" } \ No newline at end of file diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 542223c18..7b299afbf 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", + "buy_bitcoin" : "Kup Bitcoin", "buy_with" : "Kup za pomocą" } \ No newline at end of file diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index ea56dfca9..cc0d22ed2 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", + "buy_bitcoin" : "Compre Bitcoin", "buy_with" : "Compre com" } \ No newline at end of file diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 319a1753f..63831b3d0 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", + "buy_bitcoin" : "Купить Bitcoin", "buy_with" : "Купить с помощью" } \ No newline at end of file diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 06082f250..879b52102 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", + "buy_bitcoin" : "Купити Bitcoin", "buy_with" : "Купити за допомогою" } \ No newline at end of file diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 39f93ecb1..674a1ec48 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,103 @@ "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" : "提交请求", + "buy_alert_content" : "目前,我們僅支持購買比特幣。 要購買比特幣,請創建或切換到您的比特幣錢包", - "submit_request" : "提交請求", + "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", "buy_bitcoin" : "購買比特幣", "buy_with" : "與一起購買" diff --git a/screenshot-android.jpg b/screenshot-android.jpg new file mode 100644 index 000000000..9ec562026 Binary files /dev/null and b/screenshot-android.jpg differ diff --git a/screenshot-ios.jpg b/screenshot-ios.jpg new file mode 100644 index 000000000..7efa31595 Binary files /dev/null and b/screenshot-ios.jpg differ