Merge branch 'main' into CAKE-306-add-moonpay-option-for-btc-buying
# Conflicts: # lib/buy/wyre/wyre_buy_provider.dart # lib/di.dart # lib/src/screens/dashboard/dashboard_page.dart # lib/view_model/dashboard/dashboard_view_model.dart # res/values/strings_de.arb # res/values/strings_en.arb # res/values/strings_es.arb # res/values/strings_hi.arb # res/values/strings_ja.arb # res/values/strings_ko.arb # res/values/strings_nl.arb # res/values/strings_pl.arb # res/values/strings_pt.arb # res/values/strings_ru.arb # res/values/strings_uk.arb # res/values/strings_zh.arb
2
.gitignore
vendored
|
@ -96,3 +96,5 @@ vendor/
|
|||
android/app/.cxx/**
|
||||
ios/Flutter/.last_build_id
|
||||
/lib/generated/**
|
||||
#**#
|
||||
/**/#**#
|
|
@ -25,7 +25,6 @@ linter:
|
|||
- empty_constructor_bodies
|
||||
- empty_statements
|
||||
- hash_and_equals
|
||||
- implementation_imports
|
||||
- invariant_booleans
|
||||
- iterable_contains_unrelated_type
|
||||
- library_names
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
BIN
android/app/src/main/res/drawable/ic_launcher.png
Executable file
After Width: | Height: | Size: 5.3 KiB |
|
@ -0,0 +1,5 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@android:color/white" />
|
||||
<foreground android:drawable="@drawable/ic_launcher" />
|
||||
</adaptive-icon>
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 6.7 KiB |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 9 KiB |
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 6.6 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 9.1 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 3 KiB |
BIN
assets/images/litecoin_icon.png
Normal file
After Width: | Height: | Size: 5.9 KiB |
BIN
assets/images/litecoin_menu.png
Normal file
After Width: | Height: | Size: 15 KiB |
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.1 KiB |
2
assets/litecoin_electrum_server_list.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
-
|
||||
uri: ltc-electrum.cakewallet.com:50002
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
end
|
||||
|
|
|
@ -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<NativeFunction<account_size>>('account_size')
|
||||
|
@ -70,8 +71,13 @@ void _setLabelForAccount(Map<String, dynamic> args) {
|
|||
setLabelForAccountSync(label: label, accountIndex: accountIndex);
|
||||
}
|
||||
|
||||
Future<void> addAccount({String label}) async => compute(_addAccount, label);
|
||||
Future<void> addAccount({String label}) async {
|
||||
await compute(_addAccount, label);
|
||||
await store();
|
||||
}
|
||||
|
||||
Future<void> setLabelForAccount({int accountIndex, String label}) async =>
|
||||
compute(
|
||||
_setLabelForAccount, {'accountIndex': accountIndex, 'label': label});
|
||||
Future<void> setLabelForAccount({int accountIndex, String label}) async {
|
||||
await compute(
|
||||
_setLabelForAccount, {'accountIndex': accountIndex, 'label': label});
|
||||
await store();
|
||||
}
|
|
@ -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<NativeFunction<subaddrress_size>>('subaddrress_size')
|
||||
|
@ -79,14 +80,18 @@ void _setLabelForSubaddress(Map<String, dynamic> args) {
|
|||
accountIndex: accountIndex, addressIndex: addressIndex, label: label);
|
||||
}
|
||||
|
||||
Future addSubaddress({int accountIndex, String label}) async =>
|
||||
compute<Map<String, Object>, void>(
|
||||
Future addSubaddress({int accountIndex, String label}) async {
|
||||
await compute<Map<String, Object>, void>(
|
||||
_addSubaddress, {'accountIndex': accountIndex, 'label': label});
|
||||
await store();
|
||||
}
|
||||
|
||||
Future setLabelForSubaddress(
|
||||
{int accountIndex, int addressIndex, String label}) =>
|
||||
compute<Map<String, Object>, void>(_setLabelForSubaddress, {
|
||||
'accountIndex': accountIndex,
|
||||
'addressIndex': addressIndex,
|
||||
'label': label
|
||||
});
|
||||
{int accountIndex, int addressIndex, String label}) async {
|
||||
await compute<Map<String, Object>, void>(_setLabelForSubaddress, {
|
||||
'accountIndex': accountIndex,
|
||||
'addressIndex': addressIndex,
|
||||
'label': label
|
||||
});
|
||||
await store();
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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:
|
||||
|
||||
|
|
12
ios/CakeWallet/secRandom.swift
Normal file
|
@ -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
|
||||
}
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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 = "<group>"; };
|
||||
0C44A7192518EF8000B570ED /* decrypt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = decrypt.swift; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||
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 = "<group>"; };
|
||||
|
@ -65,6 +67,7 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
0C44A7192518EF8000B570ED /* decrypt.swift */,
|
||||
0C9D68C8264854B60011B691 /* secRandom.swift */,
|
||||
);
|
||||
path = CakeWallet;
|
||||
sourceTree = "<group>";
|
||||
|
@ -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";
|
||||
|
|
|
@ -2,6 +2,6 @@
|
|||
<Workspace
|
||||
version = "1.0">
|
||||
<FileRef
|
||||
location = "group:Runner.xcodeproj">
|
||||
location = "self:">
|
||||
</FileRef>
|
||||
</Workspace>
|
||||
|
|
|
@ -27,8 +27,6 @@
|
|||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||
<Testables>
|
||||
</Testables>
|
||||
<MacroExpansion>
|
||||
<BuildableReference
|
||||
BuildableIdentifier = "primary"
|
||||
|
@ -38,8 +36,8 @@
|
|||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</MacroExpansion>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
<Testables>
|
||||
</Testables>
|
||||
</TestAction>
|
||||
<LaunchAction
|
||||
buildConfiguration = "Debug"
|
||||
|
@ -61,8 +59,6 @@
|
|||
ReferencedContainer = "container:Runner.xcodeproj">
|
||||
</BuildableReference>
|
||||
</BuildableProductRunnable>
|
||||
<AdditionalOptions>
|
||||
</AdditionalOptions>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
buildConfiguration = "Profile"
|
||||
|
|
|
@ -8,9 +8,10 @@ import Flutter
|
|||
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
|
||||
) -> 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<String, Any>,
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png
Normal file
After Width: | Height: | Size: 134 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png
Normal file
After Width: | Height: | Size: 12 KiB |
BIN
ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png
Normal file
After Width: | Height: | Size: 17 KiB |
Before Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 10 KiB |
Before Width: | Height: | Size: 14 KiB |
|
@ -24,6 +24,11 @@
|
|||
<string>$(CURRENT_PROJECT_VERSION)</string>
|
||||
<key>LSRequiresIPhoneOS</key>
|
||||
<true/>
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsArbitraryLoads</key>
|
||||
<true/>
|
||||
</dict>
|
||||
<key>NSCameraUsageDescription</key>
|
||||
<string>Used for scan QR code</string>
|
||||
<key>NSDocumentsFolderUsageDescription</key>
|
||||
|
|
|
@ -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(<dynamic>[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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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});
|
||||
}
|
||||
|
|
|
@ -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<bool> prefixMatches(String source, List<String> prefixes) {
|
|||
return prefixes.map((prefix) => hx.startsWith(prefix.toLowerCase())).toList();
|
||||
}
|
||||
|
||||
String generateMnemonic({int strength = 132, String prefix = segwit}) {
|
||||
Future<String> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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.';
|
||||
}
|
||||
|
|
|
@ -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<BitcoinTransactionInfo> with Store {
|
||||
BitcoinTransactionHistoryBase(
|
||||
{this.eclient, String dirPath, @required String password})
|
||||
: path = '$dirPath/$_transactionsHistoryFileName',
|
||||
_password = password,
|
||||
_height = 0,
|
||||
_isUpdating = false {
|
||||
transactions = ObservableMap<String, BitcoinTransactionInfo>();
|
||||
}
|
||||
|
||||
BitcoinWalletBase wallet;
|
||||
final ElectrumClient eclient;
|
||||
final String path;
|
||||
final String _password;
|
||||
int _height;
|
||||
bool _isUpdating;
|
||||
|
||||
Future<void> 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<Map<String, BitcoinTransactionInfo>> 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<Map<String, BitcoinTransactionInfo>>(
|
||||
<String, BitcoinTransactionInfo>{}, (acc, tx) {
|
||||
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
Future<BitcoinTransactionInfo> 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<void> add(Map<String, BitcoinTransactionInfo> transactionsList) async {
|
||||
transactionsList.entries.forEach((entry) {
|
||||
_updateOrInsert(entry.value);
|
||||
|
||||
if (entry.value.height > _height) {
|
||||
_height = entry.value.height;
|
||||
}
|
||||
});
|
||||
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> addOne(BitcoinTransactionInfo tx) async {
|
||||
_updateOrInsert(tx);
|
||||
|
||||
if (tx.height > _height) {
|
||||
_height = tx.height;
|
||||
}
|
||||
|
||||
await save();
|
||||
}
|
||||
|
||||
BitcoinTransactionInfo get(String id) => transactions[id];
|
||||
|
||||
Future<void> 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<int>(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<void> _fetchBatchOfTransactions(
|
||||
Iterable<Map<String, dynamic>> 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<Map<String, Object>> _read() async {
|
||||
final content = await read(path: path, password: _password);
|
||||
return json.decode(content) as Map<String, Object>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, Object> ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, Object>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<LitecoinTransactionPriority> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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<BitcoinBalance> with Store {
|
||||
BitcoinWalletBase._internal(
|
||||
{@required this.eclient,
|
||||
@required this.path,
|
||||
@required String password,
|
||||
@required WalletInfo walletInfo,
|
||||
@required List<BitcoinAddressRecord> 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<BitcoinAddressRecord>.of(initialAddresses.toSet())
|
||||
: ObservableList<BitcoinAddressRecord>(),
|
||||
syncStatus = NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_accountIndex = accountIndex,
|
||||
_feeRates = <int>[],
|
||||
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 ?? <Object>[];
|
||||
final addresses = <BitcoinAddressRecord>[];
|
||||
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<BitcoinAddressRecord> 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<BitcoinWallet> 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<BitcoinUnspent> _unspent;
|
||||
|
||||
@override
|
||||
@observable
|
||||
String address;
|
||||
|
||||
@override
|
||||
@observable
|
||||
BitcoinBalance balance;
|
||||
|
||||
@override
|
||||
@observable
|
||||
SyncStatus syncStatus;
|
||||
|
||||
ObservableList<BitcoinAddressRecord> addresses;
|
||||
|
||||
List<String> 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<int> _feeRates;
|
||||
int _accountIndex;
|
||||
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
|
||||
|
||||
Future<void> 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<void> nextAddress() async {
|
||||
_accountIndex += 1;
|
||||
|
||||
if (_accountIndex >= addresses.length) {
|
||||
_accountIndex = 0;
|
||||
}
|
||||
|
||||
address = addresses[_accountIndex].address;
|
||||
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<BitcoinAddressRecord> generateNewAddress() async {
|
||||
_accountIndex += 1;
|
||||
final address = BitcoinAddressRecord(_getAddress(index: _accountIndex),
|
||||
index: _accountIndex);
|
||||
addresses.add(address);
|
||||
|
||||
await save();
|
||||
|
||||
return address;
|
||||
}
|
||||
|
||||
Future<List<BitcoinAddressRecord>> generateNewAddresses(int count,
|
||||
{int startIndex = 0}) async {
|
||||
final list = <BitcoinAddressRecord>[];
|
||||
|
||||
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<void> updateAddress(String address) async {
|
||||
for (final addr in addresses) {
|
||||
if (addr.address == address) {
|
||||
await save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> 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<void> 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<PendingBitcoinTransaction> createTransaction(
|
||||
Object credentials) async {
|
||||
const minAmount = 546;
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
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<void> 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<void> rescan({int height}) async {
|
||||
// FIXME: Unimplemented
|
||||
}
|
||||
|
||||
@override
|
||||
void close() async {
|
||||
await eclient.close();
|
||||
}
|
||||
|
||||
Future<void> _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<BitcoinBalance> _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<void> _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);
|
||||
}
|
||||
|
|
|
@ -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<WalletInfo> walletInfoSource;
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.bitcoin;
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> 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<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: WalletType.bitcoin))
|
||||
.existsSync();
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> 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<BitcoinWallet> restoreFromKeys(
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials) async {
|
||||
// TODO: implement restoreFromKeys
|
||||
throw UnimplementedError();
|
||||
}
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<BitcoinWallet> 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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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<Object> params) {
|
||||
final _params = params?.map((val) => '"${val.toString()}"')?.join(',');
|
||||
return '[$_params]';
|
||||
|
@ -53,17 +44,8 @@ class ElectrumClient {
|
|||
Timer _aliveTimer;
|
||||
String unterminatedString;
|
||||
|
||||
Future<void> 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<void> connectToUri(Uri uri) async =>
|
||||
await connect(host: uri.host, port: uri.port);
|
||||
|
||||
Future<void> connect({@required String host, @required int port}) async {
|
||||
try {
|
||||
|
@ -173,10 +155,11 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
Future<List<Map<String, dynamic>>> 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<String, Object>) {
|
||||
|
@ -259,7 +242,7 @@ class ElectrumClient {
|
|||
if (result is String) {
|
||||
return result;
|
||||
}
|
||||
print(result);
|
||||
|
||||
return '';
|
||||
});
|
||||
|
||||
|
@ -303,14 +286,24 @@ class ElectrumClient {
|
|||
});
|
||||
|
||||
Future<List<int>> 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<Object> scripthashUpdate(String scripthash) {
|
||||
|
|
|
@ -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 =>
|
98
lib/bitcoin/electrum_transaction_history.dart
Normal file
|
@ -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<ElectrumTransactionInfo> with Store {
|
||||
ElectrumTransactionHistoryBase(
|
||||
{@required this.walletInfo, @required String password})
|
||||
: _password = password,
|
||||
_height = 0 {
|
||||
transactions = ObservableMap<String, ElectrumTransactionInfo>();
|
||||
}
|
||||
|
||||
final WalletInfo walletInfo;
|
||||
final String _password;
|
||||
int _height;
|
||||
|
||||
Future<void> init() async => await _load();
|
||||
|
||||
@override
|
||||
void addOne(ElectrumTransactionInfo transaction) =>
|
||||
transactions[transaction.id] = transaction;
|
||||
|
||||
@override
|
||||
void addMany(Map<String, ElectrumTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
|
||||
@override
|
||||
Future<void> 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<Map<String, Object>> _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<String, Object>;
|
||||
}
|
||||
|
||||
Future<void> _load() async {
|
||||
try {
|
||||
final content = await _read();
|
||||
final txs = content['transactions'] as Map<String, Object> ?? {};
|
||||
|
||||
txs.entries.forEach((entry) {
|
||||
final val = entry.value;
|
||||
|
||||
if (val is Map<String, Object>) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<String, Object> obj,
|
||||
factory ElectrumTransactionInfo.fromElectrumVerbose(
|
||||
Map<String, Object> obj, WalletType type,
|
||||
{@required List<BitcoinAddressRecord> 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<Object>)?.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<Object> ?? [];
|
||||
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<String> 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<String, dynamic> data) {
|
||||
return BitcoinTransactionInfo(
|
||||
factory ElectrumTransactionInfo.fromJson(
|
||||
Map<String, dynamic> 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,
|
467
lib/bitcoin/electrum_wallet.dart
Normal file
|
@ -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<ElectrumBalance,
|
||||
ElectrumTransactionHistory, ElectrumTransactionInfo> with Store {
|
||||
ElectrumWalletBase(
|
||||
{@required String password,
|
||||
@required WalletInfo walletInfo,
|
||||
@required List<BitcoinAddressRecord> 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<BitcoinAddressRecord>.of(
|
||||
(initialAddresses ?? []).toSet()),
|
||||
syncStatus = NotConnectedSyncStatus(),
|
||||
_password = password,
|
||||
_accountIndex = accountIndex,
|
||||
_feeRates = <int>[],
|
||||
_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<BitcoinAddressRecord> addresses;
|
||||
|
||||
List<String> 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<BitcoinUnspent> _unspent;
|
||||
List<int> _feeRates;
|
||||
int _accountIndex;
|
||||
Map<String, BehaviorSubject<Object>> _scripthashesUpdateSubject;
|
||||
bool _isTransactionUpdating;
|
||||
|
||||
Future<void> init() async {
|
||||
await generateAddresses();
|
||||
address = addresses[_accountIndex].address;
|
||||
await transactionHistory.init();
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> nextAddress() async {
|
||||
_accountIndex += 1;
|
||||
|
||||
if (_accountIndex >= addresses.length) {
|
||||
_accountIndex = 0;
|
||||
}
|
||||
|
||||
address = addresses[_accountIndex].address;
|
||||
|
||||
await save();
|
||||
}
|
||||
|
||||
Future<void> generateAddresses() async {
|
||||
if (addresses.length < 33) {
|
||||
final addressesCount = 33 - addresses.length;
|
||||
await generateNewAddresses(addressesCount,
|
||||
startIndex: addresses.length, hd: hd);
|
||||
}
|
||||
}
|
||||
|
||||
Future<BitcoinAddressRecord> 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<List<BitcoinAddressRecord>> generateNewAddresses(int count,
|
||||
{int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async {
|
||||
final list = <BitcoinAddressRecord>[];
|
||||
|
||||
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<void> updateAddress(String address) async {
|
||||
for (final addr in addresses) {
|
||||
if (addr.address == address) {
|
||||
await save();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
@override
|
||||
Future<void> 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<void> 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<PendingBitcoinTransaction> createTransaction(
|
||||
Object credentials) async {
|
||||
const minAmount = 546;
|
||||
final transactionCredentials = credentials as BitcoinTransactionCredentials;
|
||||
final inputs = <BitcoinUnspent>[];
|
||||
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<void> 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<void> rescan({int height}) async => throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<void> close() async {
|
||||
try {
|
||||
await electrumClient?.close();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
String getAddress({@required int index, @required bitcoin.HDWallet hd}) => '';
|
||||
|
||||
Future<String> makePath() async =>
|
||||
pathForWallet(name: walletInfo.name, type: walletInfo.type);
|
||||
|
||||
Future<void> _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<ElectrumTransactionInfo> 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<Map<String, ElectrumTransactionInfo>> 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<Map<String, ElectrumTransactionInfo>>(
|
||||
<String, ElectrumTransactionInfo>{}, (acc, tx) {
|
||||
acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> 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<ElectrumBalance> _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<void> _updateBalance() async {
|
||||
balance = await _fetchBalances();
|
||||
await save();
|
||||
}
|
||||
}
|
42
lib/bitcoin/electrum_wallet_snapshot.dart
Normal file
|
@ -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<BitcoinAddressRecord> addresses;
|
||||
ElectrumBalance balance;
|
||||
int accountIndex;
|
||||
|
||||
Future<void> 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 ?? <Object>[];
|
||||
mnemonic = data['mnemonic'] as String;
|
||||
addresses = addressesTmp
|
||||
.whereType<String>()
|
||||
.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);
|
||||
}
|
||||
}
|
||||
}
|
9
lib/bitcoin/litecoin_network.dart
Normal file
|
@ -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);
|
88
lib/bitcoin/litecoin_wallet.dart
Normal file
|
@ -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<BitcoinAddressRecord> initialAddresses,
|
||||
ElectrumBalance initialBalance,
|
||||
int accountIndex = 0})
|
||||
: super(
|
||||
mnemonic: mnemonic,
|
||||
password: password,
|
||||
walletInfo: walletInfo,
|
||||
networkType: litecoinNetwork,
|
||||
initialAddresses: initialAddresses,
|
||||
initialBalance: initialBalance,
|
||||
accountIndex: accountIndex);
|
||||
|
||||
static Future<LitecoinWallet> 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<void> 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;
|
||||
}
|
||||
}
|
76
lib/bitcoin/litecoin_wallet_service.dart
Normal file
|
@ -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<WalletInfo> walletInfoSource;
|
||||
|
||||
@override
|
||||
WalletType getType() => WalletType.litecoin;
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> 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<bool> isWalletExit(String name) async =>
|
||||
File(await pathForWallet(name: name, type: getType())).existsSync();
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> 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<void> remove(String wallet) async =>
|
||||
File(await pathForWalletDir(name: wallet, type: getType()))
|
||||
.delete(recursive: true);
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> restoreFromKeys(
|
||||
BitcoinRestoreWalletFromWIFCredentials credentials) async =>
|
||||
throw UnimplementedError();
|
||||
|
||||
@override
|
||||
Future<LitecoinWallet> 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;
|
||||
}
|
||||
}
|
|
@ -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 = <void Function(BitcoinTransactionInfo transaction)>[];
|
||||
PendingBitcoinTransaction(this._tx, this.type,
|
||||
{@required this.electrumClient,
|
||||
@required this.amount,
|
||||
@required this.fee})
|
||||
: _listeners = <void Function(ElectrumTransactionInfo transaction)>[];
|
||||
|
||||
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<void Function(BitcoinTransactionInfo transaction)> _listeners;
|
||||
final List<void Function(ElectrumTransactionInfo transaction)> _listeners;
|
||||
|
||||
@override
|
||||
Future<void> 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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<double> _fetchPrice(Map<String, dynamic> args) async {
|
|||
|
||||
try {
|
||||
final fiatStringified = fiat.toString();
|
||||
final uri =
|
||||
Uri.https(fiatApiAuthority, fiatApiPath,
|
||||
<String, String> {'convert': fiatStringified});
|
||||
final uri = Uri.https(fiatApiAuthority, fiatApiPath,
|
||||
<String, String>{'convert': fiatStringified});
|
||||
final response = await get(uri.toString());
|
||||
|
||||
if (response.statusCode != 200) {
|
||||
|
@ -28,7 +26,7 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async {
|
|||
final data = responseJSON['data'] as List<dynamic>;
|
||||
|
||||
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<double> _fetchPriceAsync(
|
|||
compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto});
|
||||
|
||||
class FiatConversionService {
|
||||
static Future<double> fetchPrice(CryptoCurrency crypto, FiatCurrency fiat) async =>
|
||||
static Future<double> fetchPrice(
|
||||
CryptoCurrency crypto, FiatCurrency fiat) async =>
|
||||
await _fetchPriceAsync(crypto, fiat);
|
||||
}
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
import 'package:flutter/foundation.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/core/validator.dart';
|
||||
|
||||
|
|
13
lib/core/sec_random_native.dart
Normal file
|
@ -0,0 +1,13 @@
|
|||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
const utils = const MethodChannel('com.cake_wallet/native_utils');
|
||||
|
||||
Future<Uint8List> secRandom(int count) async {
|
||||
try {
|
||||
return await utils.invokeMethod<Uint8List>('sec_random', {'count': count});
|
||||
} on PlatformException catch (_) {
|
||||
return Uint8List.fromList([]);
|
||||
}
|
||||
}
|
|
@ -24,6 +24,8 @@ class SeedValidator extends Validator<MnemonicItem> {
|
|||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
return getBitcoinWordList(language);
|
||||
case WalletType.litecoin:
|
||||
return getBitcoinWordList(language);
|
||||
case WalletType.monero:
|
||||
return getMoneroWordList(language);
|
||||
default:
|
||||
|
|
|
@ -3,43 +3,50 @@ import 'package:mobx/mobx.dart';
|
|||
import 'package:cake_wallet/entities/transaction_info.dart';
|
||||
|
||||
abstract class TransactionHistoryBase<TransactionType extends TransactionInfo> {
|
||||
TransactionHistoryBase() : _isUpdating = false;
|
||||
TransactionHistoryBase();
|
||||
// : _isUpdating = false;
|
||||
|
||||
@observable
|
||||
ObservableMap<String, TransactionType> transactions;
|
||||
|
||||
bool _isUpdating;
|
||||
Future<void> save();
|
||||
|
||||
@action
|
||||
Future<void> 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<String, TransactionType> 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<void> update() async {
|
||||
// if (_isUpdating) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
Future<Map<String, TransactionType>> 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<Map<String, TransactionType>> fetchTransactions();
|
||||
}
|
||||
|
|
|
@ -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<BalanceType extends Balance> {
|
||||
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<BalanceType extends Balance> {
|
|||
|
||||
Object get keys;
|
||||
|
||||
TransactionHistoryBase transactionHistory;
|
||||
HistoryType transactionHistory;
|
||||
|
||||
Future<void> connectToNode({@required Node node});
|
||||
|
||||
|
@ -51,6 +55,12 @@ abstract class WalletBase<BalanceType extends Balance> {
|
|||
|
||||
int calculateEstimatedFee(TransactionPriority priority, int amount);
|
||||
|
||||
// void fetchTransactionsAsync(
|
||||
// void Function(TransactionType transaction) onTransactionLoaded,
|
||||
// {void Function() onFinished});
|
||||
|
||||
Future<Map<String, TransactionType>> fetchTransactions();
|
||||
|
||||
Future<void> save();
|
||||
|
||||
Future<void> rescan({int height});
|
||||
|
|
|
@ -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<N extends WalletCredentials,
|
||||
RFS extends WalletCredentials, RFK extends WalletCredentials> {
|
||||
WalletType getType();
|
||||
|
||||
Future<WalletBase> create(N credentials);
|
||||
|
||||
Future<WalletBase> restoreFromSeed(RFS credentials);
|
||||
|
|
|
@ -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<WalletService, WalletType, void>(
|
||||
(WalletType param1, __) {
|
||||
switch (param1) {
|
||||
|
@ -448,6 +451,8 @@ Future setup(
|
|||
return getIt.get<MoneroWalletService>();
|
||||
case WalletType.bitcoin:
|
||||
return getIt.get<BitcoinWalletService>();
|
||||
case WalletType.litecoin:
|
||||
return getIt.get<LitecoinWalletService>();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,7 +19,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> {
|
|||
CryptoCurrency.eos,
|
||||
CryptoCurrency.eth,
|
||||
CryptoCurrency.ltc,
|
||||
CryptoCurrency.nano,
|
||||
CryptoCurrency.trx,
|
||||
CryptoCurrency.usdt,
|
||||
CryptoCurrency.usdterc20,
|
||||
|
@ -29,7 +28,7 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> {
|
|||
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<int> with Serializable<int> {
|
|||
return CryptoCurrency.ada;
|
||||
case 'bch':
|
||||
return CryptoCurrency.bch;
|
||||
case 'bnb':
|
||||
case 'bnbmainnet':
|
||||
return CryptoCurrency.bnb;
|
||||
case 'btc':
|
||||
return CryptoCurrency.btc;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 '';
|
||||
}
|
||||
}
|
|
@ -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<void> replaceNodesMigration({@required Box<Node> 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<void> changeMoneroCurrentNodeToDefault(
|
|||
}
|
||||
|
||||
Node getBitcoinDefaultElectrumServer({@required Box<Node> 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<Node> 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<Node> nodes}) {
|
||||
final timeZone = DateTime.now().timeZoneOffset.inHours;
|
||||
var nodeUri = '';
|
||||
|
@ -192,6 +208,15 @@ Future<void> changeBitcoinCurrentElectrumServerToDefault(
|
|||
await sharedPreferences.setInt('current_node_id_btc', serverId);
|
||||
}
|
||||
|
||||
Future<void> changeLitecoinCurrentElectrumServerToDefault(
|
||||
{@required SharedPreferences sharedPreferences,
|
||||
@required Box<Node> nodes}) async {
|
||||
final server = getLitecoinDefaultElectrumServer(nodes: nodes);
|
||||
final serverId = server?.key as int ?? 0;
|
||||
|
||||
await sharedPreferences.setInt('current_node_id_ltc', serverId);
|
||||
}
|
||||
|
||||
Future<void> replaceDefaultNode(
|
||||
{@required SharedPreferences sharedPreferences,
|
||||
@required Box<Node> nodes}) async {
|
||||
|
@ -224,7 +249,12 @@ Future<void> updateNodeTypes({@required Box<Node> nodes}) async {
|
|||
}
|
||||
|
||||
Future<void> addBitcoinElectrumServerList({@required Box<Node> nodes}) async {
|
||||
final serverList = await loadElectrumServerList();
|
||||
final serverList = await loadBitcoinElectrumServerList();
|
||||
await nodes.addAll(serverList);
|
||||
}
|
||||
|
||||
Future<void> addLitecoinElectrumServerList({@required Box<Node> nodes}) async {
|
||||
final serverList = await loadLitecoinElectrumServerList();
|
||||
await nodes.addAll(serverList);
|
||||
}
|
||||
|
||||
|
@ -284,57 +314,97 @@ Future<void> changeTransactionPriorityAndFeeRateKeys(
|
|||
Future<void> changeDefaultMoneroNode(
|
||||
Box<Node> 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<void> checkCurrentNodes(Box<Node> 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<void> checkCurrentNodes(
|
||||
Box<Node> 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<void> resetElectrumServer(Box<Node> 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<void> resetBitcoinElectrumServer(
|
||||
Box<Node> 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();
|
||||
|
|
|
@ -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<void> ios_migrate_address_book(Box<Contact> contactSource) async {
|
|||
}
|
||||
|
||||
final List<dynamic> addresses =
|
||||
json.decode(addressBookJSON.readAsStringSync()) as List<dynamic>;
|
||||
json.decode(addressBookJSON.readAsStringSync()) as List<dynamic>;
|
||||
final contacts = addresses.map((dynamic item) {
|
||||
final _item = item as Map<String, dynamic>;
|
||||
final type = _item["type"] as String;
|
||||
|
@ -420,7 +418,7 @@ Future<void> ios_migrate_address_book(Box<Contact> contactSource) async {
|
|||
|
||||
await contactSource.addAll(contacts);
|
||||
await prefs.setBool('ios_migration_address_book_completed', true);
|
||||
} catch(e) {
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 = <String, String> {};
|
||||
|
||||
|
|
|
@ -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<String, dynamic>;
|
||||
} 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<String, dynamic>;
|
||||
}
|
||||
|
||||
|
@ -98,8 +119,13 @@ class Node extends HiveObject with Keyable {
|
|||
}
|
||||
}
|
||||
|
||||
Future<bool> requestBitcoinElectrumServer() async {
|
||||
// FIXME: IMPLEMENT ME
|
||||
return true;
|
||||
Future<bool> requestElectrumServer() async {
|
||||
try {
|
||||
await SecureSocket.connect(uri.host, uri.port,
|
||||
timeout: Duration(seconds: 5), onBadCertificate: (_) => true);
|
||||
return true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,9 +20,9 @@ Future<List<Node>> loadDefaultNodes() async {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
Future<List<Node>> loadElectrumServerList() async {
|
||||
Future<List<Node>> 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<List<Node>> loadElectrumServerList() async {
|
|||
}).toList();
|
||||
}
|
||||
|
||||
Future<List<Node>> 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<Node> 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);
|
||||
|
|
|
@ -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';
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -48,7 +48,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider {
|
|||
@override
|
||||
Future<Limits> 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<dynamic>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -107,7 +107,7 @@ Future<void> main() async {
|
|||
exchangeTemplates: exchangeTemplates,
|
||||
transactionDescriptions: transactionDescriptions,
|
||||
secureStorage: secureStorage,
|
||||
initialMigrationVersion: 13);
|
||||
initialMigrationVersion: 15);
|
||||
runApp(App());
|
||||
} catch (e) {
|
||||
runApp(MaterialApp(
|
||||
|
@ -135,7 +135,7 @@ Future<void> initialSetup(
|
|||
@required Box<ExchangeTemplate> exchangeTemplates,
|
||||
@required Box<TransactionDescription> transactionDescriptions,
|
||||
FlutterSecureStorage secureStorage,
|
||||
int initialMigrationVersion = 13}) async {
|
||||
int initialMigrationVersion = 15}) async {
|
||||
LanguageService.loadLocaleList();
|
||||
await defaultSettingsMigration(
|
||||
secureStorage: secureStorage,
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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<MoneroTransactionInfo> _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<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
|
||||
monero_transaction_history.refreshTransactions();
|
||||
return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
|
||||
<String, MoneroTransactionInfo>{},
|
||||
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
|
||||
acc[tx.id] = tx;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
Future<void> 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<String, MoneroTransactionInfo> transactions) =>
|
||||
this.transactions.addAll(transactions);
|
||||
}
|
||||
|
|
|
@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo {
|
|||
@override
|
||||
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
|
||||
|
||||
@override
|
||||
String feeFormatted() =>
|
||||
'${formatAmount(moneroAmountToString(amount: fee))} XMR';
|
||||
}
|
||||
|
|
|
@ -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<MoneroBalance> with Store {
|
||||
MoneroWalletBase({String filename, WalletInfo walletInfo})
|
||||
: transactionHistory = MoneroTransactionHistory(),
|
||||
accountList = MoneroAccountList(),
|
||||
abstract class MoneroWalletBase extends WalletBase<MoneroBalance,
|
||||
MoneroTransactionHistory, MoneroTransactionInfo> 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<MoneroBalance> with Store {
|
|||
|
||||
static const int _autoAfterSyncSaveInterval = 60000;
|
||||
|
||||
@override
|
||||
final MoneroTransactionHistory transactionHistory;
|
||||
|
||||
@observable
|
||||
Account account;
|
||||
|
||||
|
@ -91,19 +92,18 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
|||
|
||||
final MoneroAccountList accountList;
|
||||
|
||||
String _filename;
|
||||
SyncListener _listener;
|
||||
ReactionDisposer _onAccountChangeReaction;
|
||||
int _lastAutosaveTimestamp;
|
||||
bool _isSavingAfterSync;
|
||||
bool _isSavingAfterNewTransaction;
|
||||
bool _isTransactionUpdating;
|
||||
int _lastSaveTimestamp;
|
||||
|
||||
Future<void> 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<MoneroBalance> 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<MoneroBalance> 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<MoneroBalance> with Store {
|
|||
|
||||
@override
|
||||
Future<void> 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<MoneroBalance> with Store {
|
|||
await walletInfo.save();
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Map<String, MoneroTransactionInfo>> fetchTransactions() async {
|
||||
monero_transaction_history.refreshTransactions();
|
||||
return _getAllTransactions(null).fold<Map<String, MoneroTransactionInfo>>(
|
||||
<String, MoneroTransactionInfo>{},
|
||||
(Map<String, MoneroTransactionInfo> acc, MoneroTransactionInfo tx) {
|
||||
acc[tx.id] = tx;
|
||||
return acc;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> 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<MoneroTransactionInfo> _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<MoneroBalance> with Store {
|
|||
}
|
||||
|
||||
Future<void> _askForUpdateTransactionHistory() async =>
|
||||
await transactionHistory.update();
|
||||
await updateTransactions();
|
||||
|
||||
int _getFullBalance() =>
|
||||
monero_wallet.getFullBalance(accountIndex: account.id);
|
||||
|
@ -389,11 +430,12 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance> with Store {
|
|||
}
|
||||
}
|
||||
|
||||
void _onNewTransaction() {
|
||||
void _onNewTransaction() async {
|
||||
try {
|
||||
_askForUpdateTransactionHistory();
|
||||
await _askForUpdateTransactionHistory();
|
||||
_askForUpdateBalance();
|
||||
Timer(Duration(seconds: 1), () => _afterNewTransactionSave());
|
||||
await Future<void>.delayed(Duration(seconds: 1));
|
||||
await _afterNewTransactionSave();
|
||||
} catch (e) {
|
||||
print(e.toString());
|
||||
}
|
||||
|
|
|
@ -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<MoneroWallet> 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<bool> 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<MoneroWallet> 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<void> 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<MoneroWallet> 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<MoneroWallet> 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();
|
||||
}
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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<Balance> wallet) async {
|
||||
_onCurrentWalletChangeReaction = reaction((_) => appStore.wallet, (WalletBase<
|
||||
Balance, TransactionHistoryBase<TransactionInfo>, 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<Balance> wallet) async {
|
||||
reaction((_) => appStore.wallet, (WalletBase<Balance,
|
||||
TransactionHistoryBase<TransactionInfo>, TransactionInfo>
|
||||
wallet) async {
|
||||
try {
|
||||
fiatConversionStore.prices[wallet.currency] = 0;
|
||||
fiatConversionStore.prices[wallet.currency] =
|
||||
|
|
|
@ -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<Balance> wallet) {
|
||||
void startWalletSyncStatusChangeReaction(
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
|
||||
TransactionInfo>
|
||||
wallet) {
|
||||
_onWalletSyncStatusChangeReaction?.reaction?.dispose();
|
||||
_onWalletSyncStatusChangeReaction =
|
||||
reaction((_) => wallet.syncStatus, (SyncStatus status) async {
|
||||
if (status is ConnectedSyncStatus) {
|
||||
await wallet.startSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
if (status is ConnectedSyncStatus) {
|
||||
await wallet.startSync();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -239,9 +239,11 @@ Route<dynamic> createRoute(RouteSettings settings) {
|
|||
case Routes.unlock:
|
||||
return MaterialPageRoute<void>(
|
||||
fullscreenDialog: true,
|
||||
builder: (_) => getIt.get<AuthPage>(
|
||||
param1: settings.arguments as OnAuthenticationFinished,
|
||||
param2: false));
|
||||
builder: (_) => WillPopScope(
|
||||
child: getIt.get<AuthPage>(
|
||||
param1: settings.arguments as OnAuthenticationFinished,
|
||||
param2: false),
|
||||
onWillPop: () async => false));
|
||||
|
||||
case Routes.nodeList:
|
||||
return CupertinoPageRoute<void>(
|
||||
|
|
|
@ -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: <Widget>[
|
||||
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<void>.delayed(Duration(seconds: 1));
|
||||
await showPopUp<void>(
|
||||
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<void> _onClickBuyButton(BuildContext context) async {
|
||||
final walletType = walletViewModel.type;
|
||||
|
||||
switch (walletType) {
|
||||
case WalletType.bitcoin:
|
||||
Navigator.of(context).pushNamed(Routes.preOrder);
|
||||
break;
|
||||
default:
|
||||
await showPopUp<void>(
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ class MenuWidget extends StatefulWidget {
|
|||
class MenuWidgetState extends State<MenuWidget> {
|
||||
Image moneroIcon;
|
||||
Image bitcoinIcon;
|
||||
Image litecoinIcon;
|
||||
final largeScreen = 731;
|
||||
|
||||
double menuWidth;
|
||||
|
@ -76,6 +77,7 @@ class MenuWidgetState extends State<MenuWidget> {
|
|||
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<MenuWidget> {
|
|||
return moneroIcon;
|
||||
case WalletType.bitcoin:
|
||||
return bitcoinIcon;
|
||||
case WalletType.litecoin:
|
||||
return litecoinIcon;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -248,7 +248,8 @@ class ExchangePage extends BasePage {
|
|||
exchangeViewModel
|
||||
.isReceiveAddressEnabled,
|
||||
isAmountEstimated: true,
|
||||
currencies: CryptoCurrency.all,
|
||||
currencies:
|
||||
exchangeViewModel.receiveCurrencies,
|
||||
onCurrencySelected: (currency) =>
|
||||
exchangeViewModel
|
||||
.changeReceiveCurrency(
|
||||
|
|
|
@ -59,6 +59,8 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
|||
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<WalletTypeForm> {
|
|||
|
||||
@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<WalletTypeForm> {
|
|||
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<WalletTypeForm> {
|
|||
),
|
||||
),
|
||||
...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<WalletTypeForm> {
|
|||
return moneroIcon;
|
||||
case WalletType.bitcoin:
|
||||
return bitcoinIcon;
|
||||
case WalletType.litecoin:
|
||||
return litecoinIcon;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
|
|
|
@ -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: () =>
|
||||
|
|