Merge branch 'main' into CAKE-192-implement-unstoppable-domain

# Conflicts:
#	android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java
#	ios/Runner/AppDelegate.swift
#	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
This commit is contained in:
OleksandrSobol 2021-05-24 19:09:43 +03:00
commit 251d6ad502
129 changed files with 3151 additions and 1514 deletions

2
.gitignore vendored
View file

@ -96,3 +96,5 @@ vendor/
android/app/.cxx/**
ios/Flutter/.last_build_id
/lib/generated/**
#**#
/**/#**#

View file

@ -25,7 +25,6 @@ linter:
- empty_constructor_bodies
- empty_statements
- hash_and_equals
- implementation_imports
- invariant_booleans
- iterable_contains_unrelated_type
- library_names

View file

@ -1,10 +1,19 @@
package com.cakewallet.cake_wallet;
import androidx.annotation.NonNull;
import io.flutter.embedding.android.FlutterFragmentActivity;
import io.flutter.embedding.engine.FlutterEngine;
import io.flutter.plugins.GeneratedPluginRegistrant;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import android.os.AsyncTask;
import android.os.Handler;
import android.os.Looper;
import java.security.SecureRandom;
import com.unstoppabledomains.resolution.DomainResolution;
import com.unstoppabledomains.resolution.Resolution;
@ -16,6 +25,8 @@ import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
public class MainActivity extends FlutterFragmentActivity {
final String UTILS_CHANNEL = "com.cake_wallet/native_utils";
final String UNSTOPPABLE_DOMAIN_CHANNEL = "com.cakewallet.cake_wallet/unstoppable-domain";
@Override
@ -55,5 +66,29 @@ public class MainActivity extends FlutterFragmentActivity {
handler.post(() -> result.error("INVALID DOMAIN", e.getMessage(), null));
}
});
MethodChannel utilsChannel =
new MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(),
UTILS_CHANNEL);
utilsChannel.setMethodCallHandler(this::handle);
}
private void handle(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
Handler handler = new Handler(Looper.getMainLooper());
try {
if (call.method.equals("sec_random")) {
int count = call.argument("count");
SecureRandom random = new SecureRandom();
byte bytes[] = new byte[count];
random.nextBytes(bytes);
handler.post(() -> result.success(bytes));
} else {
handler.post(() -> result.notImplemented());
}
} catch (Exception e) {
handler.post(() -> result.error("UNCAUGHT_ERROR", e.getMessage(), null));
}
}
}

View file

@ -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);
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View file

@ -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>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.6 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 128 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View file

@ -0,0 +1,2 @@
-
uri: ltc-electrum.cakewallet.com:50002

View file

@ -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,

View file

@ -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

View file

@ -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';

View file

@ -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"

View 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
}

View file

@ -169,7 +169,7 @@ SPEC CHECKSUMS:
BigInt: f668a80089607f521586bbe29513d708491ef2f7
connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467
CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060
cw_monero: 2e1f79929880cc2293b5bc1b25e28152e4d84649
cw_monero: 78f369253cc913efc23db9cf6be81a11eaf40fe1
devicelocale: b22617f40038496deffba44747101255cee005b0
DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d
DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179

View file

@ -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;
@ -358,7 +362,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 34;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -375,7 +379,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 4.1.5;
MARKETING_VERSION = 4.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -501,7 +505,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 34;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -518,7 +522,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 4.1.5;
MARKETING_VERSION = 4.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";
@ -536,7 +540,7 @@
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES;
CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements;
CURRENT_PROJECT_VERSION = 34;
CURRENT_PROJECT_VERSION = 40;
DEVELOPMENT_TEAM = 32J6BB6VUS;
ENABLE_BITCODE = NO;
FRAMEWORK_SEARCH_PATHS = (
@ -553,7 +557,7 @@
"$(inherited)",
"$(PROJECT_DIR)/Flutter",
);
MARKETING_VERSION = 4.1.5;
MARKETING_VERSION = 4.2.0;
PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h";

View file

