diff --git a/.github/workflows/no_print_in_dart.yaml b/.github/workflows/no_print_in_dart.yaml index 8cd24edfe..b321a9cc9 100644 --- a/.github/workflows/no_print_in_dart.yaml +++ b/.github/workflows/no_print_in_dart.yaml @@ -1,8 +1,6 @@ name: No print statements in dart files -on: - pull_request: - branches: [main] +on: [pull_request] jobs: PR_test_build: diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 762144ac1..61fb67ee2 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -47,6 +47,7 @@ jobs: echo "message<> $GITHUB_ENV echo "$FULL_MESSAGE" >> $GITHUB_ENV echo "EOF" >> $GITHUB_ENV + - name: Add secrets run: | touch lib/.secrets.g.dart @@ -243,6 +244,13 @@ jobs: ./build_mwebd.sh --dont-install popd + - name: Build Decred + run: | + set -x -e + pushd scripts/android + ./build_decred.sh + popd + - name: Build generated code run: | ./model_generator.sh async @@ -281,7 +289,7 @@ jobs: set -x apk_file=$(ls build/app/outputs/flutter-apk/test-apk/${BRANCH_NAME}.apk || exit 1) echo "APK_FILE=$apk_file" >> $GITHUB_ENV - + - name: Upload artifact to slack if: ${{ !contains(env.message, 'skip slack') }} continue-on-error: true @@ -294,7 +302,7 @@ jobs: - name: cleanup run: rm -rf build/app/outputs/flutter-apk/test-apk/ - + - name: Upload Artifact to github uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index c5cb26dd9..53904149f 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -32,6 +32,7 @@ jobs: - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} + repository: ${{ github.event.pull_request.head.repo.full_name }} - name: configure git run: | git config --global --add safe.directory '*' diff --git a/.gitignore b/.gitignore index c431a7f60..e78b1c4e6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .history .svn/ .fvm/ +.fvmrc # IntelliJ related *.iml @@ -138,6 +139,7 @@ lib/solana/solana.dart lib/tron/tron.dart lib/wownero/wownero.dart lib/zano/zano.dart +lib/decred/decred.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png @@ -171,6 +173,7 @@ macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png macos/Runner/Configs/AppInfo.xcconfig +macos/Runner.xcodeproj/project.pbxproj integration_test/playground.dart diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 5a1824a17..9a324edf3 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -92,6 +92,9 @@ + + + diff --git a/assets/decred_node_list.yml b/assets/decred_node_list.yml new file mode 100644 index 000000000..cb171e701 --- /dev/null +++ b/assets/decred_node_list.yml @@ -0,0 +1,6 @@ +- + uri: default-spv-nodes + is_default: true +- + uri: dcrd.sethforprivacy.com:9108 + useSSL: true \ No newline at end of file diff --git a/assets/images/2.0x/decred.png b/assets/images/2.0x/decred.png new file mode 100644 index 000000000..2f4919cec Binary files /dev/null and b/assets/images/2.0x/decred.png differ diff --git a/assets/images/2.0x/decred_menu.png b/assets/images/2.0x/decred_menu.png new file mode 100644 index 000000000..4a41efef1 Binary files /dev/null and b/assets/images/2.0x/decred_menu.png differ diff --git a/assets/images/3.0x/decred.png b/assets/images/3.0x/decred.png new file mode 100644 index 000000000..b2c9ac818 Binary files /dev/null and b/assets/images/3.0x/decred.png differ diff --git a/assets/images/3.0x/decred_menu.png b/assets/images/3.0x/decred_menu.png new file mode 100644 index 000000000..e55b3fb5c Binary files /dev/null and b/assets/images/3.0x/decred_menu.png differ diff --git a/assets/images/dcr_icon.png b/assets/images/dcr_icon.png index 609873611..757cd0388 100644 Binary files a/assets/images/dcr_icon.png and b/assets/images/dcr_icon.png differ diff --git a/assets/images/decred.png b/assets/images/decred.png new file mode 100644 index 000000000..0b12f2ef0 Binary files /dev/null and b/assets/images/decred.png differ diff --git a/assets/images/decred_icon.png b/assets/images/decred_icon.png new file mode 100644 index 000000000..9391abc3d Binary files /dev/null and b/assets/images/decred_icon.png differ diff --git a/assets/images/decred_menu.png b/assets/images/decred_menu.png new file mode 100644 index 000000000..5c67923c5 Binary files /dev/null and b/assets/images/decred_menu.png differ diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 908897845..7135d0a7a 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -92,6 +92,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { }); } + @override + bool get hasRescan => true; + static Future create({ required String mnemonic, required String password, diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 5e71bfd6d..d54303075 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'dart:convert'; -import 'dart:typed_data'; import 'package:convert/convert.dart' as convert; import 'dart:math'; @@ -156,6 +155,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { @observable SyncStatus mwebSyncStatus = NotConnectedSyncStatus(); + @override + bool get hasRescan => true; + List get scanSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000000)).privateKey.privKey.raw; List get spendSecret => mwebHd!.childKey(Bip32KeyIndex(0x80000001)).privateKey.privKey.raw; diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 71d0cef42..e01c3834a 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -29,6 +29,7 @@ class AmountConverter { case CryptoCurrency.btc: case CryptoCurrency.bch: case CryptoCurrency.ltc: + case CryptoCurrency.dcr: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index bd1c224a3..0f913cb79 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -32,9 +32,10 @@ CryptoCurrency currencyForWalletType(WalletType type, {bool? isTestnet}) { return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( - 'Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } } @@ -65,6 +66,10 @@ WalletType? walletTypeForCurrency(CryptoCurrency currency) { return WalletType.tron; case CryptoCurrency.wow: return WalletType.wownero; + case CryptoCurrency.zano: + return WalletType.zano; + case CryptoCurrency.dcr: + return WalletType.decred; default: return null; } diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index c984cd03b..38fcde9e1 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -103,6 +103,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: case WalletType.zano: + case WalletType.decred: return Uri.parse( "http${isSSL ? "s" : ""}://$uriRaw${path!.startsWith("/") || path!.isEmpty ? path : "/$path"}"); case WalletType.none: @@ -167,6 +168,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.zano: return requestZanoNode(); + case WalletType.decred: + return requestDecredNode(); case WalletType.none: return false; } @@ -355,6 +358,21 @@ class Node extends HiveObject with Keyable { return false; } } + + Future requestDecredNode() async { + if (uri.host == "default-spv-nodes") { + // Just show default port as ok. The wallet will connect to a list of known + // nodes automatically. + return true; + } + try { + final socket = await Socket.connect(uri.host, uri.port, timeout: Duration(seconds: 5)); + socket.destroy(); + return true; + } catch (_) { + return false; + } + } } /// https://github.com/ManyMath/digest_auth/ diff --git a/cw_core/lib/receive_page_option.dart b/cw_core/lib/receive_page_option.dart index 786d07bc5..f7d69bf0a 100644 --- a/cw_core/lib/receive_page_option.dart +++ b/cw_core/lib/receive_page_option.dart @@ -2,6 +2,7 @@ import 'package:cw_core/enumerate.dart'; class ReceivePageOption implements Enumerate { static const mainnet = ReceivePageOption._('mainnet'); + static const testnet = ReceivePageOption._('testnet'); static const anonPayInvoice = ReceivePageOption._('anonPayInvoice'); static const anonPayDonationLink = ReceivePageOption._('anonPayDonationLink'); diff --git a/cw_core/lib/sync_status.dart b/cw_core/lib/sync_status.dart index 7d6b0a285..3183d9d27 100644 --- a/cw_core/lib/sync_status.dart +++ b/cw_core/lib/sync_status.dart @@ -34,6 +34,16 @@ class SyncingSyncStatus extends SyncStatus { } } +class ProcessingSyncStatus extends SyncStatus { + final String? message; + + ProcessingSyncStatus({this.message}); + + @override + double progress() => 0.99; + +} + class SyncedSyncStatus extends SyncStatus { @override double progress() => 1.0; diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 16c794a25..42bd66da0 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -60,6 +60,8 @@ abstract class WalletBase walletInfo.isHardwareWallet; + bool get hasRescan => false; + Future connectToNode({required Node node}); // there is a default definition here because only coins with a pow node (nano based) need to override this @@ -100,4 +102,6 @@ abstract class WalletBase verifyMessage(String message, String signature, {String? address = null}); bool isTestnet = false; + + bool canSend() => true; } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 79d2b002d..5ae1c1290 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -17,6 +17,7 @@ const walletTypes = [ WalletType.solana, WalletType.tron, WalletType.zano, + WalletType.decred, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -60,9 +61,11 @@ enum WalletType { @HiveField(12) wownero, - @HiveField(13) + @HiveField(13) zano, + @HiveField(14) + decred } int serializeToInt(WalletType type) { @@ -93,6 +96,8 @@ int serializeToInt(WalletType type) { return 11; case WalletType.zano: return 12; + case WalletType.decred: + return 13; case WalletType.none: return -1; } @@ -126,6 +131,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.wownero; case 12: return WalletType.zano; + case 13: + return WalletType.decred; default: throw Exception( 'Unexpected token: $raw for WalletType deserializeFromInt'); @@ -160,6 +167,8 @@ String walletTypeToString(WalletType type) { return 'Wownero'; case WalletType.zano: return 'Zano'; + case WalletType.decred: + return 'Decred'; case WalletType.none: return ''; } @@ -193,6 +202,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Wownero (WOW)'; case WalletType.zano: return 'Zano (ZANO)'; + case WalletType.decred: + return 'Decred (DCR)'; case WalletType.none: return ''; } @@ -229,6 +240,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.wow; case WalletType.zano: return CryptoCurrency.zano; + case WalletType.decred: + return CryptoCurrency.dcr; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index f023dc153..da7768ee0 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -46,6 +46,15 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" boolean_selector: dependency: transitive description: @@ -465,6 +474,15 @@ packages: url: "https://pub.dev" source: hosted version: "1.0.0" + on_chain: + dependency: "direct main" + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" package_config: dependency: transitive description: diff --git a/cw_decred/.gitignore b/cw_decred/.gitignore new file mode 100644 index 000000000..d8452de53 --- /dev/null +++ b/cw_decred/.gitignore @@ -0,0 +1,39 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.packages +build/ + +android/.externalNativeBuild/ +android/.cxx/ +android/libs +ios/External/ +macos/External/ + +*libdcrwallet.h +libdcrwallet_bindings.dart diff --git a/cw_decred/.metadata b/cw_decred/.metadata new file mode 100644 index 000000000..fa060de4b --- /dev/null +++ b/cw_decred/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled. + +version: + revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: unknown + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: android + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: ios + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + - platform: macos + create_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + base_revision: f468f3366c26a5092eb964a230ce7892fda8f2f8 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/cw_decred/CHANGELOG.md b/cw_decred/CHANGELOG.md new file mode 100644 index 000000000..ac071598e --- /dev/null +++ b/cw_decred/CHANGELOG.md @@ -0,0 +1,3 @@ +## [0.0.1] - TODO: Add release date. + +* TODO: Describe initial release. diff --git a/cw_decred/LICENSE b/cw_decred/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_decred/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_decred/README.md b/cw_decred/README.md new file mode 100644 index 000000000..d24bc80a4 --- /dev/null +++ b/cw_decred/README.md @@ -0,0 +1,3 @@ +# cw_decred + +TODO: Fill this out. diff --git a/cw_decred/analysis_options.yaml b/cw_decred/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_decred/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/cw_decred/android/.gitignore b/cw_decred/android/.gitignore new file mode 100644 index 000000000..161bdcdaf --- /dev/null +++ b/cw_decred/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/cw_decred/android/build.gradle b/cw_decred/android/build.gradle new file mode 100644 index 000000000..0fb2f3cb8 --- /dev/null +++ b/cw_decred/android/build.gradle @@ -0,0 +1,59 @@ +group 'com.cakewallet.cw_decred' +version '1.0-SNAPSHOT' + +buildscript { + ext.kotlin_version = '2.0.21' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.7.1' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +rootProject.allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + compileSdkVersion 33 + + if (project.android.hasProperty("namespace")) { + namespace 'com.cakewallet.cw_decred' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 + } + + kotlinOptions { + jvmTarget = '17' + } + sourceSets { + main { + java.srcDirs += 'src/main/kotlin' + jniLibs.srcDirs 'libs' // contains libdcrwallet.so shared libraries + } + } + defaultConfig { + minSdkVersion 21 + } + externalNativeBuild { + cmake { + } + } +} + +dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" +} diff --git a/cw_decred/android/settings.gradle b/cw_decred/android/settings.gradle new file mode 100644 index 000000000..1c81706ad --- /dev/null +++ b/cw_decred/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'cw_decred' diff --git a/cw_decred/android/src/main/AndroidManifest.xml b/cw_decred/android/src/main/AndroidManifest.xml new file mode 100644 index 000000000..ea58d3c72 --- /dev/null +++ b/cw_decred/android/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + diff --git a/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt new file mode 100644 index 000000000..4bd6f3395 --- /dev/null +++ b/cw_decred/android/src/main/kotlin/com/cakewallet/cw_decred/CwDecredPlugin.kt @@ -0,0 +1,35 @@ +package com.cakewallet.cw_decred + +import androidx.annotation.NonNull + +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +/** CwDecredPlugin */ +class CwDecredPlugin: FlutterPlugin, MethodCallHandler { + /// The MethodChannel that will the communication between Flutter and native Android + /// + /// This local reference serves to register the plugin with the Flutter Engine and unregister it + /// when the Flutter Engine is detached from the Activity + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "cw_decred") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) { + if (call.method == "getPlatformVersion") { + result.success("Android ${android.os.Build.VERSION.RELEASE}") + } else { + result.notImplemented() + } + } + + override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } +} diff --git a/cw_decred/ios/.gitignore b/cw_decred/ios/.gitignore new file mode 100644 index 000000000..0c885071e --- /dev/null +++ b/cw_decred/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/cw_decred/ios/Assets/.gitkeep b/cw_decred/ios/Assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/cw_decred/ios/Classes/CwDecredPlugin.swift b/cw_decred/ios/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..c38d15516 --- /dev/null +++ b/cw_decred/ios/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Flutter +import UIKit + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger()) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("iOS " + UIDevice.current.systemVersion) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/ios/cw_decred.podspec b/cw_decred/ios/cw_decred.podspec new file mode 100644 index 000000000..b36789e08 --- /dev/null +++ b/cw_decred/ios/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.platform = :ios, '11.0' + + s.vendored_libraries = 'External/lib/libdcrwallet.a' + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/lib/amount_format.dart b/cw_decred/lib/amount_format.dart new file mode 100644 index 000000000..c09f76b3b --- /dev/null +++ b/cw_decred/lib/amount_format.dart @@ -0,0 +1,26 @@ +import 'package:intl/intl.dart'; +import 'package:cw_core/crypto_amount_format.dart'; + +const decredAmountLength = 8; +const decredAmountDivider = 100000000; +final decredAmountFormat = NumberFormat() + ..maximumFractionDigits = decredAmountLength + ..minimumFractionDigits = 1; + +String decredAmountToString({required int amount}) => + decredAmountFormat.format(cryptoAmountToDouble(amount: amount, divider: decredAmountDivider)); + +double decredAmountToDouble({required int amount}) => + cryptoAmountToDouble(amount: amount, divider: decredAmountDivider); + +int stringDoubleToDecredAmount(String amount) { + int result = 0; + + try { + result = (double.parse(amount) * decredAmountDivider).round(); + } catch (e) { + result = 0; + } + + return result; +} diff --git a/cw_decred/lib/api/libdcrwallet.dart b/cw_decred/lib/api/libdcrwallet.dart new file mode 100644 index 000000000..6a26e64c6 --- /dev/null +++ b/cw_decred/lib/api/libdcrwallet.dart @@ -0,0 +1,693 @@ +import 'dart:convert'; +import 'dart:ffi'; +import 'dart:io'; +import 'dart:async'; +import 'dart:isolate'; +import 'package:flutter/foundation.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/api/libdcrwallet_bindings.dart'; +import 'package:cw_decred/api/util.dart'; + +final int ErrCodeNotSynced = 1; + +final String libraryName = Platform.isAndroid || Platform.isLinux // TODO: Linux. + ? 'libdcrwallet.so' + : 'cw_decred.framework/cw_decred'; + +class Libwallet { + final SendPort _commands; + final ReceivePort _responses; + final Map> _activeRequests = {}; + int _idCounter = 0; + bool _closed = false; + + static Future spawn() async { + // Create a receive port and add its initial message handler. + final initPort = RawReceivePort(); + final connection = Completer<(ReceivePort, SendPort)>.sync(); + initPort.handler = (initialMessage) { + final commandPort = initialMessage as SendPort; + connection.complete(( + ReceivePort.fromRawReceivePort(initPort), + commandPort, + )); + }; + // Spawn the isolate. + try { + await Isolate.spawn(_startRemoteIsolate, (initPort.sendPort)); + } on Object { + initPort.close(); + rethrow; + } + + final (ReceivePort receivePort, SendPort sendPort) = await connection.future; + + return Libwallet._(receivePort, sendPort); + } + + Libwallet._(this._responses, this._commands) { + _responses.listen(_handleResponsesFromIsolate); + } + + void _handleResponsesFromIsolate(dynamic message) { + final (int id, Object? response) = message as (int, Object?); + final completer = _activeRequests.remove(id)!; + + if (response is RemoteError) { + completer.completeError(response); + } else { + completer.complete(response); + } + + if (_closed && _activeRequests.isEmpty) _responses.close(); + } + + static void _handleCommandsToIsolate( + ReceivePort receivePort, + SendPort sendPort, + ) { + final dcrwalletApi = libdcrwallet(DynamicLibrary.open(libraryName)); + receivePort.listen((message) { + if (message == 'shutdown') { + receivePort.close(); + return; + } + final (int id, Map args) = message as (int, Map); + var res = PayloadResult("", "", 0); + final method = args["method"] ?? ""; + try { + switch (method) { + case "initlibdcrwallet": + final logDir = args["logdir"] ?? ""; + final cLogDir = logDir.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.initialize(cLogDir), + ptrsToFree: [cLogDir], + ); + break; + case "createwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "createwatchonlywallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.createWatchOnlyWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "loadwallet": + final config = args["config"] ?? ""; + final cConfig = config.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.loadWallet(cConfig), + ptrsToFree: [cConfig], + ); + break; + case "startsync": + final name = args["name"] ?? ""; + final peers = args["peers"] ?? ""; + final cName = name.toCString(); + final cPeers = peers.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.syncWallet(cName, cPeers), + ptrsToFree: [cName, cPeers], + ); + break; + case "closewallet": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.closeWallet(cName), + ptrsToFree: [cName], + ); + break; + case "changewalletpassword": + final name = args["name"] ?? ""; + final oldPass = args["oldpass"] ?? ""; + final newPass = args["newpass"] ?? ""; + final cName = name.toCString(); + final cOldPass = oldPass.toCString(); + final cNewPass = newPass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.changePassphrase(cName, cOldPass, cNewPass), + ptrsToFree: [cName, cOldPass, cNewPass], + ); + break; + case "walletseed": + final name = args["name"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletSeed(cName, cPass), + ptrsToFree: [cName, cPass], + ); + break; + case "syncstatus": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.syncWalletStatus(cName), + ptrsToFree: [cName], + ); + break; + case "balance": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.walletBalance(cName), + ptrsToFree: [cName], + ); + break; + case "estimatefee": + final name = args["name"] ?? ""; + final numBlocks = args["numblocks"] ?? ""; + final cName = name.toCString(); + final cNumBlocks = numBlocks.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.estimateFee(cName, cNumBlocks), + ptrsToFree: [cName, cNumBlocks], + ); + break; + case "createsignedtransaction": + final name = args["name"] ?? ""; + final signReq = args["signreq"] ?? ""; + final cName = name.toCString(); + final cSignReq = signReq.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.createSignedTransaction(cName, cSignReq), + ptrsToFree: [cName, cSignReq], + ); + break; + case "sendrawtransaction": + final name = args["name"] ?? ""; + final txHex = args["txhex"] ?? ""; + final cName = name.toCString(); + final cTxHex = txHex.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.sendRawTransaction(cName, cTxHex), + ptrsToFree: [cName, cTxHex], + ); + break; + case "listtransactions": + final name = args["name"] ?? ""; + final from = args["from"] ?? ""; + final count = args["count"] ?? ""; + final cName = name.toCString(); + final cFrom = from.toCString(); + final cCount = count.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listTransactions(cName, cFrom, cCount), + ptrsToFree: [cName, cFrom, cCount], + ); + break; + case "bestblock": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.bestBlock(cName), + ptrsToFree: [cName], + ); + break; + case "listunspents": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.listUnspents(cName), + ptrsToFree: [cName], + ); + break; + case "rescanfromheight": + final name = args["name"] ?? ""; + final height = args["height"] ?? ""; + final cName = name.toCString(); + final cHeight = height.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.rescanFromHeight(cName, cHeight), + ptrsToFree: [cName, cHeight], + ); + break; + case "signmessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final pass = args["pass"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cPass = pass.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.signMessage(cName, cMessage, cAddress, cPass), + ptrsToFree: [cName, cMessage, cAddress, cPass], + ); + break; + case "verifymessage": + final name = args["name"] ?? ""; + final message = args["message"] ?? ""; + final address = args["address"] ?? ""; + final sig = args["sig"] ?? ""; + final cName = name.toCString(); + final cMessage = message.toCString(); + final cAddress = address.toCString(); + final cSig = sig.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.verifyMessage(cName, cMessage, cAddress, cSig), + ptrsToFree: [cName, cMessage, cAddress, cSig], + ); + break; + case "newexternaladdress": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.newExternalAddress(cName), + ptrsToFree: [cName], + skipErrorCheck: true, + ); + break; + case "defaultpubkey": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.defaultPubkey(cName), + ptrsToFree: [cName], + ); + break; + case "addresses": + final name = args["name"] ?? ""; + final nUsed = args["nused"] ?? ""; + final nUnused = args["nunused"] ?? ""; + final cName = name.toCString(); + final cNUsed = nUsed.toCString(); + final cNUnused = nUnused.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.addresses(cName, cNUsed, cNUnused), + ptrsToFree: [cName, cNUsed, cNUnused], + ); + break; + case "birthstate": + final name = args["name"] ?? ""; + final cName = name.toCString(); + res = executePayloadFn( + fn: () => dcrwalletApi.birthState(cName), + ptrsToFree: [cName], + ); + break; + case "shutdown": + final name = args["name"] ?? ""; + final cName = name.toCString(); + executePayloadFn( + fn: () => dcrwalletApi.shutdown(), + ptrsToFree: [], + ); + break; + default: + res = PayloadResult("", "unknown libwallet method ${method}", 0); + } + sendPort.send((id, res)); + } catch (e) { + final errMsg = e.toString(); + printV("decred libwallet returned an error for method ${method}: ${errMsg}"); + sendPort.send((id, PayloadResult("", errMsg, 0))); + } + }); + } + + static void _startRemoteIsolate(SendPort sendPort) { + final receivePort = ReceivePort(); + sendPort.send(receivePort.sendPort); + _handleCommandsToIsolate(receivePort, sendPort); + } + + // initLibdcrwallet initializes libdcrwallet using the provided logDir and gets + // it ready for use. This must be done before attempting to create, load or use + // a wallet. + Future initLibdcrwallet(String logDir) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "initlibdcrwallet", + "logdir": logDir, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future createWatchOnlyWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createwatchonlywallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future loadWallet(String config) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "loadwallet", + "config": config, + }; + _commands.send((id, req)); + await completer.future; + } + + Future startSync(String walletName, String peers) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "startsync", + "name": walletName, + "peers": peers, + }; + _commands.send((id, req)); + await completer.future; + } + + Future closeWallet(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "closewallet", + "name": walletName, + }; + _commands.send((id, req)); + await completer.future; + } + + Future changeWalletPassword( + String walletName, String currentPassword, String newPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "changewalletpassword", + "name": walletName, + "oldpass": currentPassword, + "newpass": newPassword + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future walletSeed(String walletName, String walletPassword) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "walletseed", + "name": walletName, + "pass": walletPassword, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future syncStatus(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "syncstatus", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future balance(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "balance", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return jsonDecode(res.payload); + } + + Future estimateFee(String walletName, int numBlocks) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "estimatefee", + "name": walletName, + "numblocks": numBlocks.toString(), + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future createSignedTransaction( + String walletName, String createSignedTransactionReq) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "createsignedtransaction", + "name": walletName, + "signreq": createSignedTransactionReq, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future sendRawTransaction(String walletName, String txHex) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "sendrawtransaction", + "name": walletName, + "txhex": txHex, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listTransactions(String walletName, String from, String count) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listtransactions", + "name": walletName, + "from": from, + "count": count, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future bestBlock(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "bestblock", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future listUnspents(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "listunspents", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future rescanFromHeight(String walletName, String height) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "rescanfromheight", + "name": walletName, + "height": height, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future signMessage( + String walletName, String message, String address, String walletPass) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "signmessage", + "name": walletName, + "message": message, + "address": address, + "pass": walletPass, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future verifyMessage( + String walletName, String message, String address, String sig) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "verifymessage", + "name": walletName, + "message": message, + "address": address, + "sig": sig, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future newExternalAddress(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "newexternaladdress", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + if (res.errCode == ErrCodeNotSynced) { + // Wallet is not synced. We do not want to give out a used address so give + // nothing. + return null; + } + checkErr(res.err); + return res.payload; + } + + Future defaultPubkey(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "defaultpubkey", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future addresses(String walletName, String nUsed, String nUnused) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "addresses", + "name": walletName, + "nused": nUsed, + "nunused": nUnused, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future birthState(String walletName) async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "birthstate", + "name": walletName, + }; + _commands.send((id, req)); + final res = await completer.future as PayloadResult; + return res.payload; + } + + Future shutdown() async { + if (_closed) throw StateError('Closed'); + final completer = Completer.sync(); + final id = _idCounter++; + _activeRequests[id] = completer; + final req = { + "method": "shutdown", + }; + _commands.send((id, req)); + await completer.future as PayloadResult; + } + + void close() { + if (!_closed) { + _closed = true; + _commands.send('shutdown'); + if (_activeRequests.isEmpty) _responses.close(); + } + } +} diff --git a/cw_decred/lib/api/util.dart b/cw_decred/lib/api/util.dart new file mode 100644 index 000000000..42c3def70 --- /dev/null +++ b/cw_decred/lib/api/util.dart @@ -0,0 +1,64 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; +import 'dart:convert'; + +class PayloadResult { + final String payload; + final String err; + final int errCode; + + const PayloadResult(this.payload, this.err, this.errCode); +} + +// Executes the provided fn and converts the string response to a PayloadResult. +// Returns payload, error code, and error. +PayloadResult executePayloadFn({ + required Pointer fn(), + required List ptrsToFree, + bool skipErrorCheck = false, +}) { + final jsonStr = fn().toDartString(); + freePointers(ptrsToFree); + if (jsonStr == null) throw Exception("no json return from wallet library"); + final decoded = json.decode(jsonStr); + + final err = decoded["error"] ?? ""; + if (!skipErrorCheck) { + checkErr(err); + } + + final payload = decoded["payload"] ?? ""; + final errCode = decoded["errorcode"] ?? -1; + return new PayloadResult(payload, err, errCode); +} + +void freePointers(List ptrsToFree) { + for (final ptr in ptrsToFree) { + malloc.free(ptr); + } +} + +void checkErr(String err) { + if (err == "") return; + throw Exception(err); +} + +extension StringUtil on String { + Pointer toCString() => toNativeUtf8().cast(); +} + +extension CStringUtil on Pointer { + bool get isNull => address == nullptr.address; + + free() { + malloc.free(this); + } + + String? toDartString() { + if (isNull) return null; + + final str = cast().toDartString(); + free(); + return str; + } +} diff --git a/cw_decred/lib/balance.dart b/cw_decred/lib/balance.dart new file mode 100644 index 000000000..a88098a9f --- /dev/null +++ b/cw_decred/lib/balance.dart @@ -0,0 +1,25 @@ +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_core/balance.dart'; + +class DecredBalance extends Balance { + const DecredBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) + : super(confirmed, unconfirmed); + + factory DecredBalance.zero() => DecredBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + + final int confirmed; + final int unconfirmed; + final int frozen; + + @override + String get formattedAvailableBalance => decredAmountToString(amount: confirmed - frozen); + + @override + String get formattedAdditionalBalance => decredAmountToString(amount: unconfirmed); + + @override + String get formattedUnAvailableBalance { + final frozenFormatted = decredAmountToString(amount: frozen); + return frozenFormatted == '0.0' ? '' : frozenFormatted; + } +} diff --git a/cw_decred/lib/mnemonic.dart b/cw_decred/lib/mnemonic.dart new file mode 100644 index 000000000..bd39e2a18 --- /dev/null +++ b/cw_decred/lib/mnemonic.dart @@ -0,0 +1,2050 @@ +final wordlist = [ + "abandon", + "ability", + "able", + "about", + "above", + "absent", + "absorb", + "abstract", + "absurd", + "abuse", + "access", + "accident", + "account", + "accuse", + "achieve", + "acid", + "acoustic", + "acquire", + "across", + "act", + "action", + "actor", + "actress", + "actual", + "adapt", + "add", + "addict", + "address", + "adjust", + "admit", + "adult", + "advance", + "advice", + "aerobic", + "affair", + "afford", + "afraid", + "again", + "age", + "agent", + "agree", + "ahead", + "aim", + "air", + "airport", + "aisle", + "alarm", + "album", + "alcohol", + "alert", + "alien", + "all", + "alley", + "allow", + "almost", + "alone", + "alpha", + "already", + "also", + "alter", + "always", + "amateur", + "amazing", + "among", + "amount", + "amused", + "analyst", + "anchor", + "ancient", + "anger", + "angle", + "angry", + "animal", + "ankle", + "announce", + "annual", + "another", + "answer", + "antenna", + "antique", + "anxiety", + "any", + "apart", + "apology", + "appear", + "apple", + "approve", + "april", + "arch", + "arctic", + "area", + "arena", + "argue", + "arm", + "armed", + "armor", + "army", + "around", + "arrange", + "arrest", + "arrive", + "arrow", + "art", + "artefact", + "artist", + "artwork", + "ask", + "aspect", + "assault", + "asset", + "assist", + "assume", + "asthma", + "athlete", + "atom", + "attack", + "attend", + "attitude", + "attract", + "auction", + "audit", + "august", + "aunt", + "author", + "auto", + "autumn", + "average", + "avocado", + "avoid", + "awake", + "aware", + "away", + "awesome", + "awful", + "awkward", + "axis", + "baby", + "bachelor", + "bacon", + "badge", + "bag", + "balance", + "balcony", + "ball", + "bamboo", + "banana", + "banner", + "bar", + "barely", + "bargain", + "barrel", + "base", + "basic", + "basket", + "battle", + "beach", + "bean", + "beauty", + "because", + "become", + "beef", + "before", + "begin", + "behave", + "behind", + "believe", + "below", + "belt", + "bench", + "benefit", + "best", + "betray", + "better", + "between", + "beyond", + "bicycle", + "bid", + "bike", + "bind", + "biology", + "bird", + "birth", + "bitter", + "black", + "blade", + "blame", + "blanket", + "blast", + "bleak", + "bless", + "blind", + "blood", + "blossom", + "blouse", + "blue", + "blur", + "blush", + "board", + "boat", + "body", + "boil", + "bomb", + "bone", + "bonus", + "book", + "boost", + "border", + "boring", + "borrow", + "boss", + "bottom", + "bounce", + "box", + "boy", + "bracket", + "brain", + "brand", + "brass", + "brave", + "bread", + "breeze", + "brick", + "bridge", + "brief", + "bright", + "bring", + "brisk", + "broccoli", + "broken", + "bronze", + "broom", + "brother", + "brown", + "brush", + "bubble", + "buddy", + "budget", + "buffalo", + "build", + "bulb", + "bulk", + "bullet", + "bundle", + "bunker", + "burden", + "burger", + "burst", + "bus", + "business", + "busy", + "butter", + "buyer", + "buzz", + "cabbage", + "cabin", + "cable", + "cactus", + "cage", + "cake", + "call", + "calm", + "camera", + "camp", + "can", + "canal", + "cancel", + "candy", + "cannon", + "canoe", + "canvas", + "canyon", + "capable", + "capital", + "captain", + "car", + "carbon", + "card", + "cargo", + "carpet", + "carry", + "cart", + "case", + "cash", + "casino", + "castle", + "casual", + "cat", + "catalog", + "catch", + "category", + "cattle", + "caught", + "cause", + "caution", + "cave", + "ceiling", + "celery", + "cement", + "census", + "century", + "cereal", + "certain", + "chair", + "chalk", + "champion", + "change", + "chaos", + "chapter", + "charge", + "chase", + "chat", + "cheap", + "check", + "cheese", + "chef", + "cherry", + "chest", + "chicken", + "chief", + "child", + "chimney", + "choice", + "choose", + "chronic", + "chuckle", + "chunk", + "churn", + "cigar", + "cinnamon", + "circle", + "citizen", + "city", + "civil", + "claim", + "clap", + "clarify", + "claw", + "clay", + "clean", + "clerk", + "clever", + "click", + "client", + "cliff", + "climb", + "clinic", + "clip", + "clock", + "clog", + "close", + "cloth", + "cloud", + "clown", + "club", + "clump", + "cluster", + "clutch", + "coach", + "coast", + "coconut", + "code", + "coffee", + "coil", + "coin", + "collect", + "color", + "column", + "combine", + "come", + "comfort", + "comic", + "common", + "company", + "concert", + "conduct", + "confirm", + "congress", + "connect", + "consider", + "control", + "convince", + "cook", + "cool", + "copper", + "copy", + "coral", + "core", + "corn", + "correct", + "cost", + "cotton", + "couch", + "country", + "couple", + "course", + "cousin", + "cover", + "coyote", + "crack", + "cradle", + "craft", + "cram", + "crane", + "crash", + "crater", + "crawl", + "crazy", + "cream", + "credit", + "creek", + "crew", + "cricket", + "crime", + "crisp", + "critic", + "crop", + "cross", + "crouch", + "crowd", + "crucial", + "cruel", + "cruise", + "crumble", + "crunch", + "crush", + "cry", + "crystal", + "cube", + "culture", + "cup", + "cupboard", + "curious", + "current", + "curtain", + "curve", + "cushion", + "custom", + "cute", + "cycle", + "dad", + "damage", + "damp", + "dance", + "danger", + "daring", + "dash", + "daughter", + "dawn", + "day", + "deal", + "debate", + "debris", + "decade", + "december", + "decide", + "decline", + "decorate", + "decrease", + "deer", + "defense", + "define", + "defy", + "degree", + "delay", + "deliver", + "demand", + "demise", + "denial", + "dentist", + "deny", + "depart", + "depend", + "deposit", + "depth", + "deputy", + "derive", + "describe", + "desert", + "design", + "desk", + "despair", + "destroy", + "detail", + "detect", + "develop", + "device", + "devote", + "diagram", + "dial", + "diamond", + "diary", + "dice", + "diesel", + "diet", + "differ", + "digital", + "dignity", + "dilemma", + "dinner", + "dinosaur", + "direct", + "dirt", + "disagree", + "discover", + "disease", + "dish", + "dismiss", + "disorder", + "display", + "distance", + "divert", + "divide", + "divorce", + "dizzy", + "doctor", + "document", + "dog", + "doll", + "dolphin", + "domain", + "donate", + "donkey", + "donor", + "door", + "dose", + "double", + "dove", + "draft", + "dragon", + "drama", + "drastic", + "draw", + "dream", + "dress", + "drift", + "drill", + "drink", + "drip", + "drive", + "drop", + "drum", + "dry", + "duck", + "dumb", + "dune", + "during", + "dust", + "dutch", + "duty", + "dwarf", + "dynamic", + "eager", + "eagle", + "early", + "earn", + "earth", + "easily", + "east", + "easy", + "echo", + "ecology", + "economy", + "edge", + "edit", + "educate", + "effort", + "egg", + "eight", + "either", + "elbow", + "elder", + "electric", + "elegant", + "element", + "elephant", + "elevator", + "elite", + "else", + "embark", + "embody", + "embrace", + "emerge", + "emotion", + "employ", + "empower", + "empty", + "enable", + "enact", + "end", + "endless", + "endorse", + "enemy", + "energy", + "enforce", + "engage", + "engine", + "enhance", + "enjoy", + "enlist", + "enough", + "enrich", + "enroll", + "ensure", + "enter", + "entire", + "entry", + "envelope", + "episode", + "equal", + "equip", + "era", + "erase", + "erode", + "erosion", + "error", + "erupt", + "escape", + "essay", + "essence", + "estate", + "eternal", + "ethics", + "evidence", + "evil", + "evoke", + "evolve", + "exact", + "example", + "excess", + "exchange", + "excite", + "exclude", + "excuse", + "execute", + "exercise", + "exhaust", + "exhibit", + "exile", + "exist", + "exit", + "exotic", + "expand", + "expect", + "expire", + "explain", + "expose", + "express", + "extend", + "extra", + "eye", + "eyebrow", + "fabric", + "face", + "faculty", + "fade", + "faint", + "faith", + "fall", + "false", + "fame", + "family", + "famous", + "fan", + "fancy", + "fantasy", + "farm", + "fashion", + "fat", + "fatal", + "father", + "fatigue", + "fault", + "favorite", + "feature", + "february", + "federal", + "fee", + "feed", + "feel", + "female", + "fence", + "festival", + "fetch", + "fever", + "few", + "fiber", + "fiction", + "field", + "figure", + "file", + "film", + "filter", + "final", + "find", + "fine", + "finger", + "finish", + "fire", + "firm", + "first", + "fiscal", + "fish", + "fit", + "fitness", + "fix", + "flag", + "flame", + "flash", + "flat", + "flavor", + "flee", + "flight", + "flip", + "float", + "flock", + "floor", + "flower", + "fluid", + "flush", + "fly", + "foam", + "focus", + "fog", + "foil", + "fold", + "follow", + "food", + "foot", + "force", + "forest", + "forget", + "fork", + "fortune", + "forum", + "forward", + "fossil", + "foster", + "found", + "fox", + "fragile", + "frame", + "frequent", + "fresh", + "friend", + "fringe", + "frog", + "front", + "frost", + "frown", + "frozen", + "fruit", + "fuel", + "fun", + "funny", + "furnace", + "fury", + "future", + "gadget", + "gain", + "galaxy", + "gallery", + "game", + "gap", + "garage", + "garbage", + "garden", + "garlic", + "garment", + "gas", + "gasp", + "gate", + "gather", + "gauge", + "gaze", + "general", + "genius", + "genre", + "gentle", + "genuine", + "gesture", + "ghost", + "giant", + "gift", + "giggle", + "ginger", + "giraffe", + "girl", + "give", + "glad", + "glance", + "glare", + "glass", + "glide", + "glimpse", + "globe", + "gloom", + "glory", + "glove", + "glow", + "glue", + "goat", + "goddess", + "gold", + "good", + "goose", + "gorilla", + "gospel", + "gossip", + "govern", + "gown", + "grab", + "grace", + "grain", + "grant", + "grape", + "grass", + "gravity", + "great", + "green", + "grid", + "grief", + "grit", + "grocery", + "group", + "grow", + "grunt", + "guard", + "guess", + "guide", + "guilt", + "guitar", + "gun", + "gym", + "habit", + "hair", + "half", + "hammer", + "hamster", + "hand", + "happy", + "harbor", + "hard", + "harsh", + "harvest", + "hat", + "have", + "hawk", + "hazard", + "head", + "health", + "heart", + "heavy", + "hedgehog", + "height", + "hello", + "helmet", + "help", + "hen", + "hero", + "hidden", + "high", + "hill", + "hint", + "hip", + "hire", + "history", + "hobby", + "hockey", + "hold", + "hole", + "holiday", + "hollow", + "home", + "honey", + "hood", + "hope", + "horn", + "horror", + "horse", + "hospital", + "host", + "hotel", + "hour", + "hover", + "hub", + "huge", + "human", + "humble", + "humor", + "hundred", + "hungry", + "hunt", + "hurdle", + "hurry", + "hurt", + "husband", + "hybrid", + "ice", + "icon", + "idea", + "identify", + "idle", + "ignore", + "ill", + "illegal", + "illness", + "image", + "imitate", + "immense", + "immune", + "impact", + "impose", + "improve", + "impulse", + "inch", + "include", + "income", + "increase", + "index", + "indicate", + "indoor", + "industry", + "infant", + "inflict", + "inform", + "inhale", + "inherit", + "initial", + "inject", + "injury", + "inmate", + "inner", + "innocent", + "input", + "inquiry", + "insane", + "insect", + "inside", + "inspire", + "install", + "intact", + "interest", + "into", + "invest", + "invite", + "involve", + "iron", + "island", + "isolate", + "issue", + "item", + "ivory", + "jacket", + "jaguar", + "jar", + "jazz", + "jealous", + "jeans", + "jelly", + "jewel", + "job", + "join", + "joke", + "journey", + "joy", + "judge", + "juice", + "jump", + "jungle", + "junior", + "junk", + "just", + "kangaroo", + "keen", + "keep", + "ketchup", + "key", + "kick", + "kid", + "kidney", + "kind", + "kingdom", + "kiss", + "kit", + "kitchen", + "kite", + "kitten", + "kiwi", + "knee", + "knife", + "knock", + "know", + "lab", + "label", + "labor", + "ladder", + "lady", + "lake", + "lamp", + "language", + "laptop", + "large", + "later", + "latin", + "laugh", + "laundry", + "lava", + "law", + "lawn", + "lawsuit", + "layer", + "lazy", + "leader", + "leaf", + "learn", + "leave", + "lecture", + "left", + "leg", + "legal", + "legend", + "leisure", + "lemon", + "lend", + "length", + "lens", + "leopard", + "lesson", + "letter", + "level", + "liar", + "liberty", + "library", + "license", + "life", + "lift", + "light", + "like", + "limb", + "limit", + "link", + "lion", + "liquid", + "list", + "little", + "live", + "lizard", + "load", + "loan", + "lobster", + "local", + "lock", + "logic", + "lonely", + "long", + "loop", + "lottery", + "loud", + "lounge", + "love", + "loyal", + "lucky", + "luggage", + "lumber", + "lunar", + "lunch", + "luxury", + "lyrics", + "machine", + "mad", + "magic", + "magnet", + "maid", + "mail", + "main", + "major", + "make", + "mammal", + "man", + "manage", + "mandate", + "mango", + "mansion", + "manual", + "maple", + "marble", + "march", + "margin", + "marine", + "market", + "marriage", + "mask", + "mass", + "master", + "match", + "material", + "math", + "matrix", + "matter", + "maximum", + "maze", + "meadow", + "mean", + "measure", + "meat", + "mechanic", + "medal", + "media", + "melody", + "melt", + "member", + "memory", + "mention", + "menu", + "mercy", + "merge", + "merit", + "merry", + "mesh", + "message", + "metal", + "method", + "middle", + "midnight", + "milk", + "million", + "mimic", + "mind", + "minimum", + "minor", + "minute", + "miracle", + "mirror", + "misery", + "miss", + "mistake", + "mix", + "mixed", + "mixture", + "mobile", + "model", + "modify", + "mom", + "moment", + "monitor", + "monkey", + "monster", + "month", + "moon", + "moral", + "more", + "morning", + "mosquito", + "mother", + "motion", + "motor", + "mountain", + "mouse", + "move", + "movie", + "much", + "muffin", + "mule", + "multiply", + "muscle", + "museum", + "mushroom", + "music", + "must", + "mutual", + "myself", + "mystery", + "myth", + "naive", + "name", + "napkin", + "narrow", + "nasty", + "nation", + "nature", + "near", + "neck", + "need", + "negative", + "neglect", + "neither", + "nephew", + "nerve", + "nest", + "net", + "network", + "neutral", + "never", + "news", + "next", + "nice", + "night", + "noble", + "noise", + "nominee", + "noodle", + "normal", + "north", + "nose", + "notable", + "note", + "nothing", + "notice", + "novel", + "now", + "nuclear", + "number", + "nurse", + "nut", + "oak", + "obey", + "object", + "oblige", + "obscure", + "observe", + "obtain", + "obvious", + "occur", + "ocean", + "october", + "odor", + "off", + "offer", + "office", + "often", + "oil", + "okay", + "old", + "olive", + "olympic", + "omit", + "once", + "one", + "onion", + "online", + "only", + "open", + "opera", + "opinion", + "oppose", + "option", + "orange", + "orbit", + "orchard", + "order", + "ordinary", + "organ", + "orient", + "original", + "orphan", + "ostrich", + "other", + "outdoor", + "outer", + "output", + "outside", + "oval", + "oven", + "over", + "own", + "owner", + "oxygen", + "oyster", + "ozone", + "pact", + "paddle", + "page", + "pair", + "palace", + "palm", + "panda", + "panel", + "panic", + "panther", + "paper", + "parade", + "parent", + "park", + "parrot", + "party", + "pass", + "patch", + "path", + "patient", + "patrol", + "pattern", + "pause", + "pave", + "payment", + "peace", + "peanut", + "pear", + "peasant", + "pelican", + "pen", + "penalty", + "pencil", + "people", + "pepper", + "perfect", + "permit", + "person", + "pet", + "phone", + "photo", + "phrase", + "physical", + "piano", + "picnic", + "picture", + "piece", + "pig", + "pigeon", + "pill", + "pilot", + "pink", + "pioneer", + "pipe", + "pistol", + "pitch", + "pizza", + "place", + "planet", + "plastic", + "plate", + "play", + "please", + "pledge", + "pluck", + "plug", + "plunge", + "poem", + "poet", + "point", + "polar", + "pole", + "police", + "pond", + "pony", + "pool", + "popular", + "portion", + "position", + "possible", + "post", + "potato", + "pottery", + "poverty", + "powder", + "power", + "practice", + "praise", + "predict", + "prefer", + "prepare", + "present", + "pretty", + "prevent", + "price", + "pride", + "primary", + "print", + "priority", + "prison", + "private", + "prize", + "problem", + "process", + "produce", + "profit", + "program", + "project", + "promote", + "proof", + "property", + "prosper", + "protect", + "proud", + "provide", + "public", + "pudding", + "pull", + "pulp", + "pulse", + "pumpkin", + "punch", + "pupil", + "puppy", + "purchase", + "purity", + "purpose", + "purse", + "push", + "put", + "puzzle", + "pyramid", + "quality", + "quantum", + "quarter", + "question", + "quick", + "quit", + "quiz", + "quote", + "rabbit", + "raccoon", + "race", + "rack", + "radar", + "radio", + "rail", + "rain", + "raise", + "rally", + "ramp", + "ranch", + "random", + "range", + "rapid", + "rare", + "rate", + "rather", + "raven", + "raw", + "razor", + "ready", + "real", + "reason", + "rebel", + "rebuild", + "recall", + "receive", + "recipe", + "record", + "recycle", + "reduce", + "reflect", + "reform", + "refuse", + "region", + "regret", + "regular", + "reject", + "relax", + "release", + "relief", + "rely", + "remain", + "remember", + "remind", + "remove", + "render", + "renew", + "rent", + "reopen", + "repair", + "repeat", + "replace", + "report", + "require", + "rescue", + "resemble", + "resist", + "resource", + "response", + "result", + "retire", + "retreat", + "return", + "reunion", + "reveal", + "review", + "reward", + "rhythm", + "rib", + "ribbon", + "rice", + "rich", + "ride", + "ridge", + "rifle", + "right", + "rigid", + "ring", + "riot", + "ripple", + "risk", + "ritual", + "rival", + "river", + "road", + "roast", + "robot", + "robust", + "rocket", + "romance", + "roof", + "rookie", + "room", + "rose", + "rotate", + "rough", + "round", + "route", + "royal", + "rubber", + "rude", + "rug", + "rule", + "run", + "runway", + "rural", + "sad", + "saddle", + "sadness", + "safe", + "sail", + "salad", + "salmon", + "salon", + "salt", + "salute", + "same", + "sample", + "sand", + "satisfy", + "satoshi", + "sauce", + "sausage", + "save", + "say", + "scale", + "scan", + "scare", + "scatter", + "scene", + "scheme", + "school", + "science", + "scissors", + "scorpion", + "scout", + "scrap", + "screen", + "script", + "scrub", + "sea", + "search", + "season", + "seat", + "second", + "secret", + "section", + "security", + "seed", + "seek", + "segment", + "select", + "sell", + "seminar", + "senior", + "sense", + "sentence", + "series", + "service", + "session", + "settle", + "setup", + "seven", + "shadow", + "shaft", + "shallow", + "share", + "shed", + "shell", + "sheriff", + "shield", + "shift", + "shine", + "ship", + "shiver", + "shock", + "shoe", + "shoot", + "shop", + "short", + "shoulder", + "shove", + "shrimp", + "shrug", + "shuffle", + "shy", + "sibling", + "sick", + "side", + "siege", + "sight", + "sign", + "silent", + "silk", + "silly", + "silver", + "similar", + "simple", + "since", + "sing", + "siren", + "sister", + "situate", + "six", + "size", + "skate", + "sketch", + "ski", + "skill", + "skin", + "skirt", + "skull", + "slab", + "slam", + "sleep", + "slender", + "slice", + "slide", + "slight", + "slim", + "slogan", + "slot", + "slow", + "slush", + "small", + "smart", + "smile", + "smoke", + "smooth", + "snack", + "snake", + "snap", + "sniff", + "snow", + "soap", + "soccer", + "social", + "sock", + "soda", + "soft", + "solar", + "soldier", + "solid", + "solution", + "solve", + "someone", + "song", + "soon", + "sorry", + "sort", + "soul", + "sound", + "soup", + "source", + "south", + "space", + "spare", + "spatial", + "spawn", + "speak", + "special", + "speed", + "spell", + "spend", + "sphere", + "spice", + "spider", + "spike", + "spin", + "spirit", + "split", + "spoil", + "sponsor", + "spoon", + "sport", + "spot", + "spray", + "spread", + "spring", + "spy", + "square", + "squeeze", + "squirrel", + "stable", + "stadium", + "staff", + "stage", + "stairs", + "stamp", + "stand", + "start", + "state", + "stay", + "steak", + "steel", + "stem", + "step", + "stereo", + "stick", + "still", + "sting", + "stock", + "stomach", + "stone", + "stool", + "story", + "stove", + "strategy", + "street", + "strike", + "strong", + "struggle", + "student", + "stuff", + "stumble", + "style", + "subject", + "submit", + "subway", + "success", + "such", + "sudden", + "suffer", + "sugar", + "suggest", + "suit", + "summer", + "sun", + "sunny", + "sunset", + "super", + "supply", + "supreme", + "sure", + "surface", + "surge", + "surprise", + "surround", + "survey", + "suspect", + "sustain", + "swallow", + "swamp", + "swap", + "swarm", + "swear", + "sweet", + "swift", + "swim", + "swing", + "switch", + "sword", + "symbol", + "symptom", + "syrup", + "system", + "table", + "tackle", + "tag", + "tail", + "talent", + "talk", + "tank", + "tape", + "target", + "task", + "taste", + "tattoo", + "taxi", + "teach", + "team", + "tell", + "ten", + "tenant", + "tennis", + "tent", + "term", + "test", + "text", + "thank", + "that", + "theme", + "then", + "theory", + "there", + "they", + "thing", + "this", + "thought", + "three", + "thrive", + "throw", + "thumb", + "thunder", + "ticket", + "tide", + "tiger", + "tilt", + "timber", + "time", + "tiny", + "tip", + "tired", + "tissue", + "title", + "toast", + "tobacco", + "today", + "toddler", + "toe", + "together", + "toilet", + "token", + "tomato", + "tomorrow", + "tone", + "tongue", + "tonight", + "tool", + "tooth", + "top", + "topic", + "topple", + "torch", + "tornado", + "tortoise", + "toss", + "total", + "tourist", + "toward", + "tower", + "town", + "toy", + "track", + "trade", + "traffic", + "tragic", + "train", + "transfer", + "trap", + "trash", + "travel", + "tray", + "treat", + "tree", + "trend", + "trial", + "tribe", + "trick", + "trigger", + "trim", + "trip", + "trophy", + "trouble", + "truck", + "true", + "truly", + "trumpet", + "trust", + "truth", + "try", + "tube", + "tuition", + "tumble", + "tuna", + "tunnel", + "turkey", + "turn", + "turtle", + "twelve", + "twenty", + "twice", + "twin", + "twist", + "two", + "type", + "typical", + "ugly", + "umbrella", + "unable", + "unaware", + "uncle", + "uncover", + "under", + "undo", + "unfair", + "unfold", + "unhappy", + "uniform", + "unique", + "unit", + "universe", + "unknown", + "unlock", + "until", + "unusual", + "unveil", + "update", + "upgrade", + "uphold", + "upon", + "upper", + "upset", + "urban", + "urge", + "usage", + "use", + "used", + "useful", + "useless", + "usual", + "utility", + "vacant", + "vacuum", + "vague", + "valid", + "valley", + "valve", + "van", + "vanish", + "vapor", + "various", + "vast", + "vault", + "vehicle", + "velvet", + "vendor", + "venture", + "venue", + "verb", + "verify", + "version", + "very", + "vessel", + "veteran", + "viable", + "vibrant", + "vicious", + "victory", + "video", + "view", + "village", + "vintage", + "violin", + "virtual", + "virus", + "visa", + "visit", + "visual", + "vital", + "vivid", + "vocal", + "voice", + "void", + "volcano", + "volume", + "vote", + "voyage", + "wage", + "wagon", + "wait", + "walk", + "wall", + "walnut", + "want", + "warfare", + "warm", + "warrior", + "wash", + "wasp", + "waste", + "water", + "wave", + "way", + "wealth", + "weapon", + "wear", + "weasel", + "weather", + "web", + "wedding", + "weekend", + "weird", + "welcome", + "west", + "wet", + "whale", + "what", + "wheat", + "wheel", + "when", + "where", + "whip", + "whisper", + "wide", + "width", + "wife", + "wild", + "will", + "win", + "window", + "wine", + "wing", + "wink", + "winner", + "winter", + "wire", + "wisdom", + "wise", + "wish", + "witness", + "wolf", + "woman", + "wonder", + "wood", + "wool", + "word", + "work", + "world", + "worry", + "worth", + "wrap", + "wreck", + "wrestle", + "wrist", + "write", + "wrong", + "yard", + "year", + "yellow", + "you", + "young", + "youth", + "zebra", + "zero", + "zone", + "zoo", +]; diff --git a/cw_decred/lib/pending_transaction.dart b/cw_decred/lib/pending_transaction.dart new file mode 100644 index 000000000..63162bf40 --- /dev/null +++ b/cw_decred/lib/pending_transaction.dart @@ -0,0 +1,39 @@ +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredPendingTransaction with PendingTransaction { + DecredPendingTransaction( + {required this.txid, + required this.amount, + required this.fee, + required this.rawHex, + required this.send}); + + final int amount; + final int fee; + final String txid; + final String rawHex; + final Future Function() send; + + @override + String get id => txid; + + @override + String get amountFormatted => decredAmountToString(amount: amount); + + @override + String get feeFormatted => decredAmountToString(amount: fee); + + @override + String get hex => rawHex; + + @override + Future commit() async { + return send(); + } + + @override + Future commitUR() { + throw UnimplementedError(); + } +} diff --git a/cw_decred/lib/transaction_credentials.dart b/cw_decred/lib/transaction_credentials.dart new file mode 100644 index 000000000..5ace384f4 --- /dev/null +++ b/cw_decred/lib/transaction_credentials.dart @@ -0,0 +1,10 @@ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class DecredTransactionCredentials { + DecredTransactionCredentials(this.outputs, {required this.priority, this.feeRate}); + + final List outputs; + final DecredTransactionPriority? priority; + final int? feeRate; +} diff --git a/cw_decred/lib/transaction_history.dart b/cw_decred/lib/transaction_history.dart new file mode 100644 index 000000000..02227aa9c --- /dev/null +++ b/cw_decred/lib/transaction_history.dart @@ -0,0 +1,31 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_history.dart'; + +class DecredTransactionHistory extends TransactionHistoryBase { + DecredTransactionHistory() { + transactions = ObservableMap(); + } + + @override + void addOne(TransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => this.transactions.addAll(transactions); + + @override + Future save() async {} + + // update returns true if a known transaction that is not pending was found. + bool update(Map txs) { + var foundOldTx = false; + txs.forEach((_, tx) { + if (!this.transactions.containsKey(tx.id) || this.transactions[tx.id]!.isPending) { + this.transactions[tx.id] = tx; + } else { + foundOldTx = true; + } + }); + return foundOldTx; + } +} diff --git a/cw_decred/lib/transaction_info.dart b/cw_decred/lib/transaction_info.dart new file mode 100644 index 000000000..4afe09e1b --- /dev/null +++ b/cw_decred/lib/transaction_info.dart @@ -0,0 +1,45 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_decred/amount_format.dart'; + +class DecredTransactionInfo extends TransactionInfo { + DecredTransactionInfo({ + required String id, + required int amount, + required int fee, + required TransactionDirection direction, + required bool isPending, + required DateTime date, + required int height, + required int confirmations, + required String to, + }) { + this.id = id; + this.amount = amount; + this.fee = fee; + this.height = height; + this.direction = direction; + this.date = date; + this.isPending = isPending; + this.confirmations = confirmations; + this.to = to; + } + + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount(decredAmountToString(amount: amount))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String? feeFormatted() => + '${formatAmount(decredAmountToString(amount: fee ?? 0))} ${walletTypeToCryptoCurrency(WalletType.decred).title}'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); +} diff --git a/cw_decred/lib/transaction_priority.dart b/cw_decred/lib/transaction_priority.dart new file mode 100644 index 000000000..80a9c7e3a --- /dev/null +++ b/cw_decred/lib/transaction_priority.dart @@ -0,0 +1,69 @@ +import 'package:cw_core/transaction_priority.dart'; + +class DecredTransactionPriority extends TransactionPriority { + const DecredTransactionPriority({required String title, required int raw}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const DecredTransactionPriority slow = DecredTransactionPriority(title: 'Slow', raw: 0); + static const DecredTransactionPriority medium = + DecredTransactionPriority(title: 'Medium', raw: 1); + static const DecredTransactionPriority fast = DecredTransactionPriority(title: 'Fast', raw: 2); + + static DecredTransactionPriority deserialize({required int raw}) { + switch (raw) { + case 0: + return slow; + case 1: + return medium; + case 2: + return fast; + default: + throw Exception('Unexpected token: $raw for DecredTransactionPriority deserialize'); + } + } + + String get units => 'atom'; + + @override + String toString() { + var label = ''; + + switch (this) { + case DecredTransactionPriority.slow: + label = 'Slow ~24hrs'; // '${S.current.transaction_priority_slow} ~24hrs'; + break; + case DecredTransactionPriority.medium: + label = 'Medium'; // S.current.transaction_priority_medium; + break; + case DecredTransactionPriority.fast: + label = 'Fast'; // S.current.transaction_priority_fast; + break; + default: + break; + } + + return label; + } + + String labelWithRate(int rate) => '${toString()} ($rate ${units}/byte)'; +} + +class FeeCache { + int _feeRate; + DateTime stamp; + FeeCache(this._feeRate) : this.stamp = DateTime(0, 0, 0, 0, 0, 0, 0, 0); + + bool isOld() { + return this.stamp.add(const Duration(minutes: 30)).isBefore(DateTime.now()); + } + + void update(int feeRate) { + this._feeRate = feeRate; + this.stamp = DateTime.now(); + } + + int feeRate() { + return this._feeRate; + } +} diff --git a/cw_decred/lib/wallet.dart b/cw_decred/lib/wallet.dart new file mode 100644 index 000000000..028ba6cda --- /dev/null +++ b/cw_decred/lib/wallet.dart @@ -0,0 +1,729 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_core/exceptions.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_decred/pending_transaction.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:flutter/foundation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; + +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/transaction_history.dart'; +import 'package:cw_decred/wallet_addresses.dart'; +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/balance.dart'; +import 'package:cw_decred/transaction_info.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; + +part 'wallet.g.dart'; + +class DecredWallet = DecredWalletBase with _$DecredWallet; + +abstract class DecredWalletBase + extends WalletBase with Store { + DecredWalletBase(WalletInfo walletInfo, String password, Box unspentCoinsInfo, + Libwallet libwallet, Function() closeLibwallet) + : _password = password, + _libwallet = libwallet, + _closeLibwallet = closeLibwallet, + this.syncStatus = NotConnectedSyncStatus(), + this.unspentCoinsInfo = unspentCoinsInfo, + this.watchingOnly = + walletInfo.derivationInfo?.derivationPath == DecredWalletService.pubkeyRestorePath || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + this.balance = ObservableMap.of({CryptoCurrency.dcr: DecredBalance.zero()}), + this.isTestnet = walletInfo.derivationInfo?.derivationPath == + DecredWalletService.seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == + DecredWalletService.pubkeyRestorePathTestnet, + super(walletInfo) { + walletAddresses = DecredWalletAddresses(walletInfo, libwallet); + transactionHistory = DecredTransactionHistory(); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = enabled; + }); + } + + // NOTE: Hitting this max fee would be unexpected with current on chain use + // but this may need to be updated in the future. + final maxFeeRate = 100000; + + // syncIntervalSyncing is used up until synced, then transactions are checked + // every syncIntervalSynced. + final syncIntervalSyncing = 5; // seconds + final syncIntervalSynced = 30; // seconds + static final defaultFeeRate = 10000; + final String _password; + final Libwallet _libwallet; + final Function() _closeLibwallet; + final idPrefix = "decred_"; + + // TODO: Encrypt this. + var _seed = ""; + var _pubkey = ""; + var _unspents = []; + + // synced is used to set the syncTimer interval. + bool synced = false; + bool watchingOnly; + bool connecting = false; + String persistantPeer = "default-spv-nodes"; + FeeCache feeRateFast = FeeCache(defaultFeeRate); + FeeCache feeRateMedium = FeeCache(defaultFeeRate); + FeeCache feeRateSlow = FeeCache(defaultFeeRate); + Timer? syncTimer; + Box unspentCoinsInfo; + + @override + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap balance; + + @override + late DecredWalletAddresses walletAddresses; + + @override + String? get seed { + if (watchingOnly) { + return null; + } + return _seed; + } + + @override + Object get keys => {}; + + @override + bool isTestnet; + + String get pubkey { + return _pubkey; + } + + Future init() async { + final getSeed = () async { + if (!watchingOnly) { + _seed = await _libwallet.walletSeed(walletInfo.name, _password) ?? ""; + } + _pubkey = await _libwallet.defaultPubkey(walletInfo.name); + }; + await Future.wait([ + updateBalance(), + updateTransactionHistory(), + walletAddresses.init(), + fetchTransactions(), + updateFees(), + fetchUnspents(), + getSeed(), + ]); + } + + Future performBackgroundTasks() async { + if (!await checkSync()) { + if (synced == true) { + synced = false; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } + return; + } + // Set sync check interval lower since we are synced. + if (synced == false) { + synced = true; + if (syncTimer != null) { + syncTimer!.cancel(); + } + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSynced), (Timer t) => performBackgroundTasks()); + } + await Future.wait([ + updateTransactionHistory(), + updateFees(), + fetchUnspents(), + updateBalance(), + walletAddresses.updateAddressesInBox(), + ]); + } + + Future updateFees() async { + final feeForNb = (int nb) async { + try { + final feeStr = await _libwallet.estimateFee(walletInfo.name, nb); + var fee = int.parse(feeStr); + if (fee > maxFeeRate) { + throw "dcr fee returned from estimate fee was over max"; + } else if (fee <= 0) { + throw "dcr fee returned from estimate fee was zero"; + } + return fee; + } catch (e) { + printV(e); + return defaultFeeRate; + } + }; + if (feeRateSlow.isOld()) { + feeRateSlow.update(await feeForNb(4)); + } + if (feeRateMedium.isOld()) { + feeRateMedium.update(await feeForNb(2)); + } + if (feeRateFast.isOld()) { + feeRateFast.update(await feeForNb(1)); + } + } + + Future updateTransactionHistory() async { + // from is the number of transactions skipped from most recent, not block + // height. + var from = 0; + while (true) { + // Transactions are returned from newest to oldest. Loop fetching 5 txn + // at a time until we find a batch with txn that no longer need to be + // updated. + final txs = await this.fetchFiveTransactions(from); + if (txs.length == 0) { + return; + } + if (this.transactionHistory.update(txs)) { + return; + } + from += 5; + } + } + + Future checkSync() async { + final syncStatusJSON = await _libwallet.syncStatus(walletInfo.name); + final decoded = json.decode(syncStatusJSON); + + final syncStatusCode = decoded["syncstatuscode"] ?? 0; + // final syncStatusStr = decoded["syncstatus"] ?? ""; + final targetHeight = decoded["targetheight"] ?? 1; + final numPeers = decoded["numpeers"] ?? 0; + // final cFiltersHeight = decoded["cfiltersheight"] ?? 0; + final headersHeight = decoded["headersheight"] ?? 0; + final rescanHeight = decoded["rescanheight"] ?? 0; + + if (numPeers == 0) { + syncStatus = NotConnectedSyncStatus(); + return false; + } + + // Sync codes: + // NotStarted = 0 + // FetchingCFilters = 1 + // FetchingHeaders = 2 + // DiscoveringAddrs = 3 + // Rescanning = 4 + // Complete = 5 + + if (syncStatusCode > 4) { + syncStatus = SyncedSyncStatus(); + return true; + } + + if (syncStatusCode == 0) { + syncStatus = ConnectedSyncStatus(); + return false; + } + + if (syncStatusCode == 1) { + syncStatus = SyncingSyncStatus(targetHeight, 0.0); + return false; + } + + if (syncStatusCode == 2) { + final headersProg = headersHeight / targetHeight; + // Only allow headers progress to go up half way. + syncStatus = SyncingSyncStatus(targetHeight - headersHeight, headersProg); + return false; + } + + // TODO: This step takes a while so should really get more info to the UI + // that we are discovering addresses. + if (syncStatusCode == 3) { + // Hover at half. + syncStatus = ProcessingSyncStatus(); + return false; + } + + if (syncStatusCode == 4) { + // Start at 75%. + final rescanProg = rescanHeight / targetHeight / 4; + syncStatus = SyncingSyncStatus(targetHeight - rescanHeight, .75 + rescanProg); + return false; + } + return false; + } + + @action + @override + Future connectToNode({required Node node}) async { + if (connecting) { + return; + } + connecting = true; + String addr = "default-spv-nodes"; + if (node.uri.host != addr) { + addr = node.uri.host; + if (node.uri.port != "") { + addr += ":" + node.uri.port.toString(); + } + } + if (addr != persistantPeer) { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + persistantPeer = addr; + await _libwallet.closeWallet(walletInfo.name); + final network = isTestnet ? "testnet" : "mainnet"; + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await _libwallet.loadWallet(jsonEncode(config)); + } + await this._startSync(); + connecting = false; + } + + @action + @override + Future startSync() async { + if (connecting) { + return; + } + connecting = true; + await this._startSync(); + connecting = false; + } + + Future _startSync() async { + if (syncTimer != null) { + return; + } + try { + syncStatus = ConnectingSyncStatus(); + await _libwallet.startSync( + walletInfo.name, + persistantPeer == "default-spv-nodes" ? "" : persistantPeer, + ); + syncTimer = Timer.periodic( + Duration(seconds: syncIntervalSyncing), (Timer t) => performBackgroundTasks()); + } catch (e) { + printV(e.toString()); + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + if (watchingOnly) { + return DecredPendingTransaction( + txid: "", + amount: 0, + fee: 0, + rawHex: "", + send: () async { + throw "unable to send with watching only wallet"; + }); + } + var totalIn = 0; + final ignoreInputs = []; + this.unspentCoinsInfo.values.forEach((unspent) { + if (unspent.isFrozen || !unspent.isSending) { + final input = {"txid": unspent.hash, "vout": unspent.vout}; + ignoreInputs.add(input); + return; + } + totalIn += unspent.value; + }); + + final creds = credentials as DecredTransactionCredentials; + var totalAmt = 0; + var sendAll = false; + final outputs = []; + for (final out in creds.outputs) { + var amt = 0; + if (out.sendAll) { + if (creds.outputs.length != 1) { + throw "can only send all to one output"; + } + sendAll = true; + totalAmt = totalIn; + } else if (out.cryptoAmount != null) { + final coins = double.parse(out.cryptoAmount!); + amt = (coins * 1e8).toInt(); + } + totalAmt += amt; + final o = { + "address": out.isParsedAddress ? out.extractedAddress! : out.address, + "amount": amt + }; + outputs.add(o); + } + + // throw exception if no selected coins under coin control + // or if the total coins selected, is less than the amount the user wants to spend + if (ignoreInputs.length == unspentCoinsInfo.values.length || totalIn < totalAmt) { + throw TransactionNoInputsException(); + } + + // The inputs are always used. Currently we don't have use for this + // argument. sendall ingores output value and sends everything. + final signReq = { + // "inputs": inputs, + "ignoreInputs": ignoreInputs, + "outputs": outputs, + "feerate": creds.feeRate ?? defaultFeeRate, + "password": _password, + "sendall": sendAll, + }; + final res = await _libwallet.createSignedTransaction(walletInfo.name, jsonEncode(signReq)); + final decoded = json.decode(res); + final signedHex = decoded["signedhex"]; + final send = () async { + await _libwallet.sendRawTransaction(walletInfo.name, signedHex); + await updateBalance(); + }; + final fee = decoded["fee"] ?? 0; + if (sendAll) { + totalAmt = (totalAmt - fee).toInt(); + } + return DecredPendingTransaction( + txid: decoded["txid"] ?? "", amount: totalAmt, fee: fee, rawHex: signedHex, send: send); + } + + int feeRate(TransactionPriority priority) { + if (!(priority is DecredTransactionPriority)) { + return defaultFeeRate; + } + final p = priority; + switch (p) { + case DecredTransactionPriority.slow: + return feeRateSlow.feeRate(); + case DecredTransactionPriority.medium: + return feeRateMedium.feeRate(); + case DecredTransactionPriority.fast: + return feeRateFast.feeRate(); + } + return defaultFeeRate; + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + if (priority is DecredTransactionPriority) { + final P2PKHOutputSize = + 36; // 8 bytes value + 2 bytes version + at least 1 byte varint script size + P2PKHPkScriptSize + // MsgTxOverhead is 4 bytes version (lower 2 bytes for the real transaction + // version and upper 2 bytes for the serialization type) + 4 bytes locktime + // + 4 bytes expiry + 3 bytes of varints for the number of transaction + // inputs (x2 for witness and prefix) and outputs + final MsgTxOverhead = 15; + // TxInOverhead is the overhead for a wire.TxIn with a scriptSig length < + // 254. prefix (41 bytes) + ValueIn (8 bytes) + BlockHeight (4 bytes) + + // BlockIndex (4 bytes) + sig script var int (at least 1 byte) + final TxInOverhead = 57; + final P2PKHInputSize = + TxInOverhead + 109; // TxInOverhead (57) + var int (1) + P2PKHSigScriptSize (108) + + int inputsCount = 1; + if (amount != null) { + inputsCount += _unspents.where((e) { + amount = (amount!) - e.value; + return (amount!) > 0; + }).length; + } + + // Estimate using a transaction consuming inoutsCount and paying to one address with change. + return (this.feeRate(priority) / 1000).round() * + (MsgTxOverhead + P2PKHInputSize * inputsCount + P2PKHOutputSize * 2); + } + return 0; + } + + @override + Future> fetchTransactions() async { + return this.fetchFiveTransactions(0); + } + + Future> fetchFiveTransactions(int from) async { + final res = await _libwallet.listTransactions(walletInfo.name, from.toString(), "5"); + final decoded = json.decode(res); + var txs = {}; + for (final d in decoded) { + final txid = uniqueTxID(d["txid"] ?? "", d["vout"] ?? 0); + var direction = TransactionDirection.outgoing; + if (d["category"] == "receive") { + direction = TransactionDirection.incoming; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final feeDouble = d["fee"] ?? 0.0; + final fee = (feeDouble * 1e8).toInt().abs(); + final confs = d["confirmations"] ?? 0; + final sendTime = d["time"] ?? 0; + final height = d["height"] ?? 0; + final txInfo = DecredTransactionInfo( + id: txid, + amount: amount, + fee: fee, + direction: direction, + isPending: confs == 0, + date: DateTime.fromMillisecondsSinceEpoch(sendTime * 1000, isUtc: false), + height: height, + confirmations: confs, + to: d["address"] ?? "", + ); + txs[txid] = txInfo; + } + return txs; + } + + // uniqueTxID combines the tx id and vout to create a unique id. + String uniqueTxID(String id, int vout) { + return id + ":" + vout.toString(); + } + + @override + Future save() async {} + + @override + bool get hasRescan => walletBirthdayBlockHeight() != -1; + + @override + Future rescan({required int height}) async { + // The required height is not used. A birthday time is recorded in the + // mnemonic. As long as not private data is imported into the wallet, we + // can always rescan from there. + var rescanHeight = 0; + if (!watchingOnly) { + rescanHeight = await walletBirthdayBlockHeight(); + // Sync has not yet reached the birthday block. + if (rescanHeight == -1) { + return; + } + } + await _libwallet.rescanFromHeight(walletInfo.name, rescanHeight.toString()); + } + + @override + Future close({bool shouldCleanup = false}) async { + if (syncTimer != null) { + syncTimer!.cancel(); + syncTimer = null; + } + await _libwallet.closeWallet(walletInfo.name); + if (shouldCleanup) { + await _libwallet.shutdown(); + _closeLibwallet(); + } + } + + @override + Future changePassword(String password) async { + if (watchingOnly) { + return; + } + return () async { + await _libwallet.changeWalletPassword(walletInfo.name, _password, password); + }(); + } + + @override + Future updateBalance() async { + final balanceMap = await _libwallet.balance(walletInfo.name); + + var totalFrozen = 0; + + unspentCoinsInfo.values.forEach((info) { + _unspents.forEach((element) { + if (element.hash == info.hash && + element.vout == info.vout && + info.isFrozen && + element.value == info.value) { + totalFrozen += element.value; + } + }); + }); + + balance[CryptoCurrency.dcr] = DecredBalance( + confirmed: balanceMap["confirmed"] ?? 0, + unconfirmed: balanceMap["unconfirmed"] ?? 0, + frozen: totalFrozen, + ); + } + + @override + void setExceptionHandler(void Function(FlutterErrorDetails) onError) => onError; + + Future renameWalletFiles(String newWalletName) async { + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + + if (File(newDirPath).existsSync()) { + throw "wallet already exists at $newDirPath"; + } + + await Directory(currentDirPath).rename(newDirPath); + } + + @override + Future signMessage(String message, {String? address = null}) async { + if (watchingOnly) { + throw "a watching only wallet cannot sign"; + } + var addr = address; + if (addr == null) { + addr = walletAddresses.address; + } + if (addr == "") { + throw "unable to get an address from unsynced wallet"; + } + return await _libwallet.signMessage(walletInfo.name, message, addr, _password); + } + + Future fetchUnspents() async { + final res = await _libwallet.listUnspents(walletInfo.name); + final decoded = json.decode(res); + var unspents = []; + for (final d in decoded) { + final spendable = d["spendable"] ?? false; + if (!spendable) { + continue; + } + final amountDouble = d["amount"] ?? 0.0; + final amount = (amountDouble * 1e8).toInt().abs(); + final utxo = Unspent(d["address"] ?? "", d["txid"] ?? "", amount, d["vout"] ?? 0, null); + utxo.isChange = d["ischange"] ?? false; + unspents.add(utxo); + } + _unspents = unspents; + } + + List unspents() { + this.updateUnspents(_unspents); + return _unspents; + } + + void updateUnspents(List unspentCoins) { + if (this.unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => this.addCoinInfo(coin)); + return; + } + + if (unspentCoins.isEmpty) { + this.unspentCoinsInfo.clear(); + return; + } + + final walletID = idPrefix + walletInfo.name; + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = this.unspentCoinsInfo.values.where((element) => + element.walletId == walletID && element.hash == coin.hash && element.vout == coin.vout); + + if (coinInfoList.isEmpty) { + this.addCoinInfo(coin); + } else { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } + }); + } + + final List keys = []; + this.unspentCoinsInfo.values.forEach((element) { + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + + if (keys.isNotEmpty) { + unspentCoinsInfo.deleteAll(keys); + } + } + + void addCoinInfo(Unspent coin) { + final newInfo = UnspentCoinsInfo( + walletId: idPrefix + walletInfo.name, + hash: coin.hash, + isFrozen: false, + isSending: coin.isSending, + noteRaw: "", + address: coin.address, + value: coin.value, + vout: coin.vout, + isChange: coin.isChange, + keyImage: coin.keyImage, + ); + + unspentCoinsInfo.add(newInfo); + } + + // walletBirthdayBlockHeight checks if the wallet birthday is set and returns + // it. Returns -1 if not. + Future walletBirthdayBlockHeight() async { + final res = await _libwallet.birthState(walletInfo.name); + final decoded = json.decode(res); + // Having these values set indicates that sync has not reached the birthday + // yet, so no birthday is set. + if (decoded["setfromheight"] == true || decoded["setfromtime"] == true) { + return -1; + } + return decoded["height"] ?? 0; + } + + Future verifyMessage(String message, String signature, {String? address = null}) async { + var addr = address; + if (addr == null) { + throw "an address is required to verify message"; + } + return () async { + final verified = await _libwallet.verifyMessage(walletInfo.name, message, addr, signature); + if (verified == "true") { + return true; + } + return false; + }(); + } + + @override + String get password => _password; + + @override + bool canSend() => seed != null; +} diff --git a/cw_decred/lib/wallet_addresses.dart b/cw_decred/lib/wallet_addresses.dart new file mode 100644 index 000000000..10970b2d6 --- /dev/null +++ b/cw_decred/lib/wallet_addresses.dart @@ -0,0 +1,137 @@ +import 'dart:convert'; +import 'package:mobx/mobx.dart'; + +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_decred/api/libdcrwallet.dart'; + +part 'wallet_addresses.g.dart'; + +class DecredWalletAddresses = DecredWalletAddressesBase with _$DecredWalletAddresses; + +abstract class DecredWalletAddressesBase extends WalletAddresses with Store { + DecredWalletAddressesBase(WalletInfo walletInfo, Libwallet libwallet) + : _libwallet = libwallet, + super(walletInfo); + final Libwallet _libwallet; + String currentAddr = ''; + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @observable + String selectedAddr = ''; + + @override + @computed + String get address { + return selectedAddr; + } + + @override + set address(value) { + selectedAddr = value; + } + + @override + Future init() async { + if (walletInfo.addresses != null) { + addressesMap = walletInfo.addresses!; + } + if (walletInfo.addressInfos != null) { + addressInfos = walletInfo.addressInfos!; + } + if (walletInfo.usedAddresses != null) { + usedAddresses = {...walletInfo.usedAddresses!}; + } + await updateAddressesInBox(); + } + + @override + Future updateAddressesInBox() async { + final addrs = await libAddresses(); + final allAddrs = new List.from(addrs.usedAddrs)..addAll(addrs.unusedAddrs); + + // Add all addresses. + allAddrs.forEach((addr) { + if (addressesMap.containsKey(addr)) { + return; + } + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: "", accountIndex: 0)); + }); + + // Add used addresses. + addrs.usedAddrs.forEach((addr) { + if (!usedAddresses.contains(addr)) { + usedAddresses.add(addr); + } + }); + + if (addrs.unusedAddrs.length > 0 && addrs.unusedAddrs[0] != currentAddr) { + currentAddr = addrs.unusedAddrs[0]; + selectedAddr = currentAddr; + } + + await saveAddressesInBox(); + } + + List getAddressInfos() { + if (addressInfos.containsKey(0)) { + return addressInfos[0]!; + } + return []; + } + + Future updateAddress(String address, String label) async { + if (!addressInfos.containsKey(0)) { + return; + } + addressInfos[0]!.forEach((info) { + if (info.address == address) { + info.label = label; + } + }); + await saveAddressesInBox(); + } + + Future libAddresses() async { + final nUsed = "10"; + var nUnused = "1"; + if (this.isEnabledAutoGenerateSubaddress) { + nUnused = "3"; + } + final res = await _libwallet.addresses(walletInfo.name, nUsed, nUnused); + final decoded = json.decode(res); + final usedAddrs = List.from(decoded["used"] ?? []); + final unusedAddrs = List.from(decoded["unused"] ?? []); + // index is the index of the first unused address. + final index = decoded["index"] ?? 0; + return new LibAddresses(usedAddrs, unusedAddrs, index); + } + + Future generateNewAddress(String label) async { + // NOTE: This will ignore the gap limit and may cause problems when restoring from seed if too + // many addresses are taken and not used. + final addr = await _libwallet.newExternalAddress(walletInfo.name) ?? ''; + if (addr == "") { + return; + } + if (!addressesMap.containsKey(addr)) { + addressesMap[addr] = ""; + addressInfos[0] ??= []; + addressInfos[0]?.add(AddressInfo(address: addr, label: label, accountIndex: 0)); + } + selectedAddr = addr; + await saveAddressesInBox(); + } +} + +class LibAddresses { + final List usedAddrs, unusedAddrs; + final int firstUnusedAddrIndex; + + LibAddresses(this.usedAddrs, this.unusedAddrs, this.firstUnusedAddrIndex); +} diff --git a/cw_decred/lib/wallet_creation_credentials.dart b/cw_decred/lib/wallet_creation_credentials.dart new file mode 100644 index 000000000..ca0451447 --- /dev/null +++ b/cw_decred/lib/wallet_creation_credentials.dart @@ -0,0 +1,40 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/hardware/hardware_account_data.dart'; + +class DecredNewWalletCredentials extends WalletCredentials { + DecredNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class DecredRestoreWalletFromSeedCredentials extends WalletCredentials { + DecredRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class DecredRestoreWalletFromPubkeyCredentials extends WalletCredentials { + DecredRestoreWalletFromPubkeyCredentials( + {required String name, + required String password, + required String this.pubkey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String pubkey; +} + +class DecredRestoreWalletFromHardwareCredentials extends WalletCredentials { + DecredRestoreWalletFromHardwareCredentials( + {required String name, required this.hwAccountData, WalletInfo? walletInfo}) + : t = throw UnimplementedError(), + super(name: name, walletInfo: walletInfo); + + final HardwareAccountData hwAccountData; + final void t; +} diff --git a/cw_decred/lib/wallet_service.dart b/cw_decred/lib/wallet_service.dart new file mode 100644 index 000000000..a54833321 --- /dev/null +++ b/cw_decred/lib/wallet_service.dart @@ -0,0 +1,186 @@ +import 'dart:convert'; +import 'dart:io'; +import 'package:cw_decred/api/libdcrwallet.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:hive/hive.dart'; +import 'package:collection/collection.dart'; +import 'package:cw_core/unspent_coins_info.dart'; + +class DecredWalletService extends WalletService< + DecredNewWalletCredentials, + DecredRestoreWalletFromSeedCredentials, + DecredRestoreWalletFromPubkeyCredentials, + DecredRestoreWalletFromHardwareCredentials> { + DecredWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); + + final Box walletInfoSource; + final Box unspentCoinsInfoSource; + final seedRestorePath = "m/44'/42'"; + static final seedRestorePathTestnet = "m/44'/1'"; + static final pubkeyRestorePath = "m/44'/42'/0'"; + static final pubkeyRestorePathTestnet = "m/44'/1'/0'"; + final mainnet = "mainnet"; + final testnet = "testnet"; + Libwallet? libwallet; + + Future init() async { + if (libwallet != null) { + return; + } + libwallet = await Libwallet.spawn(); + // Use the general path for all dcr wallets as the general log directory. + // Individual wallet paths may be removed if the wallet is deleted. + final dcrLogDir = await pathForWalletDir(name: '', type: WalletType.decred); + libwallet!.initLibdcrwallet(dcrLogDir); + } + + void closeLibwallet() { + if (libwallet == null) { + return; + } + libwallet!.close(); + libwallet = null; + } + + @override + WalletType getType() => WalletType.decred; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future create(DecredNewWalletCredentials credentials, {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future openWallet(String name, String password) async { + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(name, getType()))!; + final network = walletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + walletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + + await this.init(); + final walletDirExists = Directory(walletInfo.dirPath).existsSync(); + if (!walletDirExists) { + walletInfo.dirPath = await pathForWalletDir(name: name, type: getType()); + } + + final config = { + "name": walletInfo.name, + "datadir": walletInfo.dirPath, + "net": network, + "unsyncedaddrs": true, + }; + await libwallet!.loadWallet(jsonEncode(config)); + final wallet = + DecredWallet(walletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(currentName, getType()))!; + final network = currentWalletInfo.derivationInfo?.derivationPath == seedRestorePathTestnet || + currentWalletInfo.derivationInfo?.derivationPath == pubkeyRestorePathTestnet + ? testnet + : mainnet; + final currentWallet = DecredWallet( + currentWalletInfo, password, this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + + await currentWallet.renameWalletFiles(newName); + + final newDirPath = await pathForWalletDir(name: newName, type: getType()); + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + newWalletInfo.dirPath = newDirPath; + newWalletInfo.network = network; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } + + @override + Future restoreFromSeed(DecredRestoreWalletFromSeedCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pass": credentials.password!, + "mnemonic": credentials.mnemonic, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? seedRestorePathTestnet : seedRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + // restoreFromKeys only supports restoring a watch only wallet from an account + // pubkey. + @override + Future restoreFromKeys(DecredRestoreWalletFromPubkeyCredentials credentials, + {bool? isTestnet}) async { + await this.init(); + final config = { + "name": credentials.walletInfo!.name, + "datadir": credentials.walletInfo!.dirPath, + "pubkey": credentials.pubkey, + "net": isTestnet == true ? testnet : mainnet, + "unsyncedaddrs": true, + }; + await libwallet!.createWatchOnlyWallet(jsonEncode(config)); + final di = DerivationInfo( + derivationPath: isTestnet == true ? pubkeyRestorePathTestnet : pubkeyRestorePath); + credentials.walletInfo!.derivationInfo = di; + final wallet = DecredWallet(credentials.walletInfo!, credentials.password!, + this.unspentCoinsInfoSource, libwallet!, closeLibwallet); + await wallet.init(); + return wallet; + } + + @override + Future restoreFromHardwareWallet( + DecredRestoreWalletFromHardwareCredentials credentials) async => + throw UnimplementedError(); +} diff --git a/cw_decred/macos/Classes/CwDecredPlugin.swift b/cw_decred/macos/Classes/CwDecredPlugin.swift new file mode 100644 index 000000000..72dae36f4 --- /dev/null +++ b/cw_decred/macos/Classes/CwDecredPlugin.swift @@ -0,0 +1,19 @@ +import Cocoa +import FlutterMacOS + +public class CwDecredPlugin: NSObject, FlutterPlugin { + public static func register(with registrar: FlutterPluginRegistrar) { + let channel = FlutterMethodChannel(name: "cw_decred", binaryMessenger: registrar.messenger) + let instance = CwDecredPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "getPlatformVersion": + result("macOS " + ProcessInfo.processInfo.operatingSystemVersionString) + default: + result(FlutterMethodNotImplemented) + } + } +} diff --git a/cw_decred/macos/cw_decred.podspec b/cw_decred/macos/cw_decred.podspec new file mode 100644 index 000000000..87d82f238 --- /dev/null +++ b/cw_decred/macos/cw_decred.podspec @@ -0,0 +1,22 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint cw_decred.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'cw_decred' + s.version = '0.0.1' + s.summary = 'Cake Wallet Decred' + s.description = 'Cake Wallet wrapper over Decred project' + s.homepage = 'http://cakewallet.com' + s.license = { :file => '../LICENSE' } + s.author = { 'Cake Wallet' => 'support@cakewallet.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + + s.platform = :osx, '10.11' + s.vendored_libraries = 'External/lib/libdcrwallet.a' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', "OTHER_LDFLAGS" => "-force_load $(PODS_TARGET_SRCROOT)/External/lib/libdcrwallet.a -lstdc++" } + s.swift_version = '5.0' +end diff --git a/cw_decred/pubspec.lock b/cw_decred/pubspec.lock new file mode 100644 index 000000000..e3d39372b --- /dev/null +++ b/cw_decred/pubspec.lock @@ -0,0 +1,852 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + _fe_analyzer_shared: + dependency: transitive + description: + name: _fe_analyzer_shared + sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + url: "https://pub.dev" + source: hosted + version: "72.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.2" + analyzer: + dependency: transitive + description: + name: analyzer + sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + url: "https://pub.dev" + source: hosted + version: "6.7.0" + args: + dependency: transitive + description: + name: args + sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6 + url: "https://pub.dev" + source: hosted + version: "2.6.0" + asn1lib: + dependency: transitive + description: + name: asn1lib + sha256: "4bae5ae63e6d6dd17c4aac8086f3dec26c0236f6a0f03416c6c19d830c367cf5" + url: "https://pub.dev" + source: hosted + version: "1.5.8" + async: + dependency: transitive + description: + name: async + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" + source: hosted + version: "2.11.0" + blockchain_utils: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "59fdf29d72068e0522a96a8953ed7272833a9f57" + url: "https://github.com/cake-tech/blockchain_utils" + source: git + version: "3.3.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + build: + dependency: transitive + description: + name: build + sha256: "80184af8b6cb3e5c1c4ec6d8544d27711700bc3e6d2efad04238c7b5290889f0" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + build_config: + dependency: transitive + description: + name: build_config + sha256: bf80fcfb46a29945b423bd9aad884590fb1dc69b330a4d4700cac476af1708d1 + url: "https://pub.dev" + source: hosted + version: "1.1.1" + build_daemon: + dependency: transitive + description: + name: build_daemon + sha256: "79b2aef6ac2ed00046867ed354c88778c9c0f029df8a20fe10b5436826721ef9" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + build_resolvers: + dependency: "direct dev" + description: + name: build_resolvers + sha256: "339086358431fa15d7eca8b6a36e5d783728cf025e559b834f4609a1fcfb7b0a" + url: "https://pub.dev" + source: hosted + version: "2.4.2" + build_runner: + dependency: "direct dev" + description: + name: build_runner + sha256: "028819cfb90051c6b5440c7e574d1896f8037e3c96cf17aaeb054c9311cfbf4d" + url: "https://pub.dev" + source: hosted + version: "2.4.13" + build_runner_core: + dependency: transitive + description: + name: build_runner_core + sha256: f8126682b87a7282a339b871298cc12009cb67109cfa1614d6436fb0289193e0 + url: "https://pub.dev" + source: hosted + version: "7.3.2" + built_collection: + dependency: transitive + description: + name: built_collection + sha256: "376e3dd27b51ea877c28d525560790aee2e6fbb5f20e2f85d5081027d94e2100" + url: "https://pub.dev" + source: hosted + version: "5.1.1" + built_value: + dependency: transitive + description: + name: built_value + sha256: ea90e81dc4a25a043d9bee692d20ed6d1c4a1662a28c03a96417446c093ed6b4 + url: "https://pub.dev" + source: hosted + version: "8.9.5" + cake_backup: + dependency: transitive + description: + path: "." + ref: main + resolved-ref: "3aba867dcab6737f6707782f5db15d71f303db38" + url: "https://github.com/cake-tech/cake_backup.git" + source: git + version: "1.0.0+1" + characters: + dependency: transitive + description: + name: characters + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" + source: hosted + version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" + clock: + dependency: transitive + description: + name: clock + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" + source: hosted + version: "1.1.1" + code_builder: + dependency: transitive + description: + name: code_builder + sha256: "0ec10bf4a89e4c613960bf1e8b42c64127021740fb21640c29c909826a5eea3e" + url: "https://pub.dev" + source: hosted + version: "4.10.1" + collection: + dependency: transitive + description: + name: collection + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" + source: hosted + version: "1.18.0" + convert: + dependency: transitive + description: + name: convert + sha256: b30acd5944035672bc15c6b7a8b47d773e41e2f17de064350988c5d02adb1c68 + url: "https://pub.dev" + source: hosted + version: "3.1.2" + crypto: + dependency: transitive + description: + name: crypto + sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" + url: "https://pub.dev" + source: hosted + version: "3.0.6" + cryptography: + dependency: transitive + description: + name: cryptography + sha256: d146b76d33d94548cf035233fbc2f4338c1242fa119013bead807d033fc4ae05 + url: "https://pub.dev" + source: hosted + version: "2.7.0" + cupertino_icons: + dependency: transitive + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + cw_core: + dependency: "direct main" + description: + path: "../cw_core" + relative: true + source: path + version: "0.0.1" + dart_style: + dependency: transitive + description: + name: dart_style + sha256: "7856d364b589d1f08986e140938578ed36ed948581fbc3bc9aef1805039ac5ab" + url: "https://pub.dev" + source: hosted + version: "2.3.7" + decimal: + dependency: transitive + description: + name: decimal + sha256: "24a261d5d5c87e86c7651c417a5dbdf8bcd7080dd592533910e8d0505a279f21" + url: "https://pub.dev" + source: hosted + version: "2.3.3" + encrypt: + dependency: transitive + description: + name: encrypt + sha256: "62d9aa4670cc2a8798bab89b39fc71b6dfbacf615de6cf5001fb39f7e4a996a2" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" + source: hosted + version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6" + url: "https://pub.dev" + source: hosted + version: "2.1.3" + ffigen: + dependency: "direct dev" + description: + name: ffigen + sha256: "2119b4fe3aad0db94dc9531b90283c4640a6231070e613c400b426a4da08c704" + url: "https://pub.dev" + source: hosted + version: "16.1.0" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be + url: "https://pub.dev" + source: hosted + version: "1.1.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_mobx: + dependency: transitive + description: + name: flutter_mobx + sha256: ba5e93467866a2991259dc51cffd41ef45f695c667c2b8e7b087bf24118b50fe + url: "https://pub.dev" + source: hosted + version: "2.3.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + frontend_server_client: + dependency: transitive + description: + name: frontend_server_client + sha256: f64a0333a82f30b0cca061bc3d143813a486dc086b574bfb233b7c1372427694 + url: "https://pub.dev" + source: hosted + version: "4.0.0" + glob: + dependency: transitive + description: + name: glob + sha256: c3f1ee72c96f8f78935e18aa8cecced9ab132419e8625dc187e1c2408efc20de + url: "https://pub.dev" + source: hosted + version: "2.1.3" + graphs: + dependency: transitive + description: + name: graphs + sha256: "741bbf84165310a68ff28fe9e727332eef1407342fca52759cb21ad8177bb8d0" + url: "https://pub.dev" + source: hosted + version: "2.3.2" + hive: + dependency: transitive + description: + name: hive + sha256: "8dcf6db979d7933da8217edcec84e9df1bdb4e4edc7fc77dbd5aa74356d6d941" + url: "https://pub.dev" + source: hosted + version: "2.2.3" + hive_generator: + dependency: "direct dev" + description: + name: hive_generator + sha256: "06cb8f58ace74de61f63500564931f9505368f45f98958bd7a6c35ba24159db4" + url: "https://pub.dev" + source: hosted + version: "2.0.1" + http: + dependency: transitive + description: + name: http + sha256: fe7ab022b76f3034adc518fb6ea04a82387620e19977665ea18d30a1cf43442f + url: "https://pub.dev" + source: hosted + version: "1.3.0" + http_multi_server: + dependency: transitive + description: + name: http_multi_server + sha256: aa6199f908078bb1c5efb8d8638d4ae191aac11b311132c3ef48ce352fb52ef8 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + http_parser: + dependency: transitive + description: + name: http_parser + sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" + url: "https://pub.dev" + source: hosted + version: "4.0.2" + intl: + dependency: transitive + description: + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" + source: hosted + version: "0.19.0" + io: + dependency: transitive + description: + name: io + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b + url: "https://pub.dev" + source: hosted + version: "1.0.5" + js: + dependency: transitive + description: + name: js + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 + url: "https://pub.dev" + source: hosted + version: "0.6.7" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + url: "https://pub.dev" + source: hosted + version: "10.0.5" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + url: "https://pub.dev" + source: hosted + version: "3.0.5" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + logging: + dependency: transitive + description: + name: logging + sha256: c8245ada5f1717ed44271ed1c26b8ce85ca3228fd2ffdb75468ab01979309d61 + url: "https://pub.dev" + source: hosted + version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + url: "https://pub.dev" + source: hosted + version: "0.1.2-main.4" + matcher: + dependency: transitive + description: + name: matcher + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" + source: hosted + version: "0.12.16+1" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + url: "https://pub.dev" + source: hosted + version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "41a20518f0cb1256669420fdba0cd90d21561e560ac240f26ef8322e45bb7ed6" + url: "https://pub.dev" + source: hosted + version: "2.0.0" + mobx: + dependency: transitive + description: + name: mobx + sha256: bf1a90e5bcfd2851fc6984e20eef69557c65d9e4d0a88f5be4cf72c9819ce6b0 + url: "https://pub.dev" + source: hosted + version: "2.5.0" + mobx_codegen: + dependency: "direct dev" + description: + name: mobx_codegen + sha256: "990da80722f7d7c0017dec92040b31545d625b15d40204c36a1e63d167c73cdc" + url: "https://pub.dev" + source: hosted + version: "2.7.0" + nested: + dependency: transitive + description: + name: nested + sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" + url: "https://pub.dev" + source: hosted + version: "1.0.0" + on_chain: + dependency: transitive + description: + path: "." + ref: cake-update-v2 + resolved-ref: "93440dc5126369b873ca1fccc13c3c1240b1c5c2" + url: "https://github.com/cake-tech/on_chain.git" + source: git + version: "3.7.0" + package_config: + dependency: transitive + description: + name: package_config + sha256: "92d4488434b520a62570293fbd33bb556c7d49230791c1b4bbd973baf6d2dc67" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + path: + dependency: transitive + description: + name: path + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" + source: hosted + version: "1.9.0" + path_provider: + dependency: transitive + description: + name: path_provider + sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" + url: "https://pub.dev" + source: hosted + version: "2.1.5" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" + url: "https://pub.dev" + source: hosted + version: "2.2.15" + path_provider_foundation: + dependency: transitive + description: + name: path_provider_foundation + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + pointycastle: + dependency: transitive + description: + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" + source: hosted + version: "3.9.1" + pool: + dependency: transitive + description: + name: pool + sha256: "20fe868b6314b322ea036ba325e6fc0711a22948856475e2c2b6306e8ab39c2a" + url: "https://pub.dev" + source: hosted + version: "1.5.1" + provider: + dependency: transitive + description: + name: provider + sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c + url: "https://pub.dev" + source: hosted + version: "6.1.2" + pub_semver: + dependency: transitive + description: + name: pub_semver + sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + pubspec_parse: + dependency: transitive + description: + name: pubspec_parse + sha256: "81876843eb50dc2e1e5b151792c9a985c5ed2536914115ed04e9c8528f6647b0" + url: "https://pub.dev" + source: hosted + version: "1.4.0" + quiver: + dependency: transitive + description: + name: quiver + sha256: ea0b925899e64ecdfbf9c7becb60d5b50e706ade44a85b2363be2a22d88117d2 + url: "https://pub.dev" + source: hosted + version: "3.2.2" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" + shelf: + dependency: transitive + description: + name: shelf + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 + url: "https://pub.dev" + source: hosted + version: "1.4.1" + shelf_web_socket: + dependency: transitive + description: + name: shelf_web_socket + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 + url: "https://pub.dev" + source: hosted + version: "2.0.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.99" + socks5_proxy: + dependency: transitive + description: + name: socks5_proxy + sha256: "616818a0ea1064a4823b53c9f7eaf8da64ed82dcd51ed71371c7e54751ed5053" + url: "https://pub.dev" + source: hosted + version: "1.0.6" + source_gen: + dependency: transitive + description: + name: source_gen + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" + url: "https://pub.dev" + source: hosted + version: "1.5.0" + source_helper: + dependency: transitive + description: + name: source_helper + sha256: "86d247119aedce8e63f4751bd9626fc9613255935558447569ad42f9f5b48b3c" + url: "https://pub.dev" + source: hosted + version: "1.3.5" + source_span: + dependency: transitive + description: + name: source_span + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" + source: hosted + version: "1.10.0" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" + source: hosted + version: "1.11.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" + source: hosted + version: "2.1.2" + stream_transform: + dependency: transitive + description: + name: stream_transform + sha256: ad47125e588cfd37a9a7f86c7d6356dde8dfe89d071d293f80ca9e9273a33871 + url: "https://pub.dev" + source: hosted + version: "2.1.1" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" + source: hosted + version: "1.2.0" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" + source: hosted + version: "1.2.1" + test_api: + dependency: transitive + description: + name: test_api + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + url: "https://pub.dev" + source: hosted + version: "0.7.2" + timing: + dependency: transitive + description: + name: timing + sha256: "62ee18aca144e4a9f29d212f5a4c6a053be252b895ab14b5821996cff4ed90fe" + url: "https://pub.dev" + source: hosted + version: "1.0.2" + tuple: + dependency: transitive + description: + name: tuple + sha256: a97ce2013f240b2f3807bcbaf218765b6f301c3eff91092bcfa23a039e7dd151 + url: "https://pub.dev" + source: hosted + version: "2.0.2" + typed_data: + dependency: transitive + description: + name: typed_data + sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + unorm_dart: + dependency: transitive + description: + name: unorm_dart + sha256: "23d8bf65605401a6a32cff99435fed66ef3dab3ddcad3454059165df46496a3b" + url: "https://pub.dev" + source: hosted + version: "0.3.0" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: f652077d0bdf60abe4c1f6377448e8655008eef28f128bc023f7b5e8dfeb48fc + url: "https://pub.dev" + source: hosted + version: "14.2.4" + watcher: + dependency: transitive + description: + name: watcher + sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + web_socket: + dependency: transitive + description: + name: web_socket + sha256: "3c12d96c0c9a4eec095246debcea7b86c0324f22df69893d538fcc6f1b8cce83" + url: "https://pub.dev" + source: hosted + version: "0.1.6" + web_socket_channel: + dependency: transitive + description: + name: web_socket_channel + sha256: "0b8e2457400d8a859b7b2030786835a28a8e80836ef64402abef392ff4f1d0e5" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" + yaml: + dependency: transitive + description: + name: yaml + sha256: b9da305ac7c39faa3f030eccd175340f968459dae4af175130b3fc47e40d76ce + url: "https://pub.dev" + source: hosted + version: "3.1.3" + yaml_edit: + dependency: transitive + description: + name: yaml_edit + sha256: fb38626579fb345ad00e674e2af3a5c9b0cc4b9bfb8fd7f7ff322c7c9e62aef5 + url: "https://pub.dev" + source: hosted + version: "2.2.2" +sdks: + dart: ">=3.5.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml new file mode 100644 index 000000000..fcb2ac5ec --- /dev/null +++ b/cw_decred/pubspec.yaml @@ -0,0 +1,84 @@ +name: cw_decred +description: A new Flutter plugin project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.2.0-0 <4.0.0' + flutter: ">=3.19.0" + + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + build_resolvers: ^2.0.9 + mobx_codegen: ^2.0.7 + hive_generator: ^2.0.1 + ffigen: ^16.1.0 + +ffigen: + name: libdcrwallet + description: Bindings for dcrwallet go library. + output: "lib/api/libdcrwallet_bindings.dart" + headers: + entry-points: + - "lib/api/libdcrwallet.h" + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The androidPackage and pluginClass identifiers should not ordinarily + # be modified. They are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.cakewallet.cw_decred + pluginClass: CwDecredPlugin + ios: + pluginClass: CwDecredPlugin + macos: + pluginClass: CwDecredPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 15e8f684e..ebad95f8e 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -115,6 +115,9 @@ abstract class MoneroWalletBase extends WalletBase balance; + @override + bool get hasRescan => true; + @override String get seed => monero_wallet.getSeed(); String seedLegacy(String? language) => monero_wallet.getSeedLegacy(language); diff --git a/cw_wownero/lib/wownero_wallet.dart b/cw_wownero/lib/wownero_wallet.dart index 67a9bbb45..6f4630b60 100644 --- a/cw_wownero/lib/wownero_wallet.dart +++ b/cw_wownero/lib/wownero_wallet.dart @@ -123,6 +123,9 @@ abstract class WowneroWalletBase String _password; + @override + bool get hasRescan => true; + @override MoneroWalletKeys get keys => MoneroWalletKeys( primaryAddress: wownero_wallet.getAddress(accountIndex: 0, addressIndex: 0), diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 57d4589c9..f621ab9b2 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -3,8 +3,39 @@ PODS: - Flutter - ReachabilitySwift - CryptoSwift (1.8.3) + - cw_haven (0.0.1): + - cw_haven/Boost (= 0.0.1) + - cw_haven/Haven (= 0.0.1) + - cw_haven/OpenSSL (= 0.0.1) + - cw_haven/Sodium (= 0.0.1) + - cw_shared_external + - Flutter + - cw_haven/Boost (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Haven (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/OpenSSL (0.0.1): + - cw_shared_external + - Flutter + - cw_haven/Sodium (0.0.1): + - cw_shared_external + - Flutter - cw_mweb (0.0.1): - Flutter + - cw_decred (0.0.1): + - cw_shared_external (0.0.1): + - cw_shared_external/Boost (= 0.0.1) + - cw_shared_external/OpenSSL (= 0.0.1) + - cw_shared_external/Sodium (= 0.0.1) + - Flutter + - cw_shared_external/Boost (0.0.1): + - Flutter + - cw_shared_external/OpenSSL (0.0.1): + - Flutter + - cw_shared_external/Sodium (0.0.1): + - Flutter - device_display_brightness (0.0.1): - Flutter - device_info_plus (0.0.1): @@ -106,7 +137,10 @@ PODS: DEPENDENCIES: - connectivity_plus (from `.symlinks/plugins/connectivity_plus/ios`) - CryptoSwift + - cw_haven (from `.symlinks/plugins/cw_haven/ios`) - cw_mweb (from `.symlinks/plugins/cw_mweb/ios`) + - cw_shared_external (from `.symlinks/plugins/cw_shared_external/ios`) + - cw_decred (from `.symlinks/plugins/cw_decred/ios`) - device_display_brightness (from `.symlinks/plugins/device_display_brightness/ios`) - device_info_plus (from `.symlinks/plugins/device_info_plus/ios`) - devicelocale (from `.symlinks/plugins/devicelocale/ios`) @@ -147,8 +181,14 @@ SPEC REPOS: EXTERNAL SOURCES: connectivity_plus: :path: ".symlinks/plugins/connectivity_plus/ios" + cw_haven: + :path: ".symlinks/plugins/cw_haven/ios" cw_mweb: :path: ".symlinks/plugins/cw_mweb/ios" + cw_shared_external: + :path: ".symlinks/plugins/cw_shared_external/ios" + cw_decred: + :path: ".symlinks/plugins/cw_decred/ios" device_display_brightness: :path: ".symlinks/plugins/device_display_brightness/ios" device_info_plus: @@ -203,7 +243,10 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: bf0076dd84a130856aa636df1c71ccaff908fa1d CryptoSwift: 967f37cea5a3294d9cce358f78861652155be483 + cw_haven: b3e54e1fbe7b8e6fda57a93206bc38f8e89b898a cw_mweb: 22cd01dfb8ad2d39b15332006f22046aaa8352a3 + cw_shared_external: 2972d872b8917603478117c9957dfca611845a92 + cw_decred: 9c0e1df74745b51a1289ec5e91fb9e24b68fa14a device_display_brightness: 1510e72c567a1f6ce6ffe393dcd9afd1426034f7 device_info_plus: c6fb39579d0f423935b0c9ce7ee2f44b71b9fce6 devicelocale: 35ba84dc7f45f527c3001535d8c8d104edd5d926 diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 69ab926b8..f27ef8d4f 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -242,6 +242,46 @@ wownero-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + zano + CFBundleURLSchemes + + zano + + + + CFBundleTypeRole + Viewer + CFBundleURLName + zano-wallet + CFBundleURLSchemes + + zano-wallet + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred + CFBundleURLSchemes + + decred + + + + CFBundleTypeRole + Viewer + CFBundleURLName + decred-wallet + CFBundleURLSchemes + + decred-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 888be1768..911e939d1 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -117,7 +117,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zec: pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; case CryptoCurrency.dcr: - pattern = 'D[ksecS]([0-9a-zA-Z])+'; + pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: pattern = '[Rr]([1-9a-km-zA-HJ-NP-Z]){33}'; case CryptoCurrency.near: diff --git a/lib/core/node_address_validator.dart b/lib/core/node_address_validator.dart index 0c8a0c37c..68478d501 100644 --- a/lib/core/node_address_validator.dart +++ b/lib/core/node_address_validator.dart @@ -17,3 +17,14 @@ class NodePathValidator extends TextValidator { isAutovalidate: true, ); } + +// NodeAddressValidatorDecredBlankException allows decred to send a blank ip +// address which effectively clears the current set persistant peer. +class NodeAddressValidatorDecredBlankException extends TextValidator { + NodeAddressValidatorDecredBlankException() + : super( + errorMessage: S.current.error_text_node_address, + isAutovalidate: true, + pattern: + '^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\$|^[0-9a-zA-Z.\-]+\$'); +} diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index d963fb523..5356ed875 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/utils/language_list.dart'; import 'package:cw_core/wallet_type.dart'; @@ -50,6 +51,8 @@ class SeedValidator extends Validator { return wownero!.getWowneroWordList(language); case WalletType.zano: return zano!.getWordList(language); + case WalletType.decred: + return decred!.getDecredWordList(); case WalletType.none: return []; } diff --git a/lib/core/sync_status_title.dart b/lib/core/sync_status_title.dart index 46dd62c3a..cf4cb8e88 100644 --- a/lib/core/sync_status_title.dart +++ b/lib/core/sync_status_title.dart @@ -63,5 +63,9 @@ String syncStatusTitle(SyncStatus syncStatus) { return S.current.sync_status_attempting_scan; } + if (syncStatus is ProcessingSyncStatus) { + return syncStatus.message ?? S.current.processing; + } + return ''; } diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 3fb4b5b1d..b44e56a98 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -90,6 +90,7 @@ class WalletCreationService { case WalletType.nano: case WalletType.banano: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/decred/cw_decred.dart b/lib/decred/cw_decred.dart new file mode 100644 index 000000000..283895936 --- /dev/null +++ b/lib/decred/cw_decred.dart @@ -0,0 +1,114 @@ +part of 'decred.dart'; + +class CWDecred extends Decred { + CWDecred() {} + + @override + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}) => + DecredNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}) => + DecredRestoreWalletFromSeedCredentials(name: name, mnemonic: mnemonic, password: password); + + @override + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}) => + DecredRestoreWalletFromPubkeyCredentials(name: name, pubkey: pubkey, password: password); + + @override + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource) { + return DecredWalletService(walletInfoSource, unspentCoinSource); + } + + @override + List getTransactionPriorities() => DecredTransactionPriority.all; + + @override + TransactionPriority getDecredTransactionPriorityMedium() => DecredTransactionPriority.medium; + + @override + TransactionPriority getDecredTransactionPrioritySlow() => DecredTransactionPriority.slow; + + @override + TransactionPriority deserializeDecredTransactionPriority(int raw) => + DecredTransactionPriority.deserialize(raw: raw); + + @override + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority) => + DecredTransactionCredentials( + outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: priority as DecredTransactionPriority); + + List getAddressInfos(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.walletAddresses.getAddressInfos(); + } + + @override + Future updateAddress(Object wallet, String address, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.updateAddress(address, label); + } + + @override + Future generateNewAddress(Object wallet, String label) async { + final decredWallet = wallet as DecredWallet; + await decredWallet.walletAddresses.generateNewAddress(label); + } + + @override + String formatterDecredAmountToString({required int amount}) => + decredAmountToString(amount: amount); + + @override + double formatterDecredAmountToDouble({required int amount}) => + decredAmountToDouble(amount: amount); + + @override + int formatterStringDoubleToDecredAmount(String amount) => stringDoubleToDecredAmount(amount); + + @override + List getUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.unspents(); + } + + @override + void updateUnspents(Object wallet) { + final decredWallet = wallet as DecredWallet; + decredWallet.unspents(); + } + + @override + int heightByDate(DateTime date) { + final genesisBlocktime = DateTime.fromMillisecondsSinceEpoch(1454954400 * 1000); + final minutesDiff = date.difference(genesisBlocktime).inMinutes; + // Decred has five minute blocks on mainnet. + // NOTE: This is off by about a day but is currently unused by decred as we + // rescan from the wallet birthday. + return minutesDiff ~/ 5; + } + + @override + List getDecredWordList() => wordlist; + + @override + String pubkey(Object wallet) { + final decredWallet = wallet as DecredWallet; + return decredWallet.pubkey; + } +} diff --git a/lib/di.dart b/lib/di.dart index c8d664b6e..5c6d11e4b 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -69,6 +69,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/reactions/on_authentication_state_change.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/solana/solana.dart'; @@ -990,7 +991,8 @@ Future setup({ (Node? editingNode, bool? isSelected) => NodeCreateOrEditPage( nodeCreateOrEditViewModel: getIt.get(param2: false), editingNode: editingNode, - isSelected: isSelected)); + isSelected: isSelected, + type: getIt.get().wallet!.type)); getIt.registerFactoryParam( (Node? editingNode, bool? isSelected) => PowNodeCreateOrEditPage( @@ -1115,6 +1117,8 @@ Future setup({ return wownero!.createWowneroWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.zano: return zano!.createZanoWalletService(_walletInfoSource); + case WalletType.decred: + return decred!.createDecredWalletService(_walletInfoSource, _unspentCoinsInfoSource); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index e57d71174..5ed35b5a1 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -46,6 +46,7 @@ const newCakeWalletBitcoinUri = 'btc-electrum.cakewallet.com:50002'; const wowneroDefaultNodeUri = 'node3.monerodevs.org:34568'; const zanoDefaultNodeUri = 'zano.cakewallet.com:11211'; const moneroWorldNodeUri = '.moneroworld.com'; +const decredDefaultUri = "default-spv-nodes"; Future defaultSettingsMigration( {required int version, @@ -96,30 +97,67 @@ Future defaultSettingsMigration( PreferencesKey.currentBalanceDisplayModeKey, BalanceDisplayMode.availableBalance.raw); await sharedPreferences.setBool('save_recipient_address', true); await resetToDefault(nodes); - await changeMoneroCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeBitcoinCashCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + useSSL: true, + ); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 2: await replaceNodesMigration(nodes: nodes); - await replaceDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); - + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + useSSL: true, + trusted: true, + oldUri: [ + 'xmr-node-uk.cakewallet.com:18081', + 'eu-node.cakewallet.io:18081', + 'node.cakewallet.io:18081' + ], + ); break; case 3: await updateNodeTypes(nodes: nodes); - await addBitcoinElectrumServerList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); break; case 4: - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + ); break; case 5: @@ -139,7 +177,15 @@ Future defaultSettingsMigration( break; case 11: - await changeDefaultMoneroNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + oldUri: ['.cakewallet.com'], + ); break; case 12: @@ -151,28 +197,38 @@ Future defaultSettingsMigration( break; case 15: - await addLitecoinElectrumServerList(nodes: nodes); - await changeLitecoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.litecoin, + currentNodePreferenceKey: PreferencesKey.currentLitecoinElectrumSererIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 16: - await addHavenNodeList(nodes: nodes); - await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.haven); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); await checkCurrentNodes(nodes, powNodes, sharedPreferences); break; case 17: - await changeDefaultHavenNode(nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.haven, + currentNodePreferenceKey: PreferencesKey.currentHavenNodeIdKey, + ); break; case 18: - await updateWalletTypeNodesWithNewNode( - nodes: nodes, - newNodeUri: "cakexmrl7bonq7ovjka5kuwuyd3f7qnkz6z6s6dmsy3uckwra7bvggyd.onion:18081", - type: WalletType.monero, - ); + addWalletNodeList(nodes: nodes, type: WalletType.monero); break; case 19: @@ -182,47 +238,82 @@ Future defaultSettingsMigration( await migrateExchangeStatus(sharedPreferences); break; case 21: - await addEthereumNodeList(nodes: nodes); - await changeEthereumCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.ethereum); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.ethereum, + currentNodePreferenceKey: PreferencesKey.currentEthereumNodeIdKey, + ); break; case 22: - await addNanoNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.nano); await addNanoPowNodeList(nodes: powNodes); - await changeNanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); - await changeNanoCurrentPowNodeToDefault( - sharedPreferences: sharedPreferences, nodes: powNodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + ); + await _changeDefaultNode( + nodes: powNodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + currentNodePreferenceKey: PreferencesKey.currentNanoPowNodeIdKey, + newDefaultUri: nanoDefaultPowNodeUri, + ); break; case 23: - await addBitcoinCashElectrumServerList(nodes: nodes); - await changeBitcoinCurrentElectrumServerToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.bitcoinCash); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoinCash, + currentNodePreferenceKey: PreferencesKey.currentBitcoinCashNodeIdKey, + ); break; case 24: - await addPolygonNodeList(nodes: nodes); - await changePolygonCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.polygon); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.polygon, + currentNodePreferenceKey: PreferencesKey.currentPolygonNodeIdKey, + ); break; case 25: await rewriteSecureStoragePin(secureStorage: secureStorage); break; case 26: - /// commented out as it was a probable cause for some users to have white screen issues - /// maybe due to multiple access on Secure Storage at once - /// or long await time on start of the app - // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + /// commented out as it was a probable cause for some users to have white screen issues + /// maybe due to multiple access on Secure Storage at once + /// or long await time on start of the app + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + break; case 27: - await addSolanaNodeList(nodes: nodes); - await changeSolanaCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.solana); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.solana, + currentNodePreferenceKey: PreferencesKey.currentSolanaNodeIdKey, + ); break; case 28: await _updateMoneroPriority(sharedPreferences); break; case 29: - await changeDefaultBitcoinNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['.cakewallet.com'], + ); break; case 30: await disableServiceStatusFiatDisabled(sharedPreferences); @@ -234,28 +325,50 @@ Future defaultSettingsMigration( await updateBtcNanoWalletInfos(walletInfoSource); break; case 33: - await addTronNodeList(nodes: nodes); - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.tron); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.tron, + currentNodePreferenceKey: PreferencesKey.currentTronNodeIdKey, + ); break; case 34: - await _addElectRsNode(nodes, sharedPreferences); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); case 35: - await _switchElectRsNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.bitcoin, + newDefaultUri: newCakeWalletBitcoinUri, + currentNodePreferenceKey: PreferencesKey.currentBitcoinElectrumSererIdKey, + useSSL: true, + oldUri: ['electrs.cakewallet.com'], + ); break; case 36: - await addWowneroNodeList(nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.wownero); await changeWowneroCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; case 37: - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); + // removed as it would be replaced again anyway + // await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); break; case 38: await fixBtcDerivationPaths(walletInfoSource); break; case 39: _fixNodesUseSSLFlag(nodes); - await changeDefaultNanoNode(nodes, sharedPreferences); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.nano, + newDefaultUri: nanoDefaultNodeUri, + currentNodePreferenceKey: PreferencesKey.currentNanoNodeIdKey, + useSSL: true, + oldUri: ['rpc.nano.to'], + ); break; case 40: await removeMoneroWorld(sharedPreferences: sharedPreferences, nodes: nodes); @@ -266,8 +379,8 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - await _addSethNode(nodes, sharedPreferences); - await updateTronNodesWithNowNodes(sharedPreferences: sharedPreferences, nodes: nodes); + addWalletNodeList(nodes: nodes, type: WalletType.bitcoin); + addWalletNodeList(nodes: nodes, type: WalletType.tron); break; case 42: _fixNodesUseSSLFlag(nodes); @@ -312,19 +425,8 @@ Future defaultSettingsMigration( case 45: await _backupHavenSeeds(havenSeedStore); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'matic.nownodes.io', - nodes: nodes, - type: WalletType.polygon, - useSSL: true, - ); - updateWalletTypeNodesWithNewNode( - newNodeUri: 'eth.nownodes.io', - nodes: nodes, - type: WalletType.ethereum, - useSSL: true, - ); - + addWalletNodeList(nodes: nodes, type: WalletType.polygon); + addWalletNodeList(nodes: nodes, type: WalletType.ethereum); _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -349,18 +451,7 @@ Future defaultSettingsMigration( break; case 46: await _fixNodesUseSSLFlag(nodes); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'litecoin.stackwallet.com:20063', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); - await updateWalletTypeNodesWithNewNode( - newNodeUri: 'electrum-ltc.bysh.me:50002', - nodes: nodes, - type: WalletType.litecoin, - useSSL: true, - ); + await addWalletNodeList(nodes: nodes, type: WalletType.litecoin); await _changeDefaultNode( nodes: nodes, sharedPreferences: sharedPreferences, @@ -386,10 +477,14 @@ Future defaultSettingsMigration( newUri: "polygon-bor-rpc.publicnode.com", useSSL: true, ); - break; case 47: - await addZanoNodeList(nodes: nodes); - await changeZanoCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await addWalletNodeList(nodes: nodes, type: WalletType.zano); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.zano, + currentNodePreferenceKey: PreferencesKey.currentZanoNodeIdKey, + ); _changeExchangeProviderAvailability( sharedPreferences, providerName: "SimpleSwap", @@ -400,8 +495,17 @@ Future defaultSettingsMigration( providerName: "SwapTrade", enabled: false, ); - break; + break; case 48: + await addWalletNodeList(nodes: nodes, type: WalletType.decred); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.decred, + currentNodePreferenceKey: PreferencesKey.currentDecredNodeIdKey, + ); + break; + case 49: _changeExchangeProviderAvailability( sharedPreferences, providerName: "SwapTrade", @@ -414,8 +518,9 @@ Future defaultSettingsMigration( await sharedPreferences.setInt( PreferencesKey.currentDefaultSettingsMigrationVersion, version); - } catch (e) { + } catch (e, s) { printV('Migration error: ${e.toString()}'); + printV('Migration error: ${s}'); } }); @@ -455,17 +560,24 @@ Future _changeDefaultNode({ required Box nodes, required SharedPreferences sharedPreferences, required WalletType type, - required String newDefaultUri, required String currentNodePreferenceKey, - required bool useSSL, - required List - oldUri, // leave empty if you want to force replace the node regardless of the user's current node + bool useSSL = true, + bool trusted = false, + String? newDefaultUri, // ignore, if you want to use the default node uri + List? + oldUri, // ignore, if you want to force replace the node regardless of the user's current node }) async { final currentNodeId = sharedPreferences.getInt(currentNodePreferenceKey); - final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); - final shouldReplace = oldUri.any((e) => currentNode.uriRaw.contains(e)); + final bool shouldReplace; + if (currentNodeId == null) { + shouldReplace = true; + } else { + final currentNode = nodes.values.firstWhere((node) => node.key == currentNodeId); + shouldReplace = oldUri?.any((e) => currentNode.uriRaw.contains(e)) ?? true; + } if (shouldReplace) { + newDefaultUri ??= _getDefaultNodeUri(type); var newNodeId = nodes.values.firstWhereOrNull((element) => element.uriRaw == newDefaultUri)?.key; @@ -475,6 +587,7 @@ Future _changeDefaultNode({ uri: newDefaultUri, type: type, useSSL: useSSL, + trusted: trusted, ); await nodes.add(newNode); @@ -485,23 +598,38 @@ Future _changeDefaultNode({ } } -/// Generic function for adding a new Node for a Wallet Type. -Future updateWalletTypeNodesWithNewNode({ - required Box nodes, - required WalletType type, - required String newNodeUri, - bool? useSSL, -}) async { - // If it already exists in the box of nodes, no need to add it annymore. - if (nodes.values.any((node) => node.uriRaw == newNodeUri)) return; - - await nodes.add( - Node( - uri: newNodeUri, - type: type, - useSSL: useSSL, - ), - ); +String _getDefaultNodeUri(WalletType type) { + switch (type) { + case WalletType.monero: + return newCakeWalletMoneroUri; + case WalletType.bitcoin: + return newCakeWalletBitcoinUri; + case WalletType.litecoin: + return cakeWalletLitecoinElectrumUri; + case WalletType.haven: + return havenDefaultNodeUri; + case WalletType.ethereum: + return ethereumDefaultNodeUri; + case WalletType.nano: + return nanoDefaultNodeUri; + case WalletType.bitcoinCash: + return cakeWalletBitcoinCashDefaultNodeUri; + case WalletType.polygon: + return polygonDefaultNodeUri; + case WalletType.solana: + return solanaDefaultNodeUri; + case WalletType.tron: + return tronDefaultNodeUri; + case WalletType.wownero: + return wowneroDefaultNodeUri; + case WalletType.zano: + return zanoDefaultNodeUri; + case WalletType.decred: + return decredDefaultUri; + case WalletType.banano: + case WalletType.none: + return ''; + } } void _changeExchangeProviderAvailability(SharedPreferences sharedPreferences, @@ -533,7 +661,7 @@ Future _fixNodesUseSSLFlag(Box nodes) async { } Future updateNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); + final nodeList = await loadDefaultNodes(WalletType.nano); var listOfNewEndpoints = [ "app.natrium.io", "rainstorm.city", @@ -682,87 +810,16 @@ Future replaceNodesMigration({required Box nodes}) async { }); } -Future changeMoneroCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getMoneroDefaultNode(nodes: nodes); - final nodeId = node.key as int? ?? 0; // 0 - England - - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, nodeId); -} - -Node? getBitcoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); -} - Node? getBitcoinTestnetDefaultElectrumServer({required Box nodes}) { return nodes.values .firstWhereOrNull((Node node) => node.uriRaw == publicBitcoinTestnetElectrumUri) ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoin); } -Node? getLitecoinDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletLitecoinElectrumUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.litecoin); -} - -Node? getHavenDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == havenDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); -} - -Node? getEthereumDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == ethereumDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); -} - -Node? getPolygonDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == polygonDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.polygon); -} - -Node? getNanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); -} - -Node? getNanoDefaultPowNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? - nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); -} - -Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { - return nodes.values - .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); -} - -Node? getZanoDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull( - (Node node) => node.uriRaw == zanoDefaultNodeUri) - ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.zano); -} - -Node getMoneroDefaultNode({required Box nodes}) { - var nodeUri = newCakeWalletMoneroUri; - - try { - return nodes.values.firstWhere((Node node) => node.uriRaw == nodeUri); - } catch (_) { - return nodes.values.first; - } -} - -Node? getSolanaDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == solanaDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.solana); -} - -Node? getTronDefaultNode({required Box nodes}) { - return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == tronDefaultNodeUri) ?? - nodes.values.firstWhereOrNull((node) => node.type == WalletType.tron); +Node? getDefaultNode({required Box nodes, required WalletType type}) { + final defaultUri = _getDefaultNodeUri(type); + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == defaultUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == type); } Node getWowneroDefaultNode({required Box nodes}) { @@ -899,63 +956,6 @@ Future rewriteSecureStoragePin({required SecureStorage secureStorage}) asy ); } -Future changeBitcoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, - required Box nodes, - bool? isTestnet}) async { - Node? server; - if (isTestnet == true) { - server = getBitcoinTestnetDefaultElectrumServer(nodes: nodes); - } else { - server = getBitcoinDefaultElectrumServer(nodes: nodes); - } - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinElectrumSererIdKey, serverId); -} - -Future changeLitecoinCurrentElectrumServerToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getLitecoinDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentLitecoinElectrumSererIdKey, serverId); -} - -Future changeBitcoinCashCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); -} - -Future changeHavenCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getHavenDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, nodeId); -} - -Future replaceDefaultNode( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - const nodesForReplace = [ - 'xmr-node-uk.cakewallet.com:18081', - 'eu-node.cakewallet.io:18081', - 'node.cakewallet.io:18081' - ]; - final currentNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentNode = nodes.values.firstWhereOrNull((Node node) => node.key == currentNodeId); - final needToReplace = currentNode == null ? true : nodesForReplace.contains(currentNode.uriRaw); - - if (!needToReplace) { - return; - } - - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future updateNodeTypes({required Box nodes}) async { nodes.values.forEach((node) async { if (node.type == null) { @@ -965,42 +965,6 @@ Future updateNodeTypes({required Box nodes}) async { }); } -Future addBitcoinElectrumServerList({required Box nodes}) async { - final serverList = await loadBitcoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addLitecoinElectrumServerList({required Box nodes}) async { - final serverList = await loadLitecoinElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addBitcoinCashElectrumServerList({required Box nodes}) async { - final serverList = await loadBitcoinCashElectrumServerList(); - for (var node in serverList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addHavenNodeList({required Box nodes}) async { - final nodeList = await loadDefaultHavenNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addAddressesForMoneroWallets(Box walletInfoSource) async { final moneroWalletsInfo = walletInfoSource.values.where((info) => info.type == WalletType.monero); moneroWalletsInfo.forEach((info) async { @@ -1050,31 +1014,6 @@ Future changeTransactionPriorityAndFeeRateKeys(SharedPreferences sharedPre bitcoin!.getMediumTransactionPriority().serialize()); } -Future changeDefaultMoneroNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletMoneroNodeUriPattern = '.cakewallet.com'; - final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhere((node) => node.key == currentMoneroNodeId); - final needToReplaceCurrentMoneroNode = - currentMoneroNode.uri.toString().contains(cakeWalletMoneroNodeUriPattern); - - nodeSource.values.forEach((node) async { - if (node.type == WalletType.monero && - node.uri.toString().contains(cakeWalletMoneroNodeUriPattern)) { - await node.delete(); - } - }); - - final newCakeWalletNode = - Node(uri: newCakeWalletMoneroUri, type: WalletType.monero, trusted: true); - - await nodeSource.add(newCakeWalletNode); - - if (needToReplaceCurrentMoneroNode) { - await sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, newCakeWalletNode.key as int); - } -} - Future fixBtcDerivationPaths(Box walletsInfoSource) async { for (WalletInfo walletInfo in walletsInfoSource.values) { if (walletInfo.type == WalletType.bitcoin || @@ -1102,128 +1041,6 @@ Future updateBtcNanoWalletInfos(Box walletsInfoSource) async { } } -Future changeDefaultNanoNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const oldNanoNodeUriPattern = 'rpc.nano.to'; - final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); - final currentNanoNode = nodeSource.values.firstWhere((node) => node.key == currentNanoNodeId); - - final newCakeWalletNode = Node( - uri: nanoDefaultNodeUri, - type: WalletType.nano, - useSSL: true, - ); - - await nodeSource.add(newCakeWalletNode); - - if (currentNanoNode.uri.toString().contains(oldNanoNodeUriPattern)) { - await sharedPreferences.setInt( - PreferencesKey.currentNanoNodeIdKey, newCakeWalletNode.key as int); - } -} - -Future changeDefaultBitcoinNode( - Box nodeSource, SharedPreferences sharedPreferences) async { - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newCakeWalletBitcoinNode = - Node(uri: newCakeWalletBitcoinUri, type: WalletType.bitcoin, useSSL: true); - - if (!nodeSource.values.any((element) => element.uriRaw == newCakeWalletBitcoinUri)) { - await nodeSource.add(newCakeWalletBitcoinNode); - } - - if (needToReplaceCurrentBitcoinNode) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newCakeWalletBitcoinNode.key as int); - } -} - -Future _addSethNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: "fulcrum.sethforprivacy.com:50002", - useSSL: false, - ); -} - -Future _addElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - _addBitcoinNode( - nodeSource: nodeSource, - sharedPreferences: sharedPreferences, - nodeUri: cakeWalletSilentPaymentsElectrsUri, - ); -} - -Future _addBitcoinNode({ - required Box nodeSource, - required SharedPreferences sharedPreferences, - required String nodeUri, - bool replaceExisting = false, - bool useSSL = false, -}) async { - bool isNodeExists = nodeSource.values.any((element) => element.uriRaw == nodeUri); - if (isNodeExists) { - return; - } - const cakeWalletBitcoinNodeUriPattern = '.cakewallet.com'; - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains(cakeWalletBitcoinNodeUriPattern); - - final newElectRsBitcoinNode = Node(uri: nodeUri, type: WalletType.bitcoin, useSSL: useSSL); - - await nodeSource.add(newElectRsBitcoinNode); - - if (needToReplaceCurrentBitcoinNode && replaceExisting) { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, newElectRsBitcoinNode.key as int); - } -} - -Future _switchElectRsNode(Box nodeSource, SharedPreferences sharedPreferences) async { - final currentBitcoinNodeId = - sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentBitcoinNode = - nodeSource.values.firstWhere((node) => node.key == currentBitcoinNodeId); - final needToReplaceCurrentBitcoinNode = - currentBitcoinNode.uri.toString().contains('electrs.cakewallet.com'); - - if (!needToReplaceCurrentBitcoinNode) return; - - final btcElectrumNode = nodeSource.values.firstWhereOrNull( - (node) => node.uri.toString().contains('btc-electrum.cakewallet.com'), - ); - - if (btcElectrumNode == null) { - final newBtcElectrumBitcoinNode = Node( - uri: newCakeWalletBitcoinUri, - type: WalletType.bitcoin, - useSSL: false, - ); - await nodeSource.add(newBtcElectrumBitcoinNode); - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - newBtcElectrumBitcoinNode.key as int, - ); - } else { - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - btcElectrumNode.key as int, - ); - } -} - Future checkCurrentNodes( Box nodeSource, Box powNodeSource, SharedPreferences sharedPreferences) async { final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); @@ -1236,6 +1053,7 @@ Future checkCurrentNodes( final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentDecredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final currentBitcoinCashNodeId = sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final currentSolanaNodeId = sharedPreferences.getInt(PreferencesKey.currentSolanaNodeIdKey); @@ -1256,6 +1074,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentNanoNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + final currentDecredNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentDecredNodeId); final currentNanoPowNodeServer = powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); final currentBitcoinCashNodeServer = @@ -1266,7 +1086,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentTronNodeId); final currentWowneroNodeServer = nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); - final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + final currentZanoNode = + nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); @@ -1357,6 +1178,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); } + + if (currentDecredNodeServer == null) { + final node = Node(uri: decredDefaultUri, type: WalletType.decred); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( @@ -1385,15 +1212,6 @@ Future resetBitcoinElectrumServer( await oldElectrumServer?.delete(); } -Future changeDefaultHavenNode(Box nodeSource) async { - const previousHavenDefaultNodeUri = 'vault.havenprotocol.org:443'; - final havenNodes = nodeSource.values.where((node) => node.uriRaw == previousHavenDefaultNodeUri); - havenNodes.forEach((node) async { - node.uriRaw = havenDefaultNodeUri; - await node.save(); - }); -} - Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { final isExchangeDisabled = sharedPreferences.getBool(PreferencesKey.disableExchangeKey); if (isExchangeDisabled == null) { @@ -1406,65 +1224,14 @@ Future migrateExchangeStatus(SharedPreferences sharedPreferences) async { await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } -Future addEthereumNodeList({required Box nodes}) async { - final nodeList = await loadDefaultEthereumNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeEthereumCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getEthereumDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); -} - -Future addWowneroNodeList({required Box nodes}) async { - final nodeList = await loadDefaultWowneroNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future addZanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultZanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future changeWowneroCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final node = getWowneroDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; + final nodeId = node.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, nodeId); } -Future changeZanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getZanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, nodeId); -} - -Future addNanoNodeList({required Box nodes}) async { - final nodeList = await loadDefaultNanoNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - Future addNanoPowNodeList({required Box nodes}) async { final nodeList = await loadDefaultNanoPowNodes(); for (var node in nodeList) { @@ -1474,23 +1241,13 @@ Future addNanoPowNodeList({required Box nodes}) async { } } -Future changeNanoCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, nodeId); +Node? getNanoDefaultPowNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultPowNodeUri) ?? + nodes.values.firstWhereOrNull((node) => (node.type == WalletType.nano)); } -Future changeNanoCurrentPowNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getNanoDefaultPowNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, nodeId); -} - -Future addPolygonNodeList({required Box nodes}) async { - final nodeList = await loadDefaultPolygonNodes(); +Future addWalletNodeList({required Box nodes, required WalletType type}) async { + final List nodeList = await loadDefaultNodes(type); for (var node in nodeList) { if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { await nodes.add(node); @@ -1498,74 +1255,6 @@ Future addPolygonNodeList({required Box nodes}) async { } } -Future changePolygonCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getPolygonDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); -} - -Future addSolanaNodeList({required Box nodes}) async { - final nodeList = await loadDefaultSolanaNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeSolanaCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getSolanaDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentSolanaNodeIdKey, nodeId); -} - -Future addTronNodeList({required Box nodes}) async { - final nodeList = await loadDefaultTronNodes(); - for (var node in nodeList) { - if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { - await nodes.add(node); - } - } -} - -Future changeTronCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, required Box nodes}) async { - final node = getTronDefaultNode(nodes: nodes); - final nodeId = node?.key as int? ?? 0; - - await sharedPreferences.setInt(PreferencesKey.currentTronNodeIdKey, nodeId); -} - -Future replaceTronDefaultNode({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - // Get the currently active node - final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); - final currentTronNode = - nodes.values.firstWhereOrNull((Node node) => node.key == currentTronNodeId); - - //Confirm if this node is part of the default nodes from CakeWallet - final tronDefaultNodeList = [ - 'tron-rpc.publicnode.com:443', - 'api.trongrid.io', - ]; - bool needsToBeReplaced = - currentTronNode == null ? true : tronDefaultNodeList.contains(currentTronNode.uriRaw); - - // If it's a custom node, return. We don't want to switch users from their custom nodes - if (!needsToBeReplaced) { - return; - } - - // If it's not, we switch user to the new default node: NowNodes - await changeTronCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); -} - Future removeMoneroWorld( {required SharedPreferences sharedPreferences, required Box nodes}) async { const cakeWalletMoneroNodeUriPattern = '.moneroworld.com'; @@ -1582,19 +1271,13 @@ Future removeMoneroWorld( }); if (needToReplaceCurrentMoneroNode) { - await changeMoneroCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.monero, + newDefaultUri: newCakeWalletMoneroUri, + currentNodePreferenceKey: PreferencesKey.currentNodeIdKey, + trusted: true, + ); } } - -Future updateTronNodesWithNowNodes({ - required SharedPreferences sharedPreferences, - required Box nodes, -}) async { - final tronNowNodesUri = 'trx.nownodes.io'; - - if (nodes.values.any((node) => node.uriRaw == tronNowNodesUri)) return; - - await nodes.add(Node(uri: tronNowNodesUri, type: WalletType.tron)); - - await replaceTronDefaultNode(sharedPreferences: sharedPreferences, nodes: nodes); -} diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 68f599718..2e633bce8 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -59,6 +59,7 @@ class MainActions { static MainActions sendAction = MainActions._( name: (context) => S.of(context).send, image: 'assets/images/upload.png', + isEnabled: (viewModel) => viewModel.canSend, onTap: (BuildContext context, DashboardViewModel viewModel) async { Navigator.pushNamed(context, Routes.send); }, diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 5147aa614..bb489e715 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -4,111 +4,62 @@ import "package:yaml/yaml.dart"; import 'package:cw_core/node.dart'; import 'package:cw_core/wallet_type.dart'; -Future> loadDefaultNodes() async { - final nodesRaw = await rootBundle.loadString('assets/node_list.yml'); +Future> loadDefaultNodes(WalletType type) async { + String path; + switch (type) { + case WalletType.monero: + path = 'assets/node_list.yml'; + break; + case WalletType.bitcoin: + path = 'assets/bitcoin_electrum_server_list.yml'; + break; + case WalletType.litecoin: + path = 'assets/litecoin_electrum_server_list.yml'; + break; + case WalletType.haven: + path = 'assets/haven_node_list.yml'; + break; + case WalletType.ethereum: + path = 'assets/ethereum_server_list.yml'; + break; + case WalletType.nano: + path = 'assets/nano_node_list.yml'; + break; + case WalletType.bitcoinCash: + path = 'assets/bitcoin_cash_electrum_server_list.yml'; + break; + case WalletType.polygon: + path = 'assets/polygon_node_list.yml'; + break; + case WalletType.solana: + path = 'assets/solana_node_list.yml'; + break; + case WalletType.tron: + path = 'assets/tron_node_list.yml'; + break; + case WalletType.wownero: + path = 'assets/wownero_node_list.yml'; + break; + case WalletType.zano: + path = 'assets/zano_node_list.yml'; + break; + case WalletType.decred: + path = 'assets/decred_node_list.yml'; + break; + case WalletType.banano: + case WalletType.none: + path = ''; + break; + } + + final nodesRaw = await rootBundle.loadString(path); final loadedNodes = loadYaml(nodesRaw) as YamlList; final nodes = []; for (final raw in loadedNodes) { if (raw is Map) { final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.monero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadBitcoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.bitcoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadLitecoinElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/litecoin_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.litecoin; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadDefaultHavenNodes() async { - final nodesRaw = await rootBundle.loadString('assets/haven_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.haven; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultEthereumNodes() async { - final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.ethereum; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadBitcoinCashElectrumServerList() async { - final serverListRaw = await rootBundle.loadString('assets/bitcoin_cash_electrum_server_list.yml'); - final loadedServerList = loadYaml(serverListRaw) as YamlList; - final serverList = []; - - for (final raw in loadedServerList) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.bitcoinCash; - serverList.add(node); - } - } - - return serverList; -} - -Future> loadDefaultNanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/nano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - node.type = WalletType.nano; + node.type = type; nodes.add(node); } } @@ -132,103 +83,19 @@ Future> loadDefaultNanoPowNodes() async { return nodes; } -Future> loadDefaultPolygonNodes() async { - final nodesRaw = await rootBundle.loadString('assets/polygon_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.polygon; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultSolanaNodes() async { - final nodesRaw = await rootBundle.loadString('assets/solana_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.solana; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultTronNodes() async { - final nodesRaw = await rootBundle.loadString('assets/tron_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.tron; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultWowneroNodes() async { - final nodesRaw = await rootBundle.loadString('assets/wownero_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.wownero; - nodes.add(node); - } - } - - return nodes; -} - -Future> loadDefaultZanoNodes() async { - final nodesRaw = await rootBundle.loadString('assets/zano_node_list.yml'); - final loadedNodes = loadYaml(nodesRaw) as YamlList; - final nodes = []; - - for (final raw in loadedNodes) { - if (raw is Map) { - final node = Node.fromMap(Map.from(raw)); - - node.type = WalletType.zano; - nodes.add(node); - } - } - - return nodes; -} - Future resetToDefault(Box nodeSource) async { - final moneroNodes = await loadDefaultNodes(); - final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); - final litecoinElectrumServerList = await loadLitecoinElectrumServerList(); - final bitcoinCashElectrumServerList = await loadBitcoinCashElectrumServerList(); - final havenNodes = await loadDefaultHavenNodes(); - final ethereumNodes = await loadDefaultEthereumNodes(); - final nanoNodes = await loadDefaultNanoNodes(); - final polygonNodes = await loadDefaultPolygonNodes(); - final solanaNodes = await loadDefaultSolanaNodes(); - final tronNodes = await loadDefaultTronNodes(); - final zanoNodes = await loadDefaultZanoNodes(); + final moneroNodes = await loadDefaultNodes(WalletType.monero); + final bitcoinElectrumServerList = await loadDefaultNodes(WalletType.bitcoin); + final litecoinElectrumServerList = await loadDefaultNodes(WalletType.litecoin); + final bitcoinCashElectrumServerList = await loadDefaultNodes(WalletType.bitcoinCash); + final havenNodes = await loadDefaultNodes(WalletType.haven); + final ethereumNodes = await loadDefaultNodes(WalletType.ethereum); + final nanoNodes = await loadDefaultNodes(WalletType.nano); + final polygonNodes = await loadDefaultNodes(WalletType.polygon); + final solanaNodes = await loadDefaultNodes(WalletType.solana); + final tronNodes = await loadDefaultNodes(WalletType.tron); + final decredNodes = await loadDefaultNodes(WalletType.decred); + final zanoNodes = await loadDefaultNodes(WalletType.zano); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -238,7 +105,10 @@ Future resetToDefault(Box nodeSource) async { bitcoinCashElectrumServerList + nanoNodes + polygonNodes + - solanaNodes + tronNodes + zanoNodes; + solanaNodes + + tronNodes + + zanoNodes + + decredNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 7fbdb645a..9e889ff46 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -10,6 +10,7 @@ class PreferencesKey { static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; + static const currentDecredNodeIdKey = 'current_node_id_decred'; static const currentBananoNodeIdKey = 'current_node_id_banano'; static const currentBananoPowNodeIdKey = 'current_node_id_banano_pow'; static const currentFiatCurrencyKey = 'current_fiat_currency'; @@ -48,6 +49,7 @@ class PreferencesKey { static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const zanoTransactionPriority = 'current_fee_priority_zano'; static const wowneroTransactionPriority = 'current_fee_priority_wownero'; + static const decredTransactionPriority = 'current_fee_priority_decred'; static const customBitcoinFeeRate = 'custom_electrum_fee_rate'; static const silentPaymentsCardDisplay = 'silentPaymentsCardDisplay'; static const silentPaymentsAlwaysScan = 'silentPaymentsAlwaysScan'; @@ -81,6 +83,7 @@ class PreferencesKey { static const lookupsENS = 'looks_up_ens'; static const lookupsWellKnown = 'looks_up_well_known'; static const showCameraConsent = 'show_camera_consent'; + static const showDecredInfoCard = 'show_decred_info_card'; static String moneroWalletUpdateV1Key(String name) => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index bbd98d17d..f7165500f 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -35,6 +36,8 @@ List priorityForWalletType(WalletType type) { return []; case WalletType.zano: return zano!.getTransactionPriorities(); + case WalletType.decred: + return decred!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 5888970b0..2d2a98379 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -82,6 +82,7 @@ class ProvidersHelper { ProviderType.moonpay, ProviderType.kriptonim ]; + case WalletType.decred: case WalletType.none: case WalletType.haven: case WalletType.zano: @@ -113,6 +114,7 @@ class ProvidersHelper { ]; case WalletType.monero: return [ProviderType.dfx]; + case WalletType.decred: case WalletType.nano: case WalletType.banano: case WalletType.none: diff --git a/lib/main.dart b/lib/main.dart index 1eedfa6a9..2bf0f269d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -215,7 +215,7 @@ Future initializeAppConfigs() async { secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 48, + initialMigrationVersion: 49, ); } diff --git a/lib/reactions/bip39_wallet_utils.dart b/lib/reactions/bip39_wallet_utils.dart index 8b99331ce..0c58bc76f 100644 --- a/lib/reactions/bip39_wallet_utils.dart +++ b/lib/reactions/bip39_wallet_utils.dart @@ -16,6 +16,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.wownero: case WalletType.haven: case WalletType.zano: + case WalletType.decred: case WalletType.none: return false; } diff --git a/lib/reactions/check_connection.dart b/lib/reactions/check_connection.dart index d60037543..1e8fa88fa 100644 --- a/lib/reactions/check_connection.dart +++ b/lib/reactions/check_connection.dart @@ -18,6 +18,9 @@ void startCheckConnectionReaction(WalletBase wallet, SettingsStore settingsStore if (wallet.type == WalletType.bitcoin && wallet.syncStatus is SyncingSyncStatus) { return; } + if (wallet.type == WalletType.decred && wallet.syncStatus is ProcessingSyncStatus) { + return; + } try { final connectivityResult = await (Connectivity().checkConnectivity()); diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index de3dea4a2..2ddb1c6f2 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -10,7 +10,6 @@ import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index a6475571d..513c97c4e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -74,7 +74,8 @@ void startCurrentWalletChangeReaction( wallet.type == WalletType.wownero || wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash) { + wallet.type == WalletType.bitcoinCash || + wallet.type == WalletType.decred) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 072602e5f..fc618dabd 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -25,7 +25,7 @@ void startWalletSyncStatusChangeReaction( await updateHavenRate(fiatConversionStore); } } - if (status is SyncingSyncStatus) { + if (status is SyncingSyncStatus || status is ProcessingSyncStatus) { await WakelockPlus.enable(); } if (status is SyncedSyncStatus || status is FailedSyncStatus) { diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 6804467f7..fed9cb6fe 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -47,6 +47,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -181,6 +182,8 @@ class _DesktopWalletSelectionDropDownState extends State launchUrl( - Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), - mode: LaunchMode.externalApplication, - ), - child: Text( - S.of(context).learn_more, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: - Theme.of(context).extension()!.labelTextColor, - height: 1, - ), - softWrap: true, - ), - ), - SizedBox(height: 8), - Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () => _dismissMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).primaryColor, - ), - child: Text( - S.of(context).litecoin_mweb_dismiss, - style: TextStyle(color: Colors.white), - ), - ), - ), - const SizedBox(width: 8), - Expanded( - child: ElevatedButton( - onPressed: () => _enableMweb(context), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.white, - foregroundColor: Colors.black, - ), - child: Text( - S.of(context).enable, - maxLines: 1, - ), - ), - ), - ], - ), - ], - ), - icon: Container( - decoration: BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, + description: S.of(context).litecoin_mweb_description, + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).enable, + image: 'assets/images/mweb_logo.png', + leftButtonAction: () => _dismissMweb(context), + rightButtonAction: () => _enableMweb(context), + hintWidget: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => launchUrl( + Uri.parse("https://docs.cakewallet.com/cryptos/litecoin/#mweb"), + mode: LaunchMode.externalApplication, ), - child: ImageIcon( - AssetImage('assets/images/mweb_logo.png'), - color: Color.fromARGB(255, 11, 70, 129), - size: 40, + child: Text( + S.of(context).learn_more, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).extension()!.labelTextColor, + height: 1, + ), + softWrap: true, ), ), ), ), ], + if (dashboardViewModel.showDecredInfoCard) ...[ + SizedBox(height: 10), + Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 8), + child: InfoCard( + title: S.of(context).decred_info_title, + description: S.of(context).decred_info_card_details, + image: 'assets/images/dcr_icon.png', + leftButtonTitle: S.of(context).litecoin_mweb_dismiss, + rightButtonTitle: S.of(context).learn_more, + leftButtonAction: () => dashboardViewModel.dismissDecredInfoCard(), + rightButtonAction: () => launchUrl(Uri.parse("https://docs.cakewallet.com/cryptos/decred/#spv-sync")), + ), + ), + ], ], ); }), diff --git a/lib/src/screens/dashboard/pages/navigation_dock.dart b/lib/src/screens/dashboard/pages/navigation_dock.dart index 52b39cdf7..4eda169d3 100644 --- a/lib/src/screens/dashboard/pages/navigation_dock.dart +++ b/lib/src/screens/dashboard/pages/navigation_dock.dart @@ -75,8 +75,10 @@ class NavigationDock extends StatelessWidget { .labelTextColor, ), title: action.name(context), - onClick: () async => - await action.onTap(context, dashboardViewModel), + onClick: (action.isEnabled?.call(dashboardViewModel) ?? true) + ? () async => + await action.onTap(context, dashboardViewModel) + : null, textColor: action.isEnabled?.call(dashboardViewModel) ?? true ? null : Theme.of(context) diff --git a/lib/src/screens/dashboard/widgets/info_card.dart b/lib/src/screens/dashboard/widgets/info_card.dart new file mode 100644 index 000000000..1bf8a11bf --- /dev/null +++ b/lib/src/screens/dashboard/widgets/info_card.dart @@ -0,0 +1,88 @@ +import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; +import 'package:cake_wallet/src/widgets/dashboard_card_widget.dart'; +import 'package:flutter/material.dart'; + +class InfoCard extends StatelessWidget { + final String leftButtonTitle; + final String rightButtonTitle; + final String title; + final String description; + final String image; + + final Function() leftButtonAction; + final Function() rightButtonAction; + + final Widget? hintWidget; + + const InfoCard({ + Key? key, + required this.title, + required this.description, + required this.leftButtonTitle, + required this.rightButtonTitle, + required this.leftButtonAction, + required this.rightButtonAction, + required this.image, + this.hintWidget, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return DashBoardRoundedCardWidget( + marginH: 0, + marginV: 0, + customBorder: 30, + title: title, + subTitle: description, + hint: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (hintWidget != null) hintWidget!, + if (hintWidget != null) SizedBox(height: 8), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: leftButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).primaryColor, + ), + child: Text( + leftButtonTitle, + style: TextStyle(color: Colors.white), + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton( + onPressed: rightButtonAction, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + ), + child: Text( + rightButtonTitle, + maxLines: 1, + ), + ), + ), + ], + ), + ], + ), + onTap: () => {}, + icon: Container( + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: CakeImageWidget( + imageUrl: image, + height: 40, + width: 40, + ), + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 16e0f88aa..dbf55d046 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -38,7 +38,8 @@ class MenuWidgetState extends State { this.solanaIcon = Image.asset('assets/images/sol_icon.png'), this.tronIcon = Image.asset('assets/images/trx_icon.png'), this.wowneroIcon = Image.asset('assets/images/wownero_icon.png'), - this.zanoIcon = Image.asset('assets/images/zano_icon.png'); + this.zanoIcon = Image.asset('assets/images/zano_icon.png'), + this.decredIcon = Image.asset('assets/images/decred_menu.png'); final largeScreen = 731; @@ -64,6 +65,7 @@ class MenuWidgetState extends State { Image tronIcon; Image wowneroIcon; Image zanoIcon; + Image decredIcon; @override void initState() { @@ -250,6 +252,8 @@ class MenuWidgetState extends State { return wowneroIcon; case WalletType.zano: return zanoIcon; + case WalletType.decred: + return decredIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/sync_indicator.dart b/lib/src/screens/dashboard/widgets/sync_indicator.dart index 27b3d0109..860747d1e 100644 --- a/lib/src/screens/dashboard/widgets/sync_indicator.dart +++ b/lib/src/screens/dashboard/widgets/sync_indicator.dart @@ -22,8 +22,8 @@ class SyncIndicator extends StatelessWidget { builder: (_) { final syncIndicatorWidth = 237.0; final status = dashboardViewModel.status; - final statusText = status != null ? syncStatusTitle(status) : ''; - final progress = status != null ? status.progress() : 0.0; + final statusText = syncStatusTitle(status); + final progress = status.progress(); final indicatorOffset = progress * syncIndicatorWidth; final indicatorWidth = progress < 1 ? indicatorOffset > 0 ? indicatorOffset : 0.0 diff --git a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart index e5853570e..f8901918f 100644 --- a/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart +++ b/lib/src/screens/new_wallet/advanced_privacy_settings_page.dart @@ -274,7 +274,8 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo ], ); }), - if (widget.privacySettingsViewModel.type == WalletType.bitcoin) + if (widget.privacySettingsViewModel.type == WalletType.bitcoin || + widget.privacySettingsViewModel.type == WalletType.decred) Builder(builder: (_) { final val = testnetValue ?? false; return SettingsSwitcherCell( @@ -301,7 +302,9 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo widget.nodeViewModel.save(); } - if (testnetValue == true) { + if (testnetValue == true && + widget.privacySettingsViewModel.type == + WalletType.bitcoin) { // TODO: add type (mainnet/testnet) to Node class so when switching wallets the node can be switched to a matching type // Currently this is so you can create a working testnet wallet but you need to keep switching back the node if you use multiple wallets at once widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress; diff --git a/lib/src/screens/nodes/node_create_or_edit_page.dart b/lib/src/screens/nodes/node_create_or_edit_page.dart index 53c34f302..fc6ac07e0 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:mobx/mobx.dart'; @@ -15,7 +16,7 @@ import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; class NodeCreateOrEditPage extends BasePage { - NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected}) + NodeCreateOrEditPage({required this.nodeCreateOrEditViewModel,this.editingNode, this.isSelected, this.type}) : _formKey = GlobalKey(), _addressController = TextEditingController(), _pathController = TextEditingController(), @@ -86,6 +87,7 @@ class NodeCreateOrEditPage extends BasePage { final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; + final WalletType? type; @override Widget body(BuildContext context) { @@ -130,6 +132,7 @@ class NodeCreateOrEditPage extends BasePage { formKey: _formKey, nodeViewModel: nodeCreateOrEditViewModel, editingNode: editingNode, + type: type, ), bottomSectionPadding: EdgeInsets.only(bottom: 24), bottomSection: Observer( diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 22a38f423..eeda073af 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; import 'package:cake_wallet/view_model/node_list/node_create_or_edit_view_model.dart'; import 'package:cw_core/node.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -15,6 +16,7 @@ class NodeForm extends StatelessWidget { required this.nodeViewModel, required this.formKey, this.editingNode, + this.type, }) : _addressController = TextEditingController(text: editingNode?.uri.host.toString()), _pathController = TextEditingController(text: editingNode?.path.toString()), _portController = TextEditingController(text: editingNode?.uri.port.toString()), @@ -76,6 +78,7 @@ class NodeForm extends StatelessWidget { final NodeCreateOrEditViewModel nodeViewModel; final GlobalKey formKey; final Node? editingNode; + final WalletType? type; final TextEditingController _addressController; final TextEditingController _pathController; @@ -96,7 +99,7 @@ class NodeForm extends StatelessWidget { child: BaseTextFormField( controller: _addressController, hintText: S.of(context).node_address, - validator: NodeAddressValidator(), + validator: type == WalletType.decred ? NodeAddressValidatorDecredBlankException() : NodeAddressValidator(), ), ) ], diff --git a/lib/src/screens/rescan/rescan_page.dart b/lib/src/screens/rescan/rescan_page.dart index 2c1c213c1..63f13c5df 100644 --- a/lib/src/screens/rescan/rescan_page.dart +++ b/lib/src/screens/rescan/rescan_page.dart @@ -9,41 +9,47 @@ import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/widgets/blockchain_height_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cw_core/wallet_type.dart'; class RescanPage extends BasePage { RescanPage(this._rescanViewModel) : _blockchainHeightWidgetKey = GlobalKey(); @override - String get title => - _rescanViewModel.isSilentPaymentsScan ? S.current.silent_payments_scanning : S.current.rescan; + String get title => _rescanViewModel.isSilentPaymentsScan + ? S.current.silent_payments_scanning + : S.current.rescan; final GlobalKey _blockchainHeightWidgetKey; final RescanViewModel _rescanViewModel; @override Widget body(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => FocusScope.of(context).unfocus(), - child: Padding( + Widget child; + if (_rescanViewModel.wallet.type != WalletType.decred) { + child = Padding( padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), - child: Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + child: + Column(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Observer( builder: (_) => BlockchainHeightWidget( key: _blockchainHeightWidgetKey, - onHeightOrDateEntered: (value) => _rescanViewModel.isButtonEnabled = value, + onHeightOrDateEntered: (value) => + _rescanViewModel.isButtonEnabled = value, isSilentPaymentsScan: _rescanViewModel.isSilentPaymentsScan, isMwebScan: _rescanViewModel.isMwebScan, doSingleScan: _rescanViewModel.doSingleScan, - hasDatePicker: !_rescanViewModel.isMwebScan,// disable date picker for mweb for now - toggleSingleScan: () => - _rescanViewModel.doSingleScan = !_rescanViewModel.doSingleScan, + hasDatePicker: !_rescanViewModel + .isMwebScan, // disable date picker for mweb for now + toggleSingleScan: () => _rescanViewModel.doSingleScan = + !_rescanViewModel.doSingleScan, walletType: _rescanViewModel.wallet.type, - bitcoinMempoolAPIEnabled: _rescanViewModel.isBitcoinMempoolAPIEnabled, + bitcoinMempoolAPIEnabled: + _rescanViewModel.isBitcoinMempoolAPIEnabled, )), Observer( builder: (_) => LoadingPrimaryButton( - isLoading: _rescanViewModel.state == RescanWalletState.rescaning, + isLoading: + _rescanViewModel.state == RescanWalletState.rescaning, text: S.of(context).rescan, onPressed: () async { if (_rescanViewModel.isSilentPaymentsScan) { @@ -51,7 +57,8 @@ class RescanPage extends BasePage { } _rescanViewModel.rescanCurrentWallet( - restoreHeight: _blockchainHeightWidgetKey.currentState!.height); + restoreHeight: + _blockchainHeightWidgetKey.currentState!.height); Navigator.of(context).pop(); }, @@ -60,7 +67,35 @@ class RescanPage extends BasePage { isDisabled: !_rescanViewModel.isButtonEnabled, )) ]), - ), + ); + } else { + child = Center( + child: Padding( + padding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Spacer(), + Observer( + builder: (_) => LoadingPrimaryButton( + isLoading: _rescanViewModel.state == + RescanWalletState.rescaning, + text: S.of(context).rescan, + onPressed: () async { + await _rescanViewModel.rescanCurrentWallet( + restoreHeight: 0); + Navigator.of(context).pop(); + }, + color: Theme.of(context).primaryColor, + textColor: Colors.white, + )) + ]), + )); + } + return GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => FocusScope.of(context).unfocus(), + child: child, ); } @@ -70,14 +105,16 @@ class RescanPage extends BasePage { Navigator.of(context).pop(); final needsToSwitch = - await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == false; + await bitcoin!.getNodeIsElectrsSPEnabled(_rescanViewModel.wallet) == + false; if (needsToSwitch) { return showPopUp( context: navigatorKey.currentState!.context, builder: (BuildContext _dialogContext) => AlertWithTwoActions( alertTitle: S.of(_dialogContext).change_current_node_title, - alertContent: S.of(_dialogContext).confirm_silent_payments_switch_node, + alertContent: + S.of(_dialogContext).confirm_silent_payments_switch_node, rightButtonText: S.of(_dialogContext).confirm, leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () async { diff --git a/lib/src/screens/restore/wallet_restore_from_keys_form.dart b/lib/src/screens/restore/wallet_restore_from_keys_form.dart index 3dc312702..b95fcb4c6 100644 --- a/lib/src/screens/restore/wallet_restore_from_keys_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_keys_form.dart @@ -15,6 +15,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { WalletRestoreFromKeysForm({ required this.walletRestoreViewModel, required this.onPrivateKeyChange, + required this.onViewKeyEntered, required this.displayPrivateKeyField, required this.onHeightOrDateEntered, required this.displayWalletPassword, @@ -27,6 +28,7 @@ class WalletRestoreFromKeysForm extends StatefulWidget { final Function(bool) onHeightOrDateEntered; final WalletRestoreViewModel walletRestoreViewModel; final void Function(String)? onPrivateKeyChange; + final void Function(bool)? onViewKeyEntered; final bool displayPrivateKeyField; final bool displayWalletPassword; final RestoredWallet? restoredWallet; @@ -97,6 +99,10 @@ class WalletRestoreFromKeysFormState extends State { blockchainHeightKey.currentState?.restoreHeightController.text = widget.restoredWallet!.height.toString(); } }); + + viewKeyController.addListener(() { + widget.onViewKeyEntered?.call(viewKeyController.text.isNotEmpty); + }); } @override @@ -187,6 +193,19 @@ class WalletRestoreFromKeysFormState extends State { } Widget _restoreFromKeysFormFields() { + // Decred can only restore a view only wallet with an account pubkey. Other + // fields are not used. + if (widget.walletRestoreViewModel.type == WalletType.decred) { + return Column( + children: [ + BaseTextFormField( + controller: viewKeyController, + hintText: S.of(context).view_key_public, + maxLines: null, + )], + ); + } + if (widget.displayPrivateKeyField) { // the term "private key" isn't actually what we're accepting here, and it's confusing to // users of the nano community, what this form actually accepts (when importing for nano) is a nano seed in it's hex form, referred to in code as a "seed key" diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index 31f02617d..ce8595ba4 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -168,14 +168,16 @@ class WalletRestorePage extends BasePage { credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; } else { - credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; - credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; - credentials['spendKey'] = - walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; - credentials['height'] = - walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; credentials['name'] = walletRestoreFromKeysFormKey.currentState!.nameTextEditingController.text; + credentials['viewKey'] = walletRestoreFromKeysFormKey.currentState!.viewKeyController.text; + if (walletRestoreViewModel.type != WalletType.decred) { + credentials['address'] = walletRestoreFromKeysFormKey.currentState!.addressController.text; + credentials['spendKey'] = + walletRestoreFromKeysFormKey.currentState!.spendKeyController.text; + credentials['height'] = + walletRestoreFromKeysFormKey.currentState!.blockchainHeightKey.currentState!.height; + } } } @@ -467,6 +469,11 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> widget.walletRestoreViewModel.isButtonEnabled = _isValidSeedKey(); } }, + onViewKeyEntered: (bool entered) { + if (walletRestoreViewModel.type == WalletType.decred) { + walletRestoreViewModel.isButtonEnabled = entered; + } + }, onPasswordChange: (String password) => widget.walletRestoreViewModel.walletPassword = password, onRepeatedPasswordChange: (String repeatedPassword) => @@ -538,13 +545,19 @@ class _WalletRestorePageBodyState extends State<_WalletRestorePageBody> // bip39: final validBip39SeedLengths = [12, 18, 24]; - final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven]; + final nonBip39WalletTypes = [WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred]; // if it's a bip39 wallet and the length is not valid return false if (!nonBip39WalletTypes.contains(walletRestoreViewModel.type) && !(validBip39SeedLengths.contains(seedWords.length))) { return false; } + if ((walletRestoreViewModel.type == WalletType.decred) && + seedWords.length != + WalletRestoreViewModelBase.decredSeedMnemonicLength) { + return false; + } + final words = walletRestoreFromSeedFormKey.currentState!.seedWidgetStateKey.currentState!.words.toSet(); return seedWords.toSet().difference(words).toSet().isEmpty; diff --git a/lib/src/screens/wallet_keys/wallet_keys_page.dart b/lib/src/screens/wallet_keys/wallet_keys_page.dart index be1972106..29a1bfb6f 100644 --- a/lib/src/screens/wallet_keys/wallet_keys_page.dart +++ b/lib/src/screens/wallet_keys/wallet_keys_page.dart @@ -72,6 +72,10 @@ class _WalletKeysPageBodyState extends State late bool showLegacySeedTab; late bool isLegacySeedOnly; + bool get _hasSeeds => + widget.walletKeysViewModel.legacySeedSplit.length > 10 || + widget.walletKeysViewModel.seedSplit.length > 10; + @override void initState() { super.initState(); @@ -160,11 +164,10 @@ class _WalletKeysPageBodyState extends State Widget _buildSeedTab(BuildContext context, bool isLegacySeed) { return Column( children: [ - if (isLegacySeedOnly || isLegacySeed) - ...[ - _buildHeightBox(), - const SizedBox(height: 20), - ], + if (isLegacySeedOnly || isLegacySeed) ...[ + _buildHeightBox(), + const SizedBox(height: 20), + ], (_buildPassphraseBox() ?? Container()), if (widget.walletKeysViewModel.passphrase.isNotEmpty) const SizedBox(height: 20), Expanded( @@ -175,13 +178,14 @@ class _WalletKeysPageBodyState extends State ), ), const SizedBox(height: 10), - _buildBottomActionPanel( - titleForClipboard: S.of(context).wallet_seed.toLowerCase(), - dataToCopy: isLegacySeed - ? widget.walletKeysViewModel.legacySeed - : widget.walletKeysViewModel.seed, - onShowQR: () async => _showQR(context), - ), + if (_hasSeeds) + _buildBottomActionPanel( + titleForClipboard: S.of(context).wallet_seed.toLowerCase(), + dataToCopy: isLegacySeed + ? widget.walletKeysViewModel.legacySeed + : widget.walletKeysViewModel.seed, + onShowQR: () async => _showQR(context), + ), ], ); } @@ -326,7 +330,7 @@ class _WalletKeysPageBodyState extends State ), ); } - + Widget _buildBottomActionPanel({ required String titleForClipboard, required String dataToCopy, diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index 569dce958..62f79bdc4 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -122,6 +122,7 @@ class WalletListBodyState extends State { final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); + final decredIcon = Image.asset('assets/images/decred_icon.png', height: 24, width: 24); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); final havenIcon = Image.asset('assets/images/haven_logo.png', height: 24, width: 24); final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); @@ -136,6 +137,8 @@ class WalletListBodyState extends State { final double tileHeight = 60; Flushbar? _progressBar; + bool _loadingWallet = false; + @override Widget build(BuildContext context) { final newWalletImage = Image.asset('assets/images/new_wallet.png', @@ -480,6 +483,10 @@ class WalletListBodyState extends State { } Future _loadWallet(WalletListItem wallet) async { + if (_loadingWallet) return; + + _loadingWallet = true; + if (SettingsStoreBase.walletPasswordDirectInput) { Navigator.of(context).pushNamed(Routes.walletUnlockLoadable, arguments: WalletUnlockArguments( @@ -492,13 +499,17 @@ class WalletListBodyState extends State { }, walletName: wallet.name, walletType: wallet.type)); + _loadingWallet = false; return; } await widget.authService.authenticateAction( context, onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) return; + if (!isAuthenticatedSuccessfully) { + _loadingWallet = false; + return; + } try { final requireHardwareWalletConnection = widget.walletListViewModel @@ -555,6 +566,8 @@ class WalletListBodyState extends State { .of(context) .wallet_list_failed_to_load(wallet.name, e.toString())); } + } finally { + _loadingWallet = false; } }, conditionToDetermineIfToUse2FA: diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index 650ee684d..605ae4484 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -9,6 +9,9 @@ import 'package:intl/intl.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/decred/decred.dart'; +import 'package:cw_core/wallet_type.dart'; + class BlockchainHeightWidget extends StatefulWidget { BlockchainHeightWidget({ @@ -183,7 +186,9 @@ class BlockchainHeightState extends State { bitcoinMempoolAPIEnabled: await widget.bitcoinMempoolAPIEnabled, ); } else { - if (widget.walletType == WalletType.monero) { + if (widget.walletType == WalletType.decred) { + height = decred!.heightByDate(date); + } else if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); } else { assert(widget.walletType == WalletType.wownero, diff --git a/lib/src/widgets/seed_widget.dart b/lib/src/widgets/seed_widget.dart index 7747c5fb6..9b3726afb 100644 --- a/lib/src/widgets/seed_widget.dart +++ b/lib/src/widgets/seed_widget.dart @@ -65,6 +65,9 @@ class SeedWidgetState extends State { }); widget.onSeedChange?.call(text); }); + Future.delayed(Duration.zero, () { + widget.onSeedChange?.call(text); + }); } void changeSeedLanguage(String language) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index ef9ab94a4..d43550806 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/core/secure_storage.dart'; import 'package:cake_wallet/di.dart'; @@ -139,6 +140,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority, TransactionPriority? initialZanoTransactionPriority, + TransactionPriority? initialDecredTransactionPriority, Country? initialCakePayCountry}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -225,6 +227,9 @@ abstract class SettingsStoreBase with Store { if (initialZanoTransactionPriority != null) { priority[WalletType.zano] = initialZanoTransactionPriority; } + if (initialDecredTransactionPriority != null) { + priority[WalletType.decred] = initialDecredTransactionPriority; + } if (initialCakePayCountry != null) { selectedCakePayCountry = initialCakePayCountry; @@ -280,6 +285,9 @@ abstract class SettingsStoreBase with Store { case WalletType.zano: key = PreferencesKey.zanoTransactionPriority; break; + case WalletType.decred: + key = PreferencesKey.decredTransactionPriority; + break; default: key = null; } @@ -898,6 +906,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? bitcoinCashTransactionPriority; TransactionPriority? wowneroTransactionPriority; TransactionPriority? zanoTransactionPriority; + TransactionPriority? decredTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -927,6 +936,10 @@ abstract class SettingsStoreBase with Store { zanoTransactionPriority = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + decredTransactionPriority = decred?.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); @@ -935,6 +948,7 @@ abstract class SettingsStoreBase with Store { ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); wowneroTransactionPriority ??= wownero?.getDefaultTransactionPriority(); + decredTransactionPriority ??= decred?.getDecredTransactionPriorityMedium(); polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); zanoTransactionPriority ??= zano?.getDefaultTransactionPriority(); @@ -1038,7 +1052,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); - + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1047,6 +1061,7 @@ abstract class SettingsStoreBase with Store { final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); + final decredNode = nodeSource.get(decredNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); @@ -1137,6 +1152,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + final savedSyncMode = SyncMode.all.firstWhere((element) { return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 0); }); @@ -1304,6 +1323,7 @@ abstract class SettingsStoreBase with Store { initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, + initialDecredTransactionPriority: decredTransactionPriority, initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, @@ -1378,6 +1398,11 @@ abstract class SettingsStoreBase with Store { priority[WalletType.zano] = zano!.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.zanoTransactionPriority)!); } + if (decred != null && + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority) != null) { + priority[WalletType.decred] = decred!.deserializeDecredTransactionPriority( + sharedPreferences.getInt(PreferencesKey.decredTransactionPriority)!); + } final generateSubaddresses = sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey); @@ -1489,6 +1514,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1496,11 +1522,12 @@ abstract class SettingsStoreBase with Store { final ethereumNode = nodeSource.get(ethereumNodeId); final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); - final nanoNode = nodeSource.get(nanoNodeId); + final nanoNode = nodeSource.get(nanoNodeId); final solanaNode = nodeSource.get(solanaNodeId); final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); final zanoNode = nodeSource.get(zanoNodeId); + final decredNode = nodeSource.get(decredNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -1551,6 +1578,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (decredNode != null) { + nodes[WalletType.decred] = decredNode; + } + // MIGRATED: useTOTP2FA = await SecureKey.getBool( @@ -1687,6 +1718,9 @@ abstract class SettingsStoreBase with Store { case WalletType.wownero: await _sharedPreferences.setInt(PreferencesKey.currentWowneroNodeIdKey, node.key as int); break; + case WalletType.decred: + await _sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); + break; case WalletType.zano: await _sharedPreferences.setInt(PreferencesKey.currentZanoNodeIdKey, node.key as int); default: diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index 3959afe01..803744590 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -55,6 +55,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.none: case WalletType.haven: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/view_model/contact_list/contact_list_view_model.dart b/lib/view_model/contact_list/contact_list_view_model.dart index eb3fb837e..730d0735c 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -9,7 +9,6 @@ import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/mobx.dart'; -import 'package:collection/collection.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; @@ -43,7 +42,8 @@ abstract class ContactListViewModelBase with Store { } } } else if (info.addresses?.isNotEmpty == true && info.addresses!.length > 1) { - if ([WalletType.monero, WalletType.wownero, WalletType.haven].contains(info.type)) { + if ([WalletType.monero, WalletType.wownero, WalletType.haven, WalletType.decred] + .contains(info.type)) { final address = info.address; final name = _createName(info.name, "", key: 0); walletContacts.add(WalletContact( @@ -129,11 +129,9 @@ abstract class ContactListViewModelBase with Store { (element.type == CryptoCurrency.btc || element.type == CryptoCurrency.ltc)) return false; return element.type == _currency || - (element.type.tag != null && - _currency?.tag != null && - element.type.tag == _currency?.tag) || - _currency?.toString() == element.type.tag || - _currency?.tag == element.type.toString(); + (element.type.tag != null && _currency.tag != null && element.type.tag == _currency.tag) || + _currency.toString() == element.type.tag || + _currency.tag == element.type.toString(); } void dispose() => _subscription?.cancel(); diff --git a/lib/view_model/contact_list/contact_view_model.dart b/lib/view_model/contact_list/contact_view_model.dart index 93abfb11c..efa1f09f4 100644 --- a/lib/view_model/contact_list/contact_view_model.dart +++ b/lib/view_model/contact_list/contact_view_model.dart @@ -62,12 +62,12 @@ abstract class ContactViewModelBase with Store { return; } - if (_contact != null && _contact!.original.isInBox) { - _contact?.name = name; - _contact?.address = address; - _contact?.type = currency!; - _contact?.lastChange = now; - await _contact?.save(); + if (_contact != null && _contact.original.isInBox) { + _contact.name = name; + _contact.address = address; + _contact.type = currency!; + _contact.lastChange = now; + await _contact.save(); } else { await _contacts .add(Contact(name: name, address: address, type: currency!, lastChange: now)); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 730f07a93..ef5676138 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -306,6 +306,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.monero: case WalletType.wownero: case WalletType.zano: + case WalletType.decred: return true; default: return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index ee9c57b65..49c0dbc59 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -23,7 +23,6 @@ import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/dashboard/transaction_filter_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cake_wallet/utils/mobx.dart'; import 'package:cake_wallet/view_model/dashboard/action_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; @@ -169,6 +168,9 @@ abstract class DashboardViewModelBase with Store { type = appStore.wallet!.type, transactions = ObservableList(), wallet = appStore.wallet! { + showDecredInfoCard = wallet.type == WalletType.decred && + (sharedPreferences.getBool(PreferencesKey.showDecredInfoCard) ?? true); + name = wallet.name; type = wallet.type; isShowFirstYatIntroduction = false; @@ -261,6 +263,7 @@ abstract class DashboardViewModelBase with Store { reaction((_) => appStore.wallet, (wallet) { _onWalletChange(wallet); _checkMweb(); + showDecredInfoCard = wallet?.type == WalletType.decred; }); _transactionDisposer?.reaction.dispose(); @@ -350,6 +353,10 @@ abstract class DashboardViewModelBase with Store { statusText = S.current.please_try_to_connect_to_another_node; } + if (status is ProcessingSyncStatus) { + statusText = (status as ProcessingSyncStatus).message ?? S.current.processing; + } + return statusText; } @@ -399,13 +406,7 @@ abstract class DashboardViewModelBase with Store { bool get isTestnet => wallet.type == WalletType.bitcoin && bitcoin!.isTestnet(wallet); @computed - bool get hasRescan => [ - WalletType.bitcoin, - WalletType.monero, - WalletType.litecoin, - WalletType.wownero, - WalletType.haven - ].contains(wallet.type); + bool get hasRescan => wallet.hasRescan; @computed bool get isMoneroViewOnly { @@ -490,6 +491,9 @@ abstract class DashboardViewModelBase with Store { @observable bool mwebEnabled = false; + @observable + late bool showDecredInfoCard; + @computed bool get hasEnabledMwebBefore => settingsStore.hasEnabledMwebBefore; @@ -543,6 +547,12 @@ abstract class DashboardViewModelBase with Store { bitcoin!.setMwebEnabled(wallet, false); } + @action + void dismissDecredInfoCard() { + showDecredInfoCard = false; + sharedPreferences.setBool(PreferencesKey.showDecredInfoCard, false); + } + BalanceViewModel balanceViewModel; AppStore appStore; @@ -573,6 +583,9 @@ abstract class DashboardViewModelBase with Store { @computed bool get isEnabledSwapAction => settingsStore.exchangeStatus != ExchangeApiMode.disabled; + @computed + bool get canSend => wallet.canSend(); + @observable bool hasSwapAction; @@ -596,21 +609,28 @@ abstract class DashboardViewModelBase with Store { @computed bool get hasSignMessages { - if (wallet.isHardwareWallet) return false; - - return [ - WalletType.monero, - WalletType.litecoin, - WalletType.bitcoin, - WalletType.bitcoinCash, - WalletType.ethereum, - WalletType.polygon, - WalletType.solana, - WalletType.nano, - WalletType.banano, - WalletType.tron, - WalletType.wownero - ].contains(wallet.type); + if (wallet.isHardwareWallet) { + return false; + } + switch (wallet.type) { + case WalletType.monero: + case WalletType.litecoin: + case WalletType.bitcoin: + case WalletType.bitcoinCash: + case WalletType.ethereum: + case WalletType.polygon: + case WalletType.solana: + case WalletType.nano: + case WalletType.banano: + case WalletType.tron: + case WalletType.wownero: + case WalletType.decred: + return true; + case WalletType.zano: + case WalletType.haven: + case WalletType.none: + return false; + } } bool get showRepWarning { diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index e3e02a045..4a85ec89a 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -214,6 +214,7 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.nano: case WalletType.wownero: case WalletType.bitcoinCash: + case WalletType.decred: return false; } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 744e4c58d..f15d7dad6 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -14,7 +14,9 @@ abstract class ReceiveOptionViewModelBase with Store { (_wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin ? bitcoin!.getSelectedAddressType(_wallet) - : ReceivePageOption.mainnet), + : (_wallet.type == WalletType.decred && _wallet.isTestnet) + ? ReceivePageOption.testnet + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; switch (walletType) { @@ -33,6 +35,17 @@ abstract class ReceiveOptionViewModelBase with Store { case WalletType.haven: _options = [ReceivePageOption.mainnet]; break; + case WalletType.decred: + if (_wallet.isTestnet) { + _options = [ + ReceivePageOption.testnet, + ...ReceivePageOptions.where( + (element) => element != ReceivePageOption.mainnet) + ]; + } else { + _options = ReceivePageOptions; + } + break; default: _options = ReceivePageOptions; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index 864448293..83953f9f1 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -82,7 +83,9 @@ class TransactionListItem extends ActionListItem with Keyable { if (transaction.confirmations <= 0) { str = S.current.pending; } - if ((isPegOut || fromPegOut) && transaction.confirmations >= 0 && transaction.confirmations < 6) { + if ((isPegOut || fromPegOut) && + transaction.confirmations >= 0 && + transaction.confirmations < 6) { str = " (${transaction.confirmations}/6)"; } if (isPegIn) { @@ -224,7 +227,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: zano!.formatterIntAmountToDouble(amount: transaction.amount, currency: asset, forFee: false), price: price); break; - default: + case WalletType.decred: + amount = calculateFiatAmountRaw( + cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), + price: price); + break; + case WalletType.none: + case WalletType.banano: break; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 6ea9c811f..8b7349e9a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -23,6 +23,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; @@ -167,7 +169,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with wallet.type == WalletType.bitcoinCash; bool get hideAddressAfterExchange => - wallet.type == WalletType.monero || wallet.type == WalletType.wownero; + wallet.type == WalletType.monero || + wallet.type == WalletType.wownero; bool _useTorOnly; final Box trades; @@ -316,8 +319,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with bool get isMoneroWallet => wallet.type == WalletType.monero; - - List receiveCurrencies; List depositCurrencies; @@ -778,6 +779,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.zano; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.decred: + depositCurrency = CryptoCurrency.dcr; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index 71f996aff..8fbe174e3 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -63,13 +63,13 @@ abstract class NodeCreateOrEditViewModelBase with Store { String socksProxyAddress; @computed - bool get isReady => address.isNotEmpty && port.isNotEmpty; + bool get isReady => + (address.isNotEmpty && port.isNotEmpty) || + _walletType == WalletType.decred; // Allow an empty address. bool get hasAuthCredentials => _walletType == WalletType.monero || _walletType == WalletType.wownero || _walletType == WalletType.haven; - bool get hasTestnetSupport => _walletType == WalletType.bitcoin; - bool get hasPathSupport { switch (_walletType) { case WalletType.ethereum: @@ -87,6 +87,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.bitcoin: case WalletType.zano: + case WalletType.decred: return false; } } diff --git a/lib/view_model/node_list/node_list_view_model.dart b/lib/view_model/node_list/node_list_view_model.dart index 71e77eb12..9df5f2980 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -49,50 +49,10 @@ abstract class NodeListViewModelBase with Store { await resetToDefault(_nodeSource); Node node; - - switch (_appStore.wallet!.type) { - case WalletType.bitcoin: - if (_appStore.wallet!.isTestnet) { - node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; - } else { - node = getBitcoinDefaultElectrumServer(nodes: _nodeSource)!; - } - break; - case WalletType.monero: - node = getMoneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.litecoin: - node = getLitecoinDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.haven: - node = getHavenDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.ethereum: - node = getEthereumDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.bitcoinCash: - node = getBitcoinCashDefaultElectrumServer(nodes: _nodeSource)!; - break; - case WalletType.nano: - node = getNanoDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.polygon: - node = getPolygonDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.solana: - node = getSolanaDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.tron: - node = getTronDefaultNode(nodes: _nodeSource)!; - break; - case WalletType.wownero: - node = getWowneroDefaultNode(nodes: _nodeSource); - break; - case WalletType.zano: - node = getZanoDefaultNode(nodes: _nodeSource)!; - break; - default: - throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); + if (_appStore.wallet!.type == WalletType.bitcoin && _appStore.wallet!.isTestnet) { + node = getBitcoinTestnetDefaultElectrumServer(nodes: _nodeSource)!; + } else { + node = getDefaultNode(nodes: _nodeSource, type: _appStore.wallet!.type)!; } await setAsCurrent(node); diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 2e4e2da83..c8ff81acc 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -44,6 +44,9 @@ class WalletRestoreFromQRCode { 'zano': WalletType.zano, 'zano-wallet': WalletType.zano, 'zano_wallet': WalletType.zano, + 'decred': WalletType.decred, + 'decred-wallet': WalletType.decred, + 'decred_wallet': WalletType.decred, }; static WalletType? _extractWalletType(String code) { @@ -69,8 +72,12 @@ class WalletRestoreFromQRCode { } static String? _extractAddressFromUrl(String rawString, WalletType type) { - return AddressResolver.extractAddressByType( - raw: rawString, type: walletTypeToCryptoCurrency(type)); + try { + return AddressResolver.extractAddressByType( + raw: rawString, type: walletTypeToCryptoCurrency(type)); + } catch (_) { + return null; + } } static String? _extractSeedPhraseFromUrl(String rawString, WalletType walletType) { @@ -122,7 +129,6 @@ class WalletRestoreFromQRCode { } if (queryParameters['address'] == null) { queryParameters['address'] = _extractAddressFromUrl(code, walletType); - } Map credentials = {'type': walletType, ...queryParameters, 'raw_qr': code}; diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index 088c66039..f6dd0f201 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; @@ -88,7 +89,13 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); case WalletType.polygon: return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); - default: + case WalletType.decred: + return transactionPriority == decred!.getDecredTransactionPrioritySlow(); + case WalletType.none: + case WalletType.nano: + case WalletType.banano: + case WalletType.solana: + case WalletType.tron: return false; } } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 121ffa693..fdaaeba0d 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/entities/calculate_fiat_amount_raw.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; @@ -101,6 +102,9 @@ abstract class OutputBase with Store { case WalletType.bitcoinCash: _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; + case WalletType.decred: + _amount = decred!.formatterStringDoubleToDecredAmount(_cryptoAmount); + break; case WalletType.haven: _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); break; @@ -188,6 +192,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.zano) { return zano!.formatterIntAmountToDouble(amount: fee, currency: cryptoCurrencyHandler(), forFee: true); } + + if (_wallet.type == WalletType.decred) { + return decred!.formatterDecredAmountToDouble(amount: fee); + } } catch (e) { printV(e.toString()); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 1cd6b8966..3bf85ef2f 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/exchange/provider/exchange_provider.dart'; import 'package:cake_wallet/exchange/provider/thorchain_exchange.provider.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; @@ -251,6 +252,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type == WalletType.litecoin || wallet.type == WalletType.monero || wallet.type == WalletType.wownero || + wallet.type == WalletType.decred || wallet.type == WalletType.bitcoinCash; @computed @@ -539,6 +541,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.zano: return zano!.createZanoTransactionCredentials( outputs: outputs, priority: priority!, currency: selectedCryptoCurrency); + case WalletType.decred: + this.coinTypeToSpendFrom = UnspentCoinType.any; + return decred!.createDecredTransactionCredentials(outputs, priority!); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -681,55 +686,51 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor } } - if (walletType == WalletType.bitcoin || - walletType == WalletType.litecoin || - walletType == WalletType.bitcoinCash) { - if (error is TransactionWrongBalanceException) { - if (error.amount != null) - return S.current - .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); + if (error is TransactionWrongBalanceException) { + if (error.amount != null) + return S.current + .tx_wrong_balance_with_amount_exception(currency.toString(), error.amount.toString()); - return S.current.tx_wrong_balance_exception(currency.toString()); - } - if (error is TransactionNoInputsException) { - return S.current.tx_not_enough_inputs_exception; - } - if (error is TransactionNoFeeException) { - return S.current.tx_zero_fee_exception; - } - if (error is TransactionNoDustException) { - return S.current.tx_no_dust_exception; - } - if (error is TransactionCommitFailed) { - if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { - return S.current.tx_commit_failed_no_peers; - } - return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; - } - if (error is TransactionCommitFailedDustChange) { - return S.current.tx_rejected_dust_change; - } - if (error is TransactionCommitFailedDustOutput) { - return S.current.tx_rejected_dust_output; - } - if (error is TransactionCommitFailedDustOutputSendAll) { - return S.current.tx_rejected_dust_output_send_all; - } - if (error is TransactionCommitFailedVoutNegative) { - return S.current.tx_rejected_vout_negative; - } - if (error is TransactionCommitFailedBIP68Final) { - return S.current.tx_rejected_bip68_final; - } - if (error is TransactionCommitFailedLessThanMin) { - return S.current.fee_less_than_min; - } - if (error is TransactionNoDustOnChangeException) { - return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); - } - if (error is TransactionInputNotSupported) { - return S.current.tx_invalid_input; + return S.current.tx_wrong_balance_exception(currency.toString()); + } + if (error is TransactionNoInputsException) { + return S.current.tx_not_enough_inputs_exception; + } + if (error is TransactionNoFeeException) { + return S.current.tx_zero_fee_exception; + } + if (error is TransactionNoDustException) { + return S.current.tx_no_dust_exception; + } + if (error is TransactionCommitFailed) { + if (error.errorMessage != null && error.errorMessage!.contains("no peers replied")) { + return S.current.tx_commit_failed_no_peers; } + return "${S.current.tx_commit_failed}${error.errorMessage != null ? "\n\n${error.errorMessage}" : ""}"; + } + if (error is TransactionCommitFailedDustChange) { + return S.current.tx_rejected_dust_change; + } + if (error is TransactionCommitFailedDustOutput) { + return S.current.tx_rejected_dust_output; + } + if (error is TransactionCommitFailedDustOutputSendAll) { + return S.current.tx_rejected_dust_output_send_all; + } + if (error is TransactionCommitFailedVoutNegative) { + return S.current.tx_rejected_vout_negative; + } + if (error is TransactionCommitFailedBIP68Final) { + return S.current.tx_rejected_bip68_final; + } + if (error is TransactionCommitFailedLessThanMin) { + return S.current.fee_less_than_min; + } + if (error is TransactionNoDustOnChangeException) { + return S.current.tx_commit_exception_no_dust_on_change(error.min, error.max); + } + if (error is TransactionInputNotSupported) { + return S.current.tx_invalid_input; } return errorMessage; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index e2c977590..62b656dca 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -44,7 +44,8 @@ abstract class PrivacySettingsViewModelBase with Store { _wallet.type == WalletType.wownero || _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin || - _wallet.type == WalletType.bitcoinCash; + _wallet.type == WalletType.bitcoinCash || + _wallet.type == WalletType.decred; bool get isMoneroWallet => _wallet.type == WalletType.monero; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 2dc6478f9..067ca73f9 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -48,6 +48,7 @@ abstract class TransactionDetailsViewModelBase with Store { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; + // TODO: can be cleaned further switch (wallet.type) { case WalletType.monero: _addMoneroListItems(tx, dateFormat); @@ -84,8 +85,12 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.zano: _addZanoListItems(tx, dateFormat); break; - default: + case WalletType.decred: + _addDecredListItems(tx, dateFormat); break; + case WalletType.none: + case WalletType.banano: + break; } final descriptionKey = '${transactionInfo.txHash}_${wallet.walletAddresses.primaryAddress}'; @@ -186,6 +191,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://explore.wownero.com/tx/${txId}'; case WalletType.zano: return 'https://explorer.zano.org/transaction/${txId}'; + case WalletType.decred: + return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; case WalletType.none: return ''; } @@ -218,6 +225,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'Wownero.com'; case WalletType.zano: return S.current.view_transaction_on + 'explorer.zano.org'; + case WalletType.decred: + return S.current.view_transaction_on + 'dcrdata.decred.org'; case WalletType.none: return ''; } @@ -672,6 +681,51 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addDecredListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (showRecipientAddress && tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + if (tx.from != null) + StandartListItem( + title: S.current.transaction_details_source_address, + value: tx.from!, + key: ValueKey('standard_list_item_transaction_details_source_address_key'), + ), + ]; + + items.addAll(_items); + } + @action Future _checkForRBF(TransactionInfo tx) async { if (wallet.type == WalletType.bitcoin && diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 9a8a4a8f2..d4fadb2f1 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cw_core/unspent_coin_type.dart'; @@ -92,6 +93,8 @@ abstract class UnspentCoinsListViewModelBase with Store { return wownero!.formatterWowneroAmountToString(amount: fullBalance); if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) return bitcoin!.formatterBitcoinAmountToString(amount: fullBalance); + if (wallet.type == WalletType.decred) + return decred!.formatterDecredAmountToString(amount: fullBalance); return ''; } @@ -105,7 +108,9 @@ abstract class UnspentCoinsListViewModelBase with Store { if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { await bitcoin!.updateUnspents(wallet); } - + if (wallet.type == WalletType.decred) { + decred!.updateUnspents(wallet); + } _updateUnspentCoinsInfo(); } @@ -119,6 +124,8 @@ abstract class UnspentCoinsListViewModelBase with Store { case WalletType.litecoin: case WalletType.bitcoinCash: return bitcoin!.getUnspents(wallet, coinTypeToSpendFrom: coinTypeToSpendFrom); + case WalletType.decred: + return decred!.getUnspents(wallet); default: return List.empty(); } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index d365c8e00..3a678ff0f 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -5,6 +5,7 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/haven/haven.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_type.dart'; part 'wallet_address_edit_or_create_view_model.g.dart'; @@ -27,13 +28,12 @@ class AddressEditOrCreateStateFailure extends AddressEditOrCreateState { } abstract class WalletAddressEditOrCreateViewModelBase with Store { - WalletAddressEditOrCreateViewModelBase( - {required WalletBase wallet, WalletAddressListItem? item}) + WalletAddressEditOrCreateViewModelBase({required WalletBase wallet, WalletAddressListItem? item}) : isEdit = item != null, state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', _item = item, - _wallet = wallet; + _wallet = wallet; @observable AddressEditOrCreateState state; @@ -46,7 +46,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final WalletAddressListItem? _item; final WalletBase _wallet; - bool get isElectrum => _wallet.type == WalletType.bitcoin || + bool get isElectrum => + _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.bitcoinCash || _wallet.type == WalletType.litecoin; @@ -69,39 +70,46 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (isElectrum) await bitcoin!.generateNewAddress(wallet, label); + if (isElectrum) { + await bitcoin!.generateNewAddress(wallet, label); + await wallet.save(); + } + + if (wallet.type == WalletType.decred) { + await decred!.generateNewAddress(wallet, label); + await wallet.save(); + } if (wallet.type == WalletType.monero) { - await monero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: monero!.getCurrentAccount(wallet).id, - label: label); - final addr = await monero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await monero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: monero!.getCurrentAccount(wallet).id, label: label); + final addr = await monero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.wownero) { - await wownero - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: wownero!.getCurrentAccount(wallet).id, - label: label); - final addr = await wownero!.getSubaddressList(wallet).subaddresses.first.address; // first because the order is reversed + await wownero! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: wownero!.getCurrentAccount(wallet).id, label: label); + final addr = await wownero! + .getSubaddressList(wallet) + .subaddresses + .first + .address; // first because the order is reversed wallet.walletAddresses.manualAddresses.add(addr); await wallet.save(); } if (wallet.type == WalletType.haven) { - await haven - !.getSubaddressList(wallet) - .addSubaddress( - wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - label: label); + await haven! + .getSubaddressList(wallet) + .addSubaddress(wallet, accountIndex: haven!.getCurrentAccount(wallet).id, label: label); await wallet.save(); } } @@ -111,6 +119,12 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label); + if (wallet.type == WalletType.decred) { + await decred!.updateAddress(wallet, _item!.address, label); + await wallet.save(); + return; + } + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { @@ -125,9 +139,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { } if (wallet.type == WalletType.haven) { await haven!.getSubaddressList(wallet).setLabelSubaddress(wallet, - accountIndex: haven!.getCurrentAccount(wallet).id, - addressIndex: index, - label: label); + accountIndex: haven!.getCurrentAccount(wallet).id, addressIndex: index, label: label); await wallet.save(); } } diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index ef1c52f31..0b0873a60 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -12,6 +12,7 @@ import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -222,6 +223,22 @@ class ZanoURI extends PaymentURI { } } +class DecredURI extends PaymentURI { + DecredURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'decred:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -313,7 +330,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo case WalletType.wownero: return WowneroURI(amount: amount, address: address.address); case WalletType.zano: - return ZanoURI(amount: amount, address: address.address); + return ZanoURI(amount: amount, address: address.address); + case WalletType.decred: + return DecredURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -468,6 +487,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.decred) { + final addrInfos = decred!.getAddressInfos(wallet); + addrInfos.forEach((info) { + addressList.add(new WalletAddressListItem(isPrimary: false, address: info.address, + name: info.label)); + }); + } + for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses @@ -561,7 +588,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.haven, WalletType.bitcoinCash, WalletType.bitcoin, - WalletType.litecoin + WalletType.litecoin, + WalletType.decred ].contains(wallet.type); @computed diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index 402764c40..467b7c3a5 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -12,6 +12,7 @@ import 'package:cw_core/wallet_type.dart'; import 'package:cw_monero/monero_wallet.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:polyseed/polyseed.dart'; part 'wallet_keys_view_model.g.dart'; @@ -155,6 +156,12 @@ abstract class WalletKeysViewModelBase with Store { ), ]); break; + case WalletType.decred: + final pubkey = decred!.pubkey(_appStore.wallet!); + items.addAll([ + StandartListItem(title: S.current.view_key_public, value: pubkey), + ]); + break; case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: @@ -248,6 +255,8 @@ abstract class WalletKeysViewModelBase with Store { return 'wownero-wallet'; case WalletType.zano: return 'zano-wallet'; + case WalletType.decred: + return 'decred-wallet'; default: throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 0cd730028..e82ae8773 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -21,6 +21,7 @@ import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -83,6 +84,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return 25; case WalletType.zano: return 26; + case WalletType.decred: + return 15; } } @@ -170,6 +173,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, ); + case WalletType.decred: + return decred!.createDecredNewWalletCredentials(name: name); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index e7df1c221..3e005e9bc 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -11,6 +11,7 @@ import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/tron/tron.dart'; +import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; @@ -58,6 +59,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.decred: availableModes = [WalletRestoreMode.seed, WalletRestoreMode.keys]; break; case WalletType.bitcoin: @@ -77,6 +79,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { } static const moneroSeedMnemonicLength = 25; + static const decredSeedMnemonicLength = 15; late List availableModes; final bool hasSeedLanguageSelector; @@ -171,11 +174,18 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { ); case WalletType.zano: return zano!.createZanoRestoreWalletFromSeedCredentials( - name: name, - password: password, - height: height, + name: name, + password: password, + height: height, passphrase: passphrase??'', - mnemonic: seed); + mnemonic: seed, + ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); case WalletType.none: break; } @@ -251,6 +261,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, language: 'English', ); + case WalletType.decred: + return decred!.createDecredRestoreWalletFromPubkeyCredentials( + name: name, + password: password, + pubkey: viewKey!, + ); default: break; } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 42b9fa84c..528de8c42 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import connectivity_plus +import cw_decred import cw_mweb import device_info_plus import devicelocale @@ -24,6 +25,7 @@ import wakelock_plus func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { ConnectivityPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlugin")) + CwDecredPlugin.register(with: registry.registrar(forPlugin: "CwDecredPlugin")) CwMwebPlugin.register(with: registry.registrar(forPlugin: "CwMwebPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 689f0ea03..cc6ae6e3b 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -3,6 +3,7 @@ PODS: - FlutterMacOS - ReachabilitySwift - cw_mweb (0.0.1): + - cw_decred (0.0.1): - FlutterMacOS - device_info_plus (0.0.1): - FlutterMacOS @@ -45,6 +46,7 @@ PODS: DEPENDENCIES: - connectivity_plus (from `Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos`) - cw_mweb (from `Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos`) + - cw_decred (from `Flutter/ephemeral/.symlinks/plugins/cw_decred/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) - fast_scanner (from `Flutter/ephemeral/.symlinks/plugins/fast_scanner/macos`) @@ -72,6 +74,10 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/connectivity_plus/macos cw_mweb: :path: Flutter/ephemeral/.symlinks/plugins/cw_mweb/macos + cw_decred: + :path: Flutter/ephemeral/.symlinks/plugins/cw_decred/macos + cw_monero: + :path: Flutter/ephemeral/.symlinks/plugins/cw_monero/macos device_info_plus: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: @@ -108,6 +114,8 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus: 18d3c32514c886e046de60e9c13895109866c747 cw_mweb: 7440b12ead811dda972a9918442ea2a458e8742c + cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_decred: 0c93fbeb31bd97a6ad4ec5680960af0943bfca78 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 fast_scanner: d31bae07e2653403a69dac99fb710c1722b16a97 diff --git a/model_generator.sh b/model_generator.sh index f3950e2b1..56b891903 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,22 +1,22 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano} +for cwcoin in cw_{core,evm,monero,bitcoin,haven,nano,bitcoin_cash,solana,tron,wownero,zano,decred} do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." & else - bash -c "cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .." + cd $cwcoin; flutter pub get; dart run build_runner build --delete-conflicting-outputs; cd .. fi done for cwcoin in cw_{polygon,ethereum,mweb}; -do +do if [[ "x$1" == "xasync" ]]; then bash -c "cd $cwcoin; flutter pub get; cd .." & else - bash -c "cd $cwcoin; flutter pub get; cd .." + cd $cwcoin; flutter pub get; cd .. fi done diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 767f0b1f3..7e700e588 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -189,6 +189,7 @@ flutter: - assets/tron_node_list.yml - assets/wownero_node_list.yml - assets/zano_node_list.yml + - assets/decred_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 9f1235919..6072b046c 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -208,6 +208,8 @@ "debit_card_terms": "يخضع تخزين واستخدام رقم بطاقة الدفع الخاصة بك (وبيانات الاعتماد المقابلة لرقم بطاقة الدفع الخاصة بك) في هذه المحفظة الرقمية لشروط وأحكام اتفاقية حامل البطاقة المعمول بها مع جهة إصدار بطاقة الدفع ، كما هو معمول به من وقت لآخر.", "decimal_places_error": "عدد كبير جدًا من المنازل العشرية", "decimals_cannot_be_zero": "الرمز العشري لا يمكن أن يكون الصفر.", + "decred_info_card_details": "يستخدم Decred طريقة متزامنة لا مركزية وحفاظ على الخصوصية المعروفة باسم \"SPV\" ، والتي تستغرق وقتًا أطول من محفظة Bitcoin العادية. لمعرفة المزيد ، انقر أدناه.", + "decred_info_title": "التزامن في Decred", "default_buy_provider": "مزود شراء الافتراضي", "default_sell_provider": "ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", "delete": "حذف", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "إذا لم تستمر الشاشة بعد دقيقة واحدة ، فتحقق من بريدك الإلكتروني.", "proceed_on_device": "تابع جهازك", "proceed_on_device_description": "يرجى اتباع الإرشادات المطلوبة على محفظة الأجهزة الخاصة بك", + "processing": "يعالج", "profile": "حساب تعريفي", "provider_error": "خطأ ${provider}", "public_key": "مفتاح عمومي", diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 190dd07d8..62f1bbf18 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Съхранението и използването на данните от вашата платежна карта в този дигитален портфейл подлежат на условията на съответното съгласие за картодържец от издателя на картата.", "decimal_places_error": "Твърде много знаци след десетичната запетая", "decimals_cannot_be_zero": "Десетичната точка не може да бъде нула.", + "decred_info_card_details": "DeCred използва децентрализиран и консервиращ метод за синхронизиране, известен като „SPV“, който отнема повече време от нормалния портфейл с биткойн. За да научите повече, докоснете по -долу.", + "decred_info_title": "Синхронизация в Decred", "default_buy_provider": "Доставчик по подразбиране купува", "default_sell_provider": "Доставчик за продажба по подразбиране", "delete": "Изтрий", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ако процесът продължи повече от 1 минута, проверете своя имейл.", "proceed_on_device": "Продължете на вашето устройство", "proceed_on_device_description": "Моля, следвайте инструкциите, подканени на вашия хардуер", + "processing": "Обработка", "profile": "Профил", "provider_error": "Грешка на ${provider} ", "public_key": "Публичен ключ", diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 923db16fe..92eed7a3d 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Uložení a použití vašeho čísla platební karty (a přihlašovací údaje k vašemu číslu karty) v této digitální peněžence se řídí Obchodními podmínkami smlouvy příslušného držitele karty s vydavatelem karty (v jejich nejaktuálnější verzi).", "decimal_places_error": "Příliš mnoho desetinných míst", "decimals_cannot_be_zero": "Desetinná desetinná škola nemůže být nulová.", + "decred_info_card_details": "Decred používá decentralizovanou a synchronizační metodu zachovávající soukromí známou jako „SPV“, která trvá déle než normální bitcoinová peněženka. Chcete -li se dozvědět více, klepněte na níže.", + "decred_info_title": "Synchronizace v Decred", "default_buy_provider": "Výchozí poskytovatel nákupu", "default_sell_provider": "Výchozí poskytovatel prodeje", "delete": "Smazat", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Pokud proces nepokročí během 1 minuty, zkontrolujte svůj e-mail.", "proceed_on_device": "Pokračujte ve svém zařízení", "proceed_on_device_description": "Postupujte podle pokynů na výzvu na vaší hardwarové peněžence", + "processing": "Zpracování", "profile": "Profil", "provider_error": "${provider} chyba", "public_key": "Veřejný klíč", diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 39e3113c1..43db11f18 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Die Speicherung und Nutzung Ihrer Zahlungskartennummer (und Ihrer Zahlungskartennummer entsprechenden Anmeldeinformationen) in dieser digitalen Wallet unterliegt den Allgemeinen Geschäftsbedingungen des geltenden Karteninhabervertrags mit dem Zahlungskartenaussteller, gültig ab von Zeit zu Zeit.", "decimal_places_error": "Zu viele Nachkommastellen", "decimals_cannot_be_zero": "Token-Dezimalzahl kann nicht Null sein.", + "decred_info_card_details": "Decred verwendet eine dezentrale und für Privatsphäre erziehende Synchronisation, die als „SPV“ bezeichnet wird und länger als eine normale Bitcoin-Brieftasche dauert. Um mehr zu erfahren, tippen Sie unten.", + "decred_info_title": "Synchronisation in Decred", "default_buy_provider": "Standard-Kaufanbieter", "default_sell_provider": "Standard-Verkaufsanbieter", "delete": "Löschen", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Wenn der Bildschirm nach 1 Minute nicht weitergeht, überprüfen Sie bitte Ihre E-Mail.", "proceed_on_device": "Fahren Sie auf Ihrem Gerät fort", "proceed_on_device_description": "Bitte befolgen Sie die Anweisungen, die auf Ihrer Hardware-Wallet angezeigt werden", + "processing": "Verarbeitung", "profile": "Profil", "provider_error": "${provider}-Fehler", "public_key": "Öffentlicher Schlüssel", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 921375c3c..b6023d86e 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -208,6 +208,8 @@ "debit_card_terms": "The storage and usage of your payment card number (and credentials corresponding to your payment card number) in this digital wallet are subject to the Terms and Conditions of the applicable cardholder agreement with the payment card issuer, as in effect from time to time.", "decimal_places_error": "Too many decimal places", "decimals_cannot_be_zero": "Token decimal cannot be zero.", + "decred_info_card_details": "Decred uses a decentralized and privacy-preserving sync method known as “SPV”, which takes longer than a normal Bitcoin wallet. To learn more, tap below.", + "decred_info_title": "Synchronization in Decred", "default_buy_provider": "Default Buy Provider", "default_sell_provider": "Default Sell Provider", "delete": "Delete", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "If the screen doesn’t proceed after 1 minute, check your email.", "proceed_on_device": "Proceed on your device", "proceed_on_device_description": "Please follow the instructions prompted on your hardware wallet", + "processing": "Processing", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index ad96c2a93..c0474d672 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -208,6 +208,8 @@ "debit_card_terms": "El almacenamiento y el uso de su número de tarjeta de pago (y las credenciales correspondientes a su número de tarjeta de pago) en esta billetera digital están sujetos a los Términos y condiciones del acuerdo del titular de la tarjeta aplicable con el emisor de la tarjeta de pago, en vigor desde tiempo al tiempo.", "decimal_places_error": "Demasiados lugares decimales", "decimals_cannot_be_zero": "Token Decimal no puede ser cero.", + "decred_info_card_details": "Decred utiliza un método de sincronización descentralizado y que presenta la privacidad conocido como \"SPV\", que lleva más tiempo que una billetera Bitcoin normal. Para obtener más información, toque a continuación.", + "decred_info_title": "Sincronización en Decred", "default_buy_provider": "Proveedor de compra predeterminado", "default_sell_provider": "Proveedor de venta predeterminado", "delete": "Borrar", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Si la pantalla no continúa después de 1 minuto, revisa tu correo electrónico.", "proceed_on_device": "Continúa con tu dispositivo", "proceed_on_device_description": "Sigue las instrucciones solicitadas en su billetera de hardware", + "processing": "Tratamiento", "profile": "Perfil", "provider_error": "${provider} error", "public_key": "Clave pública", diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ccd12b1c6..d779a22f5 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Le stockage et l'utilisation de votre numéro de carte de paiement (et des informations d'identification correspondant à votre numéro de carte de paiement) dans ce portefeuille (wallet) numérique peuvent être soumis aux conditions générales de l'accord du titulaire de carte parfois en vigueur avec l'émetteur de la carte de paiement.", "decimal_places_error": "Trop de décimales", "decimals_cannot_be_zero": "La décimale du jeton ne peut pas être nulle.", + "decred_info_card_details": "Decred utilise une méthode de synchronisation décentralisée et préservée de confidentialité connue sous le nom de «SPV», qui prend plus de temps qu'un portefeuille Bitcoin normal. Pour en savoir plus, appuyez sur ci-dessous.", + "decred_info_title": "Synchronisation dans Decred", "default_buy_provider": "Fournisseur d'achat par défaut", "default_sell_provider": "Fournisseur de vente par défaut", "delete": "Effacer", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Si l'écran ne s'affiche pas après 1 minute, vérifiez vos e-mails.", "proceed_on_device": "Continuez sur votre appareil", "proceed_on_device_description": "Veuillez suivre les instructions affichées sur votre portefeuille physique.", + "processing": "Traitement", "profile": "Profil", "provider_error": "Erreur de ${provider}", "public_key": "Clef publique", diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index af931e4df..13cbe1828 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Adana da amfani da lambar katin kuɗin ku (da takaddun shaida masu dacewa da lambar katin kuɗin ku) a cikin wannan walat ɗin dijital suna ƙarƙashin Sharuɗɗa da Sharuɗɗa na yarjejeniya mai amfani da katin tare da mai fitar da katin biyan kuɗi, kamar yadda yake aiki daga lokaci zuwa lokaci.", "decimal_places_error": "Wadannan suna da tsawon harsuna", "decimals_cannot_be_zero": "Alamar alama ba zata iya zama sifili ba.", + "decred_info_card_details": "An yanke amfani da shi da ingantaccen tsari da kuma tsarin adana Siscyc na sirri da aka sani da \"SPV\", wanda ke ɗaukar tsayi fiye da walatic na al'ada. Don ƙarin koyo, matsa ƙasa.", + "decred_info_title": "Aiki tare a Decred", "default_buy_provider": "Tsohuwar Siyarwa", "default_sell_provider": "Tsohuwar Mai Bayar Siyarwa", "delete": "Share", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Idan allon bai ci gaba ba bayan minti 1, duba imel ɗin ku.", "proceed_on_device": "Ci gaba akan na'urarka", "proceed_on_device_description": "Da fatan za a bi umarnin akan walatware", + "processing": "Aiki", "profile": "Rabin fuska", "provider_error": "${provider} kuskure", "public_key": "Maɓallin jama'a", diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 449a3f278..409e0fd07 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -208,6 +208,8 @@ "debit_card_terms": "इस डिजिटल वॉलेट में आपके भुगतान कार्ड नंबर (और आपके भुगतान कार्ड नंबर से संबंधित क्रेडेंशियल) का भंडारण और उपयोग भुगतान कार्ड जारीकर्ता के साथ लागू कार्डधारक समझौते के नियमों और शर्तों के अधीन है, जैसा कि प्रभावी है समय - समय पर।", "decimal_places_error": "बहुत अधिक दशमलव स्थान", "decimals_cannot_be_zero": "टोकन दशमलव शून्य नहीं हो सकता।", + "decred_info_card_details": "डिक्रेड एक विकेन्द्रीकृत और गोपनीयता-संरक्षण सिंक विधि का उपयोग करता है जिसे \"एसपीवी\" के रूप में जाना जाता है, जो एक सामान्य बिटकॉइन वॉलेट से अधिक समय लेता है। अधिक जानने के लिए, नीचे टैप करें।", + "decred_info_title": "डिक्रेड में सिंक्रनाइज़ेशन", "default_buy_provider": "डिफ़ॉल्ट खरीद प्रदाता", "default_sell_provider": "डिफ़ॉल्ट विक्रय प्रदाता", "delete": "हटाएं", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "यदि 1 मिनट के बाद भी स्क्रीन आगे नहीं बढ़ती है, तो अपना ईमेल देखें।", "proceed_on_device": "अपने डिवाइस पर आगे बढ़ें", "proceed_on_device_description": "कृपया अपने हार्डवेयर वॉलेट पर दिए गए निर्देशों का पालन करें", + "processing": "प्रसंस्करण", "profile": "प्रोफ़ाइल", "provider_error": "${provider} त्रुटि", "public_key": "सार्वजनिक कुंजी", diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 25d90e711..f93f86d1d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Pohranjivanje i korištenje broja vaše platne kartice (i vjerodajnica koje odgovaraju broju vaše platne kartice) u ovom digitalnom novčaniku podliježu Uvjetima i odredbama važećeg ugovora vlasnika kartice s izdavateljem platne kartice, koji su na snazi ​​od S vremena na vrijeme.", "decimal_places_error": "Previše decimalnih mjesta", "decimals_cannot_be_zero": "Token Decimal ne može biti nula.", + "decred_info_card_details": "DECRED koristi decentraliziranu metodu sinkronizacije za očuvanje privatnosti poznatu kao \"SPV\", koja traje duže od normalnog Bitcoin novčanika. Da biste saznali više, dodirnite u nastavku.", + "decred_info_title": "Sinkronizacija u DECRED", "default_buy_provider": "Zadani davatelj kupnje", "default_sell_provider": "Zadani dobavljač prodaje", "delete": "Izbriši", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ako se zaslon ne nastavi nakon 1 minute, provjerite svoju e-poštu.", "proceed_on_device": "Nastavite na svom uređaju", "proceed_on_device_description": "Slijedite upute zatražene na vašem hardverskom novčaniku", + "processing": "Obrada", "profile": "Profil", "provider_error": "${provider} greška", "public_key": "Javni ključ", diff --git a/res/values/strings_hy.arb b/res/values/strings_hy.arb index 6c35b2be1..3378f1a0d 100644 --- a/res/values/strings_hy.arb +++ b/res/values/strings_hy.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ձեր վճարային քարտի համարի (և ձեր վճարային քարտի համարի համապատասխան վկայականներ) պահպանումն ու օգտագործումը այս թվային դրամապանակում ենթակա են վճարային քարտ թողարկող կողմի գործող պայմանների և պայմանագրի", "decimal_places_error": "Խմբակային տեղերի սխալ", "decimals_cannot_be_zero": "Խմբակային տեղերը չեն կարող լինել զրո", + "decred_info_card_details": "DECREDE- ն օգտագործում է ապակենտրոնացված եւ գաղտնիության պահպանման համաժամացման համաժամացման մեթոդը, որը հայտնի է որպես «SPV», որը տեւում է ավելի երկար, քան նորմալ Bitcoin դրամապանակը: Ավելին իմանալու համար հպեք ստորեւ:", + "decred_info_title": "Համաժամացումը DEPRED- ում", "default_buy_provider": "Լռելյայն գնման մատակարար", "default_sell_provider": "Լռելյայն վաճառքի մատակարար", "delete": "Ջնջել", @@ -545,6 +547,7 @@ "proceed_after_one_minute": "Եթե էկրանը 1 րոպեից ավել չի անցնում, ստուգեք ձեր էլեկտրոնային փոստը", "proceed_on_device": "Շարունակեք ձեր սարքի վրա", "proceed_on_device_description": "Խնդրում ենք հետևել ձեր սարքի վրա ցուցադրվող հրահանգներին", + "processing": "Վերամշակում", "profile": "Պրոֆիլ", "provider_error": "${provider} սխալ", "public_key": "Հանրային բանալի", diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 87e45da1c..22558854f 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Penyimpanan dan penggunaan nomor kartu pembayaran Anda (dan kredensial yang sesuai dengan nomor kartu pembayaran Anda) dalam dompet digital ini tertakluk pada Syarat dan Ketentuan persetujuan pemegang kartu yang berlaku dengan penerbit kartu pembayaran, seperti yang berlaku dari waktu ke waktu.", "decimal_places_error": "Terlalu banyak tempat desimal", "decimals_cannot_be_zero": "Token desimal tidak bisa nol.", + "decred_info_card_details": "Decred menggunakan metode sinkronisasi yang terdesentralisasi dan mempertahankan privasi yang dikenal sebagai \"SPV\", yang membutuhkan waktu lebih lama dari dompet Bitcoin normal. Untuk mempelajari lebih lanjut, ketuk di bawah ini.", + "decred_info_title": "Sinkronisasi dalam dekred", "default_buy_provider": "Penyedia beli default", "default_sell_provider": "Penyedia Penjualan Default", "delete": "Hapus", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Jika layar tidak bergerak setelah 1 menit, periksa email Anda.", "proceed_on_device": "Lanjutkan di perangkat Anda", "proceed_on_device_description": "Harap ikuti instruksi yang diminta di dompet perangkat keras Anda", + "processing": "Pengolahan", "profile": "Profil", "provider_error": "${provider} error", "public_key": "Kunci publik", diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index a476bbc0c..a30944901 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -208,6 +208,8 @@ "debit_card_terms": "L'archiviazione e l'utilizzo del numero della carta di pagamento (e delle credenziali corrispondenti al numero della carta di pagamento) in questo portafoglio digitale sono soggetti ai Termini e condizioni del contratto applicabile con il titolare della carta con l'emittente della carta di pagamento, come in vigore di tanto in tanto.", "decimal_places_error": "Troppe cifre decimali", "decimals_cannot_be_zero": "Il decimale token non può essere zero.", + "decred_info_card_details": "Decred utilizza un metodo di sincronizzazione decentralizzato e di conservazione della privacy noto come \"SPV\", che richiede più tempo di un normale portafoglio Bitcoin. Per saperne di più, tocca sotto.", + "decred_info_title": "Sincronizzazione in decred", "default_buy_provider": "Provider di acquisto predefinito", "default_sell_provider": "Provider di vendita predefinito", "delete": "Elimina", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Se la schermata non procede dopo 1 minuto, controlla la tua email.", "proceed_on_device": "Procedi sul tuo dispositivo", "proceed_on_device_description": "Segui le istruzioni richieste sul portafoglio hardware", + "processing": "Elaborazione", "profile": "Profilo", "provider_error": "${provider} errore", "public_key": "Chiave pubblica", diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 10350d7be..25b16a92a 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -208,6 +208,8 @@ "debit_card_terms": "このデジタルウォレットでの支払いカード番号(および支払いカード番号に対応する資格情報)の保存と使用には、支払いカード発行者との該当するカード所有者契約の利用規約が適用されます。時々。", "decimal_places_error": "小数点以下の桁数が多すぎる", "decimals_cannot_be_zero": "トークン小数はゼロにすることはできません。", + "decred_info_card_details": "Decredは、「SPV」と呼ばれる分散型およびプライバシーを摂取する同期メソッドを使用します。これには、通常のビットコインウォレットよりも時間がかかります。詳細については、以下をタップしてください。", + "decred_info_title": "デコロードの同期", "default_buy_provider": "デフォルトの購入プロバイダー", "default_sell_provider": "デフォルトの販売プロバイダー", "delete": "削除する", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "1分経っても画面が進まない場合は、メールを確認してください。", "proceed_on_device": "デバイスに進みます", "proceed_on_device_description": "ハードウェアウォレットにプロンプ​​トされた指示に従ってください", + "processing": "処理", "profile": "プロフィール", "provider_error": "${provider} エラー", "public_key": "公開鍵", diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index a76318ad0..9849ccd09 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -208,6 +208,8 @@ "debit_card_terms": "이 디지털 지갑에 있는 귀하의 지불 카드 번호(및 귀하의 지불 카드 번호에 해당하는 자격 증명)의 저장 및 사용은 부터 발효되는 지불 카드 발행자와의 해당 카드 소지자 계약의 이용 약관을 따릅니다. 수시로.", "decimal_places_error": "소수점 이하 자릿수가 너무 많습니다.", "decimals_cannot_be_zero": "토큰 소수점은 0이 될 수 없습니다.", + "decred_info_card_details": "Decred는 정상적인 비트 코인 지갑보다 더 오래 걸리는 \"SPV\"로 알려진 분산 및 개인 정보 보호 동기화 방법을 사용합니다. 자세한 내용은 아래를 누릅니다.", + "decred_info_title": "Decred의 동기화", "default_buy_provider": "기본 구매 제공자", "default_sell_provider": "기본 판매 공급자", "delete": "지우다", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "1분 후에도 화면이 진행되지 않으면 이메일을 확인하세요.", "proceed_on_device": "장치를 진행하십시오", "proceed_on_device_description": "하드웨어 지갑에 표시된 지침을 따르십시오", + "processing": "처리", "profile": "프로필", "provider_error": "${provider} 오류", "public_key": "공개 키", diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index ac6749a7f..a8f57a824 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -208,6 +208,8 @@ "debit_card_terms": "ဤဒစ်ဂျစ်တယ်ပိုက်ဆံအိတ်ရှိ သင့်ငွေပေးချေမှုကတ်နံပါတ် (နှင့် သင့်ငွေပေးချေကတ်နံပါတ်နှင့် သက်ဆိုင်သောအထောက်အထားများ) ၏ သိုလှောင်မှုနှင့် အသုံးပြုမှုသည် အချိန်အခါနှင့်အမျှ သက်ရောက်မှုရှိသကဲ့သို့ ကတ်ကိုင်ဆောင်ထားသူ၏ သဘောတူညီချက်၏ စည်းကမ်းသတ်မှတ်ချက်များနှင့် ကိုက်ညီပါသည်။", "decimal_places_error": "ဒဿမနေရာများ များလွန်းသည်။", "decimals_cannot_be_zero": "တိုကင်ဒ decimal မသုညမဖြစ်နိုင်ပါ။", + "decred_info_card_details": "ပုံမှန် bitcoin ပိုက်ဆံအိတ်ထက်ပိုရှည်သော \"SPV\" ဟုလူသိများသောဗဟိုချုပ်ကိုင်မှုလျှော့ချခြင်းနှင့်လုံခြုံမှုထိန်းသိမ်းခြင်းကိုထိန်းသိမ်းထားသည့်ထပ်တူပြုခြင်းနည်းလမ်းကိုအသုံးပြုသည်။ ပိုမိုလေ့လာရန်အောက်ပါကိုအသာပုတ်ပါ။", + "decred_info_title": "ဒီဇင်ဘာလ၌ထပ်တူပြုခြင်း", "default_buy_provider": "Default Provider ကိုဝယ်ပါ", "default_sell_provider": "ပုံသေရောင်းချပေးသူ", "delete": "ဖျက်ပါ။", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "မျက်နှာပြင်သည် ၁ မိနစ်အကြာတွင် ဆက်လက်မလုပ်ဆောင်ပါက သင့်အီးမေးလ်ကို စစ်ဆေးပါ။", "proceed_on_device": "သင့်စက်ပေါ်တွင်ဆက်လက်ဆောင်ရွက်ပါ", "proceed_on_device_description": "သင်၏ hardware ပိုက်ဆံအိတ်ပေါ်ရှိညွှန်ကြားချက်များကိုလိုက်နာပါ", + "processing": "လုပ်ကိုင်ခြင်း", "profile": "ကိုယ်ရေးအကျဉ်း", "provider_error": "${provider} အမှား", "public_key": "အများသူငှာသော့", diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e38e1ddfd..a4f2bb9a1 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "De opslag en het gebruik van uw betaalkaartnummer (en inloggegevens die overeenkomen met uw betaalkaartnummer) in deze digitale portemonnee zijn onderworpen aan de Algemene voorwaarden van de toepasselijke kaarthouderovereenkomst met de uitgever van de betaalkaart, zoals van kracht vanaf tijd tot tijd.", "decimal_places_error": "Te veel decimalen", "decimals_cannot_be_zero": "Token decimaal kan niet nul zijn.", + "decred_info_card_details": "Decred maakt gebruik van een gedecentraliseerde en privacy-behouds-synchronisatiemethode bekend als \"SPV\", die langer duurt dan een normale Bitcoin-portemonnee. Voor meer informatie, tik hieronder.", + "decred_info_title": "Synchronisatie in Decred", "default_buy_provider": "Standaard Koopprovider", "default_sell_provider": "Standaard verkoopaanbieder", "delete": "Delete", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Als het scherm na 1 minuut niet verder gaat, controleer dan uw e-mail.", "proceed_on_device": "Ga verder met uw apparaat", "proceed_on_device_description": "Volg de instructies die zijn aangevraagd op uw hardware -portemonnee", + "processing": "Verwerking", "profile": "Profiel", "provider_error": "${provider} fout", "public_key": "Publieke sleutel", diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9ec7db278..12e867518 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Przechowywanie i używanie numeru karty płatniczej (oraz danych uwierzytelniających odpowiadających numerowi karty płatniczej) w tym portfelu cyfrowym podlega Warunkom odpowiedniej umowy posiadacza karty z wydawcą karty płatniczej, zgodnie z obowiązującym od od czasu do czasu.", "decimal_places_error": "Za dużo miejsc dziesiętnych", "decimals_cannot_be_zero": "Token dziesiętny nie może być zerowy.", + "decred_info_card_details": "Decred używa zdecentralizowanej i zachowującej prywatność metodę synchronizacji znanej jako „SPV”, która trwa dłużej niż normalny portfel bitcoin. Aby dowiedzieć się więcej, dotknij poniżej.", + "decred_info_title": "Synchronizacja w dekred", "default_buy_provider": "Domyślny dostawca zakupu", "default_sell_provider": "Domyślny dostawca sprzedaży", "delete": "Skasuj", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Jeśli ekran nie przejdzie dalej po 1 minucie, sprawdź pocztę.", "proceed_on_device": "Kontynuuj swoje urządzenie", "proceed_on_device_description": "Postępuj zgodnie z instrukcjami wyświetlonymi w portfelu sprzętowym", + "processing": "Przetwarzanie", "profile": "Profil", "provider_error": "${provider} pomyłka", "public_key": "Klucz publiczny", diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 0c0ed2515..7d60a3d79 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -208,6 +208,8 @@ "debit_card_terms": "O armazenamento e uso do número do cartão de pagamento (e credenciais correspondentes ao número do cartão de pagamento) nesta carteira digital estão sujeitos aos Termos e Condições do contrato do titular do cartão aplicável com o emissor do cartão de pagamento, em vigor a partir de tempo ao tempo.", "decimal_places_error": "Muitas casas decimais", "decimals_cannot_be_zero": "Decimal de token não pode ser zero.", + "decred_info_card_details": "O Decred usa um método de sincronização descentralizado e de preservação de privacidade, conhecido como \"SPV\", que leva mais tempo do que uma carteira normal de Bitcoin. Para saber mais, toque abaixo.", + "decred_info_title": "Sincronização em Decred", "default_buy_provider": "Provedor de compra padrão", "default_sell_provider": "Provedor de venda padrão", "delete": "Excluir", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "Se a tela não prosseguir após 1 minuto, verifique seu e-mail.", "proceed_on_device": "Prossiga no seu dispositivo", "proceed_on_device_description": "Siga as instruções solicitadas em sua carteira de hardware", + "processing": "Processamento", "profile": "Perfil", "provider_error": "${provider} erro", "public_key": "Chave pública", diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 0b511b016..9c80e483f 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Хранение и использование номера вашей платежной карты (и учетных данных, соответствующих номеру вашей платежной карты) в этом цифровом кошельке регулируются положениями и условиями применимого соглашения держателя карты с эмитентом платежной карты, действующим с время от времени.", "decimal_places_error": "Слишком много десятичных знаков", "decimals_cannot_be_zero": "Десятичный токен не может быть нулевым.", + "decred_info_card_details": "DepRed использует децентрализованный метод синхронизации и сохраняющего конфиденциальность, известный как «SPV», который занимает больше времени, чем обычный биткойн-кошелек. Чтобы узнать больше, нажмите ниже.", + "decred_info_title": "Синхронизация в декорации", "default_buy_provider": "По умолчанию поставщик покупки", "default_sell_provider": "Поставщик продаж по умолчанию", "delete": "Удалить", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Если через 1 минуту экран не отображается, проверьте свою электронную почту.", "proceed_on_device": "Пройдите на свое устройство", "proceed_on_device_description": "Пожалуйста, следуйте инструкциям, представленным на вашем аппаратном кошельке", + "processing": "Обработка", "profile": "Профиль", "provider_error": "${provider} ошибка", "public_key": "Публичный ключ", diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bbbcf7227..874e9f390 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -208,6 +208,8 @@ "debit_card_terms": "การเก็บรักษาและใช้หมายเลขบัตรจ่ายเงิน (และข้อมูลประจำตัวที่เกี่ยวข้องกับหมายเลขบัตรจ่ายเงิน) ในกระเป๋าดิจิทัลนี้ จะต้องยึดถือข้อกำหนดและเงื่อนไขของข้อตกลงผู้ใช้บัตรของผู้ถือบัตรที่เกี่ยวข้องกับบัตรผู้ถือบัตร ซึ่งจะมีผลตั้งแต่เวลานั้น", "decimal_places_error": "ทศนิยมมากเกินไป", "decimals_cannot_be_zero": "ทศนิยมโทเค็นไม่สามารถเป็นศูนย์ได้", + "decred_info_card_details": "Decred ใช้วิธีการซิงค์การกระจายอำนาจและความเป็นส่วนตัวที่เรียกว่า \"SPV\" ซึ่งใช้เวลานานกว่ากระเป๋าเงิน bitcoin ปกติ หากต้องการเรียนรู้เพิ่มเติมแตะด้านล่าง", + "decred_info_title": "การซิงโครไนซ์ใน decred", "default_buy_provider": "ผู้ให้บริการซื้อเริ่มต้น", "default_sell_provider": "ผู้ให้บริการการขายเริ่มต้น", "delete": "ลบ", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "หากหน้าจอไม่ดำเนินการหลังจาก 1 นาทีโปรดตรวจสอบอีเมลของคุณ", "proceed_on_device": "ดำเนินการบนอุปกรณ์ของคุณ", "proceed_on_device_description": "โปรดทำตามคำแนะนำที่ได้รับแจ้งไว้ในกระเป๋าเงินฮาร์ดแวร์ของคุณ", + "processing": "กำลังประมวลผล", "profile": "ประวัติโดยย่อ", "provider_error": "ข้อผิดพลาด ${provider}", "public_key": "คีย์สาธารณะ", diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 7cdbd0d8b..31683c4fc 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ang pag-iimbak at paggamit ng iyong numero sa card (at mga kredensyal na nauugnay sa numero ng iyong card sa pagbabayad) sa pagbabayad sa digital wallet na ito ay napapailalim sa mga tuntunin at kundisyon ng naaangkop na kasunduan sa may-ari ng card kasama ang nagbigay ng card ng pagbabayad, na may bisa sa pana-panahon.", "decimal_places_error": "Masyadong maraming mga lugar na desimal", "decimals_cannot_be_zero": "Ang token decimal ay hindi maaaring maging zero.", + "decred_info_card_details": "Ang DECRED ay gumagamit ng isang desentralisado at privacy-pagpapanatili ng pamamaraan ng pag-sync na kilala bilang \"SPV\", na mas matagal kaysa sa isang normal na pitaka ng bitcoin. Upang malaman ang higit pa, mag -tap sa ibaba.", + "decred_info_title": "Pag -synchronise sa Decred", "default_buy_provider": "Default na Buy Provider", "default_sell_provider": "Default na Sell Provider", "delete": "Tanggalin", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Kung ang screen ay hindi magpapatuloy pagkatapos ng 1 minuto, suriin ang iyong email.", "proceed_on_device": "Magpatuloy sa iyong hardware wallet", "proceed_on_device_description": "Mangyaring sundin ang mga tagubilin na sinenyasan sa iyong hardware wallet", + "processing": "Pagproseso", "profile": "Profile", "provider_error": "${provider} error", "public_key": "Public key", diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index bfc87a7e7..f7553d5c1 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Ödeme kartı numaranızın (ve kart numaranıza karşılık gelen kimlik bilgilerinin) bu dijital cüzdanda saklanması ve kullanılması, zaman zaman yürürlükte olan ödeme kartı veren kuruluşla yapılan ilgili kart sahibi sözleşmesinin Hüküm ve Koşullarına tabidir.", "decimal_places_error": "Çok fazla ondalık basamak", "decimals_cannot_be_zero": "Token oncial sıfır olamaz.", + "decred_info_card_details": "Decred, normal bir bitcoin cüzdanından daha uzun süren “SPV” olarak bilinen merkezi olmayan ve gizliliği koruyan bir senkronizasyon yöntemi kullanır. Daha fazla bilgi edinmek için aşağıya dokunun.", + "decred_info_title": "Senkronizasyon Dekred", "default_buy_provider": "Varsayılan Satın Alma Sağlayıcısı", "default_sell_provider": "Varsayılan Satış Sağlayıcısı", "delete": "Sil", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Ekran 1 dakika sonra ilerlemezse, e-postanızı kontrol edin.", "proceed_on_device": "Cihazınıza devam edin", "proceed_on_device_description": "Lütfen donanım cüzdanınızda istenen talimatları izleyin", + "processing": "İşleme", "profile": "Profil", "provider_error": "${provider} hatası", "public_key": "Genel Anahtar", diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 4b7818347..e3a150bca 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Зберігання та використання номера вашої платіжної картки (та облікових даних, які відповідають номеру вашої платіжної картки) у цьому цифровому гаманці регулюються Умовами відповідної угоди власника картки з емітентом платіжної картки, що діє з час від часу.", "decimal_places_error": "Забагато знаків після коми", "decimals_cannot_be_zero": "Десятковий знак не може бути нульовим.", + "decred_info_card_details": "Decred використовує децентралізований метод синхронізації, що зберігає конфіденційність, відомий як \"SPV\", який займає більше часу, ніж звичайний гаманець Bitcoin. Щоб дізнатися більше, торкніться нижче.", + "decred_info_title": "Синхронізація в Decred", "default_buy_provider": "Постачальник покупки за замовчуванням", "default_sell_provider": "Постачальник продажу за замовчуванням", "delete": "Видалити", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "Якщо екран не продовжується через 1 хвилину, перевірте свою електронну пошту.", "proceed_on_device": "Продовжуйте свій пристрій", "proceed_on_device_description": "Будь ласка, дотримуйтесь інструкцій, підказаних на вашому апаратному гаманці", + "processing": "Обробка", "profile": "Профіль", "provider_error": "${provider} помилка", "public_key": "Публічний ключ", diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index f6b92ca36..d95bb9237 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -208,6 +208,8 @@ "debit_card_terms": "اس ڈیجیٹل والیٹ میں آپ کے ادائیگی کارڈ نمبر (اور آپ کے ادائیگی کارڈ نمبر سے متعلقہ اسناد) کا ذخیرہ اور استعمال ادائیگی کارڈ جاری کنندہ کے ساتھ قابل اطلاق کارڈ ہولڈر کے معاہدے کی شرائط و ضوابط کے ساتھ مشروط ہے، جیسا کہ وقتاً فوقتاً نافذ ہوتا ہے۔", "decimal_places_error": "بہت زیادہ اعشاریہ جگہیں۔", "decimals_cannot_be_zero": "ٹوکن اعشاریہ صفر نہیں ہوسکتا۔", + "decred_info_card_details": "ڈیکریڈ ایک विकेंद्रीकृत اور رازداری سے محفوظ رکھنے والا مطابقت پذیری کا طریقہ استعمال کرتا ہے جسے \"ایس پی وی\" کہا جاتا ہے ، جو عام بٹ کوائن پرس سے زیادہ وقت لگتا ہے۔ مزید جاننے کے لئے ، نیچے ٹیپ کریں۔", + "decred_info_title": "فیصلہ شدہ میں ہم آہنگی", "default_buy_provider": "پہلے سے طے شدہ خریدنے والا", "default_sell_provider": " ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ", "delete": "حذف کریں۔", @@ -548,6 +550,7 @@ "proceed_after_one_minute": "اگر اسکرین 1 منٹ کے بعد آگے نہیں بڑھتی ہے تو اپنا ای میل چیک کریں۔", "proceed_on_device": "اپنے آلے پر آگے بڑھیں", "proceed_on_device_description": "براہ کرم اپنے ہارڈ ویئر پرس پر آنے والی ہدایات پر عمل کریں", + "processing": "پروسیسنگ", "profile": "پروفائل", "provider_error": "${provider} خرابی۔", "public_key": "عوامی کلید", diff --git a/res/values/strings_vi.arb b/res/values/strings_vi.arb index a759e87c9..3ae00e89f 100644 --- a/res/values/strings_vi.arb +++ b/res/values/strings_vi.arb @@ -207,6 +207,8 @@ "debit_card_terms": "Việc lưu trữ và sử dụng số thẻ thanh toán của bạn (và thông tin xác thực tương ứng với số thẻ thanh toán của bạn) trong ví điện tử này phải tuân theo Điều khoản và Điều kiện của thỏa thuận chủ thẻ hiện hành với tổ chức phát hành thẻ thanh toán, theo thời gian.", "decimal_places_error": "Quá nhiều chữ số thập phân", "decimals_cannot_be_zero": "Chữ số thập phân không thể là số không.", + "decred_info_card_details": "Decred sử dụng một phương pháp đồng bộ hóa bảo tồn và bảo tồn quyền riêng tư được gọi là SPV SPV, mất nhiều thời gian hơn ví Bitcoin bình thường. Để tìm hiểu thêm, nhấn bên dưới.", + "decred_info_title": "Đồng bộ hóa trong Decred", "default_buy_provider": "Nhà cung cấp Mua mặc định", "default_sell_provider": "Nhà cung cấp Bán mặc định", "delete": "Xóa", @@ -544,6 +546,7 @@ "proceed_after_one_minute": "Nếu màn hình không tiếp tục sau 1 phút, hãy kiểm tra email của bạn.", "proceed_on_device": "Tiếp tục trên thiết bị của bạn", "proceed_on_device_description": "Vui lòng làm theo các hướng dẫn được nhắc trên ví phần cứng của bạn", + "processing": "Xử lý", "profile": "Hồ sơ", "provider_error": "Lỗi ${provider}", "public_key": "Khóa công khai", diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 7d9017d0c..3502629fc 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -208,6 +208,8 @@ "debit_card_terms": "Òfin ti olùṣe àjọrò káàdì ìrajà bójú irú ọ̀nà t'á pamọ́ àti a lo òǹkà ti káàdì ìrajà yín (àti ọ̀rọ̀ ìdánimọ̀ tí káàdì náà) nínú àpamọ́wọ́ yìí.", "decimal_places_error": "Oọ̀rọ̀ ayipada ti o wa ni o dara julọ", "decimals_cannot_be_zero": "Token eleemel ko le jẹ odo.", + "decred_info_card_details": "Devend nlo ọna ti o ni itọju ati ti itọju-itọju-itọju ti o mọ bi \"SPV\", eyiti o gba to gun ju apamọwọ Bitcoin deede. Lati kọ ẹkọ diẹ sii, tẹ ni isalẹ.", + "decred_info_title": "Imuṣiṣẹpọ ni devitered", "default_buy_provider": "Aiyipada Ra Olupese", "default_sell_provider": "Aiyipada Olupese Tita", "delete": "Pa á", @@ -547,6 +549,7 @@ "proceed_after_one_minute": "Tí aṣàfihàn kò bá tẹ̀síwájú l'áàárín ìṣẹ́jú kan, ẹ tọ́ ímeèlì yín wò.", "proceed_on_device": "Tẹsiwaju lori ẹrọ rẹ", "proceed_on_device_description": "Jọwọ tẹle awọn ilana ti a ṣe lori apamọwọ ohun elo rẹ", + "processing": "Iṣaayan", "profile": "profaili", "provider_error": "Àṣìṣe ${provider}", "public_key": "Kọ́kọ́rọ́ tó kò àdáni", diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 5e009f841..741266f90 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -208,6 +208,8 @@ "debit_card_terms": "您的支付卡号(以及与您的支付卡号对应的凭证)在此数字钱包中的存储和使用受适用的持卡人与支付卡发卡机构签订的协议的条款和条件的约束,自时不时。", "decimal_places_error": "小数位太多", "decimals_cannot_be_zero": "代币十进制不能为零。", + "decred_info_card_details": "DECRED使用称为“ SPV”的分散和隐私的同步方法,该方法比普通的比特币钱包更长。要了解更多信息,请点击下面。", + "decred_info_title": "在Decred中同步", "default_buy_provider": "默认购买提供商", "default_sell_provider": "默认销售提供商", "delete": "删除", @@ -546,6 +548,7 @@ "proceed_after_one_minute": "如果屏幕在 1 分钟后没有继续,请检查您的电子邮件。", "proceed_on_device": "在设备上继续", "proceed_on_device_description": "请按照您的硬件钱包上提示的说明进行操作", + "processing": "加工", "profile": "轮廓", "provider_error": "${provider} 错误", "public_key": "公钥", diff --git a/scripts/android/.gitignore b/scripts/android/.gitignore index f7e94b7c0..3a2b4c98b 100644 --- a/scripts/android/.gitignore +++ b/scripts/android/.gitignore @@ -1 +1,2 @@ -mwebd \ No newline at end of file +mwebd +decred \ No newline at end of file diff --git a/scripts/android/build_all.sh b/scripts/android/build_all.sh index ad4ec984b..dc730af55 100755 --- a/scripts/android/build_all.sh +++ b/scripts/android/build_all.sh @@ -11,6 +11,7 @@ case $APP_ANDROID_TYPE in "monero.com") $DIR/build_monero_all.sh ;; "cakewallet") $DIR/build_monero_all.sh $DIR/build_haven_all.sh - $DIR/build_mwebd.sh ;; + $DIR/build_mwebd.sh + $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/android/build_decred.sh b/scripts/android/build_decred.sh new file mode 100755 index 000000000..d7b2fdb6b --- /dev/null +++ b/scripts/android/build_decred.sh @@ -0,0 +1,84 @@ +#!/bin/sh + +set -e +cd "$(dirname "$0")" +# . ./config.sh + +CW_DECRED_DIR=$(realpath ../..)/cw_decred +LIBWALLET_PATH="${PWD}/decred/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH/{*,.*} || true +fi +mkdir -p $LIBWALLET_PATH || true + +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [[ "x$ANDROID_HOME" == "x" ]]; +then + echo "ANDROID_HOME is missing, please declare it before building (on macos it is usually $HOME/Library/Android/sdk)" + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_HOME=\"\$HOME/Library/Android/sdk\" > ~/.zprofile" + exit 1 +fi + +if [[ "x$ANDROID_NDK_VERSION" == "x" ]]; +then + echo "ANDROID_NDK_VERSION is missing, please declare it before building" + echo "You have these versions installed on your system currently:" + ls ${ANDROID_HOME}/ndk/ | cat | awk '{ print "- " $1 }' + echo "echo > ~/.zprofile" + echo "echo 'export ANDROID_NDK_CERSION=..... > ~/.zprofile" + exit 1 +fi + +export NDK_BIN_PATH="${ANDROID_HOME}/ndk/${ANDROID_NDK_VERSION}/toolchains/llvm/prebuilt/$(uname | tr '[:upper:]' '[:lower:]')-x86_64/bin" +export ANDROID_API_VERSION=21 +# export CPATH="$(clang -v 2>&1 | grep "Selected GCC installation" | rev | cut -d' ' -f1 | rev)/include" + +for arch in "aarch" "aarch64" "x86_64" +do + TRIPLET="" + TARGET="" + ARCH_ABI="" + + case $arch in + "aarch") + TRIPLET="armv7a-linux-androideabi" + TARGET="arm" + ARCH_ABI="armeabi-v7a";; + "aarch64") + TRIPLET="aarch64-linux-android" + TARGET="arm64" + ARCH_ABI="arm64-v8a";; + "x86_64") + TRIPLET="x86_64-linux-android" + TARGET="amd64" + ARCH_ABI="x86_64";; + *) + echo "Unknown arch: $arch" + exit 1;; + esac + + # PATH="${TOOLCHAIN_BASE_DIR}_${arch}/bin:${ORIGINAL_PATH}" + if [ -e ./build ]; then + rm -fr ./build + fi + + CLANG_PATH="${NDK_BIN_PATH}/${TRIPLET}${ANDROID_API_VERSION}-clang" + CGO_ENABLED=1 GOOS=android GOARCH=${TARGET} CC=${CLANG_PATH} CXX=${CLANG_PATH}++ \ + go build -v -buildmode=c-shared -o ./build/${TRIPLET}-libdcrwallet.so ./cgo + + DEST_LIB_DIR=${CW_DECRED_DIR}/android/libs/${ARCH_ABI} + mkdir -p $DEST_LIB_DIR + cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.so $DEST_LIB_DIR/libdcrwallet.so +done + +HEADER_DIR=$CW_DECRED_DIR/lib/api +cp ${LIBWALLET_PATH}/build/${TRIPLET}-libdcrwallet.h $HEADER_DIR/libdcrwallet.h +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 5d6a24722..d3341f63d 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi @@ -26,4 +26,4 @@ flutter pub get dart run tool/generate_pubspec.dart flutter pub get dart run tool/configure.dart $CONFIG_ARGS -cd scripts/android \ No newline at end of file +cd scripts/android diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index d118370b5..32d6d5edd 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -27,18 +27,18 @@ universal_sed "s/PRODUCT_BUNDLE_IDENTIFIER = .*;/PRODUCT_BUNDLE_IDENTIFIER = $AP CONFIG_ARGS="" case $APP_IOS_TYPE in - $MONERO_COM) + $MONERO_COM) CONFIG_ARGS="--monero" ;; + $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred" if [ "$CW_WITH_HAVEN" = true ];then CONFIG_ARGS="$CONFIG_ARGS --haven" fi ;; + $HAVEN) - - CONFIG_ARGS="--haven" ;; esac diff --git a/scripts/ios/build_all.sh b/scripts/ios/build_all.sh index ba5c55a1f..f5b58d6fa 100755 --- a/scripts/ios/build_all.sh +++ b/scripts/ios/build_all.sh @@ -9,6 +9,6 @@ DIR=$(dirname "$0") case $APP_IOS_TYPE in "monero.com") $DIR/build_monero_all.sh ;; - "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh ;; + "cakewallet") $DIR/build_monero_all.sh && $DIR/build_haven.sh && $DIR/build_mwebd.sh && $DIR/build_decred.sh ;; "haven") $DIR/build_haven_all.sh ;; esac diff --git a/scripts/ios/build_decred.sh b/scripts/ios/build_decred.sh new file mode 100755 index 000000000..e6b13d0da --- /dev/null +++ b/scripts/ios/build_decred.sh @@ -0,0 +1,35 @@ +#!/bin/sh +set -e +. ./config.sh +LIBWALLET_PATH="${EXTERNAL_IOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +SYSROOT=`xcrun --sdk iphoneos --show-sdk-path` +CLANG="clang -target arm64-apple-ios -isysroot ${SYSROOT}" +CLANGXX="clang++ -target arm64-apple-ios -isysroot ${SYSROOT}" + +if [ -e ./build ]; then + rm -fr ./build +fi +CGO_ENABLED=1 GOOS=ios GOARCH=arm64 CC=$CLANG CXX=$CLANGXX \ +go build -v -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo || exit 1 + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/ios/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/ios/build_zmq.sh b/scripts/ios/build_zmq.sh index e2fd7caae..d72f322a3 100755 --- a/scripts/ios/build_zmq.sh +++ b/scripts/ios/build_zmq.sh @@ -1,4 +1,4 @@ -g#!/bin/sh +#!/bin/sh . ./config.sh diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index bb4750803..ad1f04b3e 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --decred";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/build_all.sh b/scripts/macos/build_all.sh index 030617f7d..7929bb719 100755 --- a/scripts/macos/build_all.sh +++ b/scripts/macos/build_all.sh @@ -1,3 +1,3 @@ #!/bin/sh -./build_monero_all.sh universal \ No newline at end of file +./build_monero_all.sh universal && $DIR/build_decred.sh diff --git a/scripts/macos/build_boost_arm64.sh b/scripts/macos/build_boost_arm64.sh index 11f26040f..b5761b7a2 100755 --- a/scripts/macos/build_boost_arm64.sh +++ b/scripts/macos/build_boost_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_boost_common.sh build_boost_arm64 \ No newline at end of file diff --git a/scripts/macos/build_boost_common.sh b/scripts/macos/build_boost_common.sh index 0c75be2bd..5aa17bf7c 100755 --- a/scripts/macos/build_boost_common.sh +++ b/scripts/macos/build_boost_common.sh @@ -35,6 +35,10 @@ BOOST_B2_LINKFLAGS_X86_64="-arch x86_64" BOOST_B2_BUILD_DIR_X86_64=macos-x86_64 build_boost_init_common() { + echo " + ============================ BOOST ============================ + " + CXXFLAGS=$1 CFLAGS=$2 LINKFLAGS=$3 @@ -157,8 +161,8 @@ build_boost_compile_universal() { build_boost_install_common() { ARCH=$1 LIB_DIR="" - mkdir $EXTERNAL_MACOS_LIB_DIR - mkdir $EXTERNAL_MACOS_INCLUDE_DIR + mkdir -p $EXTERNAL_MACOS_LIB_DIR + mkdir -p $EXTERNAL_MACOS_INCLUDE_DIR case $ARCH in arm64) LIB_DIR="${BOOST_B2_BUILD_DIR_ARM_64}/stage/lib";; diff --git a/scripts/macos/build_decred.sh b/scripts/macos/build_decred.sh new file mode 100755 index 000000000..d4b7d4d65 --- /dev/null +++ b/scripts/macos/build_decred.sh @@ -0,0 +1,34 @@ +#!/bin/sh + +. ./config.sh + +LIBWALLET_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libwallet" +LIBWALLET_URL="https://github.com/decred/libwallet.git" +LIBWALLET_VERSION="87b2769538db3065b334d247b25774593fc6443d" + +echo "======================= DECRED LIBWALLET =========================" + +echo "Cloning DECRED LIBWALLET from - $LIBWALLET_URL" +if [ -e $LIBWALLET_PATH ]; then + rm -fr $LIBWALLET_PATH +fi +mkdir -p $LIBWALLET_PATH +git clone $LIBWALLET_URL $LIBWALLET_PATH +cd $LIBWALLET_PATH +git checkout $LIBWALLET_VERSION + +if [ -e ./build ]; then + rm -fr ./build +fi +go build -buildmode=c-archive -o ./build/libdcrwallet.a ./cgo + +CW_DECRED_DIR=${CW_ROOT}/cw_decred +HEADER_DIR=$CW_DECRED_DIR/lib/api +mv ${LIBWALLET_PATH}/build/libdcrwallet.h $HEADER_DIR + +DEST_LIB_DIR=${CW_DECRED_DIR}/macos/External/lib +mkdir -p $DEST_LIB_DIR +mv ${LIBWALLET_PATH}/build/libdcrwallet.a $DEST_LIB_DIR + +cd $CW_DECRED_DIR +dart run ffigen diff --git a/scripts/macos/build_expat.sh b/scripts/macos/build_expat.sh index 0c5857907..8730e6992 100755 --- a/scripts/macos/build_expat.sh +++ b/scripts/macos/build_expat.sh @@ -6,6 +6,10 @@ EXPAT_VERSION=R_2_4_8 EXPAT_HASH="3bab6c09bbe8bf42d84b81563ddbcf4cca4be838" EXPAT_SRC_DIR=${EXTERNAL_MACOS_SOURCE_DIR}/libexpat +echo " +============================ EXPAT ============================ +" + git clone https://github.com/libexpat/libexpat.git -b ${EXPAT_VERSION} ${EXPAT_SRC_DIR} cd $EXPAT_SRC_DIR test `git rev-parse HEAD` = ${EXPAT_HASH} || exit 1 diff --git a/scripts/macos/build_monero.sh b/scripts/macos/build_monero.sh index 1af7ae0e3..a3b0381e3 100755 --- a/scripts/macos/build_monero.sh +++ b/scripts/macos/build_monero.sh @@ -11,6 +11,10 @@ DEST_LIB_DIR=${EXTERNAL_MACOS_LIB_DIR}/monero DEST_INCLUDE_DIR=${EXTERNAL_MACOS_INCLUDE_DIR}/monero ARCH=`uname -m` +echo " +============================ MONERO ============================ +" + echo "Cloning monero from - $MONERO_URL to - $MONERO_DIR_PATH" git clone $MONERO_URL $MONERO_DIR_PATH cd $MONERO_DIR_PATH diff --git a/scripts/macos/build_openssl_arm64.sh b/scripts/macos/build_openssl_arm64.sh index fd8d7b2f5..d320ef7fa 100755 --- a/scripts/macos/build_openssl_arm64.sh +++ b/scripts/macos/build_openssl_arm64.sh @@ -1,4 +1,5 @@ #!/bin/sh +. ./config.sh . ./build_openssl_common.sh build_openssl_arm64 \ No newline at end of file diff --git a/scripts/macos/build_openssl_common.sh b/scripts/macos/build_openssl_common.sh index 27cb1ef8c..1e4ac6f54 100755 --- a/scripts/macos/build_openssl_common.sh +++ b/scripts/macos/build_openssl_common.sh @@ -13,7 +13,9 @@ build_openssl_init_common() { # Use 1.1.1s because of https://github.com/openssl/openssl/issues/18720 OPENSSL_VERSION="1.1.1s" - echo "============================ OpenSSL ============================" + echo " + ============================ OPENSSL ============================ + " cd $EXTERNAL_MACOS_SOURCE_DIR curl -O https://www.openssl.org/source/openssl-$OPENSSL_VERSION.tar.gz diff --git a/scripts/macos/build_sodium.sh b/scripts/macos/build_sodium.sh index 19aad3c97..d754ce8e4 100755 --- a/scripts/macos/build_sodium.sh +++ b/scripts/macos/build_sodium.sh @@ -5,7 +5,9 @@ SODIUM_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libsodium" SODIUM_URL="https://github.com/jedisct1/libsodium.git" -echo "============================ SODIUM ============================" +echo " +============================ SODIUM ============================ +" echo "Cloning SODIUM from - $SODIUM_URL" git clone $SODIUM_URL $SODIUM_PATH --branch stable diff --git a/scripts/macos/build_unbound.sh b/scripts/macos/build_unbound.sh index ed115d464..6580ebc7c 100755 --- a/scripts/macos/build_unbound.sh +++ b/scripts/macos/build_unbound.sh @@ -7,7 +7,10 @@ UNBOUND_HASH="cbed768b8ff9bfcf11089a5f1699b7e5707f1ea5" UNBOUND_URL="https://www.nlnetlabs.nl/downloads/unbound/unbound-${UNBOUND_VERSION}.tar.gz" UNBOUND_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/unbound-1.16.2" -echo "============================ Unbound ============================" +echo " +============================ Unbound ============================ +" + rm -rf ${UNBOUND_DIR_PATH} git clone https://github.com/NLnetLabs/unbound.git -b ${UNBOUND_VERSION} ${UNBOUND_DIR_PATH} cd $UNBOUND_DIR_PATH diff --git a/scripts/macos/build_zmq.sh b/scripts/macos/build_zmq.sh index dd5623f06..15c31f248 100755 --- a/scripts/macos/build_zmq.sh +++ b/scripts/macos/build_zmq.sh @@ -5,7 +5,9 @@ ZMQ_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/libzmq" ZMQ_URL="https://github.com/zeromq/libzmq.git" -echo "============================ ZMQ ============================" +echo " +============================ ZMQ ============================ +" echo "Cloning ZMQ from - $ZMQ_URL" git clone $ZMQ_URL $ZMQ_PATH diff --git a/scripts/macos/gen.sh b/scripts/macos/gen.sh new file mode 100755 index 000000000..3d602c130 --- /dev/null +++ b/scripts/macos/gen.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +. ./gen_common.sh + +ARCH=`uname -m` +gen $ARCH \ No newline at end of file diff --git a/scripts/macos/gen_common.sh b/scripts/macos/gen_common.sh index 72ff638b6..95f74e748 100755 --- a/scripts/macos/gen_common.sh +++ b/scripts/macos/gen_common.sh @@ -15,6 +15,8 @@ gen_podspec() { gen_project() { ARCH=$1 CW_DIR="`pwd`/../../macos/Runner.xcodeproj" + BASE_FILENAME="project_base.pbxproj" + BASE_FILE_PATH="${CW_DIR}/${BASE_FILENAME}" DEFAULT_FILENAME="project.pbxproj" DEFAULT_FILE_PATH="${CW_DIR}/${DEFAULT_FILENAME}" universal_sed "s/ARCHS =.*/ARCHS = \"${ARCH}\";/g" $DEFAULT_FILE_PATH @@ -24,4 +26,4 @@ gen() { ARCH=$1 gen_podspec "${ARCH}" gen_project "${ARCH}" -} \ No newline at end of file +} diff --git a/tool/configure.dart b/tool/configure.dart index 259a5d9a0..d1c9326c8 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -11,6 +11,7 @@ const solanaOutputPath = 'lib/solana/solana.dart'; const tronOutputPath = 'lib/tron/tron.dart'; const wowneroOutputPath = 'lib/wownero/wownero.dart'; const zanoOutputPath = 'lib/zano/zano.dart'; +const decredOutputPath = 'lib/decred/decred.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -30,6 +31,7 @@ Future main(List args) async { final hasTron = args.contains('${prefix}tron'); final hasWownero = args.contains('${prefix}wownero'); final hasZano = args.contains('${prefix}zano'); + final hasDecred = args.contains('${prefix}decred'); final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); await generateBitcoin(hasBitcoin); @@ -44,6 +46,7 @@ Future main(List args) async { await generateWownero(hasWownero); await generateZano(hasZano); // await generateBanano(hasEthereum); + await generateDecred(hasDecred); await generatePubspec( hasMonero: hasMonero, @@ -59,6 +62,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await generateWalletTypes( hasMonero: hasMonero, @@ -73,6 +77,7 @@ Future main(List args) async { hasTron: hasTron, hasWownero: hasWownero, hasZano: hasZano, + hasDecred: hasDecred, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -1093,12 +1098,15 @@ abstract class BitcoinCash { """; const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n'; - const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; + const bitcoinCashCWDefinition = + 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; final output = '$bitcoinCashCommonHeaders\n' + (hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') + (hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') + - (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) + + (hasImplementation + ? bitcoinCashCWDefinition + : bitcoinCashEmptyDefinition) + '\n' + bitcoinCashContent; @@ -1233,7 +1241,8 @@ abstract class NanoUtil { """; const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n'; - const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; + const nanoCWDefinition = + 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; final output = '$nanoCommonHeaders\n' + (hasImplementation ? '$nanoCWHeaders\n' : '\n') + @@ -1482,6 +1491,85 @@ abstract class Zano { await outputFile.writeAsString(output); } +Future generateDecred(bool hasImplementation) async { + final outputFile = File(decredOutputPath); + const decredCommonHeaders = """ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/address_info.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/unspent_transaction_output.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:hive/hive.dart'; +"""; + const decredCWHeaders = """ +import 'package:cw_decred/transaction_priority.dart'; +import 'package:cw_decred/wallet.dart'; +import 'package:cw_decred/wallet_service.dart'; +import 'package:cw_decred/wallet_creation_credentials.dart'; +import 'package:cw_decred/amount_format.dart'; +import 'package:cw_decred/transaction_credentials.dart'; +import 'package:cw_decred/mnemonic.dart'; +"""; + const decredCwPart = "part 'cw_decred.dart';"; + const decredContent = """ + +abstract class Decred { + WalletCredentials createDecredNewWalletCredentials( + {required String name, WalletInfo? walletInfo}); + WalletCredentials createDecredRestoreWalletFromSeedCredentials( + {required String name, required String mnemonic, required String password}); + WalletCredentials createDecredRestoreWalletFromPubkeyCredentials( + {required String name, required String pubkey, required String password}); + WalletService createDecredWalletService( + Box walletInfoSource, Box unspentCoinSource); + + List getTransactionPriorities(); + TransactionPriority getDecredTransactionPriorityMedium(); + TransactionPriority getDecredTransactionPrioritySlow(); + TransactionPriority deserializeDecredTransactionPriority(int raw); + + Object createDecredTransactionCredentials(List outputs, TransactionPriority priority); + + List getAddressInfos(Object wallet); + Future updateAddress(Object wallet, String address, String label); + Future generateNewAddress(Object wallet, String label); + + String formatterDecredAmountToString({required int amount}); + double formatterDecredAmountToDouble({required int amount}); + int formatterStringDoubleToDecredAmount(String amount); + + List getUnspents(Object wallet); + void updateUnspents(Object wallet); + + int heightByDate(DateTime date); + + List getDecredWordList(); + + String pubkey(Object wallet); +} +"""; + + const decredEmptyDefinition = 'Decred? decred;\n'; + const decredCWDefinition = 'Decred? decred = CWDecred();\n'; + + final output = '$decredCommonHeaders\n' + + (hasImplementation ? '$decredCWHeaders\n' : '\n') + + (hasImplementation ? '$decredCwPart\n\n' : '\n') + + (hasImplementation ? decredCWDefinition : decredEmptyDefinition) + + '\n' + + decredContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1496,6 +1584,7 @@ Future generatePubspec({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { const cwCore = """ cw_core: @@ -1564,6 +1653,10 @@ Future generatePubspec({ cw_zano: path: ./cw_zano """; + const cwDecred = """ + cw_decred: + path: ./cw_decred + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -1613,6 +1706,10 @@ Future generatePubspec({ output += '\n$cwSharedExternal\n$cwHaven'; } + if (hasDecred) { + output += '\n$cwDecred'; + } + if (hasFlutterSecureStorage) { output += '\n$flutterSecureStorage\n'; } @@ -1654,6 +1751,7 @@ Future generateWalletTypes({ required bool hasTron, required bool hasWownero, required bool hasZano, + required bool hasDecred, }) async { final walletTypesFile = File(walletTypesPath); @@ -1709,6 +1807,10 @@ Future generateWalletTypes({ outputContent += '\tWalletType.banano,\n'; } + if (hasDecred) { + outputContent += '\tWalletType.decred,\n'; + } + if (hasWownero) { outputContent += '\tWalletType.wownero,\n'; }