diff --git a/android/app/build.gradle b/android/app/build.gradle index ee68ec0db..3251a6e96 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -71,8 +71,9 @@ android { release { signingConfig signingConfigs.release - minifyEnabled true - useProguard true + shrinkResources false + minifyEnabled false + useProguard false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } @@ -85,8 +86,9 @@ flutter { dependencies { testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + androidTestImplementation 'androidx.test:runner:1.3.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' + implementation 'com.unstoppabledomains:resolution:1.13.0' implementation 'com.google.firebase:firebase-core:19.0.0' implementation 'com.google.firebase:firebase-messaging:19.0.0' } diff --git a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java index 26cc56187..3afdc111e 100644 --- a/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java +++ b/android/app/src/main/java/com/cakewallet/cake_wallet/MainActivity.java @@ -10,14 +10,19 @@ import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import android.os.AsyncTask; +import android.os.Build; import android.os.Handler; import android.os.Looper; +import com.unstoppabledomains.resolution.DomainResolution; +import com.unstoppabledomains.resolution.Resolution; + import java.security.SecureRandom; public class MainActivity extends FlutterFragmentActivity { final String UTILS_CHANNEL = "com.cake_wallet/native_utils"; - + final int UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK = 24; + @Override public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) { GeneratedPluginRegistrant.registerWith(flutterEngine); @@ -33,17 +38,44 @@ public class MainActivity extends FlutterFragmentActivity { 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()); + switch (call.method) { + case "sec_random": + int count = call.argument("count"); + SecureRandom random = new SecureRandom(); + byte bytes[] = new byte[count]; + random.nextBytes(bytes); + handler.post(() -> result.success(bytes)); + break; + case "getUnstoppableDomainAddress": + int version = Build.VERSION.SDK_INT; + if (version >= UNSTOPPABLE_DOMAIN_MIN_VERSION_SDK) { + getUnstoppableDomainAddress(call, result); + } else { + handler.post(() -> result.success("")); + } + break; + default: + handler.post(() -> result.notImplemented()); } } catch (Exception e) { handler.post(() -> result.error("UNCAUGHT_ERROR", e.getMessage(), null)); } } -} + + private void getUnstoppableDomainAddress(@NonNull MethodCall call, @NonNull MethodChannel.Result result) { + DomainResolution resolution = new Resolution(); + Handler handler = new Handler(Looper.getMainLooper()); + String domain = call.argument("domain"); + String ticker = call.argument("ticker"); + + AsyncTask.execute(() -> { + try { + String address = resolution.getAddress(domain, ticker); + handler.post(() -> result.success(address)); + } catch (Exception e) { + System.out.println("Expected Address, but got " + e.getMessage()); + handler.post(() -> result.success("")); + } + }); + } +} \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 0d11c6b94..39e58da44 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -5,7 +5,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.4' + classpath 'com.android.tools.build:gradle:4.1.3' classpath 'com.google.gms:google-services:4.3.8' } } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index 296b146b7..b7ca2e6de 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Fri Jun 23 08:50:38 CEST 2017 +#Mon Apr 19 18:19:26 EEST 2021 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-bin.zip diff --git a/ios/Podfile b/ios/Podfile index 3cfff7b1d..3c8f789c9 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,5 +1,5 @@ # Uncomment this line to define a global platform for your project -platform :ios, '9.0' +platform :ios, '11.0' source 'https://github.com/CocoaPods/Specs.git' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. @@ -36,6 +36,7 @@ target 'Runner' do # Cake Wallet (Legacy) pod 'CryptoSwift' + pod 'UnstoppableDomainsResolution', '~> 0.3.6' pod 'Firebase/Messaging', '6.33.0' end diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 50f9b20d2..4be2c24bc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,6 +3,7 @@ PODS: - Flutter - MTBBarcodeScanner - SwiftProtobuf + - BigInt (5.2.0) - connectivity (0.0.1): - Flutter - Reachability @@ -59,6 +60,8 @@ PODS: - SwiftyGif - esys_flutter_share (0.0.1): - Flutter + - EthereumAddress (1.3.0): + - CryptoSwift (~> 1.0) - file_picker (0.0.1): - DKImagePickerController/PhotoGallery - Flutter @@ -150,6 +153,10 @@ PODS: - Flutter - SwiftProtobuf (1.12.0) - SwiftyGif (5.3.0) + - UnstoppableDomainsResolution (0.3.6): + - BigInt + - CryptoSwift (~> 1.0) + - EthereumAddress (~> 1.3) - url_launcher (0.0.1): - Flutter - webview_flutter (0.0.1): @@ -174,14 +181,17 @@ DEPENDENCIES: - permission_handler (from `.symlinks/plugins/permission_handler/ios`) - share (from `.symlinks/plugins/share/ios`) - shared_preferences (from `.symlinks/plugins/shared_preferences/ios`) + - UnstoppableDomainsResolution (~> 0.3.6) - url_launcher (from `.symlinks/plugins/url_launcher/ios`) - webview_flutter (from `.symlinks/plugins/webview_flutter/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: + - BigInt - CryptoSwift - DKImagePickerController - DKPhotoGallery + - EthereumAddress - Firebase - FirebaseCore - FirebaseCoreDiagnostics @@ -198,6 +208,7 @@ SPEC REPOS: - SDWebImage - SwiftProtobuf - SwiftyGif + - UnstoppableDomainsResolution EXTERNAL SOURCES: barcode_scan: @@ -239,6 +250,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: barcode_scan: a5c27959edfafaa0c771905bad0b29d6d39e4479 + BigInt: f668a80089607f521586bbe29513d708491ef2f7 connectivity: c4130b2985d4ef6fd26f9702e886bd5260681467 CryptoSwift: 093499be1a94b0cae36e6c26b70870668cb56060 cw_monero: 78f369253cc913efc23db9cf6be81a11eaf40fe1 @@ -246,6 +258,7 @@ SPEC CHECKSUMS: DKImagePickerController: b5eb7f7a388e4643264105d648d01f727110fc3d DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 esys_flutter_share: 403498dab005b36ce1f8d7aff377e81f0621b0b4 + EthereumAddress: 39fe8e11cf04e4e9902b55ae653dbc4e0aee5f30 file_picker: 3e6c3790de664ccf9b882732d9db5eaf6b8d4eb1 Firebase: 8db6f2d1b2c5e2984efba4949a145875a8f65fe5 firebase_core: 5d6a02f3d85acd5f8321c2d6d62877626a670659 @@ -273,9 +286,9 @@ SPEC CHECKSUMS: shared_preferences: af6bfa751691cdc24be3045c43ec037377ada40d SwiftProtobuf: 4ef85479c18ca85b5482b343df9c319c62bda699 SwiftyGif: e466e86c660d343357ab944a819a101c4127cb40 + UnstoppableDomainsResolution: 63abb84858d3e91eb838a5bfa6f7e3c0e0593f24 url_launcher: 6fef411d543ceb26efce54b05a0a40bfd74cbbef webview_flutter: 9f491a9b5a66f2573946a389b2677987b0ff8c0b -PODFILE CHECKSUM: f316539722a6f9dbb0d0f1065a27fa7ea38c6f88 -COCOAPODS: 1.9.3 +COCOAPODS: 1.10.1 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 6f6193063..8fba27457 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -366,7 +366,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; @@ -384,7 +384,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -510,7 +510,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; @@ -528,7 +528,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; @@ -546,7 +546,7 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Runner.entitlements; - CURRENT_PROJECT_VERSION = 49; + CURRENT_PROJECT_VERSION = 51; DEVELOPMENT_TEAM = 32J6BB6VUS; ENABLE_BITCODE = NO; EXCLUDED_SOURCE_FILE_NAMES = ""; @@ -564,7 +564,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 4.2.3; + MARKETING_VERSION = 4.2.4; PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 2a3505ef9..ebf578d09 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,8 +1,13 @@ import UIKit import Flutter +import UnstoppableDomainsResolution @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { + lazy var resolution : Resolution? = { + return try? Resolution() + }() + override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? @@ -60,7 +65,7 @@ import Flutter let utilsChannel = FlutterMethodChannel( name: "com.cake_wallet/native_utils", binaryMessenger: controller.binaryMessenger) - utilsChannel.setMethodCallHandler({ (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in + utilsChannel.setMethodCallHandler({ [weak self] (call: FlutterMethodCall, result: @escaping FlutterResult) -> Void in switch call.method { case "sec_random": guard let args = call.arguments as? Dictionary, @@ -70,6 +75,27 @@ import Flutter } result(secRandom(count: count)) + case "getUnstoppableDomainAddress": + guard let args = call.arguments as? Dictionary, + let domain = args["domain"], + let ticker = args["ticker"], + 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) } diff --git a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart index 9d1401818..b699ade29 100644 --- a/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart +++ b/lib/bitcoin/bitcoin_transaction_wrong_balance_exception.dart @@ -1,4 +1,10 @@ +import 'package:cake_wallet/entities/crypto_currency.dart'; + class BitcoinTransactionWrongBalanceException implements Exception { + BitcoinTransactionWrongBalanceException(this.currency); + + final CryptoCurrency currency; + @override - String toString() => 'Wrong balance. Not enough BTC on your balance.'; + String toString() => 'Wrong balance. Not enough ${currency.title} on your balance.'; } \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_unspent.dart b/lib/bitcoin/bitcoin_unspent.dart index 846eb8c7d..b95ed9bc3 100644 --- a/lib/bitcoin/bitcoin_unspent.dart +++ b/lib/bitcoin/bitcoin_unspent.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; class BitcoinUnspent { - BitcoinUnspent(this.address, this.hash, this.value, this.vout); + BitcoinUnspent(this.address, this.hash, this.value, this.vout) + : isSending = true, + isFrozen = false, + note = ''; factory BitcoinUnspent.fromJSON( BitcoinAddressRecord address, Map json) => @@ -15,4 +18,7 @@ class BitcoinUnspent { bool get isP2wpkh => address.address.startsWith('bc') || address.address.startsWith('ltc'); + bool isSending; + bool isFrozen; + String note; } diff --git a/lib/bitcoin/bitcoin_wallet.dart b/lib/bitcoin/bitcoin_wallet.dart index fd8402887..79444844d 100644 --- a/lib/bitcoin/bitcoin_wallet.dart +++ b/lib/bitcoin/bitcoin_wallet.dart @@ -1,12 +1,14 @@ +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; -import 'package:cake_wallet/bitcoin/utils.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_address_record.dart'; import 'package:cake_wallet/bitcoin/electrum_balance.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_wallet_addresses.dart'; part 'bitcoin_wallet.g.dart'; @@ -17,6 +19,7 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { {@required String mnemonic, @required String password, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -24,14 +27,22 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: bitcoin.bitcoin, initialAddresses: initialAddresses, - initialBalance: initialBalance, - accountIndex: accountIndex); + initialBalance: initialBalance) { + walletAddresses = BitcoinWalletAddresses( + walletInfo, + initialAddresses: initialAddresses, + accountIndex: accountIndex, + hd: hd, + networkType: networkType); + } static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -40,12 +51,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, 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); } diff --git a/lib/bitcoin/bitcoin_wallet_addresses.dart b/lib/bitcoin/bitcoin_wallet_addresses.dart new file mode 100644 index 000000000..ecded3cb8 --- /dev/null +++ b/lib/bitcoin/bitcoin_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/utils.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_addresses.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; + +part 'bitcoin_wallet_addresses.g.dart'; + +class BitcoinWalletAddresses = BitcoinWalletAddressesBase + with _$BitcoinWalletAddresses; + +abstract class BitcoinWalletAddressesBase extends ElectrumWalletAddresses + with Store { + BitcoinWalletAddressesBase( + WalletInfo walletInfo, + {@required List initialAddresses, + int accountIndex = 0, + @required bitcoin.HDWallet hd, + @required this.networkType}) + : super( + walletInfo, + initialAddresses: initialAddresses, + accountIndex: accountIndex, + hd: hd); + + bitcoin.NetworkType networkType; + + @override + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); +} \ No newline at end of file diff --git a/lib/bitcoin/bitcoin_wallet_service.dart b/lib/bitcoin/bitcoin_wallet_service.dart index aefe0fadf..8e5b31930 100644 --- a/lib/bitcoin/bitcoin_wallet_service.dart +++ b/lib/bitcoin/bitcoin_wallet_service.dart @@ -2,6 +2,7 @@ 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/bitcoin_wallet_creation_credentials.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:cake_wallet/core/wallet_base.dart'; import 'package:cake_wallet/core/wallet_service.dart'; import 'package:cake_wallet/bitcoin/bitcoin_wallet.dart'; @@ -14,9 +15,10 @@ class BitcoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - BitcoinWalletService(this.walletInfoSource); + BitcoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.bitcoin; @@ -26,7 +28,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; @@ -42,7 +45,8 @@ class BitcoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await BitcoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -67,7 +71,8 @@ class BitcoinWalletService extends WalletService< final wallet = BitcoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/electrum_wallet.dart b/lib/bitcoin/electrum_wallet.dart index 13913c41b..24438feca 100644 --- a/lib/bitcoin/electrum_wallet.dart +++ b/lib/bitcoin/electrum_wallet.dart @@ -1,5 +1,8 @@ import 'dart:async'; import 'dart:convert'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; +import 'package:hive/hive.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_addresses.dart'; import 'package:mobx/mobx.dart'; import 'package:rxdart/subjects.dart'; import 'package:flutter/foundation.dart'; @@ -38,30 +41,28 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo, @required List initialAddresses, @required this.networkType, @required this.mnemonic, ElectrumClient electrumClient, - int accountIndex = 0, ElectrumBalance initialBalance}) : balance = initialBalance ?? const ElectrumBalance(confirmed: 0, unconfirmed: 0), hd = bitcoin.HDWallet.fromSeed(mnemonicToSeedBytes(mnemonic), network: networkType) .derivePath("m/0'/0"), - addresses = ObservableList.of( - (initialAddresses ?? []).toSet()), syncStatus = NotConnectedSyncStatus(), _password = password, - _accountIndex = accountIndex, _feeRates = [], _isTransactionUpdating = false, super(walletInfo) { this.electrumClient = electrumClient ?? ElectrumClient(); this.walletInfo = walletInfo; + this.unspentCoinsInfo = unspentCoinsInfo; transactionHistory = ElectrumTransactionHistory(walletInfo: walletInfo, password: password); - _unspent = []; + unspentCoins = []; _scripthashesUpdateSubject = {}; } @@ -72,10 +73,10 @@ abstract class ElectrumWalletBase extends WalletBase unspentCoinsInfo; @override - @observable - String address; + ElectrumWalletAddresses walletAddresses; @override @observable @@ -85,9 +86,7 @@ abstract class ElectrumWalletBase extends WalletBase addresses; - - List get scriptHashes => addresses + List get scriptHashes => walletAddresses.addresses .map((addr) => scriptHash(addr.address, networkType: networkType)) .toList(); @@ -103,85 +102,26 @@ abstract class ElectrumWalletBase extends WalletBase _unspent; + List unspentCoins; List _feeRates; - int _accountIndex; Map> _scripthashesUpdateSubject; bool _isTransactionUpdating; Future init() async { - await generateAddresses(); - address = addresses[_accountIndex].address; + await walletAddresses.init(); await transactionHistory.init(); - } - - @action - Future nextAddress() async { - _accountIndex += 1; - - if (_accountIndex >= addresses.length) { - _accountIndex = 0; - } - - address = addresses[_accountIndex].address; - await save(); } - Future generateAddresses() async { - if (addresses.length < 33) { - final addressesCount = 33 - addresses.length; - await generateNewAddresses(addressesCount, - startIndex: addresses.length, hd: hd); - } - } - - Future generateNewAddress( - {bool isHidden = false, bitcoin.HDWallet hd}) async { - _accountIndex += 1; - final _hd = hd ?? this.hd; - final address = BitcoinAddressRecord( - getAddress(index: _accountIndex, hd: _hd), - index: _accountIndex, - isHidden: isHidden); - addresses.add(address); - await save(); - return address; - } - - Future> generateNewAddresses(int count, - {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { - final list = []; - - for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord(getAddress(index: i, hd: hd), - index: i, isHidden: isHidden); - list.add(address); - } - - addresses.addAll(list); - await save(); - return list; - } - - Future updateAddress(String address) async { - for (final addr in addresses) { - if (addr.address == address) { - await save(); - break; - } - } - } - @action @override Future startSync() async { try { syncStatus = StartingSyncStatus(); - updateTransactions(); + await updateTransactions(); _subscribeForUpdates(); await _updateBalance(); - await _updateUnspent(); + await updateUnspent(); _feeRates = await electrumClient.feeRates(); Timer.periodic(const Duration(minutes: 1), @@ -219,6 +159,23 @@ abstract class ElectrumWalletBase extends WalletBase[]; final sendItemList = transactionCredentials.sendItemList; + var allInputsAmount = 0; + + if (unspentCoins.isEmpty) { + await updateUnspent(); + } + + for (final utx in unspentCoins) { + if (utx.isSending) { + allInputsAmount += utx.value; + inputs.add(utx); + } + } + + if (inputs.isEmpty) { + throw BitcoinTransactionNoInputsException(); + } + final allAmountFee = calculateEstimatedFee(transactionCredentials.priority, null); final allAmount = balance.confirmed - allAmountFee; @@ -280,22 +237,48 @@ abstract class ElectrumWalletBase extends WalletBase balance.confirmed || totalAmount > allInputsAmount) { + throw BitcoinTransactionWrongBalanceException(currency); + } + final txb = bitcoin.TransactionBuilder(network: networkType); + final changeAddress = walletAddresses.address; + var leftAmount = totalAmount; final changeAddress = address; var leftAmount = totalAmount; var totalInputAmount = 0; - if (_unspent.isEmpty) { - await _updateUnspent(); - } + inputs.clear(); - for (final utx in _unspent) { - leftAmount = leftAmount - utx.value; - totalInputAmount += utx.value; - inputs.add(utx); + for (final utx in unspentCoins) { + if (utx.isSending) { + leftAmount = leftAmount - utx.value; + totalInputAmount += utx.value; + inputs.add(utx); - if (leftAmount <= 0) { - break; + if (leftAmount <= 0) { + break; + } } } @@ -303,6 +286,8 @@ abstract class ElectrumWalletBase extends WalletBase json.encode({ 'mnemonic': mnemonic, - 'account_index': _accountIndex.toString(), - 'addresses': addresses.map((addr) => addr.toJSON()).toList(), + 'account_index': walletAddresses.accountIndex.toString(), + 'addresses': walletAddresses.addresses.map((addr) => addr.toJSON()).toList(), 'balance': balance?.toJSON() }); @@ -387,17 +372,26 @@ abstract class ElectrumWalletBase extends WalletBase= amount) { break; } - totalValue += input.value; - inputsCount += 1; + if (input.isSending) { + totalValue += input.value; + inputsCount += 1; + } } + + if (totalValue < amount) return 0; } else { - inputsCount = _unspent.length; + for (final input in unspentCoins) { + if (input.isSending) { + inputsCount += 1; + } + } } + // If send all, then we have no change value final _outputsCount = outputsCount ?? (amount != null ? 2 : 1); @@ -428,24 +422,84 @@ abstract class ElectrumWalletBase extends WalletBase ''; - Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); - Future _updateUnspent() async { - final unspent = await Future.wait(addresses.map((address) => electrumClient + Future updateUnspent() async { + final unspent = await Future.wait(walletAddresses + .addresses.map((address) => electrumClient .getListUnspentWithAddress(address.address, networkType) .then((unspent) => unspent .map((unspent) => BitcoinUnspent.fromJSON(address, unspent))))); - _unspent = unspent.expand((e) => e).toList(); + unspentCoins = unspent.expand((e) => e).toList(); + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.hash.contains(coin.hash)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + } + + Future _addCoinInfo(BitcoinUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + note: coin.note + ); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id)); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = unspentCoins + ?.where((coin) => element.hash.contains(coin?.hash)); + + if (existUnspentCoins?.isEmpty ?? true) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } } Future fetchTransactionInfo( {@required String hash, @required int height}) async { final tx = await electrumClient.getTransactionExpanded(hash: hash); return ElectrumTransactionInfo.fromElectrumVerbose(tx, walletInfo.type, - height: height, addresses: addresses); + height: height, addresses: walletAddresses.addresses); } @override @@ -489,7 +543,7 @@ abstract class ElectrumWalletBase extends WalletBase initialAddresses, + int accountIndex = 0, + @required bitcoin.HDWallet hd}) + : super(walletInfo) { + this.hd = hd; + this.accountIndex = accountIndex; + addresses = ObservableList.of( + (initialAddresses ?? []).toSet()); + } + + @override + @observable + String address; + + bitcoin.HDWallet hd; + + ObservableList addresses; + + int accountIndex; + + @override + Future init() async { + await generateAddresses(); + address = addresses[accountIndex].address; + await updateAddressesInBox(); + } + + @action + Future nextAddress() async { + accountIndex += 1; + + if (accountIndex >= addresses.length) { + accountIndex = 0; + } + + address = addresses[accountIndex].address; + + await updateAddressesInBox(); + } + + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 33 - addresses.length; + await generateNewAddresses(addressesCount, + startIndex: addresses.length, hd: hd); + } + } + + Future generateNewAddress( + {bool isHidden = false, bitcoin.HDWallet hd}) async { + accountIndex += 1; + final _hd = hd ?? this.hd; + final address = BitcoinAddressRecord( + getAddress(index: accountIndex, hd: _hd), + index: accountIndex, + isHidden: isHidden); + addresses.add(address); + return address; + } + + Future> generateNewAddresses(int count, + {int startIndex = 0, bitcoin.HDWallet hd, bool isHidden = false}) async { + final list = []; + + for (var i = startIndex; i < count + startIndex; i++) { + final address = BitcoinAddressRecord(getAddress(index: i, hd: hd), + index: i, isHidden: isHidden); + list.add(address); + } + + addresses.addAll(list); + return list; + } + + /*Future updateAddress(String address) async { + for (final addr in addresses) { + if (addr.address == address) { + await save(); + break; + } + } + }*/ + + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => ''; + + @override + Future updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} \ No newline at end of file diff --git a/lib/bitcoin/litecoin_wallet.dart b/lib/bitcoin/litecoin_wallet.dart index 209d1904e..ac7e24349 100644 --- a/lib/bitcoin/litecoin_wallet.dart +++ b/lib/bitcoin/litecoin_wallet.dart @@ -1,8 +1,9 @@ -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/bitcoin/unspent_coins_info.dart'; +import 'package:cake_wallet/bitcoin/litecoin_wallet_addresses.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; import 'package:flutter/foundation.dart'; +import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; import 'package:cake_wallet/bitcoin/electrum_wallet_snapshot.dart'; @@ -10,7 +11,6 @@ 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'; @@ -21,6 +21,7 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { {@required String mnemonic, @required String password, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, List initialAddresses, ElectrumBalance initialBalance, int accountIndex = 0}) @@ -28,14 +29,23 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, networkType: litecoinNetwork, initialAddresses: initialAddresses, - initialBalance: initialBalance, - accountIndex: accountIndex); + initialBalance: initialBalance) { + walletAddresses = LitecoinWalletAddresses( + walletInfo, + initialAddresses: initialAddresses, + accountIndex: accountIndex, + hd: hd, + networkType: networkType, + mnemonic: mnemonic); + } static Future open({ @required String name, @required WalletInfo walletInfo, + @required Box unspentCoinsInfo, @required String password, }) async { final snp = ElectrumWallletSnapshot(name, walletInfo.type, password); @@ -44,32 +54,12 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { mnemonic: snp.mnemonic, password: password, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfo, initialAddresses: snp.addresses, initialBalance: snp.balance, accountIndex: snp.accountIndex); } - @override - String getAddress({@required int index, @required bitcoin.HDWallet hd}) => - generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); - - @override - Future generateAddresses() async { - if (addresses.length < 33) { - final addressesCount = 22 - addresses.length; - await generateNewAddresses(addressesCount, - hd: hd, startIndex: addresses.length); - - final changeRoot = bitcoin.HDWallet.fromSeed( - mnemonicToSeedBytes(mnemonic), - network: networkType) - .derivePath("m/0'/1"); - - await generateNewAddresses(11, - startIndex: 0, hd: changeRoot, isHidden: true); - } - } - @override int feeRate(TransactionPriority priority) { if (priority is LitecoinTransactionPriority) { diff --git a/lib/bitcoin/litecoin_wallet_addresses.dart b/lib/bitcoin/litecoin_wallet_addresses.dart new file mode 100644 index 000000000..19ade7566 --- /dev/null +++ b/lib/bitcoin/litecoin_wallet_addresses.dart @@ -0,0 +1,54 @@ +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; +import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; +import 'package:cake_wallet/bitcoin/utils.dart'; +import 'package:cake_wallet/bitcoin/bitcoin_address_record.dart'; +import 'package:cake_wallet/bitcoin/electrum_wallet_addresses.dart'; +import 'package:cake_wallet/entities/wallet_info.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; + +part 'litecoin_wallet_addresses.g.dart'; + +class LitecoinWalletAddresses = LitecoinWalletAddressesBase + with _$LitecoinWalletAddresses; + +abstract class LitecoinWalletAddressesBase extends ElectrumWalletAddresses + with Store { + LitecoinWalletAddressesBase( + WalletInfo walletInfo, + {@required List initialAddresses, + int accountIndex = 0, + @required bitcoin.HDWallet hd, + @required this.networkType, + @required this.mnemonic}) + : super( + walletInfo, + initialAddresses: initialAddresses, + accountIndex: accountIndex, + hd: hd); + + bitcoin.NetworkType networkType; + + final String mnemonic; + + @override + String getAddress({@required int index, @required bitcoin.HDWallet hd}) => + generateP2WPKHAddress(hd: hd, index: index, networkType: networkType); + + @override + Future generateAddresses() async { + if (addresses.length < 33) { + final addressesCount = 22 - addresses.length; + await generateNewAddresses(addressesCount, + hd: hd, startIndex: addresses.length); + + final changeRoot = bitcoin.HDWallet.fromSeed( + mnemonicToSeedBytes(mnemonic), + network: networkType) + .derivePath("m/0'/1"); + + await generateNewAddresses(11, + startIndex: 0, hd: changeRoot, isHidden: true); + } + } +} \ No newline at end of file diff --git a/lib/bitcoin/litecoin_wallet_service.dart b/lib/bitcoin/litecoin_wallet_service.dart index 053fd785f..dc5081e12 100644 --- a/lib/bitcoin/litecoin_wallet_service.dart +++ b/lib/bitcoin/litecoin_wallet_service.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.dart'; import 'package:hive/hive.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic.dart'; import 'package:cake_wallet/bitcoin/bitcoin_mnemonic_is_incorrect_exception.dart'; @@ -14,9 +15,10 @@ class LitecoinWalletService extends WalletService< BitcoinNewWalletCredentials, BitcoinRestoreWalletFromSeedCredentials, BitcoinRestoreWalletFromWIFCredentials> { - LitecoinWalletService(this.walletInfoSource); + LitecoinWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; + final Box unspentCoinsInfoSource; @override WalletType getType() => WalletType.litecoin; @@ -26,7 +28,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( mnemonic: await generateMnemonic(), password: credentials.password, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); @@ -43,7 +46,8 @@ class LitecoinWalletService extends WalletService< (info) => info.id == WalletBase.idFor(name, getType()), orElse: () => null); final wallet = await LitecoinWalletBase.open( - password: password, name: name, walletInfo: walletInfo); + password: password, name: name, walletInfo: walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; } @@ -68,7 +72,8 @@ class LitecoinWalletService extends WalletService< final wallet = LitecoinWallet( password: credentials.password, mnemonic: credentials.mnemonic, - walletInfo: credentials.walletInfo); + walletInfo: credentials.walletInfo, + unspentCoinsInfo: unspentCoinsInfoSource); await wallet.save(); await wallet.init(); return wallet; diff --git a/lib/bitcoin/unspent_coins_info.dart b/lib/bitcoin/unspent_coins_info.dart new file mode 100644 index 000000000..6ce647dce --- /dev/null +++ b/lib/bitcoin/unspent_coins_info.dart @@ -0,0 +1,32 @@ +import 'package:hive/hive.dart'; + +part 'unspent_coins_info.g.dart'; + +@HiveType(typeId: UnspentCoinsInfo.typeId) +class UnspentCoinsInfo extends HiveObject { + UnspentCoinsInfo({ + this.walletId, + this.hash, + this.isFrozen, + this.isSending, + this.note}); + + static const typeId = 9; + static const boxName = 'Unspent'; + static const boxKey = 'unspentBoxKey'; + + @HiveField(0) + String walletId; + + @HiveField(1) + String hash; + + @HiveField(2) + bool isFrozen; + + @HiveField(3) + bool isSending; + + @HiveField(4) + String note; +} \ No newline at end of file diff --git a/lib/buy/buy_provider.dart b/lib/buy/buy_provider.dart index b908d7bc5..dc450ad7e 100644 --- a/lib/buy/buy_provider.dart +++ b/lib/buy/buy_provider.dart @@ -15,7 +15,7 @@ abstract class BuyProvider { String get trackUrl; WalletType get walletType => wallet.type; - String get walletAddress => wallet.address; + String get walletAddress => wallet.walletAddresses.address; String get walletId => wallet.id; @override diff --git a/lib/core/wallet_base.dart b/lib/core/wallet_base.dart index ced918342..edcb05466 100644 --- a/lib/core/wallet_base.dart +++ b/lib/core/wallet_base.dart @@ -1,12 +1,12 @@ import 'package:cake_wallet/entities/balance.dart'; import 'package:cake_wallet/entities/transaction_info.dart'; import 'package:cake_wallet/entities/transaction_priority.dart'; +import 'package:cake_wallet/entities/wallet_addresses.dart'; import 'package:flutter/foundation.dart'; import 'package:cake_wallet/entities/wallet_info.dart'; import 'package:cake_wallet/core/pending_transaction.dart'; import 'package:cake_wallet/core/transaction_history.dart'; import 'package:cake_wallet/entities/currency_for_wallet_type.dart'; -import 'package:cake_wallet/entities/monero_transaction_priority.dart'; import 'package:cake_wallet/entities/crypto_currency.dart'; import 'package:cake_wallet/entities/sync_status.dart'; import 'package:cake_wallet/entities/node.dart'; @@ -31,9 +31,9 @@ abstract class WalletBase< String get name => walletInfo.name; - String get address; + //String get address; - set address(String address); + //set address(String address); BalanceType get balance; @@ -45,6 +45,8 @@ abstract class WalletBase< Object get keys; + WalletAddresses get walletAddresses; + HistoryType transactionHistory; Future connectToNode({@required Node node}); diff --git a/lib/di.dart b/lib/di.dart index e5e298d5d..06a5f5a4b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin_wallet_service.dart'; import 'package:cake_wallet/bitcoin/litecoin_wallet_service.dart'; +import 'package:cake_wallet/bitcoin/unspent_coins_info.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'; @@ -39,6 +40,8 @@ import 'package:cake_wallet/src/screens/setup_pin_code/setup_pin_code.dart'; import 'package:cake_wallet/src/screens/support/support_page.dart'; import 'package:cake_wallet/src/screens/trade_details/trade_details_page.dart'; import 'package:cake_wallet/src/screens/transaction_details/transaction_details_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart'; +import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart'; import 'package:cake_wallet/src/screens/wallet_keys/wallet_keys_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_page.dart'; import 'package:cake_wallet/src/screens/exchange/exchange_template_page.dart'; @@ -77,6 +80,9 @@ import 'package:cake_wallet/view_model/setup_pin_code_view_model.dart'; import 'package:cake_wallet/view_model/support_view_model.dart'; import 'package:cake_wallet/view_model/transaction_details_view_model.dart'; import 'package:cake_wallet/view_model/trade_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_details_view_model.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; +import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart'; import 'package:cake_wallet/view_model/auth_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -126,6 +132,7 @@ Box