@ -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"

View file

@ -7,16 +7,20 @@ import UnstoppableDomainsResolution
lazy var resolution : Resolution? = {
return try? Resolution()
}()
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
let controller : FlutterViewController = window?.rootViewController as! FlutterViewController
let legacyMigrationChannel = FlutterMethodChannel(
name: "com.cakewallet.cakewallet/legacy_wallet_migration",
binaryMessenger: controller.binaryMessenger)
legacyMigrationChannel.setMethodCallHandler({
let batteryChannel = FlutterMethodChannel(name: "com.cakewallet.cakewallet/legacy_wallet_migration",
binaryMessenger: controller.binaryMessenger)
let unstoppableDomainChannel = FlutterMethodChannel(name: "com.cakewallet.cake_wallet/unstoppable-domain", binaryMessenger: controller.binaryMessenger)
batteryChannel.setMethodCallHandler({
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
@ -58,7 +62,25 @@ import UnstoppableDomainsResolution
result(FlutterMethodNotImplemented)
}
})
let utilsChannel = FlutterMethodChannel(
name: "com.cake_wallet/native_utils",
binaryMessenger: controller.binaryMessenger)
utilsChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
case "sec_random":
guard let args = call.arguments as? Dictionary<String, Any>,
let count = args["count"] as? Int else {
result(nil)
return
}
result(secRandom(count: count))
default:
result(FlutterMethodNotImplemented)
}
})
unstoppableDomainChannel.setMethodCallHandler({ [weak self]
(call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in
switch call.method {
@ -69,29 +91,29 @@ import UnstoppableDomainsResolution
result(nil)
return
}
guard let resolution = self?.resolution else {
result(nil)
return
}
resolution.addr(domain: domain, ticker: ticker) { addrResult in
var address : String = ""
switch addrResult {
case .success(let returnValue):
address = returnValue
case .failure(let error):
print("Expected Address, but got \(error)")
}
result(address)
}
default:
result(FlutterMethodNotImplemented)
}
})
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}

View file

@ -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
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 14 KiB

View file

@ -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);
}
}
}

View file

@ -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});
}

View file

@ -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;
}
}

View file

@ -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.';
}

View file

@ -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;
}
}
}

View file

@ -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;
}
}

View file

@ -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');
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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) {

View file

@ -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 =>

View 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;
}
}
}

View file

@ -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,

View 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();
}
}

View 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);
}
}
}

View 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);

View 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;
}
}

View 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;
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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:

View file

@ -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';

View file

@ -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);
}

View file

@ -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();
}
}

View file

@ -1,4 +1,3 @@
import 'package:flutter/foundation.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/validator.dart';

View 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([]);
}
}

View file

@ -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:

View file

@ -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();
}

View file

@ -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});

View file

@ -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);

View file

@ -1,4 +1,5 @@
import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart';
import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart';
import 'package:cake_wallet/core/backup_service.dart';
import 'package:cake_wallet/core/wallet_service.dart';
import 'package:cake_wallet/entities/biometric_auth.dart';
@ -443,6 +444,8 @@ Future setup(
getIt.registerFactory(() => BitcoinWalletService(_walletInfoSource));
getIt.registerFactory(() => LitecoinWalletService(_walletInfoSource));
getIt.registerFactoryParam<WalletService, WalletType, void>(
(WalletType param1, __) {
switch (param1) {
@ -450,6 +453,8 @@ Future setup(
return getIt.get<MoneroWalletService>();
case WalletType.bitcoin:
return getIt.get<BitcoinWalletService>();
case WalletType.litecoin:
return getIt.get<LitecoinWalletService>();
default:
return null;
}
@ -533,8 +538,7 @@ Future setup(
TradeDetailsPage(getIt.get<TradeDetailsViewModel>(param1: trade)));
getIt.registerFactory(() {
final wallet = getIt.get<AppStore>().wallet;
return WyreService(wallet: wallet);
return WyreService(appStore: getIt.get<AppStore>());
});
getIt.registerFactory(() {
@ -546,10 +550,9 @@ Future setup(
WyrePage(getIt.get<WyreViewModel>(),
ordersStore: getIt.get<OrdersStore>(), url: url));
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>(
(order, _) => OrderDetailsViewModel(
wyreViewModel: getIt.get<WyreViewModel>(),
orderForDetails: order));
getIt.registerFactoryParam<OrderDetailsViewModel, Order, void>((order, _) =>
OrderDetailsViewModel(
wyreViewModel: getIt.get<WyreViewModel>(), orderForDetails: order));
getIt.registerFactoryParam<OrderDetailsPage, Order, void>((Order order, _) =>
OrderDetailsPage(getIt.get<OrderDetailsViewModel>(param1: order)));

View file

@ -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';
}
}

View file

@ -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;

View file

@ -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;
}
}
}

View file

@ -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 '';
}
}

View file

@ -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();

View file

@ -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());
}
}

View file

@ -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> {};

View file

@ -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;
}
}
}

View file

@ -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);

View file

@ -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';
}
}

View file

@ -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;
}

View file

@ -1,7 +1,7 @@
import 'dart:convert';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/entities/wyre_exception.dart';
import 'package:cake_wallet/exchange/trade_state.dart';
import 'package:cake_wallet/store/app_store.dart';
import 'package:flutter/foundation.dart';
import 'package:http/http.dart';
import 'package:cake_wallet/.secrets.g.dart' as secrets;
@ -9,15 +9,9 @@ import 'package:cake_wallet/entities/order.dart';
import 'package:cake_wallet/entities/wallet_type.dart';
class WyreService {
WyreService({
@required this.wallet,
this.isTestEnvironment = false}) {
baseApiUrl = isTestEnvironment
? _baseTestApiUrl
: _baseProductApiUrl;
trackUrl = isTestEnvironment
? _trackTestUrl
: _trackProductUrl;
WyreService({@required this.appStore, this.isTestEnvironment = false}) {
baseApiUrl = isTestEnvironment ? _baseTestApiUrl : _baseProductApiUrl;
trackUrl = isTestEnvironment ? _trackTestUrl : _trackProductUrl;
}
static const _baseTestApiUrl = 'https://api.testwyre.com';
@ -31,24 +25,28 @@ class WyreService {
static const _trackSuffix = '/track';
final bool isTestEnvironment;
final WalletBase wallet;
final AppStore appStore;
WalletType get walletType => wallet.type;
String get walletAddress => wallet.address;
String get walletId => wallet.id;
WalletType get walletType => appStore.wallet.type;
String get walletAddress => appStore.wallet.address;
String get walletId => appStore.wallet.id;
String baseApiUrl;
String trackUrl;
Future<String> getWyreUrl() async {
final timestamp = DateTime.now().millisecondsSinceEpoch.toString();
final url = baseApiUrl + _ordersSuffix + _reserveSuffix +
_timeStampSuffix + timestamp;
final url = baseApiUrl +
_ordersSuffix +
_reserveSuffix +
_timeStampSuffix +
timestamp;
final secretKey = secrets.wyreSecretKey;
final accountId = secrets.wyreAccountId;
final body = {
'destCurrency': walletTypeToCryptoCurrency(walletType).title,
'dest': walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
'dest':
walletTypeToString(walletType).toLowerCase() + ':' + walletAddress,
'referrerAccountId': accountId,
'lockFields': ['destCurrency', 'dest']
};
@ -79,7 +77,7 @@ class WyreService {
}
final orderResponseJSON =
json.decode(orderResponse.body) as Map<String, dynamic>;
json.decode(orderResponse.body) as Map<String, dynamic>;
final transferId = orderResponseJSON['transferId'] as String;
final from = orderResponseJSON['sourceCurrency'] as String;
final to = orderResponseJSON['destCurrency'] as String;
@ -87,7 +85,7 @@ class WyreService {
final state = TradeState.deserialize(raw: status.toLowerCase());
final createdAtRaw = orderResponseJSON['createdAt'] as int;
final createdAt =
DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal();
DateTime.fromMillisecondsSinceEpoch(createdAtRaw).toLocal();
final transferUrl =
baseApiUrl + _transferSuffix + transferId + _trackSuffix;
@ -98,7 +96,7 @@ class WyreService {
}
final transferResponseJSON =
json.decode(transferResponse.body) as Map<String, dynamic>;
json.decode(transferResponse.body) as Map<String, dynamic>;
final amount = transferResponseJSON['destAmount'] as double;
return Order(
@ -110,7 +108,6 @@ class WyreService {
createdAt: createdAt,
amount: amount.toString(),
receiveAddress: walletAddress,
walletId: walletId
);
walletId: walletId);
}
}
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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() {

View file

@ -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);
}

View file

@ -59,6 +59,7 @@ class MoneroTransactionInfo extends TransactionInfo {
@override
void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount);
@override
String feeFormatted() =>
'${formatAmount(moneroAmountToString(amount: fee))} XMR';
}

View file

@ -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,12 +92,13 @@ 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();
@ -109,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);
@ -150,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,
@ -236,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();
}
@ -262,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);
@ -313,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);
@ -387,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());
}

View file

@ -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();
}

View file

@ -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());
}
});
}

View file

@ -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] =

View file

@ -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();
}
});
}

View file

@ -237,9 +237,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>(

View file

@ -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,47 +132,37 @@ class DashboardPage extends BasePage {
image: exchangeImage,
title: S.of(context).exchange,
route: Routes.exchange),
if (walletViewModel.type == WalletType.bitcoin) Observer(
Observer(
builder: (_) => Stack(
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
if (walletViewModel.isRunningWebView) Positioned(
top: -5,
child: SpinKitRing(
color: Theme.of(context).buttonColor,
lineWidth: 3,
size: 70.0,
),
),
ActionButton(
image: buyImage,
title: S.of(context).buy,
onClick: walletViewModel.isRunningWebView
? null
: () async {
try {
walletViewModel.isRunningWebView = true;
final url =
await walletViewModel.wyreViewModel.wyreUrl;
await Navigator.of(context)
.pushNamed(Routes.wyre, arguments: url);
walletViewModel.isRunningWebView = false;
} catch(e) {
print(e.toString());
walletViewModel.isRunningWebView = false;
}
})
],
)),
clipBehavior: Clip.none,
alignment: Alignment.topCenter,
children: [
if (walletViewModel.isRunningWebView)
Positioned(
top: -5,
child: SpinKitRing(
color: Theme.of(context).buttonColor,
lineWidth: 3,
size: 70.0,
),
),
ActionButton(
image: buyImage,
title: S.of(context).buy,
onClick: walletViewModel.isRunningWebView
? null
: () async =>
await _onClickBuyButton(context))
],
)),
],
)),
),
)
],
));
}
void _setEffects() {
void _setEffects(BuildContext context) {
if (_isEffectsInstalled) {
return;
}
@ -174,6 +171,52 @@ class DashboardPage extends BasePage {
pages.add(BalancePage(dashboardViewModel: walletViewModel));
pages.add(TransactionsPage(dashboardViewModel: walletViewModel));
autorun((_) async {
if (!walletViewModel.isOutdatedElectrumWallet) {
return;
}
await Future<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:
try {
walletViewModel.isRunningWebView = true;
final url = await walletViewModel.wyreViewModel.wyreUrl;
await Navigator.of(context).pushNamed(Routes.wyre, arguments: url);
walletViewModel.isRunningWebView = false;
} catch (_) {
walletViewModel.isRunningWebView = false;
}
break;
default:
await showPopUp<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;
}
}
}

View file

@ -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;
}

View file

@ -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;
}

View file

@ -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: () =>

View file

@ -13,7 +13,7 @@ class PreSeedPage extends BasePage {
imageDark = Image.asset('assets/images/pre_seed_dark.png'),
wordsCount = type == WalletType.monero
? 25
: 12; // FIXME: Stupid fast implementation
: 24; // FIXME: Stupid fast implementation
final Image imageDark;
final Image imageLight;
@ -49,7 +49,9 @@ class PreSeedPage extends BasePage {
Padding(
padding: EdgeInsets.only(top: 70, left: 16, right: 16),
child: Text(
S.of(context).pre_seed_description(wordsCount.toString()),
S
.of(context)
.pre_seed_description(wordsCount.toString()),
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,

View file

@ -40,6 +40,8 @@ class WalletListBodyState extends State<WalletListBody> {
Image.asset('assets/images/monero_logo.png', height: 24, width: 24);
final bitcoinIcon =
Image.asset('assets/images/bitcoin.png', height: 24, width: 24);
final litecoinIcon =
Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24);
final scrollController = ScrollController();
final double tileHeight = 60;
Flushbar<void> _progressBar;
@ -193,6 +195,8 @@ class WalletListBodyState extends State<WalletListBody> {
return bitcoinIcon;
case WalletType.monero:
return moneroIcon;
case WalletType.litecoin:
return litecoinIcon;
default:
return null;
}

View file

@ -1,6 +1,8 @@
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/entities/transaction_info.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/entities/balance.dart';
import 'package:cake_wallet/core/wallet_base.dart';
import 'package:cake_wallet/core/transaction_history.dart';
import 'package:cake_wallet/store/wallet_list_store.dart';
import 'package:cake_wallet/store/authentication_store.dart';
import 'package:cake_wallet/store/settings_store.dart';
@ -20,7 +22,8 @@ abstract class AppStoreBase with Store {
AuthenticationStore authenticationStore;
@observable
WalletBase<Balance> wallet;
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo>
wallet;
WalletListStore walletList;
@ -29,7 +32,10 @@ abstract class AppStoreBase with Store {
NodeListStore nodeListStore;
@action
void changeCurrentWallet(WalletBase wallet) {
void changeCurrentWallet(
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>,
TransactionInfo>
wallet) {
this.wallet?.close();
this.wallet = wallet;
}

View file

@ -149,7 +149,7 @@ abstract class SettingsStoreBase with Store {
static Future<SettingsStore> load(
{@required Box<Node> nodeSource,
@required bool isBitcoinBuyEnabled,
@required bool isBitcoinBuyEnabled,
FiatCurrency initialFiatCurrency = FiatCurrency.usd,
MoneroTransactionPriority initialMoneroTransactionPriority =
MoneroTransactionPriority.slow,
@ -205,15 +205,19 @@ abstract class SettingsStoreBase with Store {
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId = sharedPreferences
.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
final litecoinElectrumServerId = sharedPreferences
.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey);
final moneroNode = nodeSource.get(nodeId);
final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId);
final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId);
final packageInfo = await PackageInfo.fromPlatform();
return SettingsStore(
sharedPreferences: sharedPreferences,
nodes: {
WalletType.monero: moneroNode,
WalletType.bitcoin: bitcoinElectrumServer
WalletType.bitcoin: bitcoinElectrumServer,
WalletType.litecoin: litecoinElectrumServer
},
appVersion: packageInfo.version,
isBitcoinBuyEnabled: isBitcoinBuyEnabled,
@ -263,6 +267,10 @@ abstract class SettingsStoreBase with Store {
await _sharedPreferences.setInt(
PreferencesKey.currentBitcoinElectrumSererIdKey, node.key as int);
break;
case WalletType.litecoin:
await _sharedPreferences.setInt(
PreferencesKey.currentLitecoinElectrumSererIdKey, node.key as int);
break;
case WalletType.monero:
await _sharedPreferences.setInt(
PreferencesKey.currentNodeIdKey, node.key as int);

View file

@ -110,8 +110,6 @@ abstract class AuthViewModelBase with Store {
if (isAuthenticated) {
state = ExecutedSuccessfullyState();
} else {
state = FailureState('Failure biometric authentication');
}
}
} catch(e) {

Some files were not shown because too many files have changed in this diff Show more