diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 564034f05..e2b932202 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -92,6 +92,7 @@ jobs: cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs - name: Add secrets @@ -125,6 +126,7 @@ jobs: echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=$GITHUB_HEAD_REF" > /opt/android/cake_wallet/android/app.properties diff --git a/.gitignore b/.gitignore index 9fb7fd204..6fd8f33d6 100644 --- a/.gitignore +++ b/.gitignore @@ -8,6 +8,7 @@ .buildlog/ .history .svn/ +.fvm/ # IntelliJ related *.iml @@ -89,7 +90,9 @@ android/key.properties **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.ethereum-secrets-config.json **/lib/.secrets.g.dart +**/cw_ethereum/lib/.secrets.g.dart vendor/ @@ -120,6 +123,7 @@ cw_haven/android/.cxx/ lib/bitcoin/bitcoin.dart lib/monero/monero.dart lib/haven/haven.dart +lib/ethereum/ethereum.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_180.png ios/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_120.png diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index bc189f92c..b40aeb7c8 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -14,6 +14,8 @@ android:icon="@mipmap/ic_launcher" android:allowBackup="false" android:fullBackupContent="false" + android:versionCode="__versionCode__" + android:versionName="__versionName__" android:requestLegacyExternalStorage="true"> <activity android:name=".MainActivity" diff --git a/android/gradle.properties b/android/gradle.properties index 38c8d4544..a5965ab8d 100644 --- a/android/gradle.properties +++ b/android/gradle.properties @@ -1,4 +1,4 @@ org.gradle.jvmargs=-Xmx1536M android.enableR8=true android.useAndroidX=true -android.enableJetifier=true +android.enableJetifier=true \ No newline at end of file diff --git a/assets/ethereum_server_list.yml b/assets/ethereum_server_list.yml new file mode 100644 index 000000000..125085d88 --- /dev/null +++ b/assets/ethereum_server_list.yml @@ -0,0 +1,10 @@ +- + uri: ethereum.publicnode.com +- + uri: eth.llamarpc.com +- + uri: rpc.flashbots.net +- + uri: eth-mainnet.public.blastapi.io +- + uri: ethereum.publicnode.com \ No newline at end of file diff --git a/assets/images/home_screen_settings_icon.png b/assets/images/home_screen_settings_icon.png new file mode 100644 index 000000000..6c750f5f6 Binary files /dev/null and b/assets/images/home_screen_settings_icon.png differ diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 6b5d8affe..403bb9f08 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,6 +1,8 @@ -Improved edit/delete for nodes and wallets -Wallets can now be renamed -Accessibility improvements -Improve Monero wallet rescan -Additional exchange assets: SHIB, AAVE, ARB, BAT, COMP, CRO, ENS, FTM, FRAX, GUSD, GTC, GRT, LDO, NEXO, CAKE, PEPE, STORJ, TUSD, WBTC, WETH, ZRX, DYDX, STETH -Cake Pay is temporarily removed, see https://cakelabs.com/news/cake-pay-mobile-to-shut-down/ \ No newline at end of file +Monero background syncing! See https://guides.cakewallet.com/docs/monero/#background-syncing +Cake 2FA access control settings! See https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa-presets-and-access-control-settings +Support Monero node proxy +UI improvements when sending to Address Book entry +Allow renaming Monero account names +Send templates now support multiple recipients (try using to make Monero change) +Onramper improvements +Scan node QR codes (for Umbrel) \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 01e7da1cf..cedec7b7f 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,7 +1,9 @@ -Improved edit/delete for nodes and wallets -Wallets can now be renamed -Accessibility improvements -Bitcoin transaction bug fixes -Improve Monero wallet rescan -Additional exchange assets: SHIB, AAVE, ARB, BAT, COMP, CRO, ENS, FTM, FRAX, GUSD, GTC, GRT, LDO, NEXO, CAKE, PEPE, STORJ, TUSD, WBTC, WETH, ZRX, DYDX, STETH -Cake Pay is temporarily removed, see https://cakelabs.com/news/cake-pay-mobile-to-shut-down/ \ No newline at end of file +Ethereum! Store ETH and ERC-20 tokens +Monero background syncing! See https://guides.cakewallet.com/docs/monero/#background-syncing +Cake 2FA access control settings! See https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa-presets-and-access-control-settings +Support Monero node proxy +UI improvements when sending to Address Book entry +Allow renaming Monero/Haven account names +Send templates now support multiple recipients (try using to make Monero change) +Onramper improvements +Scan node QR codes (for Umbrel) \ No newline at end of file diff --git a/configure_cake_wallet_android.sh b/configure_cake_wallet_android.sh new file mode 100644 index 000000000..b80ebc46e --- /dev/null +++ b/configure_cake_wallet_android.sh @@ -0,0 +1,10 @@ +cd scripts/android +source ./app_env.sh cakewallet +./app_config.sh +cd ../.. && flutter pub get +cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +flutter packages pub run build_runner build --delete-conflicting-outputs diff --git a/cw_bitcoin/lib/electrum_transaction_history.dart b/cw_bitcoin/lib/electrum_transaction_history.dart index 553795470..be039fa36 100644 --- a/cw_bitcoin/lib/electrum_transaction_history.dart +++ b/cw_bitcoin/lib/electrum_transaction_history.dart @@ -1,7 +1,6 @@ import 'dart:convert'; import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; -import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_bitcoin/file.dart'; @@ -67,7 +66,7 @@ abstract class ElectrumTransactionHistoryBase Future<void> _load() async { try { final content = await _read(); - final txs = content['transactions'] as Map<String, dynamic> ?? {}; + final txs = content['transactions'] as Map<String, dynamic>? ?? {}; txs.entries.forEach((entry) { final val = entry.value; diff --git a/cw_bitcoin/lib/electrum_transaction_info.dart b/cw_bitcoin/lib/electrum_transaction_info.dart index b034c06b1..bf5ec2c4f 100644 --- a/cw_bitcoin/lib/electrum_transaction_info.dart +++ b/cw_bitcoin/lib/electrum_transaction_info.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitcoin_flutter/src/payments/index.dart' show PaymentData; import 'package:cw_bitcoin/address_from_output.dart'; @@ -217,9 +216,9 @@ class ElectrumTransactionInfo extends TransactionInfo { height: info.height, amount: info.amount, fee: info.fee, - direction: direction ?? info.direction, - date: date ?? info.date, - isPending: isPending ?? info.isPending, + direction: direction, + date: date, + isPending: isPending, confirmations: info.confirmations); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index aadf87572..f9437e668 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -431,6 +431,7 @@ abstract class ElectrumWalletBase extends WalletBase<ElectrumBalance, await transactionHistory.save(); } + @override Future<void> renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); final currentWalletFile = File(currentWalletPath); diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 455ceb4a7..481a41ac5 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -27,7 +27,7 @@ dependencies: unorm_dart: ^0.2.0 cryptography: ^2.0.5 encrypt: ^5.0.1 - + dev_dependencies: flutter_test: sdk: flutter diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index 3904fc049..8ac8c1fc6 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -11,6 +11,8 @@ CryptoCurrency currencyForWalletType(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency currencyForWalletType'); } diff --git a/cw_core/lib/erc20_token.dart b/cw_core/lib/erc20_token.dart new file mode 100644 index 000000000..2e205e484 --- /dev/null +++ b/cw_core/lib/erc20_token.dart @@ -0,0 +1,64 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:hive/hive.dart'; + +part 'erc20_token.g.dart'; + +@HiveType(typeId: Erc20Token.typeId) +class Erc20Token extends CryptoCurrency with HiveObjectMixin { + @HiveField(0) + final String name; + @HiveField(1) + final String symbol; + @HiveField(2) + final String contractAddress; + @HiveField(3) + final int decimal; + @HiveField(4, defaultValue: true) + bool _enabled; + @HiveField(5) + final String? iconPath; + + bool get enabled => _enabled; + + set enabled(bool value) => _enabled = value; + + Erc20Token({ + required this.name, + required this.symbol, + required this.contractAddress, + required this.decimal, + bool enabled = true, + this.iconPath, + }) : _enabled = enabled, + super( + name: symbol.toLowerCase(), + title: symbol.toUpperCase(), + fullName: name, + tag: "ETH", + iconPath: iconPath, + ); + + Erc20Token.copyWith(Erc20Token other, String? icon) + : this.name = other.name, + this.symbol = other.symbol, + this.contractAddress = other.contractAddress, + this.decimal = other.decimal, + this._enabled = other.enabled, + this.iconPath = icon, + super( + name: other.name, + title: other.symbol.toUpperCase(), + fullName: other.name, + tag: "ETH", + iconPath: icon, + ); + + static const typeId = 12; + static const boxName = 'Erc20Tokens'; + + @override + bool operator ==(other) => other is Erc20Token && other.contractAddress == contractAddress; + + @override + int get hashCode => contractAddress.hashCode; +} diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 0848e8d94..3fa45b44c 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -75,6 +75,8 @@ class Node extends HiveObject with Keyable { return createUriFromElectrumAddress(uriRaw); case WalletType.haven: return Uri.http(uriRaw, ''); + case WalletType.ethereum: + return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -124,6 +126,8 @@ class Node extends HiveObject with Keyable { return requestElectrumServer(); case WalletType.haven: return requestMoneroNode(); + case WalletType.ethereum: + return requestElectrumServer(); default: return false; } @@ -166,7 +170,7 @@ class Node extends HiveObject with Keyable { } catch (_) { return false; } -} + } Future<bool> requestNodeWithProxy(String proxy) async { @@ -193,4 +197,17 @@ class Node extends HiveObject with Keyable { return false; } } + + Future<bool> requestEthereumServer() async { + try { + final response = await http.get( + uri, + headers: {'Content-Type': 'application/json'}, + ); + + return response.statusCode >= 200 && response.statusCode < 300; + } catch (_) { + return false; + } + } } diff --git a/cw_core/lib/set_app_secure_native.dart b/cw_core/lib/set_app_secure_native.dart index 09e01556c..84096e2d6 100644 --- a/cw_core/lib/set_app_secure_native.dart +++ b/cw_core/lib/set_app_secure_native.dart @@ -1,7 +1,9 @@ import 'package:flutter/services.dart'; -const utils = const MethodChannel('com.cake_wallet/native_utils'); - void setIsAppSecureNative(bool isAppSecure) { - utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure}); -} \ No newline at end of file + try { + final utils = const MethodChannel('com.cake_wallet/native_utils'); + + utils.invokeMethod<Uint8List>('setIsAppSecure', {'isAppSecure': isAppSecure}); + } catch (_) {} +} diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index e5f84f467..019f87631 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -75,4 +75,6 @@ abstract class WalletBase< Future<void>? updateBalance(); void setExceptionHandler(void Function(FlutterErrorDetails) onError) => null; + + Future<void> renameWalletFiles(String newWalletName); } diff --git a/cw_core/lib/wallet_service.dart b/cw_core/lib/wallet_service.dart index 5e216e225..f95bc1a44 100644 --- a/cw_core/lib/wallet_service.dart +++ b/cw_core/lib/wallet_service.dart @@ -18,5 +18,5 @@ abstract class WalletService<N extends WalletCredentials, Future<void> remove(String wallet); - Future<void> rename(String name, String password, String newName); + Future<void> rename(String currentName, String password, String newName); } diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 61a571fcf..a65839041 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -7,7 +7,8 @@ const walletTypes = [ WalletType.monero, WalletType.bitcoin, WalletType.litecoin, - WalletType.haven + WalletType.haven, + WalletType.ethereum, ]; const walletTypeTypeId = 5; @@ -27,6 +28,9 @@ enum WalletType { @HiveField(4) haven, + + @HiveField(5) + ethereum, } int serializeToInt(WalletType type) { @@ -39,6 +43,8 @@ int serializeToInt(WalletType type) { return 2; case WalletType.haven: return 3; + case WalletType.ethereum: + return 4; default: return -1; } @@ -54,6 +60,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.litecoin; case 3: return WalletType.haven; + case 4: + return WalletType.ethereum; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -69,6 +77,8 @@ String walletTypeToString(WalletType type) { return 'Litecoin'; case WalletType.haven: return 'Haven'; + case WalletType.ethereum: + return 'Ethereum'; default: return ''; } @@ -84,6 +94,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Litecoin (LTC)'; case WalletType.haven: return 'Haven (XHV)'; + case WalletType.ethereum: + return 'Ethereum (ETH)'; default: return ''; } @@ -99,6 +111,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.ltc; case WalletType.haven: return CryptoCurrency.xhv; + case WalletType.ethereum: + return CryptoCurrency.eth; default: throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } diff --git a/cw_ethereum/.gitignore b/cw_ethereum/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_ethereum/.gitignore @@ -0,0 +1,30 @@ +# 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/ diff --git a/cw_ethereum/.metadata b/cw_ethereum/.metadata new file mode 100644 index 000000000..1e05dac7f --- /dev/null +++ b/cw_ethereum/.metadata @@ -0,0 +1,10 @@ +# 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 and should not be manually edited. + +version: + revision: eb6d86ee27deecba4a83536aa20f366a6044895c + channel: stable + +project_type: package diff --git a/cw_ethereum/CHANGELOG.md b/cw_ethereum/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_ethereum/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_ethereum/LICENSE b/cw_ethereum/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_ethereum/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_ethereum/README.md b/cw_ethereum/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_ethereum/README.md @@ -0,0 +1,39 @@ +<!-- +This README describes the package. If you publish this package to pub.dev, +this README's contents appear on the landing page for your package. + +For information about how to write a good package README, see the guide for +[writing package pages](https://dart.dev/guides/libraries/writing-package-pages). + +For general information about developing packages, see the Dart guide for +[creating packages](https://dart.dev/guides/libraries/create-library-packages) +and the Flutter guide for +[developing packages and plugins](https://flutter.dev/developing-packages). +--> + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/cw_ethereum/analysis_options.yaml b/cw_ethereum/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_ethereum/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_ethereum/lib/cw_ethereum.dart b/cw_ethereum/lib/cw_ethereum.dart new file mode 100644 index 000000000..af9ea7ee0 --- /dev/null +++ b/cw_ethereum/lib/cw_ethereum.dart @@ -0,0 +1,7 @@ +library cw_ethereum; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_ethereum/lib/default_erc20_tokens.dart b/cw_ethereum/lib/default_erc20_tokens.dart new file mode 100644 index 000000000..241e301ce --- /dev/null +++ b/cw_ethereum/lib/default_erc20_tokens.dart @@ -0,0 +1,302 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultErc20Tokens { + final List<Erc20Token> _defaultTokens = [ + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USDT Tether", + symbol: "USDT", + contractAddress: "0xdac17f958d2ee523a2206206994597c13d831ec7", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "Dai", + symbol: "DAI", + contractAddress: "0x6B175474E89094C44Da98b954EedeAC495271d0F", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Pepe", + symbol: "PEPE", + contractAddress: "0x6982508145454ce325ddbe47a25d4ec3d2311933", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "SHIBA INU", + symbol: "SHIB", + contractAddress: "0x95ad61b0a150d79219dcf64e1e6cc01f0b64c4ce", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ApeCoin", + symbol: "APE", + contractAddress: "0x4d224452801aced8b2f0aebe155379bb5d594381", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Matic Token", + symbol: "MATIC", + contractAddress: "0x7D1AfA7B718fb893dB30A3aBc0Cfc608AaCfeBB0", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC", + symbol: "WBTC", + contractAddress: "0x2260fac5e5542a773aa44fbcfedf7c193bc2c599", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Gitcoin", + symbol: "GTC", + contractAddress: "0xde30da39c46104798bb5aa3fe8b9e0e1f348163f", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Compound", + symbol: "COMP", + contractAddress: "0xc00e94cb662c3520282e6f5717214004a7f26888", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Aave Token", + symbol: "AAVE", + contractAddress: "0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap", + symbol: "UNI", + contractAddress: "0x1f9840a85d5af5bf1d1762f925bdaddc4201f984", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Decentraland", + symbol: "MANA", + contractAddress: "0x0F5D2fB29fb7d3CFeE444a200298f468908cC942", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Storj", + symbol: "STORJ", + contractAddress: "0xb64ef51c888972c908cfacf59b47c1afbc0ab8ac", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Maker", + symbol: "MKR", + contractAddress: "0x9f8F72aA9304c8B593d555F12eF6589cC3A579A2", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Orchid", + symbol: "OXT", + contractAddress: "0x4575f41308EC1483f3d399aa9a2826d74Da13Deb", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Paxos Gold", + symbol: "PAXG", + contractAddress: "0x45804880De22913dAFE09f4980848ECE6EcbAf78", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Binance Coin", + symbol: "BNB", + contractAddress: "0xB8c77482e45F1F44dE1745F52C74426C631bDD52", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "stETH", + symbol: "stETH", + contractAddress: "0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Lido DAO", + symbol: "LDO", + contractAddress: "0x5A98FcBEA516Cf06857215779Fd812CA3beF1B32", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Arbitrum", + symbol: "ARB", + contractAddress: "0xB50721BCf8d664c30412Cfbc6cf7a15145234ad1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Graph Token", + symbol: "GRT", + contractAddress: "0xc944E90C64B2c07662A292be6244BDf05Cda44a7", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Frax", + symbol: "FRAX", + contractAddress: "0x853d955aCEf822Db058eb8505911ED77F175b99e", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Gemini dollar", + symbol: "GUSD", + contractAddress: "0x056Fd409E1d7A124BD7017459dFEa2F387b6d5Cd", + decimal: 2, + enabled: false, + ), + Erc20Token( + name: "Compound Ether", + symbol: "cETH", + contractAddress: "0x4Ddc2D193948926D02f9B1fE9e1daa0718270ED5", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Binance USD", + symbol: "BUSD", + contractAddress: "0x4Fabb145d64652a948d72533023f6E7A623C7C53", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "TrueUSD", + symbol: "TUSD", + contractAddress: "0x0000000000085d4780B73119b644AE5ecd22b376", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Cronos Coin", + symbol: "CRO", + contractAddress: "0xA0b73E1Ff0B80914AB6fe0444E65848C4C34450b", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Pax Dollar", + symbol: "USDP", + contractAddress: "0x8E870D67F660D95d5be530380D0eC0bd388289E1", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Fantom Token", + symbol: "FTM", + contractAddress: "0x4E15361FD6b4BB609Fa63C81A2be19d873717870", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BitTorrent", + symbol: "BTT", + contractAddress: "0xC669928185DbCE49d2230CC9B0979BE6DC797957", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Nexo", + symbol: "NEXO", + contractAddress: "0xB62132e35a6c13ee1EE0f84dC5d40bad8d815206", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "dYdX", + symbol: "DYDX", + contractAddress: "0x92D6C1e31e14520e676a687F0a93788B716BEff5", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "PancakeSwap Token", + symbol: "Cake", + contractAddress: "0x152649eA73beAb28c5b49B26eb48f7EAD6d4c898", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "BAT", + symbol: "BAT", + contractAddress: "0x0D8775F648430679A709E98d2b0Cb6250d2887EF", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "1INCH Token", + symbol: "1INCH", + contractAddress: "0x111111111117dC0aa78b770fA6A738034120C302", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Ethereum Name Service", + symbol: "ENS", + contractAddress: "0xC18360217D8F7Ab5e7c516566761Ea12Ce7F9D72", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "ZRX", + symbol: "ZRX", + contractAddress: "0xE41d2489571d322189246DaFA5ebDe1F4699F498", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Verse", + symbol: "VERSE", + contractAddress: "0x249cA82617eC3DfB2589c4c17ab7EC9765350a18", + decimal: 18, + enabled: false, + ), + ]; + + List<Erc20Token> get initialErc20Tokens => _defaultTokens.map((token) { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + if (iconPath != null) { + return Erc20Token.copyWith(token, iconPath); + } + + return token; + }).toList(); +} diff --git a/cw_ethereum/lib/erc20_balance.dart b/cw_ethereum/lib/erc20_balance.dart new file mode 100644 index 000000000..7d11f8e45 --- /dev/null +++ b/cw_ethereum/lib/erc20_balance.dart @@ -0,0 +1,47 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/balance.dart'; + +class ERC20Balance extends Balance { + ERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), + balance.toInt()); + + final BigInt balance; + final int exponent; + + @override + String get formattedAdditionalBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + @override + String get formattedAvailableBalance { + final String formattedBalance = (balance / BigInt.from(10).pow(exponent)).toString(); + return formattedBalance.substring(0, min(12, formattedBalance.length)); + } + + String toJSON() => json.encode({ + 'balanceInWei': balance.toString(), + 'exponent': exponent, + }); + + static ERC20Balance? fromJSON(String? jsonSource) { + if (jsonSource == null) { + return null; + } + + final decoded = json.decode(jsonSource) as Map; + + try { + return ERC20Balance( + BigInt.parse(decoded['balanceInWei']), + exponent: decoded['exponent'], + ); + } catch (e) { + return ERC20Balance(BigInt.zero); + } + } +} diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart new file mode 100644 index 000000000..f00e2ef7b --- /dev/null +++ b/cw_ethereum/lib/ethereum_client.dart @@ -0,0 +1,230 @@ +import 'dart:async'; +import 'dart:convert'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/pending_ethereum_transaction.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:web3dart/contracts/erc20.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/.secrets.g.dart' as secrets; + +class EthereumClient { + final _httpClient = Client(); + Web3Client? _client; + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), _httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future<EtherAmount> getBalance(EthereumAddress address) async => + await _client!.getBalance(address); + + Future<int> getGasUnitPrice() async { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } + + Future<int> getEstimatedGas() async { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } + + Future<PendingEthereumTransaction> signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EthereumTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + }) async { + assert(currency == CryptoCurrency.eth || contractAddress != null); + + bool _isEthereum = currency == CryptoCurrency.eth; + + final price = await _client!.getGasPrice(); + + final Transaction transaction = Transaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxGas: gas, + gasPrice: price, + maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), + value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + ); + + final signedTransaction = await _client!.signTransaction(privateKey, transaction); + + final Function _sendTransaction; + + if (_isEthereum) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = Erc20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + BigInt.parse(amount), + credentials: privateKey, + ); + }; + } + + return PendingEthereumTransaction( + signedTransaction: signedTransaction, + amount: amount, + fee: BigInt.from(gas) * price.getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Future<String> sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(signedTransaction); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + + // Print the receipt information + print('Transaction Hash: ${receipt.transactionHash}'); + print('Block Hash: ${receipt.blockHash}'); + print('Block Number: ${receipt.blockNumber}'); + print('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] +I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + print("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(Duration(seconds: 1)); + } + // Print the receipt information + print('Transaction Hash: ${transactionInformation.hash}'); + print('Block Hash: ${transactionInformation.blockHash}'); + print('Block Number: ${transactionInformation.blockNumber}'); + print('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 +I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 +I/flutter ( 4474): Block Number: 17120242 +I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future<ERC20Balance> fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return ERC20Balance(balance, exponent: exponent); + } + + Future<Erc20Token?> getErc20Token(String contractAddress) async { + try { + final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + void stop() { + _client?.dispose(); + } + + Future<List<EthereumTransactionModel>> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.etherScanApiKey, + })); + + final _jsonResponse = json.decode(response.body) as Map<String, dynamic>; + + if (response.statusCode >= 200 && response.statusCode < 300 && _jsonResponse['status'] != 0) { + return (_jsonResponse['result'] as List) + .map((e) => EthereumTransactionModel.fromJson(e as Map<String, dynamic>)) + .toList(); + } + + return []; + } catch (e) { + print(e); + return []; + } + } + +// Future<int> _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_ethereum/lib/ethereum_exceptions.dart b/cw_ethereum/lib/ethereum_exceptions.dart new file mode 100644 index 000000000..518f46275 --- /dev/null +++ b/cw_ethereum/lib/ethereum_exceptions.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EthereumTransactionCreationException implements Exception { + final String exceptionMessage; + + EthereumTransactionCreationException(CryptoCurrency currency) : + this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_ethereum/lib/ethereum_formatter.dart b/cw_ethereum/lib/ethereum_formatter.dart new file mode 100644 index 000000000..468c536f8 --- /dev/null +++ b/cw_ethereum/lib/ethereum_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const ethereumAmountLength = 12; +const ethereumAmountDivider = 1000000000000; +final ethereumAmountFormat = NumberFormat() + ..maximumFractionDigits = ethereumAmountLength + ..minimumFractionDigits = 1; + +class EthereumFormatter { + static int parseEthereumAmount(String amount) { + try { + return (double.parse(amount) * ethereumAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEthereumAmountToDouble(int amount) { + try { + return amount / ethereumAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics.dart b/cw_ethereum/lib/ethereum_mnemonics.dart new file mode 100644 index 000000000..8af7b10f3 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics.dart @@ -0,0 +1,2058 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} + +class EthereumMnemonics { + static const englishWordlist = <String>[ + '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_ethereum/lib/ethereum_transaction_credentials.dart b/cw_ethereum/lib/ethereum_transaction_credentials.dart new file mode 100644 index 000000000..b015b7141 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_credentials.dart @@ -0,0 +1,17 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + +class EthereumTransactionCredentials { + EthereumTransactionCredentials( + this.outputs, { + required this.priority, + required this.currency, + this.feeRate, + }); + + final List<OutputInfo> outputs; + final EthereumTransactionPriority? priority; + final int? feeRate; + final CryptoCurrency currency; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart new file mode 100644 index 000000000..4511f4436 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:core'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; + +part 'ethereum_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'transactions.json'; + +class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; + +abstract class EthereumTransactionHistoryBase + extends TransactionHistoryBase<EthereumTransactionInfo> with Store { + EthereumTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap<String, EthereumTransactionInfo>(); + } + + final WalletInfo walletInfo; + String _password; + + Future<void> init() async => await _load(); + + @override + Future<void> save() async { + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + print('Error while save ethereum transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map<String, EthereumTransactionInfo> transactions) => + this.transactions.addAll(transactions); + + Future<Map<String, dynamic>> _read() async { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + final path = '$dirPath/$transactionsHistoryFileName'; + final content = await read(path: path, password: _password); + if (content.isEmpty) { + return {}; + } + return json.decode(content) as Map<String, dynamic>; + } + + Future<void> _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map<String, dynamic>? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map<String, dynamic>) { + final tx = EthereumTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart new file mode 100644 index 000000000..efdc61407 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -0,0 +1,74 @@ +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +class EthereumTransactionInfo extends TransactionInfo { + EthereumTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + this.tokenSymbol = "ETH", + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + }) : this.amount = ethAmount.toInt(), + this.fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + + @override + String amountFormatted() => + '${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol'; + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH'; + + factory EthereumTransactionInfo.fromJson(Map<String, dynamic> data) { + return EthereumTransactionInfo( + id: data['id'] as String, + height: data['height'] as int, + ethAmount: BigInt.parse(data['amount']), + exponent: data['exponent'] as int, + ethFee: BigInt.parse(data['fee']), + direction: parseTransactionDirectionFromInt(data['direction'] as int), + date: DateTime.fromMillisecondsSinceEpoch(data['date'] as int), + isPending: data['isPending'] as bool, + confirmations: data['confirmations'] as int, + tokenSymbol: data['tokenSymbol'] as String, + ); + } + + Map<String, dynamic> toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + }; +} diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_ethereum/lib/ethereum_transaction_model.dart new file mode 100644 index 000000000..c1260795a --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_model.dart @@ -0,0 +1,47 @@ +class EthereumTransactionModel { + final DateTime date; + final String hash; + final String from; + final String to; + final BigInt amount; + final int gasUsed; + final BigInt gasPrice; + final String contractAddress; + final int confirmations; + final int blockNumber; + final String? tokenSymbol; + final int? tokenDecimal; + final bool isError; + + EthereumTransactionModel({ + required this.date, + required this.hash, + required this.from, + required this.to, + required this.amount, + required this.gasUsed, + required this.gasPrice, + required this.contractAddress, + required this.confirmations, + required this.blockNumber, + required this.tokenSymbol, + required this.tokenDecimal, + required this.isError, + }); + + factory EthereumTransactionModel.fromJson(Map<String, dynamic> json) => EthereumTransactionModel( + date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), + hash: json["hash"], + from: json["from"], + to: json["to"], + amount: BigInt.parse(json["value"]), + gasUsed: int.parse(json["gasUsed"]), + gasPrice: BigInt.parse(json["gasPrice"]), + contractAddress: json["contractAddress"], + confirmations: int.parse(json["confirmations"]), + blockNumber: int.parse(json["blockNumber"]), + tokenSymbol: json["tokenSymbol"] ?? "ETH", + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_ethereum/lib/ethereum_transaction_priority.dart b/cw_ethereum/lib/ethereum_transaction_priority.dart new file mode 100644 index 000000000..ff5668397 --- /dev/null +++ b/cw_ethereum/lib/ethereum_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EthereumTransactionPriority extends TransactionPriority { + final int tip; + + const EthereumTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List<EthereumTransactionPriority> all = [fast, medium, slow]; + static const EthereumTransactionPriority slow = + EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EthereumTransactionPriority medium = + EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EthereumTransactionPriority fast = + EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EthereumTransactionPriority 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 EthereumTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EthereumTransactionPriority.slow: + label = 'Slow'; + break; + case EthereumTransactionPriority.medium: + label = 'Medium'; + break; + case EthereumTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart new file mode 100644 index 000000000..46cb5c39f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -0,0 +1,473 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_ethereum/default_erc20_tokens.dart'; +import 'package:cw_ethereum/erc20_balance.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_exceptions.dart'; +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_ethereum/ethereum_transaction_history.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:hive/hive.dart'; +import 'package:hex/hex.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; + +part 'ethereum_wallet.g.dart'; + +class EthereumWallet = EthereumWalletBase with _$EthereumWallet; + +abstract class EthereumWalletBase + extends WalletBase<ERC20Balance, EthereumTransactionHistory, EthereumTransactionInfo> + with Store { + EthereumWalletBase({ + required WalletInfo walletInfo, + required String mnemonic, + required String password, + ERC20Balance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _isTransactionUpdating = false, + _client = EthereumClient(), + walletAddresses = EthereumWalletAddresses(walletInfo), + balance = ObservableMap<CryptoCurrency, ERC20Balance>.of( + {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password); + + if (!Hive.isAdapterRegistered(Erc20Token.typeId)) { + Hive.registerAdapter(Erc20TokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String _mnemonic; + final String _password; + + late final Box<Erc20Token> erc20TokensBox; + + late final EthPrivateKey _privateKey; + + late EthereumClient _client; + + int? _gasPrice; + int? _estimatedGas; + bool _isTransactionUpdating; + + // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter + Timer? _transactionsUpdateTimer; + + @override + WalletAddresses walletAddresses; + + @override + @observable + SyncStatus syncStatus; + + @override + @observable + late ObservableMap<CryptoCurrency, ERC20Balance> balance; + + Completer<SharedPreferences> _sharedPrefs = Completer(); + + Future<void> init() async { + erc20TokensBox = await Hive.openBox<Erc20Token>(Erc20Token.boxName); + await walletAddresses.init(); + await transactionHistory.init(); + _privateKey = await getPrivateKey(_mnemonic, _password); + walletAddresses.address = _privateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EthereumTransactionPriority) { + final priorityFee = + EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future<void> changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future<void> connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Ethereum Node connection failed"); + } + + _client.setListeners(_privateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future<PendingTransaction> createTransaction(Object credentials) async { + final _credentials = credentials as EthereumTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + final _erc20Balance = balance[_credentials.currency]!; + BigInt totalAmount = BigInt.zero; + int exponent = + _credentials.currency is Erc20Token ? (_credentials.currency as Erc20Token).decimal : 18; + num amountToEthereumMultiplier = pow(10, exponent); + + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EthereumTransactionCreationException(_credentials.currency); + } + + final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } else { + final output = outputs.first; + final BigInt allAmount = + _erc20Balance.balance - BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + final totalOriginalAmount = + EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EthereumTransactionCreationException(_credentials.currency); + } + } + + final pendingEthereumTransaction = await _client.signTransaction( + privateKey: _privateKey, + toAddress: _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: _credentials.currency, + exponent: exponent, + contractAddress: _credentials.currency is Erc20Token + ? (_credentials.currency as Erc20Token).contractAddress + : null, + ); + + return pendingEthereumTransaction; + } + + Future<void> _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true; + if (!isEtherscanEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future<Map<String, EthereumTransactionInfo>> fetchTransactions() async { + final address = _privateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List<Future<List<EthereumTransactionModel>>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map<String, EthereumTransactionInfo> result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + ); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future<void> rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future<void> save() async { + await walletAddresses.updateAddressesInBox(); + final path = await makePath(); + await write(path: path, password: _password, data: toJSON()); + await transactionHistory.save(); + } + + @override + String get seed => _mnemonic; + + @action + @override + Future<void> startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + await _updateBalance(); + await _updateTransactions(); + _gasPrice = await _client.getGasUnitPrice(); + _estimatedGas = await _client.getEstimatedGas(); + + Timer.periodic( + const Duration(minutes: 1), (timer) async => _gasPrice = await _client.getGasUnitPrice()); + Timer.periodic(const Duration(seconds: 10), + (timer) async => _estimatedGas = await _client.getEstimatedGas()); + + syncStatus = SyncedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + Future<String> makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'balance': balance[currency]!.toJSON(), + }); + + static Future<EthereumWallet> open({ + required String name, + required String password, + required WalletInfo walletInfo, + }) async { + final path = await pathForWallet(name: name, type: walletInfo.type); + final jsonSource = await read(path: path, password: password); + final data = json.decode(jsonSource) as Map; + final mnemonic = data['mnemonic'] as String; + final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? ERC20Balance(BigInt.zero); + + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + initialBalance: balance, + ); + } + + Future<void> _updateBalance() async { + balance[currency] = await _fetchEthBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future<ERC20Balance> _fetchEthBalance() async { + final balance = await _client.getBalance(_privateKey.address); + return ERC20Balance(balance.getInWei); + } + + Future<void> _fetchErc20Balances() async { + for (var token in erc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _privateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future<EthPrivateKey> getPrivateKey(String mnemonic, String password) async { + final seed = bip39.mnemonicToSeed(mnemonic); + + final root = bip32.BIP32.fromSeed(seed); + + const _hdPathEthereum = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$_hdPathEthereum/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List<int>)); + } + + Future<void>? updateBalance() async => await _updateBalance(); + + List<Erc20Token> get erc20Currencies => erc20TokensBox.values.toList(); + + Future<void> addErc20Token(Erc20Token token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final _token = Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + iconPath: iconPath, + ); + + await erc20TokensBox.put(_token.contractAddress, _token); + + if (_token.enabled) { + balance[_token] = await _client.fetchERC20Balances( + _privateKey.address, + _token.contractAddress, + ); + } else { + balance.remove(_token); + } + } + + Future<void> deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future<Erc20Token?> getErc20Token(String contractAddress) async => + await _client.getErc20Token(contractAddress); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + void addInitialTokens() { + final initialErc20Tokens = DefaultErc20Tokens().initialErc20Tokens; + + initialErc20Tokens.forEach((token) => erc20TokensBox.put(token.contractAddress, token)); + } + + @override + Future<void> renameWalletFiles(String newWalletName) async { + final currentWalletPath = await pathForWallet(name: walletInfo.name, type: type); + final currentWalletFile = File(currentWalletPath); + + final currentDirPath = await pathForWalletDir(name: walletInfo.name, type: type); + final currentTransactionsFile = File('$currentDirPath/$transactionsHistoryFileName'); + + // Copies current wallet files into new wallet name's dir and files + if (currentWalletFile.existsSync()) { + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + await currentWalletFile.copy(newWalletPath); + } + if (currentTransactionsFile.existsSync()) { + final newDirPath = await pathForWalletDir(name: newWalletName, type: type); + await currentTransactionsFile.copy('$newDirPath/$transactionsHistoryFileName'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(Duration(seconds: 10), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + void updateEtherscanUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_ethereum/lib/ethereum_wallet_addresses.dart new file mode 100644 index 000000000..4a3492e6f --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_addresses.dart @@ -0,0 +1,33 @@ +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:mobx/mobx.dart'; + +part 'ethereum_wallet_addresses.g.dart'; + +class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; + +abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { + EthereumWalletAddressesBase(WalletInfo walletInfo) + : address = '', + super(walletInfo); + + @override + String address; + + @override + Future<void> init() async { + address = walletInfo.address; + await updateAddressesInBox(); + } + + @override + Future<void> updateAddressesInBox() async { + try { + addressesMap.clear(); + addressesMap[address] = ''; + await saveAddressesInBox(); + } catch (e) { + print(e.toString()); + } + } +} diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart new file mode 100644 index 000000000..12d0d53e2 --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart @@ -0,0 +1,23 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EthereumNewWalletCredentials extends WalletCredentials { + EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { + EthereumRestoreWalletFromSeedCredentials( + {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials { + EthereumRestoreWalletFromWIFCredentials( + {required String name, required String password, required this.wif, WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String wif; +} diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart new file mode 100644 index 000000000..318f287fc --- /dev/null +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -0,0 +1,108 @@ +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:collection/collection.dart'; + +class EthereumWalletService extends WalletService<EthereumNewWalletCredentials, + EthereumRestoreWalletFromSeedCredentials, EthereumRestoreWalletFromWIFCredentials> { + EthereumWalletService(this.walletInfoSource); + + final Box<WalletInfo> walletInfoSource; + + @override + Future<EthereumWallet> create(EthereumNewWalletCredentials credentials) async { + final mnemonic = bip39.generateMnemonic(); + final wallet = EthereumWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + WalletType getType() => WalletType.ethereum; + + @override + Future<bool> isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future<EthereumWallet> openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await EthereumWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @override + Future<void> 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<EthereumWallet> restoreFromKeys(credentials) { + throw UnimplementedError(); + } + + @override + Future<EthereumWallet> restoreFromSeed( + EthereumRestoreWalletFromSeedCredentials credentials) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = EthereumWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future<void> rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWalletBase.open( + password: password, name: currentName, walletInfo: currentWalletInfo); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); + } +} diff --git a/cw_ethereum/lib/file.dart b/cw_ethereum/lib/file.dart new file mode 100644 index 000000000..8fd236ec3 --- /dev/null +++ b/cw_ethereum/lib/file.dart @@ -0,0 +1,39 @@ +import 'dart:io'; +import 'package:cw_core/key.dart'; +import 'package:encrypt/encrypt.dart' as encrypt; + +Future<void> write( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future<void> writeData( + {required String path, + required String password, + required String data}) async { + final keys = extractKeys(password); + final key = encrypt.Key.fromBase64(keys.first); + final iv = encrypt.IV.fromBase64(keys.last); + final encrypted = await encode(key: key, iv: iv, data: data); + final f = File(path); + f.writeAsStringSync(encrypted); +} + +Future<String> read({required String path, required String password}) async { + final file = File(path); + + if (!file.existsSync()) { + file.createSync(); + } + + final encrypted = file.readAsStringSync(); + + return decode(password: password, data: encrypted); +} diff --git a/cw_ethereum/lib/pending_ethereum_transaction.dart b/cw_ethereum/lib/pending_ethereum_transaction.dart new file mode 100644 index 000000000..23dfa3b87 --- /dev/null +++ b/cw_ethereum/lib/pending_ethereum_transaction.dart @@ -0,0 +1,36 @@ +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:cw_core/pending_transaction.dart'; +import 'package:web3dart/crypto.dart'; + +class PendingEthereumTransaction with PendingTransaction { + final Function sendTransaction; + final Uint8List signedTransaction; + final BigInt fee; + final String amount; + final int exponent; + + PendingEthereumTransaction({ + required this.sendTransaction, + required this.signedTransaction, + required this.fee, + required this.amount, + required this.exponent, + }); + + @override + String get amountFormatted => (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); + + @override + Future<void> commit() async => await sendTransaction(); + + @override + String get feeFormatted => (fee / BigInt.from(pow(10, 18))).toString(); + + @override + String get hex => bytesToHex(signedTransaction, include0x: true); + + @override + String get id => ''; +} diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml new file mode 100644 index 000000000..cb1046d5a --- /dev/null +++ b/cw_ethereum/pubspec.yaml @@ -0,0 +1,68 @@ +name: cw_ethereum +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=2.18.2 <3.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + web3dart: 2.3.5 + mobx: ^2.0.7+4 + bip39: ^1.0.6 + bip32: ^2.0.0 + ed25519_hd_key: ^2.2.0 + hex: ^0.2.0 + http: ^0.13.4 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + +# 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: + + # To add assets to your 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 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_ethereum/test/cw_ethereum_test.dart b/cw_ethereum/test/cw_ethereum_test.dart new file mode 100644 index 000000000..72026a4c0 --- /dev/null +++ b/cw_ethereum/test/cw_ethereum_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_ethereum/cw_ethereum.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index a38c4721c..226ace6a1 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -254,6 +254,7 @@ abstract class HavenWalletBase extends WalletBase<MoneroBalance, await haven_wallet.store(); } + @override Future<void> renameWalletFiles(String newWalletName) async { final currentWalletPath = await pathForWallet(name: name, type: type); final currentCacheFile = File(currentWalletPath); diff --git a/cw_monero/lib/monero_transaction_info.dart b/cw_monero/lib/monero_transaction_info.dart index 90cc3c279..748b65329 100644 --- a/cw_monero/lib/monero_transaction_info.dart +++ b/cw_monero/lib/monero_transaction_info.dart @@ -14,18 +14,18 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromMap(Map<String, Object?> map) : id = (map['hash'] ?? '') as String, height = (map['height'] ?? 0) as int, - direction = - parseTransactionDirectionFromNumber(map['direction'] as String) ?? - TransactionDirection.incoming, + direction = map['direction'] != null + ? parseTransactionDirectionFromNumber(map['direction'] as String) + : TransactionDirection.incoming, date = DateTime.fromMillisecondsSinceEpoch( - (int.parse(map['timestamp'] as String) ?? 0) * 1000), + (int.tryParse(map['timestamp'] as String? ?? '') ?? 0) * 1000), isPending = parseBoolFromString(map['isPending'] as String), amount = map['amount'] as int, accountIndex = int.parse(map['accountIndex'] as String), addressIndex = map['addressIndex'] as int, confirmations = map['confirmations'] as int, key = getTxKey((map['hash'] ?? '') as String), - fee = map['fee'] as int ?? 0 { + fee = map['fee'] as int? ?? 0 { additionalInfo = <String, dynamic>{ 'key': key, 'accountIndex': accountIndex, @@ -36,8 +36,7 @@ class MoneroTransactionInfo extends TransactionInfo { MoneroTransactionInfo.fromRow(TransactionInfoRow row) : id = row.getHash(), height = row.blockHeight, - direction = parseTransactionDirectionFromInt(row.direction) ?? - TransactionDirection.incoming, + direction = parseTransactionDirectionFromInt(row.direction), date = DateTime.fromMillisecondsSinceEpoch(row.getDatetime() * 1000), isPending = row.isPending != 0, amount = row.getAmount(), diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index 104b3ebe8..ef25b6b93 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -269,6 +269,7 @@ abstract class MoneroWalletBase extends WalletBase<MoneroBalance, await monero_wallet.store(); } + @override Future<void> renameWalletFiles(String newWalletName) async { final currentWalletDirPath = await pathForWalletDir(name: name, type: type); diff --git a/howto-build-android.md b/howto-build-android.md index 4ef385b9f..d37f1b417 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -6,9 +6,9 @@ The following are the system requirements to build CakeWallet for your Android d ``` Ubuntu >= 16.04 -Android SDK 28 +Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 2 or above +Flutter 3.7.x ``` ## Building CakeWallet on Android @@ -55,7 +55,7 @@ You may download and install the latest version of Android Studio [here](https:/ ### 3. Installing Flutter -Need to install flutter with version `3.x.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). +Need to install flutter with version `3.7.x`. For this please check section [Install Flutter manually](https://docs.flutter.dev/get-started/install/linux#install-flutter-manually). ### 4. Verify Installations @@ -66,9 +66,9 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.x.x, on Linux, locale en_US.UTF-8) -[✓] Android toolchain - develop for Android devices (Android SDK version 28) -[✓] Android Studio (version 4.0) +[✓] Flutter (Channel stable, 3.7.x, on Linux, locale en_US.UTF-8) +[✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) +[✓] Android Studio (version 4.0 or higher) ``` ### 5. Generate a secure keystore for Android diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 5c6ed30a6..f13c68629 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -134,6 +134,8 @@ PODS: - SDWebImage (5.16.0): - SDWebImage/Core (= 5.16.0) - SDWebImage/Core (5.16.0) + - sensitive_clipboard (0.0.1): + - Flutter - share_plus (0.0.1): - Flutter - shared_preferences_foundation (0.0.1): @@ -150,6 +152,8 @@ PODS: - Flutter - wakelock (0.0.1): - Flutter + - workmanager (0.0.1): + - Flutter DEPENDENCIES: - barcode_scan2 (from `.symlinks/plugins/barcode_scan2/ios`) @@ -173,12 +177,14 @@ DEPENDENCIES: - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) + - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock (from `.symlinks/plugins/wakelock/ios`) + - workmanager (from `.symlinks/plugins/workmanager/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -235,6 +241,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/permission_handler_apple/ios" platform_device_id: :path: ".symlinks/plugins/platform_device_id/ios" + sensitive_clipboard: + :path: ".symlinks/plugins/sensitive_clipboard/ios" share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: @@ -245,6 +253,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock: :path: ".symlinks/plugins/wakelock/ios" + workmanager: + :path: ".symlinks/plugins/workmanager/ios" SPEC CHECKSUMS: barcode_scan2: 0af2bb63c81b4565aab6cd78278e4c0fa136dbb0 @@ -270,11 +280,12 @@ SPEC CHECKSUMS: MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 platform_device_id: 81b3e2993881f87d0c82ef151dc274df4869aef5 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 + sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 @@ -283,6 +294,7 @@ SPEC CHECKSUMS: UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 wakelock: d0fc7c864128eac40eba1617cb5264d9c940b46f + workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 PODFILE CHECKSUM: 09df1114e7c360f55770d35a79356bf5446e0100 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 05cf659e8..4bc10f9be 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -606,4 +606,9 @@ /* End XCConfigurationList section */ }; rootObject = 97C146E61CF9000F007C117D /* Project object */; + SystemCapabilities = { + com.apple.BackgroundModes = { + enabled = 1; + }; + }; } diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index 401509606..acdfa4346 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -1,6 +1,7 @@ import UIKit import Flutter import UnstoppableDomainsResolution +import workmanager @UIApplicationMain @objc class AppDelegate: FlutterAppDelegate { @@ -16,6 +17,15 @@ import UnstoppableDomainsResolution UNUserNotificationCenter.current().delegate = self as? UNUserNotificationCenterDelegate } + WorkmanagerPlugin.setPluginRegistrantCallback { registry in + // Registry in this case is the FlutterEngine that is created in Workmanager's + // performFetchWithCompletionHandler or BGAppRefreshTask. + // This will make other plugins available during a background operation. + GeneratedPluginRegistrant.register(with: registry) + } + + WorkmanagerPlugin.registerTask(withIdentifier: "com.fotolockr.cakewallet.monero_sync_task") + makeSecure() let controller : FlutterViewController = window?.rootViewController as! FlutterViewController diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 29cd24cb4..821df195e 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -2,6 +2,10 @@ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <plist version="1.0"> <dict> + <key>BGTaskSchedulerPermittedIdentifiers</key> + <array> + <string>com.fotolockr.cakewallet.monero_sync_task</string> + </array> <key>CFBundleDevelopmentRegion</key> <string>$(DEVELOPMENT_LANGUAGE)</string> <key>CFBundleDisplayName</key> @@ -113,6 +117,7 @@ <key>UIBackgroundModes</key> <array> <string>fetch</string> + <string>processing</string> <string>remote-notification</string> </array> <key>UILaunchStoryboardName</key> diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index 630ecf27f..dfd3b1538 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -80,7 +80,7 @@ class CWBitcoin extends Bitcoin { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority != null ? priority as BitcoinTransactionPriority : null, + priority: priority as BitcoinTransactionPriority, feeRate: feeRate); @override diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 2b3056343..f2a235363 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -2,6 +2,7 @@ import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; class AddressValidator extends TextValidator { AddressValidator({required CryptoCurrency type}) @@ -14,6 +15,9 @@ class AddressValidator extends TextValidator { length: getLength(type)); static String getPattern(CryptoCurrency type) { + if (type is Erc20Token) { + return '0x[0-9a-zA-Z]'; + } switch (type) { case CryptoCurrency.xmr: return '^4[0-9a-zA-Z]{94}\$|^8[0-9a-zA-Z]{94}\$|^[0-9a-zA-Z]{106}\$'; @@ -56,6 +60,7 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: return '0x[0-9a-zA-Z]'; case CryptoCurrency.xrp: return '^[0-9a-zA-Z]{34}\$|^X[0-9a-zA-Z]{46}\$'; @@ -116,17 +121,14 @@ class AddressValidator extends TextValidator { } static List<int>? getLength(CryptoCurrency type) { + if (type is Erc20Token) { + return [42]; + } switch (type) { case CryptoCurrency.xmr: return null; case CryptoCurrency.ada: return null; - case CryptoCurrency.avaxc: - return [42]; - case CryptoCurrency.bch: - return [42]; - case CryptoCurrency.bnb: - return [42]; case CryptoCurrency.btc: return null; case CryptoCurrency.dash: @@ -166,6 +168,10 @@ class AddressValidator extends TextValidator { case CryptoCurrency.zrx: case CryptoCurrency.dydx: case CryptoCurrency.steth: + case CryptoCurrency.shib: + case CryptoCurrency.avaxc: + case CryptoCurrency.bch: + case CryptoCurrency.bnb: return [42]; case CryptoCurrency.ltc: return [34, 43, 63]; @@ -203,11 +209,8 @@ class AddressValidator extends TextValidator { case CryptoCurrency.xusd: return [98, 99, 106]; case CryptoCurrency.btt: - return [34]; case CryptoCurrency.bttc: - return [34]; case CryptoCurrency.doge: - return [34]; case CryptoCurrency.firo: return [34]; case CryptoCurrency.hbar: @@ -258,6 +261,8 @@ class AddressValidator extends TextValidator { return '([^0-9a-zA-Z]|^)^L[a-zA-Z0-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)[LM][a-km-zA-HJ-NP-Z1-9]{26,33}([^0-9a-zA-Z]|\$)' '|([^0-9a-zA-Z]|^)ltc[a-zA-Z0-9]{26,45}([^0-9a-zA-Z]|\$)'; + case CryptoCurrency.eth: + return '0x[0-9a-zA-Z]{42}'; default: return null; } diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index 8091740e6..854640015 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -25,6 +25,10 @@ class AuthService with Store { Routes.setupPin, Routes.setup_2faPage, Routes.modify2FAPage, + Routes.newWallet, + Routes.newWalletType, + Routes.addressBookAddContact, + Routes.restoreOptions, ]; final FlutterSecureStorage secureStorage; @@ -81,21 +85,26 @@ class AuthService with Store { } Future<void> authenticateAction(BuildContext context, - {Function(bool)? onAuthSuccess, String? route, Object? arguments}) async { + {Function(bool)? onAuthSuccess, + String? route, + Object? arguments, + required bool conditionToDetermineIfToUse2FA}) async { assert(route != null || onAuthSuccess != null, 'Either route or onAuthSuccess param must be passed.'); - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { - if (onAuthSuccess != null) { - onAuthSuccess(true); - } else { - Navigator.of(context).pushNamed( - route ?? '', - arguments: arguments, - ); + if (!conditionToDetermineIfToUse2FA) { + if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (onAuthSuccess != null) { + onAuthSuccess(true); + } else { + Navigator.of(context).pushNamed( + route ?? '', + arguments: arguments, + ); + } + return; } - return; - } +} Navigator.of(context).pushNamed(Routes.auth, @@ -104,7 +113,7 @@ class AuthService with Store { onAuthSuccess?.call(false); return; } else { - if (settingsStore.useTOTP2FA) { + if (settingsStore.useTOTP2FA && conditionToDetermineIfToUse2FA) { auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 2870c4488..2e27d83c9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; import 'dart:typed_data'; +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; @@ -19,8 +20,8 @@ import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cake_backup/backup.dart' as cake_backup; class BackupService { - BackupService(this._flutterSecureStorage, this._walletInfoSource, - this._keyService, this._sharedPreferences) + BackupService( + this._flutterSecureStorage, this._walletInfoSource, this._keyService, this._sharedPreferences) : _cipher = Cryptography.instance.chacha20Poly1305Aead(), _correctWallets = <WalletInfo>[]; @@ -67,9 +68,8 @@ class BackupService { } @Deprecated('Use v2 instead') - Future<Uint8List> _exportBackupV1(String password, - {String nonce = secrets.backupSalt}) async - => throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); + Future<Uint8List> _exportBackupV1(String password, {String nonce = secrets.backupSalt}) async => + throw Exception('Deprecated. Export for backups v1 is deprecated. Please use export v2.'); Future<Uint8List> _exportBackupV2(String password) async { final zipEncoder = ZipFileEncoder(); @@ -112,8 +112,7 @@ class BackupService { return await _encryptV2(content, password); } - Future<void> _importBackupV1(Uint8List data, String password, - {required String nonce}) async { + Future<void> _importBackupV1(Uint8List data, String password, {required String nonce}) async { final appDir = await getApplicationDocumentsDirectory(); final decryptedData = await _decryptV1(data, password, nonce); final zip = ZipDecoder().decodeBytes(decryptedData); @@ -161,10 +160,8 @@ class BackupService { Future<void> _verifyWallets() async { final walletInfoSource = await _reloadHiveWalletInfoBox(); - _correctWallets = walletInfoSource - .values - .where((info) => availableWalletTypes.contains(info.type)) - .toList(); + _correctWallets = + walletInfoSource.values.where((info) => availableWalletTypes.contains(info.type)).toList(); if (_correctWallets.isEmpty) { throw Exception('Correct wallets not detected'); @@ -191,14 +188,12 @@ class BackupService { return; } - final data = - json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>; + final data = json.decode(preferencesFile.readAsStringSync()) as Map<String, dynamic>; String currentWalletName = data[PreferencesKey.currentWalletName] as String; int currentWalletType = data[PreferencesKey.currentWalletType] as int; final isCorrentCurrentWallet = _correctWallets - .any((info) => info.name == currentWalletName && - info.type.index == currentWalletType); + .any((info) => info.name == currentWalletName && info.type.index == currentWalletType); if (!isCorrentCurrentWallet) { currentWalletName = _correctWallets.first.name; @@ -212,138 +207,193 @@ class BackupService { final isAppSecure = data[PreferencesKey.isAppSecureKey] as bool?; final disableBuy = data[PreferencesKey.disableBuyKey] as bool?; final disableSell = data[PreferencesKey.disableSellKey] as bool?; - final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; - final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; + final currentTransactionPriorityKeyLegacy = + data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; + final allowBiometricalAuthentication = + data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; + final currentBitcoinElectrumSererId = + data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; final displayActionListMode = data[PreferencesKey.displayActionListModeKey] as int?; final fiatApiMode = data[PreferencesKey.currentFiatApiModeKey] as int?; final currentPinLength = data[PreferencesKey.currentPinLength] as int?; final currentTheme = data[PreferencesKey.currentTheme] as int?; final exchangeStatus = data[PreferencesKey.exchangeStatusKey] as int?; - final currentDefaultSettingsMigrationVersion = data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; + final currentDefaultSettingsMigrationVersion = + data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; + final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?; + final shouldRequireTOTP2FAForAccessingWallet = + data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?; + final shouldRequireTOTP2FAForSendsToContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?; + final shouldRequireTOTP2FAForSendsToNonContact = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?; + final shouldRequireTOTP2FAForSendsToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?; + final shouldRequireTOTP2FAForExchangesToInternalWallets = + data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?; + final shouldRequireTOTP2FAForAddingContacts = + data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?; + final shouldRequireTOTP2FAForCreatingNewWallets = + data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; + final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; + final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; + final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; + final syncAll = data[PreferencesKey.syncAllKey] as bool?; + final syncMode = data[PreferencesKey.syncModeKey] as int?; - await _sharedPreferences.setString(PreferencesKey.currentWalletName, - currentWalletName); + await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); if (currentNodeId != null) - await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, - currentNodeId); + await _sharedPreferences.setInt(PreferencesKey.currentNodeIdKey, currentNodeId); if (currentBalanceDisplayMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentBalanceDisplayModeKey, - currentBalanceDisplayMode); + await _sharedPreferences.setInt( + PreferencesKey.currentBalanceDisplayModeKey, currentBalanceDisplayMode); - await _sharedPreferences.setInt(PreferencesKey.currentWalletType, - currentWalletType); + await _sharedPreferences.setInt(PreferencesKey.currentWalletType, currentWalletType); if (currentFiatCurrency != null) - await _sharedPreferences.setString(PreferencesKey.currentFiatCurrencyKey, - currentFiatCurrency); + await _sharedPreferences.setString( + PreferencesKey.currentFiatCurrencyKey, currentFiatCurrency); if (shouldSaveRecipientAddress != null) await _sharedPreferences.setBool( - PreferencesKey.shouldSaveRecipientAddressKey, - shouldSaveRecipientAddress); + PreferencesKey.shouldSaveRecipientAddressKey, shouldSaveRecipientAddress); if (isAppSecure != null) - await _sharedPreferences.setBool( - PreferencesKey.isAppSecureKey, - isAppSecure); + await _sharedPreferences.setBool(PreferencesKey.isAppSecureKey, isAppSecure); if (disableBuy != null) - await _sharedPreferences.setBool( - PreferencesKey.disableBuyKey, - disableBuy); + await _sharedPreferences.setBool(PreferencesKey.disableBuyKey, disableBuy); if (disableSell != null) - await _sharedPreferences.setBool( - PreferencesKey.disableSellKey, - disableSell); + await _sharedPreferences.setBool(PreferencesKey.disableSellKey, disableSell); if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( - PreferencesKey.currentTransactionPriorityKeyLegacy, - currentTransactionPriorityKeyLegacy); + PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); if (allowBiometricalAuthentication != null) await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, - allowBiometricalAuthentication); + PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( - PreferencesKey.currentBitcoinElectrumSererIdKey, - currentBitcoinElectrumSererId); + PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); if (currentLanguageCode != null) - await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, - currentLanguageCode); + await _sharedPreferences.setString(PreferencesKey.currentLanguageCode, currentLanguageCode); if (displayActionListMode != null) - await _sharedPreferences.setInt(PreferencesKey.displayActionListModeKey, - displayActionListMode); + await _sharedPreferences.setInt( + PreferencesKey.displayActionListModeKey, displayActionListMode); if (fiatApiMode != null) - await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, - fiatApiMode); + await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); if (currentPinLength != null) - await _sharedPreferences.setInt(PreferencesKey.currentPinLength, - currentPinLength); + await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); if (currentTheme != null) - await _sharedPreferences.setInt( - PreferencesKey.currentTheme, currentTheme); + await _sharedPreferences.setInt(PreferencesKey.currentTheme, currentTheme); if (exchangeStatus != null) - await _sharedPreferences.setInt( - PreferencesKey.exchangeStatusKey, exchangeStatus); + await _sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, exchangeStatus); if (currentDefaultSettingsMigrationVersion != null) - await _sharedPreferences.setInt( - PreferencesKey.currentDefaultSettingsMigrationVersion, - currentDefaultSettingsMigrationVersion); + await _sharedPreferences.setInt(PreferencesKey.currentDefaultSettingsMigrationVersion, + currentDefaultSettingsMigrationVersion); if (moneroTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.moneroTransactionPriority, - moneroTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.moneroTransactionPriority, moneroTransactionPriority); if (bitcoinTransactionPriority != null) - await _sharedPreferences.setInt(PreferencesKey.bitcoinTransactionPriority, - bitcoinTransactionPriority); + await _sharedPreferences.setInt( + PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); + + if (selectedCake2FAPreset != null) + await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset); + + if (shouldRequireTOTP2FAForAccessingWallet != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, + shouldRequireTOTP2FAForAccessingWallet); + + if (shouldRequireTOTP2FAForSendsToContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact, + shouldRequireTOTP2FAForSendsToContact); + + if (shouldRequireTOTP2FAForSendsToNonContact != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, + shouldRequireTOTP2FAForSendsToNonContact); + + if (shouldRequireTOTP2FAForSendsToInternalWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, + shouldRequireTOTP2FAForSendsToInternalWallets); + + if (shouldRequireTOTP2FAForExchangesToInternalWallets != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForExchangesToInternalWallets); + + if (shouldRequireTOTP2FAForAddingContacts != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts, + shouldRequireTOTP2FAForAddingContacts); + + if (shouldRequireTOTP2FAForCreatingNewWallets != null) + await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, + shouldRequireTOTP2FAForCreatingNewWallets); + + if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null) + await _sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + + if (sortBalanceTokensBy != null) + await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); + + if (pinNativeTokenAtTop != null) + await _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop); + + if (useEtherscan != null) + await _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan); + + if (syncAll != null) + await _sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + if (syncMode != null) + await _sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode); await preferencesFile.delete(); } Future<void> _importKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async { + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV1( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map<String, dynamic>; + final decryptedKeychainDumpFileData = + await _decryptV1(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password', nonce); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map<String, dynamic>; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -352,27 +402,24 @@ class BackupService { {String keychainSalt = secrets.backupKeychainSalt}) async { final appDir = await getApplicationDocumentsDirectory(); final keychainDumpFile = File('${appDir.path}/~_keychain_dump'); - final decryptedKeychainDumpFileData = await _decryptV2( - keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); - final keychainJSON = json.decode(utf8.decode(decryptedKeychainDumpFileData)) - as Map<String, dynamic>; + final decryptedKeychainDumpFileData = + await _decryptV2(keychainDumpFile.readAsBytesSync(), '$keychainSalt$password'); + final keychainJSON = + json.decode(utf8.decode(decryptedKeychainDumpFileData)) as Map<String, dynamic>; final keychainWalletsInfo = keychainJSON['wallets'] as List; final decodedPin = keychainJSON['pin'] as String; final pinCodeKey = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); final backupPassword = keychainJSON[backupPasswordKey] as String; - await _flutterSecureStorage.write( - key: backupPasswordKey, value: backupPassword); + await _flutterSecureStorage.write(key: backupPasswordKey, value: backupPassword); keychainWalletsInfo.forEach((dynamic rawInfo) async { final info = rawInfo as Map<String, dynamic>; await importWalletKeychainInfo(info); }); - await _flutterSecureStorage.write( - key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); + await _flutterSecureStorage.write(key: pinCodeKey, value: encodedPinCode(pin: decodedPin)); keychainDumpFile.deleteSync(); } @@ -386,35 +433,26 @@ class BackupService { @Deprecated('Use v2 instead') Future<Uint8List> _exportKeychainDumpV1(String password, - {required String nonce, - String keychainSalt = secrets.backupKeychainSalt}) async - => throw Exception('Deprecated'); + {required String nonce, String keychainSalt = secrets.backupKeychainSalt}) async => + throw Exception('Deprecated'); Future<Uint8List> _exportKeychainDumpV2(String password, {String keychainSalt = secrets.backupKeychainSalt}) async { final key = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); final encodedPin = await _flutterSecureStorage.read(key: key); final decodedPin = decodedPinCode(pin: encodedPin!); - final wallets = - await Future.wait(_walletInfoSource.values.map((walletInfo) async { + final wallets = await Future.wait(_walletInfoSource.values.map((walletInfo) async { return { 'name': walletInfo.name, 'type': walletInfo.type.toString(), - 'password': - await _keyService.getWalletPassword(walletName: walletInfo.name) + 'password': await _keyService.getWalletPassword(walletName: walletInfo.name) }; })); - final backupPasswordKey = - generateStoreKeyFor(key: SecretStoreKey.backupPassword); - final backupPassword = - await _flutterSecureStorage.read(key: backupPasswordKey); - final data = utf8.encode(json.encode({ - 'pin': decodedPin, - 'wallets': wallets, - backupPasswordKey: backupPassword - })); - final encrypted = await _encryptV2( - Uint8List.fromList(data), '$keychainSalt$password'); + final backupPasswordKey = generateStoreKeyFor(key: SecretStoreKey.backupPassword); + final backupPassword = await _flutterSecureStorage.read(key: backupPasswordKey); + final data = utf8.encode( + json.encode({'pin': decodedPin, 'wallets': wallets, backupPasswordKey: backupPassword})); + final encrypted = await _encryptV2(Uint8List.fromList(data), '$keychainSalt$password'); return encrypted; } @@ -423,46 +461,67 @@ class BackupService { final preferences = <String, dynamic>{ PreferencesKey.currentWalletName: _sharedPreferences.getString(PreferencesKey.currentWalletName), - PreferencesKey.currentNodeIdKey: - _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), - PreferencesKey.currentBalanceDisplayModeKey: _sharedPreferences - .getInt(PreferencesKey.currentBalanceDisplayModeKey), - PreferencesKey.currentWalletType: - _sharedPreferences.getInt(PreferencesKey.currentWalletType), + PreferencesKey.currentNodeIdKey: _sharedPreferences.getInt(PreferencesKey.currentNodeIdKey), + PreferencesKey.currentBalanceDisplayModeKey: + _sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey), + PreferencesKey.currentWalletType: _sharedPreferences.getInt(PreferencesKey.currentWalletType), PreferencesKey.currentFiatCurrencyKey: _sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey), - PreferencesKey.shouldSaveRecipientAddressKey: _sharedPreferences - .getBool(PreferencesKey.shouldSaveRecipientAddressKey), - PreferencesKey.disableBuyKey: _sharedPreferences - .getBool(PreferencesKey.disableBuyKey), - PreferencesKey.disableSellKey: _sharedPreferences - .getBool(PreferencesKey.disableSellKey), + PreferencesKey.shouldSaveRecipientAddressKey: + _sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey), + PreferencesKey.disableBuyKey: _sharedPreferences.getBool(PreferencesKey.disableBuyKey), + PreferencesKey.disableSellKey: _sharedPreferences.getBool(PreferencesKey.disableSellKey), PreferencesKey.isDarkThemeLegacy: _sharedPreferences.getBool(PreferencesKey.isDarkThemeLegacy), - PreferencesKey.currentPinLength: - _sharedPreferences.getInt(PreferencesKey.currentPinLength), - PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences - .getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: _sharedPreferences - .getBool(PreferencesKey.allowBiometricalAuthenticationKey), - PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences - .getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), + PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), + PreferencesKey.currentTransactionPriorityKeyLegacy: + _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), + PreferencesKey.allowBiometricalAuthenticationKey: + _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey), + PreferencesKey.currentBitcoinElectrumSererIdKey: + _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: _sharedPreferences.getString(PreferencesKey.currentLanguageCode), PreferencesKey.displayActionListModeKey: _sharedPreferences.getInt(PreferencesKey.displayActionListModeKey), - PreferencesKey.currentTheme: - _sharedPreferences.getInt(PreferencesKey.currentTheme), - PreferencesKey.exchangeStatusKey: - _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), - PreferencesKey.currentDefaultSettingsMigrationVersion: _sharedPreferences - .getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), + PreferencesKey.currentTheme: _sharedPreferences.getInt(PreferencesKey.currentTheme), + PreferencesKey.exchangeStatusKey: _sharedPreferences.getInt(PreferencesKey.exchangeStatusKey), + PreferencesKey.currentDefaultSettingsMigrationVersion: + _sharedPreferences.getInt(PreferencesKey.currentDefaultSettingsMigrationVersion), PreferencesKey.bitcoinTransactionPriority: _sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority), PreferencesKey.moneroTransactionPriority: _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: - _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), + PreferencesKey.selectedCake2FAPreset: + _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset), + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet), + PreferencesKey.shouldRequireTOTP2FAForSendsToContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact), + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets), + PreferencesKey.shouldRequireTOTP2FAForAddingContacts: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts), + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets: + _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), + PreferencesKey.sortBalanceBy: + _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), + PreferencesKey.pinNativeTokenAtTop: + _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), + PreferencesKey.useEtherscan: + _sharedPreferences.getBool(PreferencesKey.useEtherscan), + PreferencesKey.syncModeKey: + _sharedPreferences.getInt(PreferencesKey.syncModeKey), + PreferencesKey.syncAllKey: + _sharedPreferences.getBool(PreferencesKey.syncAllKey), }; return json.encode(preferences); @@ -476,28 +535,23 @@ class BackupService { } @Deprecated('Use v2 instead') - Future<Uint8List> _encryptV1( - Uint8List data, String secretKeySource, String nonceBase64) async - => throw Exception('Deprecated'); + Future<Uint8List> _encryptV1(Uint8List data, String secretKeySource, String nonceBase64) async => + throw Exception('Deprecated'); - Future<Uint8List> _decryptV1( - Uint8List data, String secretKeySource, String nonceBase64, {int macLength = 16}) async { + Future<Uint8List> _decryptV1(Uint8List data, String secretKeySource, String nonceBase64, + {int macLength = 16}) async { final secretKeyHash = await Cryptography.instance.sha256().hash(utf8.encode(secretKeySource)); final secretKey = SecretKey(secretKeyHash.bytes); final nonce = base64.decode(nonceBase64).toList(); - final box = SecretBox( - Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), - nonce: nonce, - mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); + final box = SecretBox(Uint8List.sublistView(data, 0, data.lengthInBytes - macLength).toList(), + nonce: nonce, mac: Mac(Uint8List.sublistView(data, data.lengthInBytes - macLength))); final plainData = await _cipher.decrypt(box, secretKey: secretKey); return Uint8List.fromList(plainData); } - Future<Uint8List> _encryptV2( - Uint8List data, String passphrase) async - => cake_backup.encrypt(passphrase, data, version: _v2); + Future<Uint8List> _encryptV2(Uint8List data, String passphrase) async => + cake_backup.encrypt(passphrase, data, version: _v2); - Future<Uint8List> _decryptV2( - Uint8List data, String passphrase) async - => cake_backup.decrypt(passphrase, data); + Future<Uint8List> _decryptV2(Uint8List data, String passphrase) async => + cake_backup.decrypt(passphrase, data); } diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 11aef1374..9690c430a 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -5,21 +5,20 @@ import 'package:flutter/foundation.dart'; import 'package:http/http.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; - const _fiatApiClearNetAuthority = 'fiat-api.cakewallet.com'; const _fiatApiOnionAuthority = 'n4z7bdcmwk2oyddxvzaap3x2peqcplh3pzdy7tpkk5ejz5n4mhfvoxqd.onion'; const _fiatApiPath = '/v2/rates'; Future<double> _fetchPrice(Map<String, dynamic> args) async { - final crypto = args['crypto'] as CryptoCurrency; - final fiat = args['fiat'] as FiatCurrency; + final crypto = args['crypto'] as String; + final fiat = args['fiat'] as String; final torOnly = args['torOnly'] as bool; final Map<String, String> queryParams = { 'interval_count': '1', - 'base': crypto.toString(), - 'quote': fiat.toString(), - 'key' : secrets.fiatApiKey, + 'base': crypto, + 'quote': fiat, + 'key': secrets.fiatApiKey, }; double price = 0.0; @@ -52,7 +51,11 @@ Future<double> _fetchPrice(Map<String, dynamic> args) async { } Future<double> _fetchPriceAsync(CryptoCurrency crypto, FiatCurrency fiat, bool torOnly) async => - compute(_fetchPrice, {'fiat': fiat, 'crypto': crypto, 'torOnly': torOnly}); + compute(_fetchPrice, { + 'fiat': fiat.toString(), + 'crypto': crypto.toString(), + 'torOnly': torOnly, + }); class FiatConversionService { static Future<double> fetchPrice({ diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index fe9a25f85..eba1bbda4 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/core/validator.dart'; import 'package:cake_wallet/entities/mnemonic_item.dart'; @@ -25,6 +26,8 @@ class SeedValidator extends Validator<MnemonicItem> { return monero!.getMoneroWordList(language); case WalletType.haven: return haven!.getMoneroWordList(language); + case WalletType.ethereum: + return ethereum!.getEthereumWordList(language); default: return []; } diff --git a/lib/di.dart b/lib/di.dart index b7b012925..b02742e64 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -4,9 +4,11 @@ import 'package:cake_wallet/anonpay/anonpay_invoice_info.dart'; import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/ionia/ionia_anypay.dart'; import 'package:cake_wallet/ionia/ionia_gift_card.dart'; import 'package:cake_wallet/ionia/ionia_tip.dart'; @@ -16,10 +18,13 @@ import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_dashboard_page.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart'; import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart'; +import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; +import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -40,6 +45,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/view_model/dashboard/desktop_sidebar_view_model.dart'; import 'package:cake_wallet/view_model/anon_invoice_page_view_model.dart'; import 'package:cake_wallet/view_model/anonpay_details_view_model.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/market_place_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart'; import 'package:cake_wallet/view_model/ionia/ionia_auth_view_model.dart'; @@ -70,6 +76,7 @@ import 'package:cake_wallet/view_model/advanced_privacy_settings_view_model.dart import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_edit_view_model.dart'; import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cake_wallet/core/backup_service.dart'; import 'package:cw_core/wallet_service.dart'; @@ -239,15 +246,21 @@ Future setup({ getIt.registerSingletonAsync<SharedPreferences>(() => SharedPreferences.getInstance()); } - final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty ?? false) && - (secrets.wyreApiKey.isNotEmpty ?? false) && - (secrets.wyreAccountId.isNotEmpty ?? false); + if (!_isSetupFinished) { + getIt.registerFactory(() => BackgroundTasks()); + } + + final isBitcoinBuyEnabled = (secrets.wyreSecretKey.isNotEmpty) && + (secrets.wyreApiKey.isNotEmpty) && + (secrets.wyreAccountId.isNotEmpty); final settingsStore = await SettingsStoreBase.load( nodeSource: _nodeSource, isBitcoinBuyEnabled: isBitcoinBuyEnabled, // Enforce darkTheme on platforms other than mobile till the design for other themes is completed - initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile ? null : ThemeList.darkTheme, + initialTheme: ResponsiveLayoutUtil.instance.isMobile && DeviceInfo.instance.isMobile + ? null + : ThemeList.darkTheme, ); if (_isSetupFinished) { @@ -389,7 +402,9 @@ Future setup({ final authStore = getIt.get<AuthenticationStore>(); final appStore = getIt.get<AppStore>(); final useTotp = appStore.settingsStore.useTOTP2FA; - if (useTotp) { + final shouldUseTotp2FAToAccessWallets = + appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { authPageState.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( @@ -525,17 +540,22 @@ Future setup({ getIt.get<SendTemplateStore>(), getIt.get<FiatConversionStore>())); - getIt.registerFactory<SendViewModel>(() => SendViewModel( + getIt.registerFactory<SendViewModel>( + () => SendViewModel( getIt.get<AppStore>().wallet!, getIt.get<AppStore>().settingsStore, getIt.get<SendTemplateViewModel>(), getIt.get<FiatConversionStore>(), getIt.get<BalanceViewModel>(), - _transactionDescriptionBox)); + getIt.get<ContactListViewModel>(), + _transactionDescriptionBox, + ), + ); getIt.registerFactoryParam<SendPage, PaymentRequest?, void>( (PaymentRequest? initialPaymentRequest, _) => SendPage( sendViewModel: getIt.get<SendViewModel>(), + authService: getIt.get<AuthService>(), initialPaymentRequest: initialPaymentRequest, )); @@ -570,8 +590,8 @@ Future setup({ )); getIt.registerFactoryParam<WalletEditViewModel, WalletListViewModel, void>( - (WalletListViewModel walletListViewModel, _) => WalletEditViewModel( - walletListViewModel, getIt.get<WalletLoadingService>())); + (WalletListViewModel walletListViewModel, _) => + WalletEditViewModel(walletListViewModel, getIt.get<WalletLoadingService>())); getIt.registerFactoryParam<WalletEditPage, List<dynamic>, void>((args, _) { final walletListViewModel = args.first as WalletListViewModel; @@ -583,7 +603,6 @@ Future setup({ editingWallet: editingWallet); }); - getIt.registerFactory(() { final wallet = getIt.get<AppStore>().wallet!; @@ -630,7 +649,7 @@ Future setup({ }); getIt.registerFactory(() { - return PrivacySettingsViewModel(getIt.get<SettingsStore>()); + return PrivacySettingsViewModel(getIt.get<SettingsStore>(), getIt.get<AppStore>().wallet!); }); getIt.registerFactory(() { @@ -654,10 +673,11 @@ Future setup({ (ContactRecord? contact, _) => ContactViewModel(_contactSource, contact: contact)); getIt.registerFactoryParam<ContactListViewModel, CryptoCurrency?, void>( - (CryptoCurrency? cur, _) => ContactListViewModel(_contactSource, _walletInfoSource, cur)); + (CryptoCurrency? cur, _) => + ContactListViewModel(_contactSource, _walletInfoSource, cur, getIt.get<SettingsStore>())); - getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>( - (CryptoCurrency? cur, _) => ContactListPage(getIt.get<ContactListViewModel>(param1: cur))); + getIt.registerFactoryParam<ContactListPage, CryptoCurrency?, void>((CryptoCurrency? cur, _) => + ContactListPage(getIt.get<ContactListViewModel>(param1: cur), getIt.get<AuthService>())); getIt.registerFactoryParam<ContactPage, ContactRecord?, void>( (ContactRecord? contact, _) => ContactPage(getIt.get<ContactViewModel>(param1: contact))); @@ -667,8 +687,7 @@ Future setup({ return NodeListViewModel(_nodeSource, appStore); }); - getIt.registerFactory( - () => ConnectionSyncPage(getIt.get<NodeListViewModel>(), getIt.get<DashboardViewModel>())); + getIt.registerFactory(() => ConnectionSyncPage(getIt.get<DashboardViewModel>())); getIt.registerFactory( () => SecurityBackupPage(getIt.get<SecuritySettingsViewModel>(), getIt.get<AuthService>())); @@ -702,13 +721,13 @@ Future setup({ )); getIt.registerFactory(() => ExchangeViewModel( - getIt.get<AppStore>().wallet!, - _tradesSource, - getIt.get<ExchangeTemplateStore>(), - getIt.get<TradesStore>(), - getIt.get<AppStore>().settingsStore, - getIt.get<SharedPreferences>(), - )); + getIt.get<AppStore>().wallet!, + _tradesSource, + getIt.get<ExchangeTemplateStore>(), + getIt.get<TradesStore>(), + getIt.get<AppStore>().settingsStore, + getIt.get<SharedPreferences>(), + getIt.get<ContactListViewModel>())); getIt.registerFactory(() => ExchangeTradeViewModel( wallet: getIt.get<AppStore>().wallet!, @@ -716,7 +735,8 @@ Future setup({ tradesStore: getIt.get<TradesStore>(), sendViewModel: getIt.get<SendViewModel>())); - getIt.registerFactory(() => ExchangePage(getIt.get<ExchangeViewModel>())); + getIt.registerFactory( + () => ExchangePage(getIt.get<ExchangeViewModel>(), getIt.get<AuthService>())); getIt.registerFactory(() => ExchangeConfirmPage(tradesStore: getIt.get<TradesStore>())); @@ -735,6 +755,8 @@ Future setup({ return bitcoin!.createBitcoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.litecoin: return bitcoin!.createLitecoinWalletService(_walletInfoSource, _unspentCoinsInfoSource!); + case WalletType.ethereum: + return ethereum!.createEthereumWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } @@ -777,8 +799,8 @@ Future setup({ transactionDetailsViewModel: getIt.get<TransactionDetailsViewModel>(param1: transactionInfo))); - getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), void>( - (param1, _) => NewWalletTypePage(onTypeSelected: param1)); + getIt.registerFactoryParam<NewWalletTypePage, void Function(BuildContext, WalletType), bool?>( + (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam<PreSeedPage, WalletType, void>( (WalletType type, _) => PreSeedPage(type)); @@ -890,7 +912,7 @@ Future setup({ getIt.registerFactory(() => IoniaGiftCardsListViewModel(ioniaService: getIt.get<IoniaService>())); - getIt.registerFactory(()=> MarketPlaceViewModel(getIt.get<IoniaService>())); + getIt.registerFactory(() => MarketPlaceViewModel(getIt.get<IoniaService>())); getIt.registerFactory(() => IoniaAuthViewModel(ioniaService: getIt.get<IoniaService>())); @@ -1024,5 +1046,21 @@ Future setup({ getIt.registerFactoryParam<AdvancedPrivacySettingsViewModel, WalletType, void>( (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get<SettingsStore>())); + getIt.registerFactoryParam<HomeSettingsPage, BalanceViewModel, void>((balanceViewModel, _) => + HomeSettingsPage(getIt.get<HomeSettingsViewModel>(param1: balanceViewModel))); + + getIt.registerFactoryParam<HomeSettingsViewModel, BalanceViewModel, void>( + (balanceViewModel, _) => HomeSettingsViewModel(getIt.get<SettingsStore>(), balanceViewModel)); + + getIt.registerFactoryParam<EditTokenPage, HomeSettingsViewModel, Map<String, dynamic>>( + (homeSettingsViewModel, arguments) => EditTokenPage( + homeSettingsViewModel: homeSettingsViewModel, + erc20token: arguments['token'] as Erc20Token?, + initialContractAddress: arguments['contractAddress'] as String?, + ), + ); + + getIt.registerFactory<ManageNodesPage>(() => ManageNodesPage(getIt.get<NodeListViewModel>())); + _isSetupFinished = true; } diff --git a/lib/entities/background_tasks.dart b/lib/entities/background_tasks.dart new file mode 100644 index 000000000..ce1e2f6d8 --- /dev/null +++ b/lib/entities/background_tasks.dart @@ -0,0 +1,164 @@ +import 'dart:io'; + +import 'package:cake_wallet/core/wallet_loading_service.dart'; +import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_item.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:flutter/foundation.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:cake_wallet/main.dart'; +import 'package:cake_wallet/di.dart'; + +const moneroSyncTaskKey = "com.fotolockr.cakewallet.monero_sync_task"; + +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + try { + switch (task) { + case moneroSyncTaskKey: + + /// The work manager runs on a separate isolate from the main flutter isolate. + /// thus we initialize app configs first; hive, getIt, etc... + await initializeAppConfigs(); + + final walletLoadingService = getIt.get<WalletLoadingService>(); + + final node = getIt.get<SettingsStore>().getCurrentNode(WalletType.monero); + + final typeRaw = getIt.get<SharedPreferences>().getInt(PreferencesKey.currentWalletType); + + WalletBase? wallet; + + if (inputData!['sync_all'] as bool) { + /// get all Monero wallets of the user and sync them + final List<WalletListItem> moneroWallets = getIt + .get<WalletListViewModel>() + .wallets + .where((element) => element.type == WalletType.monero) + .toList(); + + for (int i = 0; i < moneroWallets.length; i++) { + wallet = await walletLoadingService.load(WalletType.monero, moneroWallets[i].name); + + await wallet.connectToNode(node: node); + await wallet.startSync(); + } + } else { + /// if the user chose to sync only active wallet + /// if the current wallet is monero; sync it only + if (typeRaw == WalletType.monero.index) { + final name = + getIt.get<SharedPreferences>().getString(PreferencesKey.currentWalletName); + + wallet = await walletLoadingService.load(WalletType.monero, name!); + + await wallet.connectToNode(node: node); + await wallet.startSync(); + } + } + + if (wallet?.syncStatus.progress() == null) { + return Future.error("No Monero wallet found"); + } + + for (int i = 0;; i++) { + await Future<void>.delayed(const Duration(seconds: 1)); + if (wallet?.syncStatus.progress() == 1.0) { + break; + } + if (i > 600) { + return Future.error("Synchronization Timed out"); + } + } + break; + } + + return Future.value(true); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + return Future.error(error); + } + }); +} + +class BackgroundTasks { + void registerSyncTask({bool changeExisting = false}) async { + try { + bool hasMonero = getIt + .get<WalletListViewModel>() + .wallets + .any((element) => element.type == WalletType.monero); + + /// if its not android nor ios, or the user has no monero wallets; exit + if (!DeviceInfo.instance.isMobile || !hasMonero) { + return; + } + + final settingsStore = getIt.get<SettingsStore>(); + + final SyncMode syncMode = settingsStore.currentSyncMode; + final bool syncAll = settingsStore.currentSyncAll; + + if (syncMode.type == SyncType.disabled) { + cancelSyncTask(); + return; + } + + await Workmanager().initialize( + callbackDispatcher, + isInDebugMode: kDebugMode, + ); + + final inputData = <String, dynamic>{"sync_all": syncAll}; + final constraints = Constraints( + networkType: + syncMode.type == SyncType.unobtrusive ? NetworkType.unmetered : NetworkType.connected, + requiresBatteryNotLow: syncMode.type == SyncType.unobtrusive, + requiresCharging: syncMode.type == SyncType.unobtrusive, + requiresDeviceIdle: syncMode.type == SyncType.unobtrusive, + ); + + if (Platform.isIOS) { + await Workmanager().registerOneOffTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + existingWorkPolicy: ExistingWorkPolicy.replace, + inputData: inputData, + constraints: constraints, + ); + return; + } + + await Workmanager().registerPeriodicTask( + moneroSyncTaskKey, + moneroSyncTaskKey, + initialDelay: syncMode.frequency, + frequency: syncMode.frequency, + existingWorkPolicy: changeExisting ? ExistingWorkPolicy.replace : ExistingWorkPolicy.keep, + inputData: inputData, + constraints: constraints, + ); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } + + void cancelSyncTask() { + try { + Workmanager().cancelByUniqueName(moneroSyncTaskKey); + } catch (error, stackTrace) { + print(error); + print(stackTrace); + } + } +} diff --git a/lib/entities/cake_2fa_preset_options.dart b/lib/entities/cake_2fa_preset_options.dart new file mode 100644 index 000000000..2aa6c4215 --- /dev/null +++ b/lib/entities/cake_2fa_preset_options.dart @@ -0,0 +1,35 @@ +import 'package:cw_core/enumerable_item.dart'; + +class Cake2FAPresetsOptions extends EnumerableItem<int> with Serializable<int> { + const Cake2FAPresetsOptions({required String super.title, required int super.raw}); + + static const narrow = Cake2FAPresetsOptions(title: 'Narrow', raw: 0); + static const normal = Cake2FAPresetsOptions(title: 'Normal', raw: 1); + static const aggressive = Cake2FAPresetsOptions(title: 'Aggressive', raw: 2); + + static Cake2FAPresetsOptions deserialize({required int raw}) { + switch (raw) { + case 0: + return Cake2FAPresetsOptions.narrow; + case 1: + return Cake2FAPresetsOptions.normal; + case 2: + return Cake2FAPresetsOptions.aggressive; + default: + throw Exception( + 'Incorrect Cake 2FA Preset $raw for Cake2FAPresetOptions deserialize', + ); + } + } +} + +enum VerboseControlSettings { + accessWallet, + addingContacts, + sendsToContacts, + sendsToNonContacts, + sendsToInternalWallets, + exchangesToInternalWallets, + securityAndBackupSettings, + creatingNewWallets, +} diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 77298c2b5..b4cb23131 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -26,6 +26,7 @@ const newCakeWalletMoneroUri = 'xmr-node.cakewallet.com:18081'; const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; +const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; Future defaultSettingsMigration( {required int version, @@ -157,6 +158,12 @@ Future defaultSettingsMigration( case 20: await migrateExchangeStatus(sharedPreferences); break; + case 21: + await addEthereumNodeList(nodes: nodes); + await changeEthereumCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + default: break; } @@ -242,6 +249,12 @@ Node? getHavenDefaultNode({required Box<Node> nodes}) { ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.haven); } +Node? getEthereumDefaultNode({required Box<Node> nodes}) { + return nodes.values.firstWhereOrNull( + (Node node) => node.uriRaw == ethereumDefaultNodeUri) + ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); +} + Node getMoneroDefaultNode({required Box<Node> nodes}) { final timeZone = DateTime.now().timeZoneOffset.inHours; var nodeUri = ''; @@ -438,6 +451,8 @@ Future<void> checkCurrentNodes( .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final currentHavenNodeId = sharedPreferences .getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences + .getInt(PreferencesKey.currentEthereumNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull( (node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( @@ -446,6 +461,8 @@ Future<void> checkCurrentNodes( (node) => node.key == currentLitecoinElectrumSeverId); final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( (node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( + (node) => node.key == currentEthereumNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = @@ -479,6 +496,13 @@ Future<void> checkCurrentNodes( await sharedPreferences.setInt( PreferencesKey.currentHavenNodeIdKey, node.key as int); } + + if (currentEthereumNodeServer == null) { + final node = Node(uri: ethereumDefaultNodeUri, type: WalletType.ethereum); + await nodeSource.add(node); + await sharedPreferences.setInt( + PreferencesKey.currentEthereumNodeIdKey, node.key as int); + } } Future<void> resetBitcoinElectrumServer( @@ -522,8 +546,26 @@ Future<void> migrateExchangeStatus(SharedPreferences sharedPreferences) async { return; } - await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled + await sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, isExchangeDisabled ? ExchangeApiMode.disabled.raw : ExchangeApiMode.enabled.raw); - + await sharedPreferences.remove(PreferencesKey.disableExchangeKey); } + +Future<void> addEthereumNodeList({required Box<Node> 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<void> changeEthereumCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, + required Box<Node> nodes}) async { + final node = getEthereumDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, nodeId); +} diff --git a/lib/entities/load_current_wallet.dart b/lib/entities/load_current_wallet.dart index 882d1840e..d758b6697 100644 --- a/lib/entities/load_current_wallet.dart +++ b/lib/entities/load_current_wallet.dart @@ -1,8 +1,7 @@ import 'package:cake_wallet/di.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/core/key_service.dart'; -import 'package:cw_core/wallet_service.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; @@ -24,4 +23,6 @@ Future<void> loadCurrentWallet() async { final walletLoadingService = getIt.get<WalletLoadingService>(); final wallet = await walletLoadingService.load(type, name); appStore.changeCurrentWallet(wallet); + + getIt.get<BackgroundTasks>().registerSyncTask(); } diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 0cf3cead4..d6a7445f9 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -46,6 +46,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledBuyAction) { final uri = getIt.get<OnRamperBuyProvider>().requestUrl(); if (DeviceInfo.instance.isMobile) { @@ -116,6 +117,7 @@ class MainActions { switch (walletType) { case WalletType.bitcoin: case WalletType.litecoin: + case WalletType.ethereum: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); final uri = await moonPaySellProvider.requestUrl( diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 58847ccfa..b06351a79 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -70,6 +70,22 @@ Future<List<Node>> loadDefaultHavenNodes() async { return nodes; } +Future<List<Node>> loadDefaultEthereumNodes() async { + final nodesRaw = await rootBundle.loadString('assets/ethereum_server_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = <Node>[]; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map<String, Object>.from(raw)); + node.type = WalletType.ethereum; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box<Node> nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 3bbaf4941..c50629c1b 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -5,6 +5,7 @@ class PreferencesKey { static const currentBitcoinElectrumSererIdKey = 'current_node_id_btc'; static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; + static const currentEthereumNodeIdKey = 'current_node_id_eth'; static const currentFiatCurrencyKey = 'current_fiat_currency'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; @@ -31,23 +32,45 @@ class PreferencesKey { static const bitcoinTransactionPriority = 'current_fee_priority_bitcoin'; static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; + static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; + static const syncModeKey = 'sync_mode'; + static const syncAllKey = 'sync_all'; static const pinTimeOutDuration = 'pin_timeout_duration'; static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; + static const sortBalanceBy = 'sort_balance_by'; + static const pinNativeTokenAtTop = 'pin_native_token_at_top'; + static const useEtherscan = 'use_etherscan'; - - - static String moneroWalletUpdateV1Key(String name) - => '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; + static String moneroWalletUpdateV1Key(String name) => + '${PreferencesKey.moneroWalletPasswordUpdateV1Base}_${name}'; static const exchangeProvidersSelection = 'exchange-providers-selection'; - static const clearnetDonationLink = 'clearnet_donation_link'; + static const clearnetDonationLink = 'clearnet_donation_link'; static const onionDonationLink = 'onion_donation_link'; static const lastSeenAppVersion = 'last_seen_app_version'; - static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; + static const shouldShowMarketPlaceInDashboard = + 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; + static const shouldRequireTOTP2FAForAccessingWallet = + 'should_require_totp_2fa_for_accessing_wallets'; + static const shouldRequireTOTP2FAForSendsToContact = + 'should_require_totp_2fa_for_sends_to_contact'; + static const shouldRequireTOTP2FAForSendsToNonContact = + 'should_require_totp_2fa_for_sends_to_non_contact'; + static const shouldRequireTOTP2FAForSendsToInternalWallets = + 'should_require_totp_2fa_for_sends_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToInternalWallets = + 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForAddingContacts = + 'should_require_totp_2fa_for_adding_contacts'; + static const shouldRequireTOTP2FAForCreatingNewWallets = + 'should_require_totp_2fa_for_creating_new_wallets'; + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + 'should_require_totp_2fa_for_all_security_and_backup_settings'; + static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; } diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index 927ab8803..eb9417763 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cw_core/transaction_priority.dart'; @@ -14,6 +15,8 @@ List<TransactionPriority> priorityForWalletType(WalletType type) { return bitcoin!.getLitecoinTransactionPriorities(); case WalletType.haven: return haven!.getTransactionPriorities(); + case WalletType.ethereum: + return ethereum!.getTransactionPriorities(); default: return []; } diff --git a/lib/entities/sort_balance_types.dart b/lib/entities/sort_balance_types.dart new file mode 100644 index 000000000..5db64884e --- /dev/null +++ b/lib/entities/sort_balance_types.dart @@ -0,0 +1,19 @@ +import 'package:cake_wallet/generated/i18n.dart'; + +enum SortBalanceBy { + FiatBalance, + GrossBalance, + Alphabetical; + + @override + String toString() { + switch (this) { + case SortBalanceBy.FiatBalance: + return S.current.fiat_balance; + case SortBalanceBy.GrossBalance: + return S.current.gross_balance; + case SortBalanceBy.Alphabetical: + return S.current.alphabetical; + } + } +} \ No newline at end of file diff --git a/lib/entities/template.dart b/lib/entities/template.dart index 8224ecdd8..6955136e0 100644 --- a/lib/entities/template.dart +++ b/lib/entities/template.dart @@ -55,5 +55,5 @@ class Template extends HiveObject { String get amount => amountRaw ?? ''; - List<Template>? get additionalRecipients => additionalRecipientsRaw ?? null; + List<Template>? get additionalRecipients => additionalRecipientsRaw; } diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart new file mode 100644 index 000000000..f38cafbbd --- /dev/null +++ b/lib/ethereum/cw_ethereum.dart @@ -0,0 +1,126 @@ +part of 'ethereum.dart'; + +class CWEthereum extends Ethereum { + @override + List<String> getEthereumWordList(String language) => EthereumMnemonics.englishWordlist; + + WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource) => + EthereumWalletService(walletInfoSource); + + @override + WalletCredentials createEthereumNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + EthereumNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + }) => + EthereumRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + + @override + String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; + + @override + TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium; + + @override + List<TransactionPriority> getTransactionPriorities() => EthereumTransactionPriority.all; + + @override + TransactionPriority deserializeEthereumTransactionPriority(int raw) => + EthereumTransactionPriority.deserialize(raw: raw); + + Object createEthereumTransactionCredentials( + List<Output> outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }) => + EthereumTransactionCredentials( + 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 EthereumTransactionPriority, + currency: currency, + feeRate: feeRate, + ); + + Object createEthereumTransactionCredentialsRaw( + List<OutputInfo> outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }) => + EthereumTransactionCredentials( + outputs, + priority: priority as EthereumTransactionPriority?, + currency: currency, + feeRate: feeRate, + ); + + @override + int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); + + @override + double formatterEthereumAmountToDouble( + {TransactionInfo? transaction, BigInt? amount, int exponent = 18}) { + assert(transaction != null || amount != null); + + if (transaction != null) { + transaction as EthereumTransactionInfo; + return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); + } else { + return (amount!) / BigInt.from(10).pow(exponent); + } + } + + @override + List<Erc20Token> getERC20Currencies(WalletBase wallet) { + final ethereumWallet = wallet as EthereumWallet; + return ethereumWallet.erc20Currencies; + } + + @override + Future<void> addErc20Token(WalletBase wallet, Erc20Token token) async => + await (wallet as EthereumWallet).addErc20Token(token); + + @override + Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token) async => + await (wallet as EthereumWallet).deleteErc20Token(token); + + @override + Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress) async { + final ethereumWallet = wallet as EthereumWallet; + return await ethereumWallet.getErc20Token(contractAddress); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as EthereumTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.eth.title) { + return CryptoCurrency.eth; + } + + wallet as EthereumWallet; + return wallet.erc20Currencies + .firstWhere((element) => transaction.tokenSymbol == element.symbol); + } + + @override + void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { + (wallet as EthereumWallet).updateEtherscanUsageState(isEnabled); + } +} diff --git a/lib/main.dart b/lib/main.dart index b1e4ed582..ab85ad531 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -58,97 +58,103 @@ Future<void> main() async { return true; }; - final appDir = await getApplicationDocumentsDirectory(); await Hive.close(); - Hive.init(appDir.path); - if (!Hive.isAdapterRegistered(Contact.typeId)) { - Hive.registerAdapter(ContactAdapter()); - } + await initializeAppConfigs(); - if (!Hive.isAdapterRegistered(Node.typeId)) { - Hive.registerAdapter(NodeAdapter()); - } - - if (!Hive.isAdapterRegistered(TransactionDescription.typeId)) { - Hive.registerAdapter(TransactionDescriptionAdapter()); - } - - if (!Hive.isAdapterRegistered(Trade.typeId)) { - Hive.registerAdapter(TradeAdapter()); - } - - if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { - Hive.registerAdapter(WalletInfoAdapter()); - } - - if (!Hive.isAdapterRegistered(walletTypeTypeId)) { - Hive.registerAdapter(WalletTypeAdapter()); - } - - if (!Hive.isAdapterRegistered(Template.typeId)) { - Hive.registerAdapter(TemplateAdapter()); - } - - if (!Hive.isAdapterRegistered(ExchangeTemplate.typeId)) { - Hive.registerAdapter(ExchangeTemplateAdapter()); - } - - if (!Hive.isAdapterRegistered(Order.typeId)) { - Hive.registerAdapter(OrderAdapter()); - } - - if (!isMoneroOnly && !Hive.isAdapterRegistered(UnspentCoinsInfo.typeId)) { - Hive.registerAdapter(UnspentCoinsInfoAdapter()); - } - - if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) { - Hive.registerAdapter(AnonpayInvoiceInfoAdapter()); - } - - final secureStorage = FlutterSecureStorage(); - final transactionDescriptionsBoxKey = - await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); - final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); - final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey); - final contacts = await Hive.openBox<Contact>(Contact.boxName); - final nodes = await Hive.openBox<Node>(Node.boxName); - final transactionDescriptions = await Hive.openBox<TransactionDescription>( - TransactionDescription.boxName, - encryptionKey: transactionDescriptionsBoxKey); - final trades = await Hive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey); - final orders = await Hive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey); - final walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); - final templates = await Hive.openBox<Template>(Template.boxName); - final exchangeTemplates = await Hive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName); - final anonpayInvoiceInfo = await Hive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName); - Box<UnspentCoinsInfo>? unspentCoinsInfoSource; - - if (!isMoneroOnly) { - unspentCoinsInfoSource = await Hive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName); - } - - await initialSetup( - sharedPreferences: await SharedPreferences.getInstance(), - nodes: nodes, - walletInfoSource: walletInfoSource, - contactSource: contacts, - tradesSource: trades, - ordersSource: orders, - unspentCoinsInfoSource: unspentCoinsInfoSource, - // fiatConvertationService: fiatConvertationService, - templates: templates, - exchangeTemplates: exchangeTemplates, - transactionDescriptions: transactionDescriptions, - secureStorage: secureStorage, - anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 19); runApp(App()); }, (error, stackTrace) async { ExceptionHandler.onError(FlutterErrorDetails(exception: error, stack: stackTrace)); }); } +Future<void> initializeAppConfigs() async { + final appDir = await getApplicationDocumentsDirectory(); + Hive.init(appDir.path); + + if (!Hive.isAdapterRegistered(Contact.typeId)) { + Hive.registerAdapter(ContactAdapter()); + } + + if (!Hive.isAdapterRegistered(Node.typeId)) { + Hive.registerAdapter(NodeAdapter()); + } + + if (!Hive.isAdapterRegistered(TransactionDescription.typeId)) { + Hive.registerAdapter(TransactionDescriptionAdapter()); + } + + if (!Hive.isAdapterRegistered(Trade.typeId)) { + Hive.registerAdapter(TradeAdapter()); + } + + if (!Hive.isAdapterRegistered(WalletInfo.typeId)) { + Hive.registerAdapter(WalletInfoAdapter()); + } + + if (!Hive.isAdapterRegistered(walletTypeTypeId)) { + Hive.registerAdapter(WalletTypeAdapter()); + } + + if (!Hive.isAdapterRegistered(Template.typeId)) { + Hive.registerAdapter(TemplateAdapter()); + } + + if (!Hive.isAdapterRegistered(ExchangeTemplate.typeId)) { + Hive.registerAdapter(ExchangeTemplateAdapter()); + } + + if (!Hive.isAdapterRegistered(Order.typeId)) { + Hive.registerAdapter(OrderAdapter()); + } + + if (!isMoneroOnly && !Hive.isAdapterRegistered(UnspentCoinsInfo.typeId)) { + Hive.registerAdapter(UnspentCoinsInfoAdapter()); + } + + if (!Hive.isAdapterRegistered(AnonpayInvoiceInfo.typeId)) { + Hive.registerAdapter(AnonpayInvoiceInfoAdapter()); + } + + final secureStorage = FlutterSecureStorage(); + final transactionDescriptionsBoxKey = + await getEncryptionKey(secureStorage: secureStorage, forKey: TransactionDescription.boxKey); + final tradesBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Trade.boxKey); + final ordersBoxKey = await getEncryptionKey(secureStorage: secureStorage, forKey: Order.boxKey); + final contacts = await Hive.openBox<Contact>(Contact.boxName); + final nodes = await Hive.openBox<Node>(Node.boxName); + final transactionDescriptions = await Hive.openBox<TransactionDescription>( + TransactionDescription.boxName, + encryptionKey: transactionDescriptionsBoxKey); + final trades = await Hive.openBox<Trade>(Trade.boxName, encryptionKey: tradesBoxKey); + final orders = await Hive.openBox<Order>(Order.boxName, encryptionKey: ordersBoxKey); + final walletInfoSource = await Hive.openBox<WalletInfo>(WalletInfo.boxName); + final templates = await Hive.openBox<Template>(Template.boxName); + final exchangeTemplates = await Hive.openBox<ExchangeTemplate>(ExchangeTemplate.boxName); + final anonpayInvoiceInfo = await Hive.openBox<AnonpayInvoiceInfo>(AnonpayInvoiceInfo.boxName); + Box<UnspentCoinsInfo>? unspentCoinsInfoSource; + + if (!isMoneroOnly) { + unspentCoinsInfoSource = await Hive.openBox<UnspentCoinsInfo>(UnspentCoinsInfo.boxName); + } + + await initialSetup( + sharedPreferences: await SharedPreferences.getInstance(), + nodes: nodes, + walletInfoSource: walletInfoSource, + contactSource: contacts, + tradesSource: trades, + ordersSource: orders, + unspentCoinsInfoSource: unspentCoinsInfoSource, + // fiatConvertationService: fiatConvertationService, + templates: templates, + exchangeTemplates: exchangeTemplates, + transactionDescriptions: transactionDescriptions, + secureStorage: secureStorage, + anonpayInvoiceInfo: anonpayInvoiceInfo, + initialMigrationVersion: 21); + } + Future<void> initialSetup( {required SharedPreferences sharedPreferences, required Box<Node> nodes, @@ -329,7 +335,7 @@ class _HomeState extends State<_Home> { } } - + @override Widget build(BuildContext context) { return const SizedBox.shrink(); diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 336f04970..141401e6a 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:cake_wallet/core/fiat_conversion_service.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; +import 'package:cake_wallet/ethereum/ethereum.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'; @@ -31,6 +32,20 @@ Future<void> startFiatRateUpdate( fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); } + + if (appStore.wallet!.type == WalletType.ethereum) { + final currencies = + ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + + for (final currency in currencies) { + () async { + fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( + crypto: currency, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + }.call(); + } + } } catch (e) { print(e); } diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 7fdb6b84f..89f39d5f9 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -1,6 +1,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/update_haven_rate.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -97,6 +97,20 @@ void startCurrentWalletChangeReaction(AppStore appStore, crypto: wallet.currency, fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + + if (wallet.type == WalletType.ethereum) { + final currencies = + ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + + for (final currency in currencies) { + () async { + fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( + crypto: currency, + fiat: settingsStore.fiatCurrency, + torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + }.call(); + } + } } catch (e) { print(e.toString()); } diff --git a/lib/router.dart b/lib/router.dart index 74043a191..031a1075b 100644 --- a/lib/router.dart +++ b/lib/router.dart @@ -10,6 +10,8 @@ import 'package:cake_wallet/src/screens/backup/edit_backup_password_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_webview_page.dart'; import 'package:cake_wallet/src/screens/buy/webview_page.dart'; import 'package:cake_wallet/src/screens/buy/pre_order_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/edit_token_page.dart'; +import 'package:cake_wallet/src/screens/dashboard/home_settings_page.dart'; import 'package:cake_wallet/src/screens/restore/sweeping_wallet_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_invoice_page.dart'; import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart'; @@ -17,6 +19,7 @@ import 'package:cake_wallet/src/screens/dashboard/desktop_widgets/desktop_dashbo import 'package:cake_wallet/src/screens/dashboard/widgets/transactions_page.dart'; import 'package:cake_wallet/src/screens/settings/desktop_settings/desktop_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/display_settings_page.dart'; +import 'package:cake_wallet/src/screens/settings/manage_nodes_page.dart'; import 'package:cake_wallet/src/screens/settings/other_settings_page.dart'; import 'package:cake_wallet/src/screens/settings/privacy_page.dart'; import 'package:cake_wallet/src/screens/settings/security_backup_page.dart'; @@ -313,7 +316,7 @@ Route<dynamic> createRoute(RouteSettings settings) { return CupertinoPageRoute<void>( fullscreenDialog: true, builder: (_) => getIt.get<SecurityBackupPage>()); - + case Routes.privacyPage: return CupertinoPageRoute<void>( fullscreenDialog: true, @@ -328,7 +331,7 @@ Route<dynamic> createRoute(RouteSettings settings) { return CupertinoPageRoute<void>( fullscreenDialog: true, builder: (_) => getIt.get<OtherSettingsPage>()); - + case Routes.newNode: final args = settings.arguments as Map<String, dynamic>?; return CupertinoPageRoute<void>( @@ -336,7 +339,7 @@ Route<dynamic> createRoute(RouteSettings settings) { param1: args?['editingNode'] as Node?, param2: args?['isSelected'] as bool?)); - + case Routes.accountCreation: return CupertinoPageRoute<String>( @@ -466,7 +469,7 @@ Route<dynamic> createRoute(RouteSettings settings) { fullscreenDialog: true, builder: (_) => getIt.get<IoniaWelcomePage>(), ); - + case Routes.ioniaLoginPage: return CupertinoPageRoute<void>( builder: (_) => getIt.get<IoniaLoginPage>()); @@ -480,7 +483,7 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.ioniaBuyGiftCardPage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardPage>(param1: args)); - + case Routes.ioniaBuyGiftCardDetailPage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaBuyGiftCardDetailPage>(param1: args)); @@ -497,7 +500,7 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.ioniaAccountPage: return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountPage>()); - + case Routes.ioniaAccountCardsPage: return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaAccountCardsPage>()); @@ -508,11 +511,11 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.ioniaGiftCardDetailPage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaGiftCardDetailPage>(param1: args.first)); - + case Routes.ioniaCustomRedeemPage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaCustomRedeemPage>(param1: args)); - + case Routes.ioniaMoreOptionsPage: final args = settings.arguments as List; return CupertinoPageRoute<void>(builder: (_) => getIt.get<IoniaMoreOptionsPage>(param1: args)); @@ -584,6 +587,28 @@ Route<dynamic> createRoute(RouteSettings settings) { case Routes.modify2FAPage: return MaterialPageRoute<void>(builder: (_) => getIt.get<Modify2FAPage>()); + case Routes.homeSettings: + return CupertinoPageRoute<void>( + builder: (_) => getIt.get<HomeSettingsPage>(param1: settings.arguments), + ); + + case Routes.editToken: + final args = settings.arguments as Map<String, dynamic>; + + return CupertinoPageRoute<void>( + settings: RouteSettings(name: Routes.editToken), + builder: (_) => getIt.get<EditTokenPage>( + param1: args['homeSettingsViewModel'], + param2: { + 'token': args['token'], + 'contractAddress': args['contractAddress'], + }, + ), + ); + + case Routes.manageNodes: + return MaterialPageRoute<void>(builder: (_) => getIt.get<ManageNodesPage>()); + default: return MaterialPageRoute<void>( builder: (_) => Scaffold( diff --git a/lib/routes.dart b/lib/routes.dart index 762a7d2f5..7187a7263 100644 --- a/lib/routes.dart +++ b/lib/routes.dart @@ -88,4 +88,7 @@ class Routes { static const setup_2faQRPage = '/setup_2fa_qr_page'; static const totpAuthCodePage = '/totp_auth_code_page'; static const modify2FAPage = '/modify_2fa_page'; + static const homeSettings = '/home_settings'; + static const editToken = '/edit_token'; + static const manageNodes = '/manage_nodes'; } diff --git a/lib/src/screens/contact/contact_list_page.dart b/lib/src/screens/contact/contact_list_page.dart index ec8620ac6..601133c56 100644 --- a/lib/src/screens/contact/contact_list_page.dart +++ b/lib/src/screens/contact/contact_list_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/utils/show_bar.dart'; @@ -15,9 +16,10 @@ import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart import 'package:cake_wallet/src/widgets/collapsible_standart_list.dart'; class ContactListPage extends BasePage { - ContactListPage(this.contactListViewModel); + ContactListPage(this.contactListViewModel, this.authService); final ContactListViewModel contactListViewModel; + final AuthService authService; @override String get title => S.current.address_book; @@ -26,95 +28,99 @@ class ContactListPage extends BasePage { Widget? trailing(BuildContext context) { return MergeSemantics( child: Container( - width: 32.0, - height: 32.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context) - .accentTextTheme! - .bodySmall! - .color!), - child: Stack( - alignment: Alignment.center, - children: <Widget>[ - Icon(Icons.add, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!, - size: 22.0), - ButtonTheme( - minWidth: 32.0, - height: 32.0, - child: Semantics( - label: S.of(context).add, - child: TextButton( - // FIX-ME: Style - //shape: CircleBorder(), - onPressed: () async { - await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact); - }, - child: Offstage()), - ), - ) - ], - )), + width: 32.0, + height: 32.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context).accentTextTheme!.bodySmall!.color!), + child: Stack( + alignment: Alignment.center, + children: <Widget>[ + Icon( + Icons.add, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, + size: 22.0, + ), + ButtonTheme( + minWidth: 32.0, + height: 32.0, + child: TextButton( + // FIX-ME: Style + //shape: CircleBorder(), + onPressed: () async { + if (contactListViewModel + .shouldRequireTOTP2FAForAddingContacts) { + authService.authenticateAction( + context, + route: Routes.addressBookAddContact, + conditionToDetermineIfToUse2FA: contactListViewModel + .shouldRequireTOTP2FAForAddingContacts, + ); + } else { + await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact); + } + }, + child: Offstage()), + ) + ], + ), + ), ); } @override Widget body(BuildContext context) { - return Container( padding: EdgeInsets.only(top: 20.0, bottom: 20.0), - child: Observer( - builder: (_) { + child: Observer(builder: (_) { final contacts = contactListViewModel.contactsToShow; final walletContacts = contactListViewModel.walletContactsToShow; return CollapsibleSectionList( - context: context, - sectionCount: 2, - themeColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!, - dividerThemeColor: - Theme.of(context).primaryTextTheme!.bodySmall!.decorationColor!, - sectionTitleBuilder: (_, int sectionIndex) { - var title = S.current.contact_list_contacts; + context: context, + sectionCount: 2, + themeColor: Theme.of(context).primaryTextTheme.titleLarge!.color!, + dividerThemeColor: + Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!, + sectionTitleBuilder: (_, int sectionIndex) { + var title = S.current.contact_list_contacts; - if (sectionIndex == 0) { - title = S.current.contact_list_wallets; - } + if (sectionIndex == 0) { + title = S.current.contact_list_wallets; + } - return Container( - padding: EdgeInsets.only(bottom: 10), - child: Text(title, style: TextStyle(fontSize: 36))); - }, - itemCounter: (int sectionIndex) => sectionIndex == 0 - ? walletContacts.length - : contacts.length, - itemBuilder: (_, sectionIndex, index) { - if (sectionIndex == 0) { - final walletInfo = walletContacts[index]; - return generateRaw(context, walletInfo); - } + return Container( + padding: EdgeInsets.only(bottom: 10), + child: Text(title, style: TextStyle(fontSize: 36))); + }, + itemCounter: (int sectionIndex) => + sectionIndex == 0 ? walletContacts.length : contacts.length, + itemBuilder: (_, sectionIndex, index) { + if (sectionIndex == 0) { + final walletInfo = walletContacts[index]; + return generateRaw(context, walletInfo); + } - final contact = contacts[index]; - final content = generateRaw(context, contact); - return contactListViewModel.isEditable - ? Slidable( - key: Key('${contact.key}'), - endActionPane: _actionPane(context, contact), - child: content, - ) - : content; - }, - );}) - ); + final contact = contacts[index]; + final content = generateRaw(context, contact); + return contactListViewModel.isEditable + ? Slidable( + key: Key('${contact.key}'), + endActionPane: _actionPane(context, contact), + child: content, + ) + : content; + }, + ); + })); } Widget generateRaw(BuildContext context, ContactBase contact) { final image = contact.type.iconPath; - final currencyIcon = image != null ? Image.asset(image, height: 24, width: 24) + final currencyIcon = image != null + ? Image.asset(image, height: 24, width: 24) : const SizedBox(height: 24, width: 24); - return GestureDetector( onTap: () async { if (!contactListViewModel.isEditable) { @@ -128,30 +134,28 @@ class ContactListPage extends BasePage { if (isCopied) { await Clipboard.setData(ClipboardData(text: contact.address)); await showBar<void>(context, S.of(context).copied_to_clipboard); - } }, child: Container( color: Colors.transparent, - padding: - const EdgeInsets.only(top: 16, bottom: 16, right: 24), + padding: const EdgeInsets.only(top: 16, bottom: 16, right: 24), child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, children: <Widget>[ currencyIcon, Expanded( - child: Padding( - padding: EdgeInsets.only(left: 12), - child: Text( - contact.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.normal, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!), + child: Padding( + padding: EdgeInsets.only(left: 12), + child: Text( + contact.name, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, ), - ) - ) + ), + )) ], ), ), @@ -160,60 +164,61 @@ class ContactListPage extends BasePage { Future<bool> showAlertDialog(BuildContext context) async { return await showPopUp<bool>( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.of(context).address_remove_contact, - alertContent: S.of(context).address_remove_content, - rightButtonText: S.of(context).remove, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? false; + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).address_remove_contact, + alertContent: S.of(context).address_remove_content, + rightButtonText: S.of(context).remove, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; } Future<bool> showNameAndAddressDialog( BuildContext context, String name, String address) async { return await showPopUp<bool>( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: name, - alertContent: address, - rightButtonText: S.of(context).copy, - leftButtonText: S.of(context).cancel, - actionRightButton: () => Navigator.of(context).pop(true), - actionLeftButton: () => Navigator.of(context).pop(false)); - }) ?? false; + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: name, + alertContent: address, + rightButtonText: S.of(context).copy, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; } - ActionPane _actionPane(BuildContext context, ContactRecord contact) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.4, - children: [ - SlidableAction( - onPressed: (_) async => await Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, - arguments: contact), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - SlidableAction( - onPressed: (_) async { - final isDelete = - await showAlertDialog(context); + ActionPane _actionPane(BuildContext context, ContactRecord contact) => + ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.4, + children: [ + SlidableAction( + onPressed: (_) async => await Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: contact), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + SlidableAction( + onPressed: (_) async { + final isDelete = await showAlertDialog(context); - if (isDelete) { - await contactListViewModel.delete(contact); - } - }, - backgroundColor: Colors.red, - foregroundColor: Colors.white, - icon: CupertinoIcons.delete, - label: S.of(context).delete, - ), - ], - ); + if (isDelete) { + await contactListViewModel.delete(contact); + } + }, + backgroundColor: Colors.red, + foregroundColor: Colors.white, + icon: CupertinoIcons.delete, + label: S.of(context).delete, + ), + ], + ); } diff --git a/lib/src/screens/dashboard/dashboard_page.dart b/lib/src/screens/dashboard/dashboard_page.dart index 0c46aaf25..56b778e08 100644 --- a/lib/src/screens/dashboard/dashboard_page.dart +++ b/lib/src/screens/dashboard/dashboard_page.dart @@ -298,24 +298,7 @@ class _DashboardPageView extends BasePage { } }); - final sharedPrefs = await SharedPreferences.getInstance(); - final currentAppVersion = - VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion); - final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion); - final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall); - - if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) { - await Future<void>.delayed(Duration(seconds: 1)); - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return ReleaseNotesScreen( - title: 'Version ${dashboardViewModel.settingsStore.appVersion}'); - }); - sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); - } else if (isNewInstall!) { - sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); - } + _showReleaseNotesPopup(context); var needToPresentYat = false; var isInactive = false; @@ -341,4 +324,27 @@ class _DashboardPageView extends BasePage { needToPresentYat = true; }); } + + void _showReleaseNotesPopup(BuildContext context) async { + final sharedPrefs = await SharedPreferences.getInstance(); + final currentAppVersion = + VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion); + final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion); + final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall); + + if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) { + Future<void>.delayed(Duration(seconds: 1), () { + showPopUp<void>( + context: context, + builder: (BuildContext context) { + return ReleaseNotesScreen( + title: 'Version ${dashboardViewModel.settingsStore.appVersion}'); + }); + }); + + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } else if (isNewInstall!) { + sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion); + } + } } 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 501252978..089987d5d 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 @@ -30,6 +30,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD final bitcoinIcon = Image.asset('assets/images/bitcoin.png', height: 24, width: 24); final litecoinIcon = Image.asset('assets/images/litecoin_icon.png', height: 24, width: 24); final 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); final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24); Image _newWalletImage(BuildContext context) => Image.asset( @@ -136,6 +137,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD return litecoinIcon; case WalletType.haven: return havenIcon; + case WalletType.ethereum: + return ethereumIcon; default: return nonWalletTypeIcon; } @@ -156,15 +159,29 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD } catch (e) { changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); } - }); + }, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, + ); } void _navigateToCreateWallet() { if (isSingleCoin) { - Navigator.of(context) - .pushNamed(Routes.newWallet, arguments: widget.walletListViewModel.currentWalletType); + widget._authService.authenticateAction( + context, + route: Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + conditionToDetermineIfToUse2FA: widget + .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); } else { - Navigator.of(context).pushNamed(Routes.newWalletType); + widget._authService.authenticateAction( + context, + route: Routes.newWalletType, + conditionToDetermineIfToUse2FA: widget + .walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } } diff --git a/lib/src/screens/dashboard/edit_token_page.dart b/lib/src/screens/dashboard/edit_token_page.dart new file mode 100644 index 000000000..9d1abc893 --- /dev/null +++ b/lib/src/screens/dashboard/edit_token_page.dart @@ -0,0 +1,309 @@ +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/address_text_field.dart'; +import 'package:cake_wallet/src/widgets/base_text_form_field.dart'; +import 'package:cake_wallet/src/widgets/checkbox_widget.dart'; +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class EditTokenPage extends BasePage { + EditTokenPage({ + Key? key, + required this.homeSettingsViewModel, + this.erc20token, + this.initialContractAddress, + }) : assert(erc20token == null || initialContractAddress == null); + + final HomeSettingsViewModel homeSettingsViewModel; + final Erc20Token? erc20token; + final String? initialContractAddress; + + @override + String? get title => S.current.edit_token; + + @override + Widget body(BuildContext context) { + return EditTokenPageBody( + homeSettingsViewModel: homeSettingsViewModel, + erc20token: erc20token, + initialContractAddress: initialContractAddress, + ); + } +} + +class EditTokenPageBody extends StatefulWidget { + const EditTokenPageBody({ + Key? key, + required this.homeSettingsViewModel, + this.erc20token, + this.initialContractAddress, + }) : super(key: key); + + final HomeSettingsViewModel homeSettingsViewModel; + final Erc20Token? erc20token; + final String? initialContractAddress; + + @override + State<EditTokenPageBody> createState() => _EditTokenPageBodyState(); +} + +class _EditTokenPageBodyState extends State<EditTokenPageBody> { + final TextEditingController _contractAddressController = TextEditingController(); + final TextEditingController _tokenNameController = TextEditingController(); + final TextEditingController _tokenSymbolController = TextEditingController(); + final TextEditingController _tokenDecimalController = TextEditingController(); + + final FocusNode _contractAddressFocusNode = FocusNode(); + final FocusNode _tokenNameFocusNode = FocusNode(); + final FocusNode _tokenSymbolFocusNode = FocusNode(); + final FocusNode _tokenDecimalFocusNode = FocusNode(); + + final GlobalKey<FormState> _formKey = GlobalKey<FormState>(); + + bool _showDisclaimer = false; + bool _disclaimerChecked = false; + + @override + void initState() { + super.initState(); + + if (widget.erc20token != null) { + _contractAddressController.text = widget.erc20token!.contractAddress; + _tokenNameController.text = widget.erc20token!.name; + _tokenSymbolController.text = widget.erc20token!.symbol; + _tokenDecimalController.text = widget.erc20token!.decimal.toString(); + } + + if (widget.initialContractAddress != null) { + _contractAddressController.text = widget.initialContractAddress!; + _getTokenInfo(); + } + + _contractAddressFocusNode.addListener(() { + if (!_contractAddressFocusNode.hasFocus) { + _getTokenInfo(); + } + + final contractAddress = _contractAddressController.text; + if (contractAddress.isNotEmpty && contractAddress != widget.erc20token?.contractAddress) { + setState(() { + _showDisclaimer = true; + }); + } + }); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () => FocusScope.of(context).unfocus(), + child: ScrollableWithBottomSection( + contentPadding: EdgeInsets.zero, + content: Padding( + padding: EdgeInsets.symmetric(horizontal: 25), + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 16, horizontal: 28), + decoration: BoxDecoration( + color: Theme.of(context).accentTextTheme.bodySmall!.color!, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Image.asset('assets/images/restore_keys.png'), + const SizedBox(width: 24), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context).warning, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, + ), + ), + Padding( + padding: EdgeInsets.only(top: 5), + child: Text( + S.of(context).add_token_warning, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.normal, + color: Theme.of(context).primaryTextTheme.labelSmall!.color!, + ), + ), + ), + ], + ), + ), + ], + ), + ), + SizedBox(height: 50), + _tokenForm(), + ], + ), + ), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSection: Column( + children: [ + if (_showDisclaimer) ...[ + CheckboxWidget( + value: _disclaimerChecked, + caption: S.of(context).add_token_disclaimer_check, + onChanged: (value) { + _disclaimerChecked = value; + }, + ), + SizedBox(height: 20), + ], + Row( + children: <Widget>[ + Expanded( + child: PrimaryButton( + onPressed: () async { + if (widget.erc20token != null) { + await widget.homeSettingsViewModel.deleteErc20Token(widget.erc20token!); + } + Navigator.pop(context); + }, + text: widget.erc20token != null ? S.of(context).delete : S.of(context).cancel, + color: Colors.red, + textColor: Colors.white, + ), + ), + SizedBox(width: 20), + Expanded( + child: PrimaryButton( + onPressed: () async { + if (_formKey.currentState!.validate() && + (!_showDisclaimer || _disclaimerChecked)) { + await widget.homeSettingsViewModel.addErc20Token(Erc20Token( + name: _tokenNameController.text, + symbol: _tokenSymbolController.text, + contractAddress: _contractAddressController.text, + decimal: int.parse(_tokenDecimalController.text), + )); + Navigator.pop(context); + } + }, + text: S.of(context).save, + color: Theme.of(context).accentTextTheme.bodyLarge!.color!, + textColor: Colors.white, + ), + ), + ], + ), + ], + ), + ), + ); + } + + void _getTokenInfo() async { + if (_contractAddressController.text.isNotEmpty) { + final token = + await widget.homeSettingsViewModel.getErc20Token(_contractAddressController.text); + + if (token != null) { + if (_tokenNameController.text.isEmpty) _tokenNameController.text = token.name; + if (_tokenSymbolController.text.isEmpty) _tokenSymbolController.text = token.symbol; + if (_tokenDecimalController.text.isEmpty) + _tokenDecimalController.text = token.decimal.toString(); + } + } + } + + Future<void> _pasteText() async { + final value = await Clipboard.getData('text/plain'); + + if (value?.text?.isNotEmpty ?? false) { + _contractAddressController.text = value!.text!; + + _getTokenInfo(); + setState(() { + _showDisclaimer = true; + }); + } + } + + Widget _tokenForm() { + return Form( + key: _formKey, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + AddressTextField( + controller: _contractAddressController, + focusNode: _contractAddressFocusNode, + placeholder: S.of(context).token_contract_address, + options: [AddressTextFieldOption.paste], + buttonColor: Theme.of(context).hintColor, + validator: AddressValidator(type: widget.homeSettingsViewModel.nativeToken), + onPushPasteButton: (_) { + _pasteText(); + }, + ), + const SizedBox(height: 8), + BaseTextFormField( + controller: _tokenNameController, + focusNode: _tokenNameFocusNode, + onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenSymbolFocusNode), + textInputAction: TextInputAction.next, + hintText: S.of(context).token_name, + validator: (text) { + if (text?.isNotEmpty ?? false) { + return null; + } + + return S.of(context).field_required; + }, + ), + const SizedBox(height: 8), + BaseTextFormField( + controller: _tokenSymbolController, + focusNode: _tokenSymbolFocusNode, + onSubmit: (_) => FocusScope.of(context).requestFocus(_tokenDecimalFocusNode), + textInputAction: TextInputAction.next, + hintText: S.of(context).token_symbol, + validator: (text) { + if (text?.isNotEmpty ?? false) { + return null; + } + + return S.of(context).field_required; + }, + ), + const SizedBox(height: 8), + BaseTextFormField( + controller: _tokenDecimalController, + focusNode: _tokenDecimalFocusNode, + textInputAction: TextInputAction.done, + hintText: S.of(context).token_decimal, + validator: (text) { + if (text?.isEmpty ?? true) { + return S.of(context).field_required; + } + if (int.tryParse(text!) == null) { + return S.of(context).invalid_input; + } + + return null; + }, + ), + SizedBox(height: 24), + ], + ), + ); + } +} diff --git a/lib/src/screens/dashboard/home_settings_page.dart b/lib/src/screens/dashboard/home_settings_page.dart new file mode 100644 index 000000000..c6b005bde --- /dev/null +++ b/lib/src/screens/dashboard/home_settings_page.dart @@ -0,0 +1,164 @@ +import 'dart:math'; + +import 'package:cake_wallet/core/address_validator.dart'; +import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/view_model/dashboard/home_settings_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class HomeSettingsPage extends BasePage { + HomeSettingsPage(this._homeSettingsViewModel); + + final HomeSettingsViewModel _homeSettingsViewModel; + + final TextEditingController _searchController = TextEditingController(); + + @override + String? get title => S.current.home_screen_settings; + + @override + Widget body(BuildContext context) { + return SingleChildScrollView( + child: Column( + children: [ + Observer( + builder: (_) => SettingsPickerCell<SortBalanceBy>( + title: S.current.sort_by, + items: SortBalanceBy.values, + selectedItem: _homeSettingsViewModel.sortBalanceBy, + onItemSelected: _homeSettingsViewModel.setSortBalanceBy, + ), + ), + Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!), + Observer( + builder: (_) => SettingsSwitcherCell( + title: S.of(context).pin_at_top(_homeSettingsViewModel.nativeToken.title), + value: _homeSettingsViewModel.pinNativeToken, + onValueChange: (_, bool value) { + _homeSettingsViewModel.setPinNativeToken(value); + }, + ), + ), + Divider(color: Theme.of(context).primaryTextTheme.bodySmall!.decorationColor!), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(start: 16), + child: TextFormField( + controller: _searchController, + style: TextStyle(color: Theme.of(context).primaryTextTheme.titleLarge!.color!), + decoration: InputDecoration( + hintText: S.of(context).search_add_token, + prefixIcon: Image.asset("assets/images/search_icon.png"), + filled: true, + fillColor: Theme.of(context).accentTextTheme.displaySmall!.color!, + alignLabelWithHint: false, + contentPadding: const EdgeInsets.symmetric(vertical: 4, horizontal: 16), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: const BorderSide(color: Colors.transparent), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(30), + borderSide: const BorderSide(color: Colors.transparent), + ), + ), + onChanged: (String text) => _homeSettingsViewModel.changeSearchText(text), + ), + ), + ), + RawMaterialButton( + onPressed: () async { + Navigator.pushNamed(context, Routes.editToken, arguments: { + 'homeSettingsViewModel': _homeSettingsViewModel, + if (AddressValidator(type: _homeSettingsViewModel.nativeToken) + .isValid(_searchController.text)) + 'contractAddress': _searchController.text, + }); + }, + elevation: 0, + fillColor: Theme.of(context).accentTextTheme.bodySmall!.color!, + child: Icon( + Icons.add, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, + size: 22.0, + ), + padding: EdgeInsets.all(12), + shape: CircleBorder(), + splashColor: Theme.of(context).accentTextTheme.bodySmall!.color!, + ), + ], + ), + Padding( + padding: const EdgeInsets.only(bottom: 16, left: 16, right: 16), + child: Observer( + builder: (_) => ListView.builder( + itemCount: _homeSettingsViewModel.tokens.length, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemBuilder: (context, index) { + return Container( + margin: EdgeInsets.only(top: 16), + child: Observer( + builder: (_) { + final token = _homeSettingsViewModel.tokens.elementAt(index); + + return SettingsSwitcherCell( + title: "${token.name} " + "(${token.symbol})", + value: token.enabled, + onValueChange: (_, bool value) { + _homeSettingsViewModel.changeTokenAvailability(token, value); + }, + onTap: (_) { + Navigator.pushNamed(context, Routes.editToken, arguments: { + 'homeSettingsViewModel': _homeSettingsViewModel, + 'token': token, + }); + }, + leading: token.iconPath != null + ? Container( + child: Image.asset( + token.iconPath!, + height: 30.0, + width: 30.0, + ), + ) + : Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + token.symbol.substring(0, min(token.symbol.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + decoration: BoxDecoration( + color: Theme.of(context).accentTextTheme.bodySmall!.color!, + borderRadius: BorderRadius.circular(30), + ), + ); + }, + ), + ); + }, + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/dashboard/widgets/address_page.dart b/lib/src/screens/dashboard/widgets/address_page.dart index dcfeb6030..18f85fa07 100644 --- a/lib/src/screens/dashboard/widgets/address_page.dart +++ b/lib/src/screens/dashboard/widgets/address_page.dart @@ -27,15 +27,15 @@ class AddressPage extends BasePage { required this.addressListViewModel, required this.dashboardViewModel, required this.receiveOptionViewModel, - }) : _cryptoAmountFocus = FocusNode(), - _formKey = GlobalKey<FormState>(), - _amountController = TextEditingController(){ - _amountController.addListener(() { - if (_formKey.currentState!.validate()) { - addressListViewModel.changeAmount( - _amountController.text, - ); - } + }) : _cryptoAmountFocus = FocusNode(), + _formKey = GlobalKey<FormState>(), + _amountController = TextEditingController() { + _amountController.addListener(() { + if (_formKey.currentState!.validate()) { + addressListViewModel.changeAmount( + _amountController.text, + ); + } }); } @@ -63,15 +63,11 @@ class AddressPage extends BasePage { Widget? leading(BuildContext context) { final _backButton = Icon( Icons.arrow_back_ios, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, size: 16, ); - final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme - : closeButtonImage; + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; @@ -82,13 +78,10 @@ class AddressPage extends BasePage { child: ButtonTheme( minWidth: double.minPositive, child: Semantics( - label: !isMobileView - ? S.of(context).close - : S.of(context).seed_alert_back, + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, child: TextButton( style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent), + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), ), onPressed: () => onClose(context), child: !isMobileView ? _closeButton : _backButton, @@ -100,8 +93,7 @@ class AddressPage extends BasePage { } @override - Widget middle(BuildContext context) => - PresentReceiveOptionPicker( + Widget middle(BuildContext context) => PresentReceiveOptionPicker( receiveOptionViewModel: receiveOptionViewModel, hasWhiteBackground: currentTheme.type == ThemeType.light, ); @@ -136,10 +128,7 @@ class AddressPage extends BasePage { icon: Icon( Icons.share, size: 20, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, ), ), ); @@ -180,10 +169,7 @@ class AddressPage extends BasePage { tapOutsideToDismiss: true, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context) - .accentTextTheme! - .bodyLarge! - .backgroundColor!, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!, nextFocus: false, actions: [ KeyboardActionsItem( @@ -205,62 +191,54 @@ class AddressPage extends BasePage { isLight: dashboardViewModel.settingsStore.currentTheme.type == ThemeType.light))), Observer(builder: (_) { - return addressListViewModel.hasAddressList - ? GestureDetector( - onTap: () => Navigator.of(context).pushNamed(Routes.receive), - child: Container( - height: 50, - padding: EdgeInsets.only(left: 24, right: 12), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(25)), - border: Border.all( - color: Theme.of(context) - .textTheme! - .titleMedium! - .color!, - width: 1), - color: Theme.of(context) - .textTheme! - .titleLarge! - .backgroundColor!), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: <Widget>[ - Observer( - builder: (_) => Text( - addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!), - )), - Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - ) - ], - ), - ), - ) - : Text(S.of(context).electrum_address_disclaimer, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 15, - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .backgroundColor!)); + if (addressListViewModel.hasAddressList) { + return GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.receive), + child: Container( + height: 50, + padding: EdgeInsets.only(left: 24, right: 12), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(25)), + border: Border.all( + color: Theme.of(context).textTheme.titleMedium!.color!, width: 1), + color: Theme.of(context).textTheme.titleLarge!.backgroundColor!), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Observer( + builder: (_) => Text( + addressListViewModel.hasAccounts + ? S.of(context).accounts_subaddresses + : S.of(context).addresses, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context) + .accentTextTheme.displayMedium! + .backgroundColor!), + )), + Icon( + Icons.arrow_forward_ios, + size: 14, + color: + Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + ) + ], + ), + ), + ); + } else if (addressListViewModel.showElectrumAddressDisclaimer) { + return Text(S.of(context).electrum_address_disclaimer, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 15, + color: + Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!)); + } else { + return const SizedBox(); + } }) ], ), diff --git a/lib/src/screens/dashboard/widgets/balance_page.dart b/lib/src/screens/dashboard/widgets/balance_page.dart index 9d05eefb7..0fc0f8112 100644 --- a/lib/src/screens/dashboard/widgets/balance_page.dart +++ b/lib/src/screens/dashboard/widgets/balance_page.dart @@ -1,8 +1,8 @@ +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; -import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; @@ -20,51 +20,78 @@ class BalancePage extends StatelessWidget { @override Widget build(BuildContext context) { return GestureDetector( - onLongPress: () => dashboardViewModel.balanceViewModel.isReversing = - !dashboardViewModel.balanceViewModel.isReversing, - onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = - !dashboardViewModel.balanceViewModel.isReversing, - child: SingleChildScrollView( - child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox(height: 56), - Container( + onLongPress: () => dashboardViewModel.balanceViewModel.isReversing = + !dashboardViewModel.balanceViewModel.isReversing, + onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing = + !dashboardViewModel.balanceViewModel.isReversing, + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 56), + Container( margin: const EdgeInsets.only(left: 24, bottom: 16), - child: Observer(builder: (_) { - return Text(dashboardViewModel.balanceViewModel.asset, - style: TextStyle( - fontSize: 24, - fontFamily: 'Lato', - fontWeight: FontWeight.w600, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - height: 1), - maxLines: 1, - textAlign: TextAlign.center); - })), - Observer(builder: (_) { - if (dashboardViewModel.balanceViewModel.isShowCard && FeatureFlag.isCakePayEnabled) { - return IntroducingCard( - title: S.of(context).introducing_cake_pay, - subTitle: S.of(context).cake_pay_learn_more, - borderColor: settingsStore.currentTheme.type == ThemeType.bright - ? Color.fromRGBO(255, 255, 255, 0.2) - : Colors.transparent, - closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); - } - return Container(); - }), - Observer(builder: (_) { - return ListView.separated( - physics: NeverScrollableScrollPhysics(), - shrinkWrap: true, - separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), - itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, - itemBuilder: (__, index) { - final balance = - dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); - return buildBalanceRow(context, + child: Observer( + builder: (_) { + return Row( + children: [ + Text( + dashboardViewModel.balanceViewModel.asset, + style: TextStyle( + fontSize: 24, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + if (dashboardViewModel.balanceViewModel.isHomeScreenSettingsEnabled) + InkWell( + onTap: () => Navigator.pushNamed(context, Routes.homeSettings, + arguments: dashboardViewModel.balanceViewModel), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Image.asset( + 'assets/images/home_screen_settings_icon.png', + color: + Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + ), + ), + ), + ], + ); + }, + ), + ), + Observer( + builder: (_) { + if (dashboardViewModel.balanceViewModel.isShowCard && + FeatureFlag.isCakePayEnabled) { + return IntroducingCard( + title: S.of(context).introducing_cake_pay, + subTitle: S.of(context).cake_pay_learn_more, + borderColor: settingsStore.currentTheme.type == ThemeType.bright + ? Color.fromRGBO(255, 255, 255, 0.2) + : Colors.transparent, + closeCard: dashboardViewModel.balanceViewModel.disableIntroCakePayCard); + } + return Container(); + }, + ), + Observer( + builder: (_) { + return ListView.separated( + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 8)), + itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length, + itemBuilder: (__, index) { + final balance = + dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index); + return buildBalanceRow( + context, availableBalanceLabel: '${dashboardViewModel.balanceViewModel.availableBalanceLabel}', availableBalance: balance.availableBalance, @@ -75,45 +102,57 @@ class BalancePage extends StatelessWidget { additionalFiatBalance: balance.fiatAdditionalBalance, frozenBalance: balance.frozenBalance, frozenFiatBalance: balance.fiatFrozenBalance, - currency: balance.formattedAssetTitle); - }); - }) - ]))); + currency: balance.formattedAssetTitle, + hasAdditionalBalance: + dashboardViewModel.balanceViewModel.hasAdditionalBalance, + ); + }, + ); + }, + ) + ], + ), + ), + ); } - Widget buildBalanceRow(BuildContext context, - {required String availableBalanceLabel, - required String availableBalance, - required String availableFiatBalance, - required String additionalBalanceLabel, - required String additionalBalance, - required String additionalFiatBalance, - required String frozenBalance, - required String frozenFiatBalance, - required String currency}) { + Widget buildBalanceRow( + BuildContext context, { + required String availableBalanceLabel, + required String availableBalance, + required String availableFiatBalance, + required String additionalBalanceLabel, + required String additionalBalance, + required String additionalFiatBalance, + required String frozenBalance, + required String frozenFiatBalance, + required String currency, + required bool hasAdditionalBalance, + }) { return Container( margin: const EdgeInsets.only(left: 16, right: 16), decoration: BoxDecoration( - borderRadius: BorderRadius.circular(30.0), - border: Border.all( - color: settingsStore.currentTheme.type == ThemeType.bright - ? Color.fromRGBO(255, 255, 255, 0.2) - : Colors.transparent, - width: 1, - ), - color: Theme.of(context).textTheme!.titleLarge!.backgroundColor!), + borderRadius: BorderRadius.circular(30.0), + border: Border.all( + color: settingsStore.currentTheme.type == ThemeType.bright + ? Color.fromRGBO(255, 255, 255, 0.2) + : Colors.transparent, + width: 1, + ), + color: Theme.of(context).textTheme.titleLarge!.backgroundColor!, + ), child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ + margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, children: [ GestureDetector( behavior: HitTestBehavior.opaque, - onTap: () => _showBalanceDescription(context), + onTap: hasAdditionalBalance ? () => _showBalanceDescription(context) : null, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -129,19 +168,19 @@ class BalancePage extends StatelessWidget { .displaySmall! .backgroundColor!, height: 1)), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 4), - child: Icon(Icons.help_outline, - size: 16, - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .backgroundColor!), - ) + if (hasAdditionalBalance) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 4), + child: Icon(Icons.help_outline, + size: 16, + color: Theme.of(context) + .accentTextTheme! + .displaySmall! + .backgroundColor!), + ), ], - ),SizedBox( - height: 6, ), + SizedBox(height: 6), AutoSizeText(availableBalance, style: TextStyle( fontSize: 24, @@ -154,9 +193,7 @@ class BalancePage extends StatelessWidget { height: 1), maxLines: 1, textAlign: TextAlign.start), - SizedBox( - height: 6, - ), + SizedBox(height: 6), Text('${availableFiatBalance}', textAlign: TextAlign.center, style: TextStyle( @@ -168,7 +205,6 @@ class BalancePage extends StatelessWidget { .displayMedium! .backgroundColor!, height: 1)), - ], ), ), @@ -177,97 +213,99 @@ class BalancePage extends StatelessWidget { fontSize: 28, fontFamily: 'Lato', fontWeight: FontWeight.w800, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, + color: Theme.of(context).accentTextTheme!.displayMedium!.backgroundColor!, height: 1)), ], ), - SizedBox(height: 26), if (frozenBalance.isNotEmpty) - Column(crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(S.current.frozen_balance, + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 26), + Text( + S.current.frozen_balance, textAlign: TextAlign.center, style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .backgroundColor!, - height: 1)), - SizedBox(height: 8), - AutoSizeText(frozenBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - height: 1), - maxLines: 1, - textAlign: TextAlign.center), - SizedBox(height: 4), - Text( - frozenFiatBalance, - textAlign: TextAlign.center, - style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - height: 1), - ), - SizedBox(height: 24) - ]), - Text('${additionalBalanceLabel}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .backgroundColor!, - height: 1)), - SizedBox(height: 8), - AutoSizeText(additionalBalance, - style: TextStyle( - fontSize: 20, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - height: 1), - maxLines: 1, - textAlign: TextAlign.center), - SizedBox( - height: 4, - ), - Text( - '${additionalFiatBalance}', - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 12, - fontFamily: 'Lato', - fontWeight: FontWeight.w400, - color: Theme.of(context) - .accentTextTheme! - .displayMedium! - .backgroundColor!, - height: 1), - ) - ])), + color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + frozenBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Text( + frozenFiatBalance, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + height: 1, + ), + ), + ], + ), + if (hasAdditionalBalance) + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SizedBox(height: 24), + Text( + '${additionalBalanceLabel}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme.displaySmall!.backgroundColor!, + height: 1, + ), + ), + SizedBox(height: 8), + AutoSizeText( + additionalBalance, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + height: 1, + ), + maxLines: 1, + textAlign: TextAlign.center, + ), + SizedBox(height: 4), + Text( + '${additionalFiatBalance}', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w400, + color: Theme.of(context).accentTextTheme.displayMedium!.backgroundColor!, + height: 1, + ), + ), + ], + ), + ], + ), + ), ); } diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 56b2e87e6..9dd08add9 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -19,17 +19,18 @@ class MenuWidget extends StatefulWidget { class MenuWidgetState extends State<MenuWidget> { MenuWidgetState() - : this.menuWidth = 0, - this.screenWidth = 0, - this.screenHeight = 0, - this.headerHeight = 120, - this.tileHeight = 60, - this.fromTopEdge = 50, - this.fromBottomEdge = 25, - this.moneroIcon = Image.asset('assets/images/monero_menu.png'), - this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), - this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), - this.havenIcon = Image.asset('assets/images/haven_menu.png'); + : this.menuWidth = 0, + this.screenWidth = 0, + this.screenHeight = 0, + this.headerHeight = 120, + this.tileHeight = 60, + this.fromTopEdge = 50, + this.fromBottomEdge = 25, + this.moneroIcon = Image.asset('assets/images/monero_menu.png'), + this.bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png'), + this.litecoinIcon = Image.asset('assets/images/litecoin_menu.png'), + this.havenIcon = Image.asset('assets/images/haven_menu.png'), + this.ethereumIcon = Image.asset('assets/images/eth_icon.png'); final largeScreen = 731; @@ -46,6 +47,7 @@ class MenuWidgetState extends State<MenuWidget> { Image bitcoinIcon; Image litecoinIcon; Image havenIcon; + Image ethereumIcon; @override void initState() { @@ -85,16 +87,14 @@ class MenuWidgetState extends State<MenuWidget> { moneroIcon = Image.asset('assets/images/monero_menu.png', color: Theme.of(context) - .accentTextTheme! + .accentTextTheme .labelSmall! .decorationColor!); bitcoinIcon = Image.asset('assets/images/bitcoin_menu.png', color: Theme.of(context) - .accentTextTheme! + .accentTextTheme .labelSmall! .decorationColor!); - litecoinIcon = Image.asset('assets/images/litecoin_menu.png'); - havenIcon = Image.asset('assets/images/haven_menu.png'); return Row( mainAxisSize: MainAxisSize.max, @@ -178,7 +178,7 @@ class MenuWidgetState extends State<MenuWidget> { index--; final item = SettingActions.all[index]; - + final isLastTile = index == itemCount - 1; return SettingActionButton( @@ -215,6 +215,8 @@ class MenuWidgetState extends State<MenuWidget> { return litecoinIcon; case WalletType.haven: return havenIcon; + case WalletType.ethereum: + return ethereumIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index 58026d312..53d823f18 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; @@ -37,7 +38,7 @@ import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; class ExchangePage extends BasePage { - ExchangePage(this.exchangeViewModel) { + ExchangePage(this.exchangeViewModel, this.authService) { depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr ? exchangeViewModel.wallet.name : null; @@ -47,6 +48,7 @@ class ExchangePage extends BasePage { } final ExchangeViewModel exchangeViewModel; + final AuthService authService; final depositKey = GlobalKey<ExchangeCardState>(); final receiveKey = GlobalKey<ExchangeCardState>(); final _formKey = GlobalKey<FormState>(); @@ -89,16 +91,17 @@ class ExchangePage extends BasePage { @override Widget middle(BuildContext context) => Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Padding( - padding: const EdgeInsets.only(right:6.0), - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),) - ), - PresentProviderPicker(exchangeViewModel: exchangeViewModel) - ], - ); - + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Padding( + padding: const EdgeInsets.only(right: 6.0), + child: Observer( + builder: (_) => + SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus), + )), + PresentProviderPicker(exchangeViewModel: exchangeViewModel) + ], + ); @override Widget trailing(BuildContext context) => TrailButton( @@ -110,12 +113,13 @@ class ExchangePage extends BasePage { @override Widget? leading(BuildContext context) { - final _backButton = Icon(Icons.arrow_back_ios, + final _backButton = Icon( + Icons.arrow_back_ios, color: titleColor, size: 16, ); - final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme : closeButtonImage; + final _closeButton = + currentTheme.type == ThemeType.dark ? closeButtonImageDarkTheme : closeButtonImage; bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; @@ -126,13 +130,10 @@ class ExchangePage extends BasePage { child: ButtonTheme( minWidth: double.minPositive, child: Semantics( - label: !isMobileView - ? S.of(context).close - : S.of(context).seed_alert_back, + label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back, child: TextButton( style: ButtonStyle( - overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent), + overlayColor: MaterialStateColor.resolveWith((states) => Colors.transparent), ), onPressed: () => onClose(context), child: !isMobileView ? _closeButton : _backButton, @@ -145,23 +146,19 @@ class ExchangePage extends BasePage { @override Widget body(BuildContext context) { - WidgetsBinding.instance - .addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); + WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel)); return KeyboardActions( disableScroll: true, config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: - Theme.of(context).accentTextTheme!.bodyLarge!.backgroundColor!, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!, nextFocus: false, actions: [ KeyboardActionsItem( - focusNode: _depositAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()]), + focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]), KeyboardActionsItem( - focusNode: _receiveAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()]) + focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]) ]), child: Container( color: Theme.of(context).colorScheme.background, @@ -169,30 +166,28 @@ class ExchangePage extends BasePage { key: _formKey, child: ScrollableWithBottomSection( contentPadding: EdgeInsets.only(bottom: 24), - content: Observer(builder: (_) => Column( - children: <Widget>[ - _exchangeCardsSection(context), - Padding( - padding: EdgeInsets.only(top: 12, left: 24), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - StandardCheckbox( - value: exchangeViewModel.isFixedRateMode, - caption: S.of(context).fixed_rate, - onChanged: (value) => - exchangeViewModel.isFixedRateMode = value, - ), - ], - ) - ), - SizedBox(height: 30), - _buildTemplateSection(context) + content: Observer( + builder: (_) => Column( + children: <Widget>[ + _exchangeCardsSection(context), + Padding( + padding: EdgeInsets.only(top: 12, left: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + StandardCheckbox( + value: exchangeViewModel.isFixedRateMode, + caption: S.of(context).fixed_rate, + onChanged: (value) => exchangeViewModel.isFixedRateMode = value, + ), + ], + )), + SizedBox(height: 30), + _buildTemplateSection(context) ], ), ), - bottomSectionPadding: - EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column(children: <Widget>[ Padding( padding: EdgeInsets.only(bottom: 15), @@ -210,8 +205,7 @@ class ExchangePage extends BasePage { textAlign: TextAlign.center, style: TextStyle( color: Theme.of(context) - .primaryTextTheme! - .displayLarge! + .primaryTextTheme.displayLarge! .decorationColor!, fontWeight: FontWeight.w500, fontSize: 12), @@ -223,29 +217,34 @@ class ExchangePage extends BasePage { builder: (_) => LoadingPrimaryButton( text: S.of(context).exchange, onPressed: () { - if (_formKey.currentState != null && _formKey.currentState!.validate()) { - if ((exchangeViewModel.depositCurrency == - CryptoCurrency.xmr) && - (!(exchangeViewModel.status - is SyncedSyncStatus))) { + if (_formKey.currentState != null && + _formKey.currentState!.validate()) { + if ((exchangeViewModel.depositCurrency == CryptoCurrency.xmr) && + (!(exchangeViewModel.status is SyncedSyncStatus))) { showPopUp<void>( context: context, builder: (BuildContext context) { return AlertWithOneAction( alertTitle: S.of(context).exchange, - alertContent: S - .of(context) - .exchange_sync_alert_content, + alertContent: S.of(context).exchange_sync_alert_content, buttonText: S.of(context).ok, - buttonAction: () => - Navigator.of(context).pop()); + buttonAction: () => Navigator.of(context).pop()); }); } else { - exchangeViewModel.createTrade(); + final check = exchangeViewModel.shouldDisplayTOTP(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) { + if (value) { + exchangeViewModel.createTrade(); + } + }, + ); } } }, - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!, + color: Theme.of(context).accentTextTheme.bodyLarge!.color!, textColor: Colors.white, isDisabled: exchangeViewModel.selectedProviders.isEmpty, isLoading: exchangeViewModel.tradeState is TradeIsCreating)), @@ -264,7 +263,7 @@ class ExchangePage extends BasePage { child: Observer( builder: (_) { final templates = exchangeViewModel.templates; - + return Row( children: <Widget>[ AddTemplateButton( @@ -293,18 +292,15 @@ class ExchangePage extends BasePage { builder: (dialogContext) { return AlertWithTwoActions( alertTitle: S.of(context).template, - alertContent: - S.of(context).confirm_delete_template, + alertContent: S.of(context).confirm_delete_template, rightButtonText: S.of(context).delete, leftButtonText: S.of(context).cancel, actionRightButton: () { Navigator.of(dialogContext).pop(); - exchangeViewModel.removeTemplate( - template: template); + exchangeViewModel.removeTemplate(template: template); exchangeViewModel.updateTemplate(); }, - actionLeftButton: () => - Navigator.of(dialogContext).pop()); + actionLeftButton: () => Navigator.of(dialogContext).pop()); }); }, ); @@ -318,8 +314,8 @@ class ExchangePage extends BasePage { ); } - void applyTemplate(BuildContext context, - ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { + void applyTemplate( + BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async { exchangeViewModel.changeDepositCurrency( currency: CryptoCurrency.fromString(template.depositCurrency)); exchangeViewModel.changeReceiveCurrency( @@ -333,22 +329,19 @@ class ExchangePage extends BasePage { var domain = template.depositAddress; var ticker = template.depositCurrency.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker); domain = template.receiveAddress; ticker = template.receiveCurrency.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker); } - void _setReactions( - BuildContext context, ExchangeViewModel exchangeViewModel) { + void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) { if (_isReactionsSet) { return; } - if (exchangeViewModel.isLowFee) { + if (exchangeViewModel.isLowFee) { _showFeeAlert(context); } @@ -359,42 +352,30 @@ class ExchangePage extends BasePage { final limitsState = exchangeViewModel.limitsState; if (limitsState is LimitsLoadedSuccessfully) { - final min = limitsState.limits.min != null - ? limitsState.limits.min.toString() - : null; - final max = limitsState.limits.max != null - ? limitsState.limits.max.toString() - : null; - final key = exchangeViewModel.isFixedRateMode - ? receiveKey - : depositKey; + final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null; + final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null; + final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey; key.currentState!.changeLimits(min: min, max: max); } - _onCurrencyChange( - exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); - _onCurrencyChange( - exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); + _onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey); + _onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey); reaction( (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( - exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey)); + (String _) => + _onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey)); reaction( (_) => exchangeViewModel.wallet.name, - (String _) => _onWalletNameChange( - exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); + (String _) => + _onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey)); - reaction( - (_) => exchangeViewModel.receiveCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, exchangeViewModel, receiveKey)); + reaction((_) => exchangeViewModel.receiveCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey)); - reaction( - (_) => exchangeViewModel.depositCurrency, - (CryptoCurrency currency) => - _onCurrencyChange(currency, exchangeViewModel, depositKey)); + reaction((_) => exchangeViewModel.depositCurrency, + (CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey)); reaction((_) => exchangeViewModel.depositAmount, (String amount) { if (depositKey.currentState!.amountController.text != amount) { @@ -408,8 +389,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.isDepositAddressEnabled, - (bool isEnabled) { + reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) { depositKey.currentState!.isAddressEditable(isEditable: isEnabled); }); @@ -425,13 +405,11 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.isReceiveAddressEnabled, - (bool isEnabled) { + reaction((_) => exchangeViewModel.isReceiveAddressEnabled, (bool isEnabled) { receiveKey.currentState!.isAddressEditable(isEditable: isEnabled); }); - reaction((_) => exchangeViewModel.isReceiveAmountEditable, - (bool isReceiveAmountEditable) { + reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) { receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable); }); @@ -483,20 +461,20 @@ class ExchangePage extends BasePage { } }); - depositAddressController.addListener( - () => exchangeViewModel.depositAddress = depositAddressController.text); + depositAddressController + .addListener(() => exchangeViewModel.depositAddress = depositAddressController.text); depositAmountController.addListener(() { if (depositAmountController.text != exchangeViewModel.depositAmount) { - _depositAmountDebounce.run(() { + _depositAmountDebounce.run(() { exchangeViewModel.changeDepositAmount(amount: depositAmountController.text); exchangeViewModel.isReceiveAmountEntered = false; }); } }); - receiveAddressController.addListener( - () => exchangeViewModel.receiveAddress = receiveAddressController.text); + receiveAddressController + .addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text); receiveAmountController.addListener(() { if (receiveAmountController.text != exchangeViewModel.receiveAmount) { @@ -507,8 +485,7 @@ class ExchangePage extends BasePage { } }); - reaction((_) => exchangeViewModel.wallet.walletAddresses.address, - (String address) { + reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) { if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) { depositKey.currentState!.changeAddress(address: address); } @@ -519,22 +496,18 @@ class ExchangePage extends BasePage { }); _depositAddressFocus.addListener(() async { - if (!_depositAddressFocus.hasFocus && - depositAddressController.text.isNotEmpty) { + if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) { final domain = depositAddressController.text; final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.depositAddress = await fetchParsedAddress(context, domain, ticker); } }); _receiveAddressFocus.addListener(() async { - if (!_receiveAddressFocus.hasFocus && - receiveAddressController.text.isNotEmpty) { + if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) { final domain = receiveAddressController.text; final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); + exchangeViewModel.receiveAddress = await fetchParsedAddress(context, domain, ticker); } }); @@ -554,29 +527,26 @@ class ExchangePage extends BasePage { _isReactionsSet = true; } - void _onCurrencyChange(CryptoCurrency currency, - ExchangeViewModel exchangeViewModel, GlobalKey<ExchangeCardState> key) { + void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel, + GlobalKey<ExchangeCardState> key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; key.currentState!.changeSelectedCurrency(currency); - key.currentState!.changeWalletName( - isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); + key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : ''); key.currentState!.changeAddress( - address: isCurrentTypeWallet - ? exchangeViewModel.wallet.walletAddresses.address : ''); + address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : ''); key.currentState!.changeAmount(amount: ''); } - void _onWalletNameChange(ExchangeViewModel exchangeViewModel, - CryptoCurrency currency, GlobalKey<ExchangeCardState> key) { + void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency, + GlobalKey<ExchangeCardState> key) { final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency; if (isCurrentTypeWallet) { key.currentState!.changeWalletName(exchangeViewModel.wallet.name); - key.currentState!.addressController.text = - exchangeViewModel.wallet.walletAddresses.address; + key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address; } else if (key.currentState!.addressController.text == exchangeViewModel.wallet.walletAddresses.address) { key.currentState!.changeWalletName(''); @@ -584,8 +554,7 @@ class ExchangePage extends BasePage { } } - Future<String> fetchParsedAddress( - BuildContext context, String domain, String ticker) async { + Future<String> fetchParsedAddress(BuildContext context, String domain, String ticker) async { final parsedAddress = await getIt.get<AddressResolver>().resolve(domain, ticker); final address = await extractAddressFromParsed(context, parsedAddress); return address; @@ -594,16 +563,17 @@ class ExchangePage extends BasePage { void _showFeeAlert(BuildContext context) async { await Future<void>.delayed(Duration(seconds: 1)); final confirmed = await showPopUp<bool>( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: S.of(context).low_fee, - alertContent: S.of(context).low_fee_alert, - leftButtonText: S.of(context).ignor, - rightButtonText: S.of(context).use_suggested, - actionLeftButton: () => Navigator.of(dialogContext).pop(false), - actionRightButton: () => Navigator.of(dialogContext).pop(true)); - }) ?? false; + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: S.of(context).low_fee, + alertContent: S.of(context).low_fee_alert, + leftButtonText: S.of(context).ignor, + rightButtonText: S.of(context).use_suggested, + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); + }) ?? + false; if (confirmed) { exchangeViewModel.setDefaultTransactionPriority(); } @@ -612,126 +582,122 @@ class ExchangePage extends BasePage { void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel(); Widget _exchangeCardsSection(BuildContext context) { - final firstExchangeCard = Observer(builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - hasAllAmount: exchangeViewModel.hasAllAmount, - allAmount: exchangeViewModel.hasAllAmount - ? () => exchangeViewModel.calculateDepositAllAmount() - : null, - amountFocusNode: _depositAmountFocus, - addressFocusNode: _depositAddressFocus, - key: depositKey, - title: S.of(context).you_will_send, - initialCurrency: exchangeViewModel.depositCurrency, - initialWalletName: depositWalletName ?? '', - initialAddress: - exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.depositAddress, - initialIsAmountEditable: true, - initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, - isAmountEstimated: false, - hasRefundAddress: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.depositCurrencies, - onCurrencySelected: (currency) { - // FIXME: need to move it into view model - if (currency == CryptoCurrency.xmr && - exchangeViewModel.wallet.type != WalletType.monero) { - showPopUp<void>( - context: context, - builder: (dialogContext) { - return AlertWithOneAction( - alertTitle: S.of(context).error, - alertContent: - S.of(context).exchange_incorrect_current_wallet_for_xmr, - buttonText: S.of(context).ok, - buttonAction: () => Navigator.of(dialogContext).pop()); - }); - return; - } + final firstExchangeCard = Observer( + builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + hasAllAmount: exchangeViewModel.hasAllAmount, + allAmount: exchangeViewModel.hasAllAmount + ? () => exchangeViewModel.calculateDepositAllAmount() + : null, + amountFocusNode: _depositAmountFocus, + addressFocusNode: _depositAddressFocus, + key: depositKey, + title: S.of(context).you_will_send, + initialCurrency: exchangeViewModel.depositCurrency, + initialWalletName: depositWalletName ?? '', + initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.depositAddress, + initialIsAmountEditable: true, + initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled, + isAmountEstimated: false, + hasRefundAddress: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.depositCurrencies, + onCurrencySelected: (currency) { + // FIXME: need to move it into view model + if (currency == CryptoCurrency.xmr && + exchangeViewModel.wallet.type != WalletType.monero) { + showPopUp<void>( + context: context, + builder: (dialogContext) { + return AlertWithOneAction( + alertTitle: S.of(context).error, + alertContent: S.of(context).exchange_incorrect_current_wallet_for_xmr, + buttonText: S.of(context).ok, + buttonAction: () => Navigator.of(dialogContext).pop()); + }); + return; + } - exchangeViewModel.changeDepositCurrency(currency: currency); - }, - imageArrow: arrowBottomPurple, - currencyButtonColor: Colors.transparent, - addressButtonsColor: Theme.of(context).focusColor!, - borderColor: Theme.of(context).primaryTextTheme!.bodyLarge!.color!, - currencyValueValidator: (value) { - return !exchangeViewModel.isFixedRateMode - ? AmountValidator( - isAutovalidate: true, - currency: exchangeViewModel.depositCurrency, - minValue: exchangeViewModel.limits.min.toString(), - maxValue: exchangeViewModel.limits.max.toString(), - ).call(value) - : null; - }, - addressTextFieldValidator: - AddressValidator(type: exchangeViewModel.depositCurrency), - onPushPasteButton: (context) async { - final domain = exchangeViewModel.depositAddress; - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = exchangeViewModel.depositAddress; - final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); - exchangeViewModel.depositAddress = - await fetchParsedAddress(context, domain, ticker); - }, - )); + exchangeViewModel.changeDepositCurrency(currency: currency); + }, + imageArrow: arrowBottomPurple, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor, + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.color!, + currencyValueValidator: (value) { + return !exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.depositCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.depositAddress; + final ticker = exchangeViewModel.depositCurrency.title.toLowerCase(); + exchangeViewModel.depositAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); - final secondExchangeCard = Observer(builder: (_) => ExchangeCard( - onDispose: disposeBestRateSync, - amountFocusNode: _receiveAmountFocus, - addressFocusNode: _receiveAddressFocus, - key: receiveKey, - title: S.of(context).you_will_get, - initialCurrency: exchangeViewModel.receiveCurrency, - initialWalletName: receiveWalletName ?? '', - initialAddress: - exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency - ? exchangeViewModel.wallet.walletAddresses.address - : exchangeViewModel.receiveAddress, - initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, - initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, - isAmountEstimated: true, - isMoneroWallet: exchangeViewModel.isMoneroWallet, - currencies: exchangeViewModel.receiveCurrencies, - onCurrencySelected: (currency) => - exchangeViewModel.changeReceiveCurrency(currency: currency), - imageArrow: arrowBottomCakeGreen, - currencyButtonColor: Colors.transparent, - addressButtonsColor: Theme.of(context).focusColor!, - borderColor: - Theme.of(context).primaryTextTheme!.bodyLarge!.decorationColor!, - currencyValueValidator: (value) { - return exchangeViewModel.isFixedRateMode - ? AmountValidator( - isAutovalidate: true, - currency: exchangeViewModel.receiveCurrency, - minValue: exchangeViewModel.limits.min.toString(), - maxValue: exchangeViewModel.limits.max.toString(), - ).call(value) - : null; - }, - addressTextFieldValidator: - AddressValidator(type: exchangeViewModel.receiveCurrency), - onPushPasteButton: (context) async { - final domain = exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); - }, - onPushAddressBookButton: (context) async { - final domain = exchangeViewModel.receiveAddress; - final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); - exchangeViewModel.receiveAddress = - await fetchParsedAddress(context, domain, ticker); - }, - )); + final secondExchangeCard = Observer( + builder: (_) => ExchangeCard( + onDispose: disposeBestRateSync, + amountFocusNode: _receiveAmountFocus, + addressFocusNode: _receiveAddressFocus, + key: receiveKey, + title: S.of(context).you_will_get, + initialCurrency: exchangeViewModel.receiveCurrency, + initialWalletName: receiveWalletName ?? '', + initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency + ? exchangeViewModel.wallet.walletAddresses.address + : exchangeViewModel.receiveAddress, + initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable, + initialIsAddressEditable: exchangeViewModel.isReceiveAddressEnabled, + isAmountEstimated: true, + isMoneroWallet: exchangeViewModel.isMoneroWallet, + currencies: exchangeViewModel.receiveCurrencies, + onCurrencySelected: (currency) => + exchangeViewModel.changeReceiveCurrency(currency: currency), + imageArrow: arrowBottomCakeGreen, + currencyButtonColor: Colors.transparent, + addressButtonsColor: Theme.of(context).focusColor, + borderColor: Theme.of(context).primaryTextTheme.bodyLarge!.decorationColor!, + currencyValueValidator: (value) { + return exchangeViewModel.isFixedRateMode + ? AmountValidator( + isAutovalidate: true, + currency: exchangeViewModel.receiveCurrency, + minValue: exchangeViewModel.limits.min.toString(), + maxValue: exchangeViewModel.limits.max.toString(), + ).call(value) + : null; + }, + addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency), + onPushPasteButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + onPushAddressBookButton: (context) async { + final domain = exchangeViewModel.receiveAddress; + final ticker = exchangeViewModel.receiveCurrency.title.toLowerCase(); + exchangeViewModel.receiveAddress = + await fetchParsedAddress(context, domain, ticker); + }, + )); if (ResponsiveLayoutUtil.instance.isMobile) { return MobileExchangeCardsSection( diff --git a/lib/src/screens/exchange/widgets/exchange_card.dart b/lib/src/screens/exchange/widgets/exchange_card.dart index f77e885c2..ce8928cc6 100644 --- a/lib/src/screens/exchange/widgets/exchange_card.dart +++ b/lib/src/screens/exchange/widgets/exchange_card.dart @@ -511,17 +511,16 @@ class ExchangeCardState extends State<ExchangeCard> { void _presentPicker(BuildContext context) { showPopUp<void>( - builder: (_) => CurrencyPicker( - selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), - items: widget.currencies, - hintText: S.of(context).search_currency, - isMoneroWallet: _isMoneroWallet, - isConvertFrom: widget.hasRefundAddress, - onItemSelected: (Currency item) => - widget.onCurrencySelected != null - ? widget.onCurrencySelected(item as CryptoCurrency) - : null), - context: context); + context: context, + builder: (_) => CurrencyPicker( + selectedAtIndex: widget.currencies.indexOf(_selectedCurrency), + items: widget.currencies, + hintText: S.of(context).search_currency, + isMoneroWallet: _isMoneroWallet, + isConvertFrom: widget.hasRefundAddress, + onItemSelected: (Currency item) => widget.onCurrencySelected(item as CryptoCurrency), + ), + ); } void _showAmountPopup(BuildContext context, PaymentRequest paymentRequest) { diff --git a/lib/src/screens/monero_accounts/widgets/account_tile.dart b/lib/src/screens/monero_accounts/widgets/account_tile.dart index b0769e9c2..d034ca11a 100644 --- a/lib/src/screens/monero_accounts/widgets/account_tile.dart +++ b/lib/src/screens/monero_accounts/widgets/account_tile.dart @@ -1,4 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:cake_wallet/generated/i18n.dart'; class AccountTile extends StatelessWidget { AccountTile( @@ -19,16 +21,17 @@ class AccountTile extends StatelessWidget { @override Widget build(BuildContext context) { final color = isCurrent - ? Theme.of(context).textTheme!.titleSmall!.decorationColor! - : Theme.of(context).textTheme!.displayLarge!.decorationColor!; + ? Theme.of(context).textTheme.titleSmall!.decorationColor! + : Theme.of(context).textTheme.displayLarge!.decorationColor!; final textColor = isCurrent - ? Theme.of(context).textTheme!.titleSmall!.color! - : Theme.of(context).textTheme!.displayLarge!.color!; + ? Theme.of(context).textTheme.titleSmall!.color! + : Theme.of(context).textTheme.displayLarge!.color!; final Widget cell = GestureDetector( onTap: onTap, child: Container( height: 77, + width: double.infinity, padding: EdgeInsets.only(left: 24, right: 24), color: color, child: Wrap( @@ -58,7 +61,7 @@ class AccountTile extends StatelessWidget { fontSize: 15, fontWeight: FontWeight.w600, fontFamily: 'Lato', - color: Theme.of(context).textTheme!.headlineMedium!.color!, + color: Theme.of(context).textTheme.headlineMedium!.color!, decoration: TextDecoration.none, ), ), @@ -67,18 +70,26 @@ class AccountTile extends StatelessWidget { ), ), ); - // FIX-ME: Splidable - return cell; - // return Slidable( - // key: Key(accountName), - // child: cell, - // actionPane: SlidableDrawerActionPane(), - // secondaryActions: <Widget>[ - // IconSlideAction( - // caption: S.of(context).edit, - // color: Colors.blue, - // icon: Icons.edit, - // onTap: () => onEdit?.call()) - // ]); + + // return cell; + return Slidable( + key: Key(accountName), + child: cell, + endActionPane: _actionPane(context) + ); } + + ActionPane _actionPane(BuildContext context) => ActionPane( + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (_) => onEdit.call(), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + ], + ); } diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 7c5031217..d28ff9d77 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -11,14 +11,17 @@ import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; class NewWalletTypePage extends BasePage { - NewWalletTypePage({required this.onTypeSelected}); + NewWalletTypePage({required this.onTypeSelected, required this.isCreate}); final void Function(BuildContext, WalletType) onTypeSelected; + final bool isCreate; + final walletTypeImage = Image.asset('assets/images/wallet_type.png'); final walletTypeLightImage = Image.asset('assets/images/wallet_type_light.png'); @override - String get title => S.current.wallet_list_restore_wallet; + String get title => + isCreate ? S.current.wallet_list_create_new_wallet : S.current.wallet_list_restore_wallet; @override Widget body(BuildContext context) => WalletTypeForm( 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 216238b54..c1b07d8c5 100644 --- a/lib/src/screens/nodes/node_create_or_edit_page.dart +++ b/lib/src/screens/nodes/node_create_or_edit_page.dart @@ -66,6 +66,19 @@ class NodeCreateOrEditPage extends BasePage { @override String get title => editingNode != null ? S.current.edit_node : S.current.node_new; + @override + Widget trailing(BuildContext context) => IconButton( + onPressed: () async { + await nodeCreateOrEditViewModel.scanQRCodeForNewNode(); + }, + splashColor: Colors.transparent, + highlightColor: Colors.transparent, + hoverColor: Colors.transparent, + icon: Image.asset( + 'assets/images/qr_code_icon.png', + ), + ); + final NodeCreateOrEditViewModel nodeCreateOrEditViewModel; final Node? editingNode; final bool? isSelected; diff --git a/lib/src/screens/nodes/widgets/node_form.dart b/lib/src/screens/nodes/widgets/node_form.dart index 8ee3ecd2f..91974fce5 100644 --- a/lib/src/screens/nodes/widgets/node_form.dart +++ b/lib/src/screens/nodes/widgets/node_form.dart @@ -44,6 +44,17 @@ class NodeForm extends StatelessWidget { } }); } + reaction((_) => nodeViewModel.address, (String address) { + if (address != _addressController.text) { + _addressController.text = address; + } + }); + + reaction((_) => nodeViewModel.port, (String port) { + if (port != _portController.text) { + _portController.text = port; + } + }); _addressController.addListener(() => nodeViewModel.address = _addressController.text); _portController.addListener(() => nodeViewModel.port = _portController.text); diff --git a/lib/src/screens/restore/restore_wallet_options_page.dart b/lib/src/screens/restore/restore_wallet_options_page.dart deleted file mode 100644 index bf36ddd10..000000000 --- a/lib/src/screens/restore/restore_wallet_options_page.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:cake_wallet/src/screens/restore/widgets/restore_button.dart'; -import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/generated/i18n.dart'; - -class RestoreWalletOptionsPage extends BasePage { - RestoreWalletOptionsPage( - {required this.type, - required this.onRestoreFromSeed, - required this.onRestoreFromKeys}); - - final WalletType type; - final Function(BuildContext context) onRestoreFromSeed; - final Function(BuildContext context) onRestoreFromKeys; - - @override - String get title => S.current.restore_restore_wallet; - - final imageSeed = Image.asset('assets/images/restore_seed.png'); - final imageKeys = Image.asset('assets/images/restore_keys.png'); - - @override - Widget body(BuildContext context) { - return Container( - width: double.infinity, - height: double.infinity, - padding: EdgeInsets.all(24), - child: SingleChildScrollView( - child: Column( - children: <Widget>[ - RestoreButton( - onPressed: () => onRestoreFromSeed(context), - image: imageSeed, - title: S.of(context).restore_title_from_seed, - description: _fromSeedDescription(context)), - Padding( - padding: EdgeInsets.only(top: 24), - child: RestoreButton( - onPressed: () => onRestoreFromKeys(context), - image: imageKeys, - title: _fromKeyTitle(context), - description: _fromKeyDescription(context)), - ) - ], - ), - )); - } - - String _fromSeedDescription(BuildContext context) { - switch (type) { - case WalletType.monero: - return S.of(context).restore_description_from_seed; - case WalletType.bitcoin: - // TODO: Add transaction for bitcoin description. - return S.of(context).restore_bitcoin_description_from_seed; - default: - return ''; - } - } - - String _fromKeyDescription(BuildContext context) { - switch (type) { - case WalletType.monero: - return S.of(context).restore_description_from_keys; - case WalletType.bitcoin: - // TODO: Add transaction for bitcoin description. - return S.of(context).restore_bitcoin_description_from_keys; - default: - return ''; - } - } - - String _fromKeyTitle(BuildContext context) { - switch (type) { - case WalletType.monero: - return S.of(context).restore_title_from_keys; - case WalletType.bitcoin: - // TODO: Add transaction for bitcoin description. - return S.of(context).restore_bitcoin_title_from_keys; - default: - return ''; - } - } -} diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 3ce7e133a..3298a50c0 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -97,7 +97,8 @@ class RootState extends State<Root> with WidgetsBindingObserver { return; } - if (!_isInactive && widget.authenticationStore.state == AuthenticationState.allowed) { + if (!_isInactive && + widget.authenticationStore.state == AuthenticationState.allowed) { setState(() => _setInactive(true)); } @@ -124,13 +125,16 @@ class RootState extends State<Root> with WidgetsBindingObserver { return; } else { final useTotp = widget.appStore.settingsStore.useTOTP2FA; - if (useTotp) { + final shouldUseTotp2FAToAccessWallets = widget.appStore + .settingsStore.shouldRequireTOTP2FAForAccessingWallet; + if (useTotp && shouldUseTotp2FAToAccessWallets) { _reset(); auth.close( route: Routes.totpAuthCodePage, arguments: TotpAuthArgumentsModel( onTotpAuthenticationFinished: - (bool isAuthenticatedSuccessfully, TotpAuthCodePageState totpAuth) { + (bool isAuthenticatedSuccessfully, + TotpAuthCodePageState totpAuth) { if (!isAuthenticatedSuccessfully) { return; } @@ -151,15 +155,11 @@ class RootState extends State<Root> with WidgetsBindingObserver { route: launchUri != null ? Routes.send : null, arguments: PaymentRequest.fromUri(launchUri), ); - launchUri = null; + launchUri = null; } } - - - }, - ); - - + }, + ); }); } else if (launchUri != null) { widget.navigatorKey.currentState?.pushNamed( diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 11bae8ac9..f1e7a0334 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -11,9 +11,7 @@ class PreSeedPage extends BasePage { PreSeedPage(this.type) : imageLight = Image.asset('assets/images/pre_seed_light.png'), imageDark = Image.asset('assets/images/pre_seed_dark.png'), - wordsCount = type == WalletType.monero - ? 25 - : 24; // FIXME: Stupid fast implementation + wordsCount = _wordsCount(type); final Image imageDark; final Image imageLight; @@ -68,4 +66,15 @@ class PreSeedPage extends BasePage { ), )); } + + static int _wordsCount(WalletType type) { + switch (type) { + case WalletType.monero: + return 25; + case WalletType.ethereum: + return 12; + default: + return 24; + } + } } diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 3b8bf6877..c32fa6f35 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/entities/template.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; @@ -32,10 +33,12 @@ import 'package:cw_core/crypto_currency.dart'; class SendPage extends BasePage { SendPage({ required this.sendViewModel, + required this.authService, this.initialPaymentRequest, }) : _formKey = GlobalKey<FormState>(); final SendViewModel sendViewModel; + final AuthService authService; final GlobalKey<FormState> _formKey; final controller = PageController(initialPage: 0); final PaymentRequest? initialPaymentRequest; @@ -56,12 +59,14 @@ class SendPage extends BasePage { @override Widget? leading(BuildContext context) { - final _backButton = Icon(Icons.arrow_back_ios, + final _backButton = Icon( + Icons.arrow_back_ios, color: titleColor, size: 16, ); final _closeButton = currentTheme.type == ThemeType.dark - ? closeButtonImageDarkTheme : closeButtonImage; + ? closeButtonImageDarkTheme + : closeButtonImage; bool isMobileView = ResponsiveLayoutUtil.instance.isMobile; @@ -78,7 +83,7 @@ class SendPage extends BasePage { child: TextButton( style: ButtonStyle( overlayColor: MaterialStateColor.resolveWith( - (states) => Colors.transparent), + (states) => Colors.transparent), ), onPressed: () => onClose(context), child: !isMobileView ? _closeButton : _backButton, @@ -114,11 +119,13 @@ class SendPage extends BasePage { mainAxisAlignment: MainAxisAlignment.center, children: [ Padding( - padding: const EdgeInsets.only(right:8.0), - child: Observer(builder: (_) => SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend),), + padding: const EdgeInsets.only(right: 8.0), + child: Observer( + builder: (_) => + SyncIndicatorIcon(isSynced: sendViewModel.isReadyForSend), + ), ), - if (supMiddle != null) - supMiddle + if (supMiddle != null) supMiddle ], ); } @@ -200,12 +207,12 @@ class SendPage extends BasePage { dotWidth: 6.0, dotHeight: 6.0, dotColor: Theme.of(context) - .primaryTextTheme - !.displaySmall! + .primaryTextTheme! + .displaySmall! .backgroundColor!, activeDotColor: Theme.of(context) - .primaryTextTheme - !.displayMedium! + .primaryTextTheme! + .displayMedium! .backgroundColor!), ) : Offstage(); @@ -213,115 +220,108 @@ class SendPage extends BasePage { ), ), ), - if (sendViewModel.hasMultiRecipient) - Container( - height: 40, - width: double.infinity, - padding: EdgeInsets.only(left: 24), - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Observer( - builder: (_) { - final templates = sendViewModel.templates; - final itemCount = templates.length; + Container( + height: 40, + width: double.infinity, + padding: EdgeInsets.only(left: 24), + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Observer( + builder: (_) { + final templates = sendViewModel.templates; + final itemCount = templates.length; - return Row( - children: <Widget>[ - AddTemplateButton( - onTap: () => Navigator.of(context) - .pushNamed(Routes.sendTemplate), - currentTemplatesLength: templates.length, - ), - ListView.builder( - scrollDirection: Axis.horizontal, - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - itemCount: itemCount, - itemBuilder: (context, index) { - final template = templates[index]; - return TemplateTile( - key: UniqueKey(), - to: template.name, - hasMultipleRecipients: - template.additionalRecipients != - null && - template.additionalRecipients! - .length > 1, - amount: template.isCurrencySelected - ? template.amount - : template.amountFiat, - from: template.isCurrencySelected - ? template.cryptoCurrency - : template.fiatCurrency, - onTap: () async { - if (template.additionalRecipients != - null) { - sendViewModel.clearOutputs(); + return Row( + children: <Widget>[ + AddTemplateButton( + onTap: () => Navigator.of(context) + .pushNamed(Routes.sendTemplate), + currentTemplatesLength: templates.length, + ), + ListView.builder( + scrollDirection: Axis.horizontal, + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + itemCount: itemCount, + itemBuilder: (context, index) { + final template = templates[index]; + return TemplateTile( + key: UniqueKey(), + to: template.name, + hasMultipleRecipients: + template.additionalRecipients != null && + template.additionalRecipients!.length > 1, + amount: template.isCurrencySelected + ? template.amount + : template.amountFiat, + from: template.isCurrencySelected + ? template.cryptoCurrency + : template.fiatCurrency, + onTap: () async { + if (template.additionalRecipients?.isNotEmpty ?? false) { + sendViewModel.clearOutputs(); - template.additionalRecipients! - .forEach((currentElement) async { - int i = template - .additionalRecipients! - .indexOf(currentElement); + for (int i = 0;i < template.additionalRecipients!.length;i++) { + Output output; + try { + output = sendViewModel.outputs[i]; + } catch (e) { + sendViewModel.addOutput(); + output = sendViewModel.outputs[i]; + } - Output output; - try { - output = sendViewModel.outputs[i]; - } catch (e) { - sendViewModel.addOutput(); - output = sendViewModel.outputs[i]; - } - - await _setInputsFromTemplate( - context, - output: output, - template: currentElement); - }); - } else { - final output = _defineCurrentOutput(); - await _setInputsFromTemplate( - context, - output: output, - template: template); + await _setInputsFromTemplate( + context, + output: output, + template: template.additionalRecipients![i], + ); } - }, - onRemove: () { - showPopUp<void>( - context: context, - builder: (dialogContext) { - return AlertWithTwoActions( - alertTitle: - S.of(context).template, - alertContent: S - .of(context) - .confirm_delete_template, - rightButtonText: - S.of(context).delete, - leftButtonText: - S.of(context).cancel, - actionRightButton: () { - Navigator.of(dialogContext) - .pop(); - sendViewModel - .sendTemplateViewModel - .removeTemplate( - template: template); - }, - actionLeftButton: () => - Navigator.of(dialogContext) - .pop()); - }, + } else { + final output = _defineCurrentOutput(); + await _setInputsFromTemplate( + context, + output: output, + template: template, ); - }, - ); - }, - ), - ], - ); - }, - ), + } + }, + onRemove: () { + showPopUp<void>( + context: context, + builder: (dialogContext) { + return AlertWithTwoActions( + alertTitle: + S.of(context).template, + alertContent: S + .of(context) + .confirm_delete_template, + rightButtonText: + S.of(context).delete, + leftButtonText: + S.of(context).cancel, + actionRightButton: () { + Navigator.of(dialogContext) + .pop(); + sendViewModel + .sendTemplateViewModel + .removeTemplate( + template: template); + }, + actionLeftButton: () => + Navigator.of(dialogContext) + .pop()); + }, + ); + }, + ); + }, + ), + ], + ); + }, ), - ) + ), + ), ], ), ), @@ -339,11 +339,11 @@ class SendPage extends BasePage { 'Change your asset (${sendViewModel.selectedCryptoCurrency})', color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme - !.displaySmall! + .accentTextTheme! + .displaySmall! .decorationColor!, ))), - if (sendViewModel.hasMultiRecipient) + if (sendViewModel.sendTemplateViewModel.hasMultiRecipient) Padding( padding: EdgeInsets.only(bottom: 12), child: PrimaryButton( @@ -357,13 +357,13 @@ class SendPage extends BasePage { text: S.of(context).add_receiver, color: Colors.transparent, textColor: Theme.of(context) - .accentTextTheme - !.displaySmall! + .accentTextTheme! + .displaySmall! .decorationColor!, isDottedBorder: true, borderColor: Theme.of(context) - .primaryTextTheme - !.displaySmall! + .primaryTextTheme! + .displaySmall! .decorationColor!, )), Observer( @@ -390,7 +390,16 @@ class SendPage extends BasePage { return; } - await sendViewModel.createTransaction(); + final check = sendViewModel.shouldDisplayTotp(); + authService.authenticateAction( + context, + conditionToDetermineIfToUse2FA: check, + onAuthSuccess: (value) async { + if (value) { + await sendViewModel.createTransaction(); + } + }, + ); }, text: S.of(context).send, color: @@ -502,6 +511,7 @@ class SendPage extends BasePage { output.address = template.address; if (template.isCurrencySelected) { + sendViewModel.setSelectedCryptoCurrency(template.cryptoCurrency); output.setCryptoAmount(template.amount); } else { sendViewModel.setFiatCurrency(fiatFromTemplate); diff --git a/lib/src/screens/send/send_template_page.dart b/lib/src/screens/send/send_template_page.dart index 01f173daf..fb06f00ee 100644 --- a/lib/src/screens/send/send_template_page.dart +++ b/lib/src/screens/send/send_template_page.dart @@ -67,8 +67,7 @@ class SendTemplatePage extends BasePage { controller: controller, itemCount: sendTemplateViewModel.recipients.length, itemBuilder: (_, index) { - final template = - sendTemplateViewModel.recipients[index]; + final template = sendTemplateViewModel.recipients[index]; return SendTemplateCard( template: template, index: index, @@ -76,8 +75,7 @@ class SendTemplatePage extends BasePage { }); })), Padding( - padding: EdgeInsets.only( - top: 10, left: 24, right: 24, bottom: 10), + padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10), child: Container( height: 10, child: Observer( @@ -107,55 +105,42 @@ class SendTemplatePage extends BasePage { ), ), ])), - bottomSectionPadding: - EdgeInsets.only(left: 24, right: 24, bottom: 24), + bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24), bottomSection: Column(children: [ - // if (sendViewModel.hasMultiRecipient) - Padding( - padding: EdgeInsets.only(bottom: 12), - child: PrimaryButton( - onPressed: () { - sendTemplateViewModel.addRecipient(); - Future.delayed(const Duration(milliseconds: 250), () { - controller.jumpToPage( - sendTemplateViewModel.recipients.length - 1); - }); - }, - text: S.of(context).add_receiver, - color: Colors.transparent, - textColor: Theme.of(context) - .accentTextTheme - .displaySmall! - .decorationColor!, - isDottedBorder: true, - borderColor: Theme.of(context) - .primaryTextTheme - .displaySmall! - .decorationColor!)), + if (sendTemplateViewModel.hasMultiRecipient) + Padding( + padding: EdgeInsets.only(bottom: 12), + child: PrimaryButton( + onPressed: () { + sendTemplateViewModel.addRecipient(); + Future.delayed(const Duration(milliseconds: 250), () { + controller.jumpToPage(sendTemplateViewModel.recipients.length - 1); + }); + }, + text: S.of(context).add_receiver, + color: Colors.transparent, + textColor: Theme.of(context).accentTextTheme.displaySmall!.decorationColor!, + isDottedBorder: true, + borderColor: + Theme.of(context).primaryTextTheme.displaySmall!.decorationColor!)), PrimaryButton( onPressed: () { - if (_formKey.currentState != null && - _formKey.currentState!.validate()) { + if (_formKey.currentState != null && _formKey.currentState!.validate()) { final mainTemplate = sendTemplateViewModel.recipients[0]; - print(sendTemplateViewModel.recipients.map((element) => - element.toTemplate( - cryptoCurrency: - sendTemplateViewModel.cryptoCurrency.title, - fiatCurrency: - sendTemplateViewModel.fiatCurrency))); + final additionalRecipients = sendTemplateViewModel.recipients + .map((element) => element.toTemplate( + cryptoCurrency: element.selectedCurrency.title, + fiatCurrency: sendTemplateViewModel.fiatCurrency)) + .toList(); + sendTemplateViewModel.addTemplate( isCurrencySelected: mainTemplate.isCurrencySelected, name: mainTemplate.name, address: mainTemplate.address, + cryptoCurrency: mainTemplate.selectedCurrency.title, amount: mainTemplate.output.cryptoAmount, amountFiat: mainTemplate.output.fiatAmount, - additionalRecipients: sendTemplateViewModel.recipients - .map((element) => element.toTemplate( - cryptoCurrency: sendTemplateViewModel - .cryptoCurrency.title, - fiatCurrency: - sendTemplateViewModel.fiatCurrency)) - .toList()); + additionalRecipients: additionalRecipients); Navigator.of(context).pop(); } }, diff --git a/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart b/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart index 0a684a7df..6e2428857 100644 --- a/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart +++ b/lib/src/screens/send/widgets/prefix_currency_icon_widget.dart @@ -4,29 +4,53 @@ class PrefixCurrencyIcon extends StatelessWidget { PrefixCurrencyIcon({ required this.isSelected, required this.title, + this.onTap, }); final bool isSelected; final String title; + final Function()? onTap; @override Widget build(BuildContext context) { - return Padding( + return GestureDetector( + onTap: onTap, + child: Padding( padding: EdgeInsets.fromLTRB(0, 6.0, 8.0, 0), - child: Column(children: [ - Container( - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(26), - color: isSelected ? Colors.green : Colors.transparent, + child: Column( + children: [ + Container( + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(26), + color: isSelected ? Colors.green : Colors.transparent, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + if (onTap != null) + Padding( + padding: EdgeInsets.only(right: 5), + child: Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ), + ), + Text( + title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), + ], + ), ), - child: Text(title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - ) - ])); + ], + ), + ), + ); } } diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index d07516f79..8cdcd6dac 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -1,7 +1,10 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/utils/payment_request.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; @@ -32,18 +35,14 @@ class SendCard extends StatefulWidget { @override SendCardState createState() => SendCardState( - output: output, - sendViewModel: sendViewModel, - initialPaymentRequest: initialPaymentRequest, - ); + output: output, + sendViewModel: sendViewModel, + initialPaymentRequest: initialPaymentRequest, + ); } -class SendCardState extends State<SendCard> - with AutomaticKeepAliveClientMixin<SendCard> { - SendCardState({ - required this.output, - required this.sendViewModel, - this.initialPaymentRequest}) +class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<SendCard> { + SendCardState({required this.output, required this.sendViewModel, this.initialPaymentRequest}) : addressController = TextEditingController(), cryptoAmountController = TextEditingController(), fiatAmountController = TextEditingController(), @@ -100,40 +99,41 @@ class SendCardState extends State<SendCard> return Stack( children: [ KeyboardActions( - config: KeyboardActionsConfig( - keyboardActionsPlatform: KeyboardActionsPlatform.IOS, - keyboardBarColor: Theme.of(context) - .accentTextTheme - .bodyLarge! - .backgroundColor!, - nextFocus: false, - actions: [ - KeyboardActionsItem( - focusNode: cryptoAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ), - KeyboardActionsItem( - focusNode: fiatAmountFocus, - toolbarButtons: [(_) => KeyboardDoneButton()], - ) - ]), - child: Container( - height: 0, - color: Colors.transparent, - )), + config: KeyboardActionsConfig( + keyboardActionsPlatform: KeyboardActionsPlatform.IOS, + keyboardBarColor: Theme.of(context).accentTextTheme.bodyLarge!.backgroundColor!, + nextFocus: false, + actions: [ + KeyboardActionsItem( + focusNode: cryptoAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ), + KeyboardActionsItem( + focusNode: fiatAmountFocus, + toolbarButtons: [(_) => KeyboardDoneButton()], + ) + ], + ), + child: Container( + height: 0, + color: Colors.transparent, + ), + ), Container( - decoration: ResponsiveLayoutUtil.instance.isMobile ? BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).primaryTextTheme.titleMedium!.color!, - Theme.of(context) - .primaryTextTheme - .titleMedium! - .decorationColor!, - ], begin: Alignment.topLeft, end: Alignment.bottomRight), - ) : null, + decoration: ResponsiveLayoutUtil.instance.isMobile + ? BoxDecoration( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient( + colors: [ + Theme.of(context).primaryTextTheme.titleMedium!.color!, + Theme.of(context).primaryTextTheme.titleMedium!.decorationColor!, + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + ) + : null, child: Padding( padding: EdgeInsets.fromLTRB( 24, @@ -142,7 +142,8 @@ class SendCardState extends State<SendCard> ResponsiveLayoutUtil.instance.isMobile ? 32 : 0, ), child: SingleChildScrollView( - child: Observer(builder: (_) => Column( + child: Observer( + builder: (_) => Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ Observer(builder: (_) { @@ -164,25 +165,15 @@ class SendCardState extends State<SendCard> AddressTextFieldOption.qrCode, AddressTextFieldOption.addressBook ], - buttonColor: Theme.of(context) - .primaryTextTheme - .headlineMedium! - .color!, - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, + buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), hintStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!), + color: + Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!), onPushPasteButton: (context) async { output.resetParsedAddress(); await output.fetchParsedAddress(context); @@ -197,170 +188,192 @@ class SendCardState extends State<SendCard> selectedCurrency: sendViewModel.currency, ); }), - if (output.isParsedAddress) Padding( - padding: const EdgeInsets.only(top: 20), - child: BaseTextFormField( - controller: extractedAddressController, - readOnly: true, - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - validator: sendViewModel.addressValidator - ) - ), + if (output.isParsedAddress) + Padding( + padding: const EdgeInsets.only(top: 20), + child: BaseTextFormField( + controller: extractedAddressController, + readOnly: true, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: TextStyle( + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + validator: sendViewModel.addressValidator)), Observer( - builder: (_) => Padding( - padding: const EdgeInsets.only(top: 20), - child: Row( - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Row( - children: [ - Text( - sendViewModel.selectedCryptoCurrency.title, - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), - sendViewModel.selectedCryptoCurrency.tag != null ? Padding( - padding: const EdgeInsets.fromLTRB(3.0,0,3.0,0), - child: Container( - height: 32, - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme - .headlineMedium! - .color!, - borderRadius: - BorderRadius.all(Radius.circular(6))), - child: Center( - child: Padding( - padding: const EdgeInsets.all(6.0), - child: Text( sendViewModel.selectedCryptoCurrency.tag!, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.bold, - color: Theme.of(context) - .primaryTextTheme - .headlineMedium! - .decorationColor!)), + builder: (_) => Padding( + padding: const EdgeInsets.only(top: 20), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Row( + children: [ + sendViewModel.hasMultipleTokens + ? Container( + padding: EdgeInsets.only(right: 8), + height: 32, + child: InkWell( + onTap: () => _presentPicker(context), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.min, + children: <Widget>[ + Padding( + padding: EdgeInsets.only(right: 5), + child: Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + color: Colors.white, + height: 8, + ), + ), + Text( + sendViewModel.selectedCryptoCurrency.title, + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white), + ), + ], + ), ), - ), - ), - ) : Container(), - Padding( - padding: const EdgeInsets.only(right: 10.0), - child: Text(':', + ) + : Text( + sendViewModel.selectedCryptoCurrency.title, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, - color: Colors.white)), - ), - ], - ), - ), - Expanded( - child: Stack( - children: [ - BaseTextFormField( - focusNode: cryptoAmountFocus, - controller: cryptoAmountController, - keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) - ], - suffixIcon: SizedBox( - width: prefixIconWidth, - ), - hintText: '0.0000', - borderColor: Colors.transparent, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, color: Colors.white), - placeholderTextStyle: TextStyle( + ), + sendViewModel.selectedCryptoCurrency.tag != null + ? Padding( + padding: const EdgeInsets.fromLTRB(3.0, 0, 3.0, 0), + child: Container( + height: 32, + decoration: BoxDecoration( + color: Theme.of(context) + .primaryTextTheme + .headlineMedium! + .color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + )), + child: Center( + child: Padding( + padding: const EdgeInsets.all(6.0), + child: Text( + sendViewModel.selectedCryptoCurrency.tag!, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .headlineMedium! + .decorationColor!), + ), + ), + ), + ), + ) + : Container(), + Padding( + padding: const EdgeInsets.only(right: 10.0), + child: Text( + ':', + style: TextStyle( + fontWeight: FontWeight.w600, + fontSize: 16, + color: Colors.white), + ), + ), + ], + ), + ), + Expanded( + child: Stack( + children: [ + BaseTextFormField( + focusNode: cryptoAmountFocus, + controller: cryptoAmountController, + keyboardType: TextInputType.numberWithOptions( + signed: false, decimal: true), + inputFormatters: [ + FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) + ], + suffixIcon: SizedBox( + width: prefixIconWidth, + ), + hintText: '0.0000', + borderColor: Colors.transparent, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context) + .primaryTextTheme + .headlineSmall! + .decorationColor!, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: output.sendAll + ? sendViewModel.allAmountValidator + : sendViewModel.amountValidator, + ), + if (!sendViewModel.isBatchSending) + Positioned( + top: 2, + right: 0, + child: Container( + width: prefixIconWidth, + height: prefixIconHeight, + child: InkWell( + onTap: () async => output.setSendAll(), + child: Container( + decoration: BoxDecoration( color: Theme.of(context) .primaryTextTheme - .headlineSmall! - .decorationColor!, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: output.sendAll - ? sendViewModel.allAmountValidator - : sendViewModel - .amountValidator), - if (!sendViewModel.isBatchSending) Positioned( - top: 2, - right: 0, - child: Container( - width: prefixIconWidth, - height: prefixIconHeight, - child: InkWell( - onTap: () async => - output.setSendAll(), - child: Container( - decoration: BoxDecoration( - color: Theme.of(context) - .primaryTextTheme - .headlineMedium! - .color!, - borderRadius: - BorderRadius.all( - Radius.circular(6))), - child: Center( - child: Text( - S.of(context).all, - textAlign: - TextAlign.center, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.bold, - color: - Theme.of(context) - .primaryTextTheme - .headlineMedium! - .decorationColor!))), - ))))]), + .headlineMedium! + .color!, + borderRadius: BorderRadius.all( + Radius.circular(6), + ), + ), + child: Center( + child: Text( + S.of(context).all, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.bold, + color: Theme.of(context) + .primaryTextTheme + .headlineMedium! + .decorationColor!, + ), + ), + ), + ), + ), + ), + ), + ], ), - ], - ) - )), - Divider(height: 1,color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!), + ), + ], + )), + ), + Divider( + height: 1, + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!), Observer( - builder: (_) => Padding( - padding: EdgeInsets.only(top: 10), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: <Widget>[ - Expanded( - child: Text( - S.of(context).available_balance + - ':', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!), - )), - Text( - sendViewModel.balance, + builder: (_) => Padding( + padding: EdgeInsets.only(top: 10), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: <Widget>[ + Expanded( + child: Text( + S.of(context).available_balance + ':', style: TextStyle( fontSize: 12, fontWeight: FontWeight.w600, @@ -368,10 +381,22 @@ class SendCardState extends State<SendCard> .primaryTextTheme .headlineSmall! .decorationColor!), - ) - ], - ), - )), + ), + ), + Text( + sendViewModel.balance, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headlineSmall! + .decorationColor!), + ) + ], + ), + ), + ), if (!sendViewModel.isFiatDisabled) Padding( padding: const EdgeInsets.only(top: 20), @@ -379,171 +404,155 @@ class SendCardState extends State<SendCard> focusNode: fiatAmountFocus, controller: fiatAmountController, keyboardType: - TextInputType.numberWithOptions( - signed: false, decimal: true), + TextInputType.numberWithOptions(signed: false, decimal: true), inputFormatters: [ - FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]')) + FilteringTextInputFormatter.deny( + RegExp('[\\-|\\ ]'), + ) ], prefixIcon: Padding( padding: EdgeInsets.only(top: 9), - child: - Text(sendViewModel.fiat.title + ':', - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.w600, - color: Colors.white, - )), + child: Text( + sendViewModel.fiat.title + ':', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Colors.white, + ), + ), ), hintText: '0.00', - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), placeholderTextStyle: TextStyle( color: Theme.of(context) - .primaryTextTheme.headlineSmall!.decorationColor!, + .primaryTextTheme + .headlineSmall! + .decorationColor!, fontWeight: FontWeight.w500, fontSize: 14), - )), + ), + ), Padding( padding: EdgeInsets.only(top: 20), child: BaseTextFormField( controller: noteController, keyboardType: TextInputType.multiline, maxLines: null, - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), hintText: S.of(context).note_optional, placeholderTextStyle: TextStyle( fontSize: 14, fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!), + color: + Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!), ), ), Observer( - builder: (_) => GestureDetector( - onTap: () => - _setTransactionPriority(context), - child: Container( - padding: EdgeInsets.only(top: 24), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Text( - S - .of(context) - .send_estimated_fee, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w500, - //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, - color: Colors.white)), - Container( - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: <Widget>[ - Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - output - .estimatedFee - .toString() + - ' ' + - sendViewModel - .selectedCryptoCurrency.toString(), - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, - color: - Colors.white)), - Padding( - padding: - EdgeInsets.only(top: 5), - child: sendViewModel.isFiatDisabled - ? const SizedBox(height: 14) - : Text(output - .estimatedFeeFiatAmount - + ' ' + - sendViewModel - .fiat.title, - style: TextStyle( - fontSize: 12, - fontWeight: - FontWeight.w600, - color: Theme - .of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!)) + builder: (_) => GestureDetector( + onTap: () => _setTransactionPriority(context), + child: Container( + padding: EdgeInsets.only(top: 24), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Text( + S.of(context).send_estimated_fee, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w500, + //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, + color: Colors.white), + ), + Container( + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: <Widget>[ + Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + output.estimatedFee.toString() + + ' ' + + sendViewModel.selectedCryptoCurrency.toString(), + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + //color: Theme.of(context).primaryTextTheme!.displaySmall!.color!, + color: Colors.white, ), - ], - ), - Padding( - padding: EdgeInsets.only( - top: 2, - left: 5), - child: Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, ), - ) - ], - ), - ) + Padding( + padding: EdgeInsets.only(top: 5), + child: sendViewModel.isFiatDisabled + ? const SizedBox(height: 14) + : Text( + output.estimatedFeeFiatAmount + + ' ' + + sendViewModel.fiat.title, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Theme.of(context) + .primaryTextTheme + .headlineSmall! + .decorationColor!, + ), + ), + ), + ], + ), + Padding( + padding: EdgeInsets.only(top: 2, left: 5), + child: Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), + ) + ], + ), + ) + ], + ), + ), + ), + ), + if (sendViewModel.isElectrumWallet) + Padding( + padding: EdgeInsets.only(top: 6), + child: GestureDetector( + onTap: () => Navigator.of(context).pushNamed(Routes.unspentCoinsList), + child: Container( + color: Colors.transparent, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + S.of(context).coin_control, + style: TextStyle( + fontSize: 12, + fontWeight: FontWeight.w600, + color: Colors.white), + ), + Icon( + Icons.arrow_forward_ios, + size: 12, + color: Colors.white, + ), ], ), ), - )), - if (sendViewModel.isElectrumWallet) Padding( - padding: EdgeInsets.only(top: 6), - child: GestureDetector( - onTap: () => Navigator.of(context) - .pushNamed(Routes.unspentCoinsList), - child: Container( - color: Colors.transparent, - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - S.of(context).coin_control, - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - color: Colors.white)), - Icon( - Icons.arrow_forward_ios, - size: 12, - color: Colors.white, - ) - ], - ) - ) - ) - ) + ), + ), ], - )) + ), + ), ), ), ) @@ -552,10 +561,10 @@ class SendCardState extends State<SendCard> } void _setEffects(BuildContext context) { - if (_effectsInstalled) { + if (_effectsInstalled) { return; } - + if (output.address.isNotEmpty) { addressController.text = output.address; } @@ -664,16 +673,30 @@ class SendCardState extends State<SendCard> final selectedItem = items.indexOf(sendViewModel.transactionPriority); await showPopUp<void>( - builder: (_) => Picker( - items: items, - displayItem: sendViewModel.displayFeeRate, - selectedAtIndex: selectedItem, - title: S.of(context).please_select, - mainAxisAlignment: MainAxisAlignment.center, - onItemSelected: (TransactionPriority priority) => - sendViewModel.setTransactionPriority(priority), - ), - context: context); + context: context, + builder: (_) => Picker( + items: items, + displayItem: sendViewModel.displayFeeRate, + selectedAtIndex: selectedItem, + title: S.of(context).please_select, + mainAxisAlignment: MainAxisAlignment.center, + onItemSelected: (TransactionPriority priority) => + sendViewModel.setTransactionPriority(priority), + ), + ); + } + + void _presentPicker(BuildContext context) { + showPopUp<void>( + context: context, + builder: (_) => CurrencyPicker( + selectedAtIndex: sendViewModel.currencies.indexOf(sendViewModel.selectedCryptoCurrency), + items: sendViewModel.currencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => + sendViewModel.selectedCryptoCurrency = (cur as CryptoCurrency), + ), + ); } @override diff --git a/lib/src/screens/send/widgets/send_template_card.dart b/lib/src/screens/send/widgets/send_template_card.dart index 623f50b15..54538ce5d 100644 --- a/lib/src/screens/send/widgets/send_template_card.dart +++ b/lib/src/screens/send/widgets/send_template_card.dart @@ -1,6 +1,10 @@ +import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart'; import 'package:cake_wallet/src/screens/send/widgets/prefix_currency_icon_widget.dart'; import 'package:cake_wallet/utils/payment_request.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/send/template_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/currency.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -35,161 +39,140 @@ class SendTemplateCard extends StatelessWidget { _setEffects(context); return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.only( - bottomLeft: Radius.circular(24), - bottomRight: Radius.circular(24)), - gradient: LinearGradient(colors: [ - Theme.of(context).primaryTextTheme.titleMedium!.color!, - Theme.of(context).primaryTextTheme.titleMedium!.decorationColor! - ], begin: Alignment.topLeft, end: Alignment.bottomRight)), - child: Column(children: <Widget>[ + decoration: BoxDecoration( + borderRadius: + BorderRadius.only(bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)), + gradient: LinearGradient(colors: [ + Theme.of(context).primaryTextTheme.titleMedium!.color!, + Theme.of(context).primaryTextTheme.titleMedium!.decorationColor! + ], begin: Alignment.topLeft, end: Alignment.bottomRight)), + child: Column( + children: <Widget>[ Padding( - padding: EdgeInsets.fromLTRB(24, 90, 24, 32), - child: Column(children: <Widget>[ + padding: EdgeInsets.fromLTRB(24, 90, 24, 32), + child: Column( + children: <Widget>[ if (index == 0) BaseTextFormField( controller: _nameController, hintText: sendTemplateViewModel.recipients.length > 1 ? S.of(context).template_name : S.of(context).send_name, - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!, + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!, fontWeight: FontWeight.w500, fontSize: 14), validator: sendTemplateViewModel.templateValidator), Padding( - padding: EdgeInsets.only(top: 20), - child: AddressTextField( - selectedCurrency: sendTemplateViewModel.cryptoCurrency, - controller: _addressController, - onURIScanned: (uri) { - final paymentRequest = PaymentRequest.fromUri(uri); - _addressController.text = paymentRequest.address; - _cryptoAmountController.text = paymentRequest.amount; - }, - options: [ - AddressTextFieldOption.paste, - AddressTextFieldOption.qrCode, - AddressTextFieldOption.addressBook - ], - onPushPasteButton: (context) async { - template.output.resetParsedAddress(); - await template.output.fetchParsedAddress(context); - }, - onPushAddressBookButton: (context) async { - template.output.resetParsedAddress(); - await template.output.fetchParsedAddress(context); - }, - buttonColor: Theme.of(context) - .primaryTextTheme - .headlineMedium! - .color!, - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - hintStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!), - validator: sendTemplateViewModel.addressValidator)), + padding: EdgeInsets.only(top: 20), + child: AddressTextField( + selectedCurrency: sendTemplateViewModel.cryptoCurrency, + controller: _addressController, + onURIScanned: (uri) { + final paymentRequest = PaymentRequest.fromUri(uri); + _addressController.text = paymentRequest.address; + _cryptoAmountController.text = paymentRequest.amount; + }, + options: [ + AddressTextFieldOption.paste, + AddressTextFieldOption.qrCode, + AddressTextFieldOption.addressBook + ], + onPushPasteButton: (context) async { + template.output.resetParsedAddress(); + await template.output.fetchParsedAddress(context); + }, + onPushAddressBookButton: (context) async { + template.output.resetParsedAddress(); + await template.output.fetchParsedAddress(context); + }, + buttonColor: Theme.of(context).primaryTextTheme.headlineMedium!.color!, + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Colors.white, + ), + hintStyle: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w500, + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!, + ), + validator: sendTemplateViewModel.addressValidator, + ), + ), Padding( - padding: const EdgeInsets.only(top: 20), - child: Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - template.selectCurrency(); - } - }, - child: BaseTextFormField( - focusNode: _cryptoAmountFocus, - controller: _cryptoAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny( - RegExp('[\\-|\\ ]')) - ], - prefixIcon: Observer( - builder: (_) => PrefixCurrencyIcon( - title: sendTemplateViewModel - .cryptoCurrency.title, - isSelected: template.isCurrencySelected)), - hintText: '0.0000', - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!, - fontWeight: FontWeight.w500, - fontSize: 14), - validator: sendTemplateViewModel.amountValidator))), + padding: const EdgeInsets.only(top: 20), + child: Focus( + onFocusChange: (hasFocus) { + if (hasFocus) { + template.selectCurrency(); + } + }, + child: BaseTextFormField( + focusNode: _cryptoAmountFocus, + controller: _cryptoAmountController, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], + prefixIcon: Observer( + builder: (_) => PrefixCurrencyIcon( + title: template.selectedCurrency.title, + isSelected: template.isCurrencySelected, + onTap: sendTemplateViewModel.walletCurrencies.length > 1 + ? () => _presentPicker(context) + : null, + ), + ), + hintText: '0.0000', + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!, + fontWeight: FontWeight.w500, + fontSize: 14), + validator: sendTemplateViewModel.amountValidator, + ), + ), + ), Padding( - padding: const EdgeInsets.only(top: 20), - child: Focus( - onFocusChange: (hasFocus) { - if (hasFocus) { - template.selectFiat(); - } - }, - child: BaseTextFormField( - focusNode: _fiatAmountFocus, - controller: _fiatAmountController, - keyboardType: TextInputType.numberWithOptions( - signed: false, decimal: true), - inputFormatters: [ - FilteringTextInputFormatter.deny( - RegExp('[\\-|\\ ]')) - ], - prefixIcon: Observer( - builder: (_) => PrefixCurrencyIcon( - title: sendTemplateViewModel.fiatCurrency, - isSelected: template.isFiatSelected)), - hintText: '0.00', - borderColor: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .color!, - textStyle: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Colors.white), - placeholderTextStyle: TextStyle( - color: Theme.of(context) - .primaryTextTheme - .headlineSmall! - .decorationColor!, - fontWeight: FontWeight.w500, - fontSize: 14)))) - ])) - ])); + padding: const EdgeInsets.only(top: 20), + child: Focus( + onFocusChange: (hasFocus) { + if (hasFocus) { + template.selectFiat(); + } + }, + child: BaseTextFormField( + focusNode: _fiatAmountFocus, + controller: _fiatAmountController, + keyboardType: TextInputType.numberWithOptions(signed: false, decimal: true), + inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))], + prefixIcon: Observer( + builder: (_) => PrefixCurrencyIcon( + title: sendTemplateViewModel.fiatCurrency, + isSelected: template.isFiatSelected)), + hintText: '0.00', + borderColor: Theme.of(context).primaryTextTheme.headlineSmall!.color!, + textStyle: + TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: Colors.white), + placeholderTextStyle: TextStyle( + color: Theme.of(context).primaryTextTheme.headlineSmall!.decorationColor!, + fontWeight: FontWeight.w500, + fontSize: 14, + ), + ), + ), + ), + ], + ), + ) + ], + ), + ); } void _setEffects(BuildContext context) { @@ -264,4 +247,16 @@ class SendTemplateCard extends StatelessWidget { _effectsInstalled = true; } + + void _presentPicker(BuildContext context) { + showPopUp<void>( + context: context, + builder: (_) => CurrencyPicker( + selectedAtIndex: sendTemplateViewModel.walletCurrencies.indexOf(template.selectedCurrency), + items: sendTemplateViewModel.walletCurrencies, + hintText: S.of(context).search_currency, + onItemSelected: (Currency cur) => template.changeSelectedCurrency(cur as CryptoCurrency), + ), + ); + } } diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 4a60ab136..c59e71be4 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -1,25 +1,25 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; +import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; -import 'package:cw_core/node.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; -import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; class ConnectionSyncPage extends BasePage { - ConnectionSyncPage(this.nodeListViewModel, this.dashboardViewModel); + ConnectionSyncPage(this.dashboardViewModel); @override String get title => S.current.connection_sync; - final NodeListViewModel nodeListViewModel; final DashboardViewModel dashboardViewModel; @override @@ -33,72 +33,39 @@ class ConnectionSyncPage extends BasePage { title: S.current.reconnect, handler: (context) => _presentReconnectAlert(context), ), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - if (dashboardViewModel.hasRescan) + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + if (dashboardViewModel.hasRescan) ...[ SettingsCellWithArrow( title: S.current.rescan, handler: (context) => Navigator.of(context).pushNamed(Routes.rescan), ), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - Semantics( - button: true, - child: NodeHeaderListRow( - title: S.of(context).add_new_node, - onTap: (_) async => - await Navigator.of(context).pushNamed(Routes.newNode), - ), - ), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - SizedBox(height: 100), - Observer( - builder: (BuildContext context) { - return Flexible( - child: SectionStandardList( - sectionCount: 1, - context: context, - dividerPadding: EdgeInsets.symmetric(horizontal: 24), - itemCounter: (int sectionIndex) { - return nodeListViewModel.nodes.length; - }, - itemBuilder: (_, sectionIndex, index) { - final node = nodeListViewModel.nodes[index]; - final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex; - final nodeListRow = NodeListRow( - title: node.uriRaw, - node: node, - isSelected: isSelected, - onTap: (_) async { - if (isSelected) { - return; - } - - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: - S.of(context).change_current_node_title, - alertContent: nodeListViewModel - .getAlertContent(node.uriRaw), - leftButtonText: S.of(context).cancel, - rightButtonText: S.of(context).change, - actionLeftButton: () => - Navigator.of(context).pop(), - actionRightButton: () async { - await nodeListViewModel.setAsCurrent(node); - Navigator.of(context).pop(); - }, - ); - }); - }, - ); - - return nodeListRow; - }, - ), - ); - }, + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + if (DeviceInfo.instance.isMobile) ...[ + Observer(builder: (context) { + return SettingsPickerCell<SyncMode>( + title: S.current.background_sync_mode, + items: SyncMode.all, + displayItem: (SyncMode syncMode) => syncMode.name, + selectedItem: dashboardViewModel.syncMode, + onItemSelected: dashboardViewModel.setSyncMode, + ); + }), + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer(builder: (context) { + return SettingsSwitcherCell( + title: S.current.sync_all_wallets, + value: dashboardViewModel.syncAll, + onValueChange: (_, bool value) => dashboardViewModel.setSyncAll(value), + ); + }), + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + ], + ], + SettingsCellWithArrow( + title: S.current.manage_nodes, + handler: (context) => Navigator.of(context).pushNamed(Routes.manageNodes), ), + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), ], ), ); diff --git a/lib/src/screens/settings/manage_nodes_page.dart b/lib/src/screens/settings/manage_nodes_page.dart new file mode 100644 index 000000000..d4c4507e6 --- /dev/null +++ b/lib/src/screens/settings/manage_nodes_page.dart @@ -0,0 +1,85 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/routes.dart'; +import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/screens/nodes/widgets/node_list_row.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cake_wallet/view_model/node_list/node_list_view_model.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; + +class ManageNodesPage extends BasePage { + ManageNodesPage(this.nodeListViewModel); + + final NodeListViewModel nodeListViewModel; + + @override + String get title => S.current.manage_nodes; + + @override + Widget body(BuildContext context) { + return Padding( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + Semantics( + button: true, + child: NodeHeaderListRow( + title: S.of(context).add_new_node, + onTap: (_) async => await Navigator.of(context).pushNamed(Routes.newNode), + ), + ), + const StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + SizedBox(height: 20), + Observer( + builder: (BuildContext context) { + return Flexible( + child: SectionStandardList( + sectionCount: 1, + context: context, + dividerPadding: EdgeInsets.symmetric(horizontal: 24), + itemCounter: (int sectionIndex) { + return nodeListViewModel.nodes.length; + }, + itemBuilder: (_, sectionIndex, index) { + final node = nodeListViewModel.nodes[index]; + final isSelected = node.keyIndex == nodeListViewModel.currentNode.keyIndex; + final nodeListRow = NodeListRow( + title: node.uriRaw, + node: node, + isSelected: isSelected, + onTap: (_) async { + if (isSelected) { + return; + } + + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).change_current_node_title, + alertContent: nodeListViewModel.getAlertContent(node.uriRaw), + leftButtonText: S.of(context).cancel, + rightButtonText: S.of(context).change, + actionLeftButton: () => Navigator.of(context).pop(), + actionRightButton: () async { + await nodeListViewModel.setAsCurrent(node); + Navigator.of(context).pop(); + }, + ); + }); + }, + ); + + return nodeListRow; + }, + ), + ); + }, + ), + ], + ), + ); + } +} diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index e055e2e8e..aea58eb73 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -20,26 +20,32 @@ class OtherSettingsPage extends BasePage { @override Widget body(BuildContext context) { - return Observer(builder: (_) { - return Container( - padding: EdgeInsets.only(top: 10), - child: Column(children: [ - SettingsPickerCell( - title: S.current.settings_fee_priority, - items: priorityForWalletType(_otherSettingsViewModel.walletType), - displayItem: _otherSettingsViewModel.getDisplayPriority, - selectedItem: _otherSettingsViewModel.transactionPriority, - onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, + return Observer( + builder: (_) { + return Container( + padding: EdgeInsets.only(top: 10), + child: Column( + children: [ + SettingsPickerCell( + title: S.current.settings_fee_priority, + items: priorityForWalletType(_otherSettingsViewModel.walletType), + displayItem: _otherSettingsViewModel.getDisplayPriority, + selectedItem: _otherSettingsViewModel.transactionPriority, + onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected, + ), + SettingsCellWithArrow( + title: S.current.settings_terms_and_conditions, + handler: (BuildContext context) => + Navigator.of(context).pushNamed(Routes.readDisclaimer), + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Spacer(), + SettingsVersionCell( + title: S.of(context).version(_otherSettingsViewModel.currentVersion)) + ], ), - SettingsCellWithArrow( - title: S.current.settings_terms_and_conditions, - handler: (BuildContext context) => Navigator.of(context).pushNamed(Routes.readDisclaimer), - ), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - Spacer(), - SettingsVersionCell(title: S.of(context).version(_otherSettingsViewModel.currentVersion)) - ]), - ); - }); + ); + }, + ); } } diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index 186e19d37..ae6bfe6c8 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -40,7 +40,8 @@ class PrivacyPage extends BasePage { title: S.current.exchange, items: ExchangeApiMode.all, selectedItem: _privacySettingsViewModel.exchangeStatus, - onItemSelected: (ExchangeApiMode mode) => _privacySettingsViewModel.setExchangeApiMode(mode), + onItemSelected: (ExchangeApiMode mode) => + _privacySettingsViewModel.setExchangeApiMode(mode), ), ), SettingsSwitcherCell( @@ -68,6 +69,13 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setDisableSell(value); }), + if (_privacySettingsViewModel.canUseEtherscan) + SettingsSwitcherCell( + title: S.current.etherscan_history, + value: _privacySettingsViewModel.useEtherscan, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setUseEtherscan(value); + }), ], ); }), diff --git a/lib/src/screens/settings/security_backup_page.dart b/lib/src/screens/settings/security_backup_page.dart index 6b72a480a..a0fb16cb6 100644 --- a/lib/src/screens/settings/security_backup_page.dart +++ b/lib/src/screens/settings/security_backup_page.dart @@ -30,12 +30,22 @@ class SecurityBackupPage extends BasePage { child: Column(mainAxisSize: MainAxisSize.min, children: [ SettingsCellWithArrow( title: S.current.show_keys, - handler: (_) => _authService.authenticateAction(context, route: Routes.showKeys), + handler: (_) => _authService.authenticateAction( + context, + route: Routes.showKeys, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( title: S.current.create_backup, - handler: (_) => _authService.authenticateAction(context, route: Routes.backup), + handler: (_) => _authService.authenticateAction( + context, + route: Routes.backup, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), SettingsCellWithArrow( @@ -46,6 +56,8 @@ class SecurityBackupPage extends BasePage { arguments: (PinCodeState<PinCodeWidget> setupPinContext, String _) { setupPinContext.close(); }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), @@ -67,7 +79,10 @@ class SecurityBackupPage extends BasePage { _securitySettingsViewModel .setAllowBiometricalAuthentication(isAuthenticatedSuccessfully); } - }); + }, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ); } else { _securitySettingsViewModel.setAllowBiometricalAuthentication(value); } @@ -94,6 +109,8 @@ class SecurityBackupPage extends BasePage { route: _securitySettingsViewModel.useTotp2FA ? Routes.modify2FAPage : Routes.setup_2faPage, + conditionToDetermineIfToUse2FA: _securitySettingsViewModel + .shouldRequireTOTP2FAForAllSecurityAndBackupSettings, ), ); }, diff --git a/lib/src/screens/settings/widgets/settings_choices_cell.dart b/lib/src/screens/settings/widgets/settings_choices_cell.dart index 49bc301f1..c03c923c3 100644 --- a/lib/src/screens/settings/widgets/settings_choices_cell.dart +++ b/lib/src/screens/settings/widgets/settings_choices_cell.dart @@ -22,7 +22,7 @@ class SettingsChoicesCell extends StatelessWidget { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, ), ), ], @@ -34,10 +34,7 @@ class SettingsChoicesCell extends StatelessWidget { child: Container( decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), - color: Theme.of(context) - .accentTextTheme! - .displaySmall! - .color!, + color: Theme.of(context).accentTextTheme.displaySmall!.color!, ), child: Row( mainAxisAlignment: MainAxisAlignment.center, @@ -52,10 +49,7 @@ class SettingsChoicesCell extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(30), color: isSelected - ? Theme.of(context) - .accentTextTheme! - .bodyLarge! - .color! + ? Theme.of(context).accentTextTheme.bodyLarge!.color! : null, ), child: Text( @@ -63,10 +57,7 @@ class SettingsChoicesCell extends StatelessWidget { style: TextStyle( color: isSelected ? Colors.white - : Theme.of(context) - .primaryTextTheme! - .bodySmall! - .color!, + : Theme.of(context).primaryTextTheme.bodySmall!.color!, fontWeight: isSelected ? FontWeight.w700 : FontWeight.normal, ), ), diff --git a/lib/src/screens/settings/widgets/settings_switcher_cell.dart b/lib/src/screens/settings/widgets/settings_switcher_cell.dart index c1d7fa150..0e5c04524 100644 --- a/lib/src/screens/settings/widgets/settings_switcher_cell.dart +++ b/lib/src/screens/settings/widgets/settings_switcher_cell.dart @@ -3,14 +3,23 @@ import 'package:cake_wallet/src/widgets/standard_list.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart'; class SettingsSwitcherCell extends StandardListRow { - SettingsSwitcherCell( - {required String title, required this.value, this.onValueChange}) - : super(title: title, isSelected: false); + SettingsSwitcherCell({ + required String title, + required this.value, + this.onValueChange, + Decoration? decoration, + this.leading, + void Function(BuildContext context)? onTap, + }) : super(title: title, isSelected: false, decoration: decoration, onTap: onTap); final bool value; final void Function(BuildContext context, bool value)? onValueChange; + final Widget? leading; @override - Widget buildTrailing(BuildContext context) => StandardSwitch( - value: value, onTaped: () => onValueChange?.call(context, !value)); + Widget buildTrailing(BuildContext context) => + StandardSwitch(value: value, onTaped: () => onValueChange?.call(context, !value)); + + @override + Widget? buildLeading(BuildContext context) => leading; } diff --git a/lib/src/screens/setup_2fa/modify_2fa_page.dart b/lib/src/screens/setup_2fa/modify_2fa_page.dart index 05c06fe3e..148e3076e 100644 --- a/lib/src/screens/setup_2fa/modify_2fa_page.dart +++ b/lib/src/screens/setup_2fa/modify_2fa_page.dart @@ -1,12 +1,16 @@ +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_choices_cell.dart'; +import 'package:cake_wallet/src/screens/settings/widgets/settings_switcher_cell.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:flutter/cupertino.dart'; +import 'package:cake_wallet/view_model/settings/choices_list_item.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart'; import 'package:cake_wallet/view_model/set_up_2fa_viewmodel.dart'; import 'package:cake_wallet/src/widgets/standard_list.dart'; +import 'package:flutter_mobx/flutter_mobx.dart'; import '../../../routes.dart'; @@ -21,35 +25,148 @@ class Modify2FAPage extends BasePage { @override Widget body(BuildContext context) { return SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SettingsCellWithArrow( - title: S.current.disable_cake_2fa, - handler: (_) async { - await showPopUp<void>( - context: context, - builder: (BuildContext context) { - return AlertWithTwoActions( - alertTitle: S.current.disable_cake_2fa, - alertContent: S.current.question_to_disable_2fa, - leftButtonText: S.current.cancel, - rightButtonText: S.current.disable, - actionLeftButton: () { - Navigator.of(context).pop(); - }, - actionRightButton: () { - setup2FAViewModel.setUseTOTP2FA(false); - Navigator.pushNamedAndRemoveUntil( - context, Routes.dashboard, (route) => false); - }, - ); - }, - ); - }), - StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), - ], - ), + child: _2FAControlsWidget(setup2FAViewModel: setup2FAViewModel), + ); + } +} + +class _2FAControlsWidget extends StatelessWidget { + const _2FAControlsWidget({required this.setup2FAViewModel}); + + final Setup2FAViewModel setup2FAViewModel; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SettingsCellWithArrow( + title: S.current.disable_cake_2fa, + handler: (_) async { + await showPopUp<void>( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.current.disable_cake_2fa, + alertContent: S.current.question_to_disable_2fa, + leftButtonText: S.current.cancel, + rightButtonText: S.current.disable, + actionLeftButton: () => Navigator.of(context).pop(), + actionRightButton: () { + setup2FAViewModel.setUseTOTP2FA(false); + Navigator.pushNamedAndRemoveUntil(context, Routes.dashboard, (route) => false); + }, + ); + }, + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsChoicesCell( + ChoicesListItem<Cake2FAPresetsOptions>( + title: S.current.cake_2fa_preset, + onItemSelected: setup2FAViewModel.selectCakePreset, + selectedItem: setup2FAViewModel.selectedCake2FAPreset, + items: [ + Cake2FAPresetsOptions.narrow, + Cake2FAPresetsOptions.normal, + Cake2FAPresetsOptions.aggressive, + ], + ), + ); + }, + ), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_assessing_wallet, + value: setup2FAViewModel.shouldRequireTOTP2FAForAccessingWallet, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForAccessingWallet(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_non_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToNonContact, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToNonContact(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToContact, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToContact(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_sends_to_internal_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForSendsToInternalWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForSendsToInternalWallets(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_exchanges_to_internal_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForExchangesToInternalWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForExchangesToInternalWallets(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_adding_contacts, + value: setup2FAViewModel.shouldRequireTOTP2FAForAddingContacts, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForAddingContacts(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_creating_new_wallets, + value: setup2FAViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + onValueChange: (context, value) async => + setup2FAViewModel.switchShouldRequireTOTP2FAForCreatingNewWallet(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + Observer( + builder: (context) { + return SettingsSwitcherCell( + title: S.current.require_for_all_security_and_backup_settings, + value: setup2FAViewModel.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + onValueChange: (context, value) async => setup2FAViewModel + .switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(value), + ); + }, + ), + StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), + ], ); } } diff --git a/lib/src/screens/setup_2fa/setup_2fa.dart b/lib/src/screens/setup_2fa/setup_2fa.dart index 5da0fb120..6c60dcd1e 100644 --- a/lib/src/screens/setup_2fa/setup_2fa.dart +++ b/lib/src/screens/setup_2fa/setup_2fa.dart @@ -52,7 +52,8 @@ class Setup2FAPage extends BasePage { SizedBox(height: 86), SettingsCellWithArrow( title: S.current.setup_totp_recommended, - handler: (_) => Navigator.of(context).pushNamed(Routes.setup_2faQRPage), + handler: (_) => Navigator.of(context) + .pushReplacementNamed(Routes.setup_2faQRPage), ), StandardListSeparator(padding: EdgeInsets.symmetric(horizontal: 24)), ], diff --git a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart index e88b1090b..c4bee2b12 100644 --- a/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart +++ b/lib/src/screens/setup_2fa/setup_2fa_enter_code_page.dart @@ -18,7 +18,8 @@ import 'package:mobx/mobx.dart'; import '../../../palette.dart'; import '../../../routes.dart'; -typedef OnTotpAuthenticationFinished = void Function(bool, TotpAuthCodePageState); +typedef OnTotpAuthenticationFinished = void Function( + bool, TotpAuthCodePageState); class TotpAuthCodePage extends StatefulWidget { TotpAuthCodePage( @@ -43,8 +44,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> { @override void initState() { - if(widget.totpArguments.onTotpAuthenticationFinished != null) { - _reaction ??= reaction((_) => widget.setup2FAViewModel.state, (ExecutionState state) { + if (widget.totpArguments.onTotpAuthenticationFinished != null) { + _reaction ??= reaction((_) => widget.setup2FAViewModel.state, + (ExecutionState state) { WidgetsBinding.instance.addPostFrameCallback((_) { if (state is ExecutedSuccessfullyState) { widget.totpArguments.onTotpAuthenticationFinished!(true, this); @@ -57,9 +59,9 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> { if (state is AuthenticationBanned) { widget.totpArguments.onTotpAuthenticationFinished!(false, this); - } - }); - }); + } + }); + }); } super.initState(); @@ -73,7 +75,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> { void changeProcessText(String text) { dismissFlushBar(_authBar); - _progressBar = createBar<void>(text, duration: null)..show(_key.currentContext!); + _progressBar = createBar<void>(text, duration: null) + ..show(_key.currentContext!); } Future<void> close({String? route, dynamic arguments}) async { @@ -82,7 +85,8 @@ class TotpAuthCodePageState extends State<TotpAuthCodePage> { } await Future<void>.delayed(Duration(milliseconds: 50)); if (route != null) { - Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments); + Navigator.of(_key.currentContext!) + .pushReplacementNamed(route, arguments: arguments); } else { Navigator.of(_key.currentContext!).pop(); } @@ -120,7 +124,8 @@ class TOTPEnterCode extends BasePage { } @override - String get title => isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa; + String get title => + isForSetup ? S.current.setup_2fa : S.current.verify_with_2fa; Widget? leading(BuildContext context) { return isClosable ? super.leading(context) : null; @@ -166,21 +171,24 @@ class TOTPEnterCode extends BasePage { return PrimaryButton( isDisabled: setup2FAViewModel.enteredOTPCode.length != 8, onPressed: () async { - final result = - await setup2FAViewModel.totp2FAAuth(totpController.text, isForSetup); - final bannedState = setup2FAViewModel.state is AuthenticationBanned; + final result = await setup2FAViewModel.totp2FAAuth( + totpController.text, isForSetup); + final bannedState = + setup2FAViewModel.state is AuthenticationBanned; await showPopUp<void>( context: context, builder: (BuildContext context) { return PopUpCancellableAlertDialog( - contentText: _textDisplayedInPopupOnResult(result, bannedState, context), + contentText: _textDisplayedInPopupOnResult( + result, bannedState, context), actionButtonText: S.of(context).ok, buttonAction: () { result ? setup2FAViewModel.success() : null; if (isForSetup && result) { - Navigator.pushNamedAndRemoveUntil( - context, Routes.dashboard, (route) => false); + Navigator.pop(context); + // Navigator.of(context) + // .popAndPushNamed(Routes.modify2FAPage); } else { Navigator.of(context).pop(result); } @@ -188,6 +196,11 @@ class TOTPEnterCode extends BasePage { ); }, ); + if (isForSetup && result) { + Navigator.pushReplacementNamed( + context, Routes.modify2FAPage); + } + }, text: S.of(context).continue_text, color: Theme.of(context).accentTextTheme.bodyLarge!.color!, @@ -201,10 +214,13 @@ class TOTPEnterCode extends BasePage { ); } - String _textDisplayedInPopupOnResult(bool result, bool bannedState, BuildContext context) { + String _textDisplayedInPopupOnResult( + bool result, bool bannedState, BuildContext context) { switch (result) { case true: - return isForSetup ? S.current.totp_2fa_success : S.current.totp_verification_success; + return isForSetup + ? S.current.totp_2fa_success + : S.current.totp_verification_success; case false: if (bannedState) { final state = setup2FAViewModel.state as AuthenticationBanned; diff --git a/lib/src/screens/wallet/wallet_edit_page.dart b/lib/src/screens/wallet/wallet_edit_page.dart index 3ff27f02f..64575b722 100644 --- a/lib/src/screens/wallet/wallet_edit_page.dart +++ b/lib/src/screens/wallet/wallet_edit_page.dart @@ -125,7 +125,9 @@ class WalletEditPage extends BasePage { } _onSuccessfulAuth(context); - }); + }, + conditionToDetermineIfToUse2FA: false, + ); } void _onSuccessfulAuth(BuildContext context) async { diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index c1f684898..81086fac9 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -46,6 +46,7 @@ class WalletListBodyState extends State<WalletListBody> { final litecoinIcon = Image.asset('assets/images/litecoin_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); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar<void>? _progressBar; @@ -55,7 +56,7 @@ class WalletListBodyState extends State<WalletListBody> { final newWalletImage = Image.asset('assets/images/new_wallet.png', height: 12, width: 12, color: Colors.white); final restoreWalletImage = Image.asset('assets/images/restore_wallet.png', - height: 12, width: 12, color: Theme.of(context).primaryTextTheme!.titleLarge!.color!); + height: 12, width: 12, color: Theme.of(context).primaryTextTheme.titleLarge!.color!); return Container( padding: EdgeInsets.only(top: 16), @@ -72,7 +73,7 @@ class WalletListBodyState extends State<WalletListBody> { itemBuilder: (__, index) { final wallet = widget.walletListViewModel.wallets[index]; final currentColor = wallet.isCurrent - ? Theme.of(context).accentTextTheme!.titleSmall!.decorationColor! + ? Theme.of(context).accentTextTheme.titleSmall!.decorationColor! : Theme.of(context).colorScheme.background; final row = GestureDetector( onTap: () => wallet.isCurrent ? null : _loadWallet(wallet), @@ -131,8 +132,7 @@ class WalletListBodyState extends State<WalletListBody> { : Row(children: [ Expanded(child: row), GestureDetector( - onTap: () => Navigator.of(context).pushNamed( - Routes.walletEdit, + onTap: () => Navigator.of(context).pushNamed(Routes.walletEdit, arguments: [widget.walletListViewModel, wallet]), child: Container( padding: EdgeInsets.only(right: 20), @@ -150,10 +150,7 @@ class WalletListBodyState extends State<WalletListBody> { child: Icon( Icons.edit, size: 14, - color: Theme.of(context) - .textTheme - .headlineMedium! - .color!, + color: Theme.of(context).textTheme.headlineMedium!.color!, ), ), ), @@ -167,27 +164,59 @@ class WalletListBodyState extends State<WalletListBody> { bottomSection: Column(children: <Widget>[ PrimaryImageButton( onPressed: () { + //TODO(David): Find a way to optimize this if (isSingleCoin) { - Navigator.of(context).pushNamed(Routes.newWallet, - arguments: widget.walletListViewModel.currentWalletType); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed( + Routes.newWallet, + arguments: widget.walletListViewModel.currentWalletType, + ); + } } else { - Navigator.of(context).pushNamed(Routes.newWalletType); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.newWalletType, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.newWalletType); + } } }, image: newWalletImage, text: S.of(context).wallet_list_create_new_wallet, - color: Theme.of(context).accentTextTheme!.bodyLarge!.color!, + color: Theme.of(context).accentTextTheme.bodyLarge!.color!, textColor: Colors.white, ), SizedBox(height: 10.0), PrimaryImageButton( onPressed: () { - Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); + if (widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets) { + widget.authService.authenticateAction( + context, + route: Routes.restoreOptions, + arguments: false, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets, + ); + } else { + Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false); + } }, image: restoreWalletImage, text: S.of(context).wallet_list_restore_wallet, - color: Theme.of(context).accentTextTheme!.bodySmall!.color!, - textColor: Theme.of(context).primaryTextTheme!.titleLarge!.color!) + color: Theme.of(context).accentTextTheme.bodySmall!.color!, + textColor: Theme.of(context).primaryTextTheme.titleLarge!.color!) ])), ); } @@ -202,33 +231,39 @@ class WalletListBodyState extends State<WalletListBody> { return litecoinIcon; case WalletType.haven: return havenIcon; + case WalletType.ethereum: + return ethereumIcon; default: return nonWalletTypeIcon; } } Future<void> _loadWallet(WalletListItem wallet) async { - await widget.authService.authenticateAction(context, - onAuthSuccess: (isAuthenticatedSuccessfully) async { - if (!isAuthenticatedSuccessfully) { - return; - } - - try { - changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); - await widget.walletListViewModel.loadWallet(wallet); - await hideProgressText(); - // only pop the wallets route in mobile as it will go back to dashboard page - // in desktop platforms the navigation tree is different - if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { - WidgetsBinding.instance.addPostFrameCallback((_) { - Navigator.of(context).pop(); - }); + await widget.authService.authenticateAction( + context, + onAuthSuccess: (isAuthenticatedSuccessfully) async { + if (!isAuthenticatedSuccessfully) { + return; } - } catch (e) { - changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); - } - }); + + try { + changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name)); + await widget.walletListViewModel.loadWallet(wallet); + await hideProgressText(); + // only pop the wallets route in mobile as it will go back to dashboard page + // in desktop platforms the navigation tree is different + if (ResponsiveLayoutUtil.instance.shouldRenderMobileUI()) { + WidgetsBinding.instance.addPostFrameCallback((_) { + Navigator.of(context).pop(); + }); + } + } catch (e) { + changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString())); + } + }, + conditionToDetermineIfToUse2FA: + widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet, + ); } void changeProcessText(String text) { diff --git a/lib/src/widgets/checkbox_widget.dart b/lib/src/widgets/checkbox_widget.dart index a2603ad19..20c900b25 100644 --- a/lib/src/widgets/checkbox_widget.dart +++ b/lib/src/widgets/checkbox_widget.dart @@ -1,13 +1,8 @@ -import 'dart:ui'; import 'package:cake_wallet/palette.dart'; -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; class CheckboxWidget extends StatefulWidget { - CheckboxWidget({ - required this.value, - required this.caption, - required this.onChanged}); + CheckboxWidget({required this.value, required this.caption, required this.onChanged}); final bool value; final String caption; @@ -26,55 +21,45 @@ class CheckboxWidgetState extends State<CheckboxWidget> { @override Widget build(BuildContext context) { - return GestureDetector( + return InkWell( onTap: () { value = !value; onChanged(value); setState(() {}); }, child: Row( - mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: <Widget>[ Container( - height: 16, - width: 16, + height: 24.0, + width: 24.0, + margin: EdgeInsets.only(right: 10.0), decoration: BoxDecoration( - color: value - ? Palette.blueCraiola - : Theme.of(context) - .accentTextTheme! - .titleMedium! - .decorationColor!, - borderRadius: BorderRadius.all(Radius.circular(2)), - border: Border.all( - color: value - ? Palette.blueCraiola - : Theme.of(context) - .accentTextTheme! - .labelSmall! - .color!, - width: 1)), - child: value - ? Center( - child: Icon( - Icons.done, - color: Colors.white, - size: 14, + border: Border.all( + color: Theme.of(context).primaryTextTheme.bodySmall!.color!, + width: 1.0, ), - ) - : Offstage(), + borderRadius: BorderRadius.all( + Radius.circular(8.0), + ), + color: Theme.of(context).colorScheme.background, + ), + child: value + ? Icon( + Icons.check, + color: Colors.blue, + size: 20.0, + ) + : null, ), - Padding( - padding: EdgeInsets.only(left: 16), + Expanded( child: Text( caption, style: TextStyle( - color: Theme.of(context).primaryTextTheme!.titleLarge!.color!, - fontSize: 18, - fontFamily: 'Lato', - fontWeight: FontWeight.w500, - decoration: TextDecoration.none + fontWeight: FontWeight.bold, + fontSize: 14.0, + color: Theme.of(context).primaryTextTheme.titleLarge!.color!, ), ), ) @@ -82,4 +67,4 @@ class CheckboxWidgetState extends State<CheckboxWidget> { ), ); } -} \ No newline at end of file +} diff --git a/lib/src/widgets/picker.dart b/lib/src/widgets/picker.dart index f95ad4919..c9d3be757 100644 --- a/lib/src/widgets/picker.dart +++ b/lib/src/widgets/picker.dart @@ -1,5 +1,7 @@ // ignore_for_file: deprecated_member_use +import 'dart:math'; + import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; import 'package:flutter/material.dart'; @@ -145,8 +147,7 @@ class _PickerState<Item> extends State<Picker<Item>> { borderRadius: BorderRadius.all(Radius.circular(30)), child: Container( color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .color!, child: ConstrainedBox( constraints: BoxConstraints( @@ -163,8 +164,7 @@ class _PickerState<Item> extends State<Picker<Item>> { ), Divider( color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .backgroundColor!, height: 1, ), @@ -194,8 +194,7 @@ class _PickerState<Item> extends State<Picker<Item>> { fontFamily: 'Lato', decoration: TextDecoration.none, color: Theme.of(context) - .primaryTextTheme! - .titleLarge! + .primaryTextTheme.titleLarge! .color!, ), ), @@ -217,8 +216,7 @@ class _PickerState<Item> extends State<Picker<Item>> { Widget itemsList() { return Container( color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .backgroundColor!, child: widget.isGridView ? GridView.builder( @@ -240,8 +238,7 @@ class _PickerState<Item> extends State<Picker<Item>> { separatorBuilder: (context, index) => widget.isSeparated ? Divider( color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .backgroundColor!, height: 1, ) @@ -254,15 +251,9 @@ class _PickerState<Item> extends State<Picker<Item>> { Widget buildItem(int index) { final item = filteredItems[index]; - final tag = item is Currency ? item.tag : null; - final icon = item is Currency && item.iconPath != null - ? Image.asset( - item.iconPath!, - height: 20.0, - width: 20.0, - ) - : null; + final tag = item is Currency ? item.tag : null; + final icon = _getItemIcon(item); final image = images.isNotEmpty ? filteredImages[index] : icon; @@ -274,8 +265,7 @@ class _PickerState<Item> extends State<Picker<Item>> { child: Container( height: 55, color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .color!, padding: EdgeInsets.symmetric(horizontal: 24), child: Row( @@ -298,8 +288,7 @@ class _PickerState<Item> extends State<Picker<Item>> { fontFamily: 'Lato', fontWeight: FontWeight.w600, color: Theme.of(context) - .primaryTextTheme! - .titleLarge! + .primaryTextTheme.titleLarge! .color!, decoration: TextDecoration.none, ), @@ -318,8 +307,7 @@ class _PickerState<Item> extends State<Picker<Item>> { fontSize: 7.0, fontFamily: 'Lato', color: Theme.of(context) - .textTheme! - .bodyMedium! + .textTheme.bodyMedium! .color!), ), ), @@ -327,8 +315,7 @@ class _PickerState<Item> extends State<Picker<Item>> { borderRadius: BorderRadius.circular(6.0), //border: Border.all(color: ), color: Theme.of(context) - .textTheme! - .bodyMedium! + .textTheme.bodyMedium! .decorationColor!, ), ), @@ -345,15 +332,9 @@ class _PickerState<Item> extends State<Picker<Item>> { Widget buildSelectedItem(int index) { final item = items[index]; - final tag = item is Currency ? item.tag : null; - final icon = item is Currency && item.iconPath != null - ? Image.asset( - item.iconPath!, - height: 20.0, - width: 20.0, - ) - : null; + final tag = item is Currency ? item.tag : null; + final icon = _getItemIcon(item); final image = images.isNotEmpty ? images[index] : icon; @@ -364,8 +345,7 @@ class _PickerState<Item> extends State<Picker<Item>> { child: Container( height: 55, color: Theme.of(context) - .accentTextTheme! - .titleLarge! + .accentTextTheme.titleLarge! .color!, padding: EdgeInsets.symmetric(horizontal: 24), child: Row( @@ -388,8 +368,7 @@ class _PickerState<Item> extends State<Picker<Item>> { fontFamily: 'Lato', fontWeight: FontWeight.w700, color: Theme.of(context) - .primaryTextTheme! - .titleLarge! + .primaryTextTheme.titleLarge! .color!, decoration: TextDecoration.none, ), @@ -408,8 +387,7 @@ class _PickerState<Item> extends State<Picker<Item>> { fontSize: 7.0, fontFamily: 'Lato', color: Theme.of(context) - .textTheme! - .bodyMedium! + .textTheme.bodyMedium! .color!), ), ), @@ -417,8 +395,7 @@ class _PickerState<Item> extends State<Picker<Item>> { borderRadius: BorderRadius.circular(6.0), //border: Border.all(color: ), color: Theme.of(context) - .textTheme! - .bodyMedium! + .textTheme.bodyMedium! .decorationColor!, ), ), @@ -429,12 +406,40 @@ class _PickerState<Item> extends State<Picker<Item>> { ), Icon(Icons.check_circle, color: Theme.of(context) - .accentTextTheme! - .bodyLarge! + .accentTextTheme.bodyLarge! .color!), ], ), ), ); } + + Widget? _getItemIcon(Item item) { + if (item is Currency) { + if (item.iconPath != null) { + return Image.asset( + item.iconPath!, + height: 20.0, + width: 20.0, + ); + } else { + return Container( + height: 20.0, + width: 20.0, + child: Center( + child: Text( + item.name.substring(0, min(item.name.length, 2)).toUpperCase(), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ); + } + } + + return null; + } } diff --git a/lib/src/widgets/standard_list.dart b/lib/src/widgets/standard_list.dart index 7b2a0c004..5fc094755 100644 --- a/lib/src/widgets/standard_list.dart +++ b/lib/src/widgets/standard_list.dart @@ -5,11 +5,12 @@ import 'package:flutter/material.dart'; class StandardListRow extends StatelessWidget { StandardListRow( - {required this.title, required this.isSelected, this.onTap}); + {required this.title, required this.isSelected, this.onTap, this.decoration}); final String title; final bool isSelected; final void Function(BuildContext context)? onTap; + final Decoration? decoration; @override Widget build(BuildContext context) { @@ -19,9 +20,11 @@ class StandardListRow extends StatelessWidget { return InkWell( onTap: () => onTap?.call(context), child: Container( - color: _backgroundColor(context), height: 56, padding: EdgeInsets.only(left: 24, right: 24), + decoration: decoration ?? BoxDecoration( + color: _backgroundColor(context), + ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: <Widget>[ @@ -54,7 +57,7 @@ class StandardListRow extends StatelessWidget { Color titleColor(BuildContext context) => isSelected ? Palette.blueCraiola - : Theme.of(context).primaryTextTheme!.titleLarge!.color!; + : Theme.of(context).primaryTextTheme.titleLarge!.color!; Color _backgroundColor(BuildContext context) { return Theme.of(context).colorScheme.background; @@ -75,7 +78,7 @@ class SectionHeaderListRow extends StatelessWidget { class StandardListSeparator extends StatelessWidget { - StandardListSeparator({this.padding, this.height = 1}); + const StandardListSeparator({this.padding, this.height = 1}); final EdgeInsets? padding; final double height; @@ -89,7 +92,7 @@ class StandardListSeparator extends StatelessWidget { child: Container( height: height, color: Theme.of(context) - .primaryTextTheme! + .primaryTextTheme .titleLarge ?.backgroundColor)); } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 87e2cee4c..703c7d73e 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -1,10 +1,15 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/utils/device_info.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; @@ -31,7 +36,8 @@ class SettingsStore = SettingsStoreBase with _$SettingsStore; abstract class SettingsStoreBase with Store { SettingsStoreBase( - {required SharedPreferences sharedPreferences, + {required BackgroundTasks backgroundTasks, + required SharedPreferences sharedPreferences, required bool initialShouldShowMarketPlaceInDashboard, required FiatCurrency initialFiatCurrency, required BalanceDisplayMode initialBalanceDisplayMode, @@ -48,6 +54,8 @@ abstract class SettingsStoreBase with Store { required ThemeBase initialTheme, required int initialPinLength, required String initialLanguageCode, + required SyncMode initialSyncMode, + required bool initialSyncAll, // required String initialCurrentLocale, required this.appVersion, required this.deviceName, @@ -56,17 +64,32 @@ abstract class SettingsStoreBase with Store { required this.isBitcoinBuyEnabled, required this.actionlistDisplayMode, required this.pinTimeOutDuration, + required Cake2FAPresetsOptions initialCake2FAPresetOptions, + required bool initialShouldRequireTOTP2FAForAccessingWallet, + required bool initialShouldRequireTOTP2FAForSendsToContact, + required bool initialShouldRequireTOTP2FAForSendsToNonContact, + required bool initialShouldRequireTOTP2FAForSendsToInternalWallets, + required bool initialShouldRequireTOTP2FAForExchangesToInternalWallets, + required bool initialShouldRequireTOTP2FAForAddingContacts, + required bool initialShouldRequireTOTP2FAForCreatingNewWallets, + required bool initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, + required this.sortBalanceBy, + required this.pinNativeTokenAtTop, + required this.useEtherscan, TransactionPriority? initialBitcoinTransactionPriority, TransactionPriority? initialMoneroTransactionPriority, TransactionPriority? initialHavenTransactionPriority, - TransactionPriority? initialLitecoinTransactionPriority}) + TransactionPriority? initialLitecoinTransactionPriority, + TransactionPriority? initialEthereumTransactionPriority}) : nodes = ObservableMap<WalletType, Node>.of(nodes), _sharedPreferences = sharedPreferences, + _backgroundTasks = backgroundTasks, fiatCurrency = initialFiatCurrency, balanceDisplayMode = initialBalanceDisplayMode, shouldSaveRecipientAddress = initialSaveRecipientAddress, fiatApiMode = initialFiatMode, allowBiometricalAuthentication = initialAllowBiometricalAuthentication, + selectedCake2FAPreset = initialCake2FAPresetOptions, totpSecretKey = initialTotpSecretKey, useTOTP2FA = initialUseTOTP2FA, numberOfFailedTokenTrials = initialFailedTokenTrial, @@ -78,6 +101,20 @@ abstract class SettingsStoreBase with Store { currentTheme = initialTheme, pinCodeLength = initialPinLength, languageCode = initialLanguageCode, + shouldRequireTOTP2FAForAccessingWallet = initialShouldRequireTOTP2FAForAccessingWallet, + shouldRequireTOTP2FAForSendsToContact = initialShouldRequireTOTP2FAForSendsToContact, + shouldRequireTOTP2FAForSendsToNonContact = initialShouldRequireTOTP2FAForSendsToNonContact, + shouldRequireTOTP2FAForSendsToInternalWallets = + initialShouldRequireTOTP2FAForSendsToInternalWallets, + shouldRequireTOTP2FAForExchangesToInternalWallets = + initialShouldRequireTOTP2FAForExchangesToInternalWallets, + shouldRequireTOTP2FAForAddingContacts = initialShouldRequireTOTP2FAForAddingContacts, + shouldRequireTOTP2FAForCreatingNewWallets = + initialShouldRequireTOTP2FAForCreatingNewWallets, + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings, + currentSyncMode = initialSyncMode, + currentSyncAll = initialSyncAll, priority = ObservableMap<WalletType, TransactionPriority>() { //this.nodes = ObservableMap<WalletType, Node>.of(nodes); @@ -97,6 +134,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.litecoin] = initialLitecoinTransactionPriority; } + if (initialEthereumTransactionPriority != null) { + priority[WalletType.ethereum] = initialEthereumTransactionPriority; + } + reaction( (_) => fiatCurrency, (FiatCurrency fiatCurrency) => sharedPreferences.setString( @@ -122,6 +163,9 @@ abstract class SettingsStoreBase with Store { case WalletType.haven: key = PreferencesKey.havenTransactionPriority; break; + case WalletType.ethereum: + key = PreferencesKey.ethereumTransactionPriority; + break; default: key = null; } @@ -166,6 +210,57 @@ abstract class SettingsStoreBase with Store { (bool biometricalAuthentication) => sharedPreferences.setBool( PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); + reaction( + (_) => selectedCake2FAPreset, + (Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt( + PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize())); + + reaction( + (_) => shouldRequireTOTP2FAForAccessingWallet, + (bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, + requireTOTP2FAForAccessingWallet)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToContact, + (bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToNonContact, + (bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, + requireTOTP2FAForSendsToNonContact)); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToInternalWallets, + (bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, + requireTOTP2FAForSendsToInternalWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, + (bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + requireTOTP2FAForExchangesToInternalWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForAddingContacts, + (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts)); + + reaction( + (_) => shouldRequireTOTP2FAForCreatingNewWallets, + (bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, + requireTOTP2FAForCreatingNewWallets)); + + reaction( + (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + (bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool( + PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + requireTOTP2FAForAllSecurityAndBackupSettings)); + reaction( (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use)); @@ -200,11 +295,38 @@ abstract class SettingsStoreBase with Store { (BalanceDisplayMode mode) => sharedPreferences.setInt( PreferencesKey.currentBalanceDisplayModeKey, mode.serialize())); + reaction((_) => currentSyncMode, (SyncMode syncMode) { + sharedPreferences.setInt(PreferencesKey.syncModeKey, syncMode.type.index); + + _backgroundTasks.registerSyncTask(changeExisting: true); + }); + + reaction((_) => currentSyncAll, (bool syncAll) { + sharedPreferences.setBool(PreferencesKey.syncAllKey, syncAll); + + _backgroundTasks.registerSyncTask(changeExisting: true); + }); + reaction( (_) => exchangeStatus, (ExchangeApiMode mode) => sharedPreferences.setInt(PreferencesKey.exchangeStatusKey, mode.serialize())); + reaction( + (_) => sortBalanceBy, + (SortBalanceBy sortBalanceBy) => + _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceBy.index)); + + reaction( + (_) => pinNativeTokenAtTop, + (bool pinNativeTokenAtTop) => + _sharedPreferences.setBool(PreferencesKey.pinNativeTokenAtTop, pinNativeTokenAtTop)); + + reaction( + (_) => useEtherscan, + (bool useEtherscan) => + _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan)); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -249,6 +371,33 @@ abstract class SettingsStoreBase with Store { @observable bool allowBiometricalAuthentication; + @observable + bool shouldRequireTOTP2FAForAccessingWallet; + + @observable + bool shouldRequireTOTP2FAForSendsToContact; + + @observable + bool shouldRequireTOTP2FAForSendsToNonContact; + + @observable + bool shouldRequireTOTP2FAForSendsToInternalWallets; + + @observable + bool shouldRequireTOTP2FAForExchangesToInternalWallets; + + @observable + Cake2FAPresetsOptions selectedCake2FAPreset; + + @observable + bool shouldRequireTOTP2FAForAddingContacts; + + @observable + bool shouldRequireTOTP2FAForCreatingNewWallets; + + @observable + bool shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + @observable String totpSecretKey; @@ -284,11 +433,27 @@ abstract class SettingsStoreBase with Store { @observable ObservableMap<WalletType, TransactionPriority> priority; + @observable + SortBalanceBy sortBalanceBy; + + @observable + bool pinNativeTokenAtTop; + + @observable + bool useEtherscan; + + @observable + SyncMode currentSyncMode; + + @observable + bool currentSyncAll; + String appVersion; String deviceName; - SharedPreferences _sharedPreferences; + final SharedPreferences _sharedPreferences; + final BackgroundTasks _backgroundTasks; ObservableMap<WalletType, Node> nodes; @@ -317,6 +482,7 @@ abstract class SettingsStoreBase with Store { BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance, ThemeBase? initialTheme}) async { final sharedPreferences = await getIt.getAsync<SharedPreferences>(); + final backgroundTasks = getIt.get<BackgroundTasks>(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); @@ -328,6 +494,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? havenTransactionPriority; TransactionPriority? litecoinTransactionPriority; + TransactionPriority? ethereumTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { havenTransactionPriority = monero?.deserializeMoneroTransactionPriority( @@ -337,11 +504,16 @@ abstract class SettingsStoreBase with Store { litecoinTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority( sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { + ethereumTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!); + } moneroTransactionPriority ??= monero?.getDefaultTransactionPriority(); bitcoinTransactionPriority ??= bitcoin?.getMediumTransactionPriority(); havenTransactionPriority ??= monero?.getDefaultTransactionPriority(); litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium(); + ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -356,6 +528,29 @@ abstract class SettingsStoreBase with Store { FiatApiMode.enabled.raw); final allowBiometricalAuthentication = sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; + final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.normal.raw); + final shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; + final shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; + final shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; + final shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? + false; + final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? + false; + final shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; + final shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? + false; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? + false; final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false; final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0; @@ -378,6 +573,12 @@ abstract class SettingsStoreBase with Store { final pinCodeTimeOutDuration = timeOutDuration != null ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) : defaultPinCodeTimeOutDuration; + final sortBalanceBy = + SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0]; + final pinNativeTokenAtTop = + sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; + final useEtherscan = + sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; // If no value if (pinLength == null || pinLength == 0) { @@ -392,10 +593,12 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); + final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); + final ethereumNode = nodeSource.get(ethereumNodeId); final packageInfo = await PackageInfo.fromPlatform(); final deviceName = await _getDeviceName() ?? ''; final shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? true; @@ -418,6 +621,15 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.haven] = havenNode; } + if (ethereumNode != null) { + nodes[WalletType.ethereum] = ethereumNode; + } + + final savedSyncMode = SyncMode.all.firstWhere((element) { + return element.type.index == (sharedPreferences.getInt(PreferencesKey.syncModeKey) ?? 1); + }); + final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; + return SettingsStore( sharedPreferences: sharedPreferences, initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, @@ -433,6 +645,7 @@ abstract class SettingsStoreBase with Store { initialDisableSell: disableSell, initialFiatMode: currentFiatApiMode, initialAllowBiometricalAuthentication: allowBiometricalAuthentication, + initialCake2FAPresetOptions: selectedCake2FAPreset, initialTotpSecretKey: totpSecretKey, initialUseTOTP2FA: useTOTP2FA, initialFailedTokenTrial: tokenTrialNumber, @@ -442,10 +655,28 @@ abstract class SettingsStoreBase with Store { initialPinLength: pinLength, pinTimeOutDuration: pinCodeTimeOutDuration, initialLanguageCode: savedLanguageCode, + sortBalanceBy: sortBalanceBy, + pinNativeTokenAtTop: pinNativeTokenAtTop, + useEtherscan: useEtherscan, initialMoneroTransactionPriority: moneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, + initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, + initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, + initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, + initialShouldRequireTOTP2FAForSendsToInternalWallets: + shouldRequireTOTP2FAForSendsToInternalWallets, + initialShouldRequireTOTP2FAForExchangesToInternalWallets: + shouldRequireTOTP2FAForExchangesToInternalWallets, + initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, + initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: + shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + initialEthereumTransactionPriority: ethereumTransactionPriority, + backgroundTasks: backgroundTasks, + initialSyncMode: savedSyncMode, + initialSyncAll: savedSyncAll, shouldShowYatPopup: shouldShowYatPopup); } @@ -472,6 +703,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!) ?? priority[WalletType.litecoin]!; } + if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { + priority[WalletType.ethereum] = ethereum?.deserializeEthereumTransactionPriority( + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ?? + priority[WalletType.ethereum]!; + } balanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -480,6 +716,7 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress; totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA; + numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? @@ -490,9 +727,35 @@ abstract class SettingsStoreBase with Store { allowBiometricalAuthentication = sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? allowBiometricalAuthentication; + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.normal.raw); + shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; + shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; + shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; + shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? + false; + shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? + false; + shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; + shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? + false; + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences + .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? + false; shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? + Cake2FAPresetsOptions.narrow.raw); exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); @@ -514,6 +777,10 @@ abstract class SettingsStoreBase with Store { languageCode = sharedPreferences.getString(PreferencesKey.currentLanguageCode) ?? languageCode; shouldShowYatPopup = sharedPreferences.getBool(PreferencesKey.shouldShowYatPopup) ?? shouldShowYatPopup; + sortBalanceBy = SortBalanceBy + .values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index]; + pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; + useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final bitcoinElectrumServerId = @@ -521,10 +788,12 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServerId = sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); + final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); final moneroNode = nodeSource.get(nodeId); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); + final ethereumNode = nodeSource.get(ethereumNodeId); if (moneroNode != null) { nodes[WalletType.monero] = moneroNode; @@ -541,6 +810,10 @@ abstract class SettingsStoreBase with Store { if (havenNode != null) { nodes[WalletType.haven] = havenNode; } + + if (ethereumNode != null) { + nodes[WalletType.ethereum] = ethereumNode; + } } Future<void> _saveCurrentNode(Node node, WalletType walletType) async { @@ -559,6 +832,9 @@ abstract class SettingsStoreBase with Store { case WalletType.haven: await _sharedPreferences.setInt(PreferencesKey.currentHavenNodeIdKey, node.key as int); break; + case WalletType.ethereum: + await _sharedPreferences.setInt(PreferencesKey.currentEthereumNodeIdKey, node.key as int); + break; default: break; } diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 5c0ec49cc..6a33c842d 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -144,6 +144,7 @@ class ExceptionHandler { "Connection closed before full header was received", "Connection terminated during handshake", "PERMISSION_NOT_GRANTED", + "Failed host lookup: ", ]; static Future<void> _addDeviceInfo(File file) async { 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 5b0187fc8..c99984ebc 100644 --- a/lib/view_model/contact_list/contact_list_view_model.dart +++ b/lib/view_model/contact_list/contact_list_view_model.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:cake_wallet/entities/contact_base.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -11,10 +13,12 @@ import 'package:cw_core/crypto_currency.dart'; part 'contact_list_view_model.g.dart'; -class ContactListViewModel = ContactListViewModelBase with _$ContactListViewModel; +class ContactListViewModel = ContactListViewModelBase + with _$ContactListViewModel; abstract class ContactListViewModelBase with Store { - ContactListViewModelBase(this.contactSource, this.walletInfoSource, this._currency) + ContactListViewModelBase(this.contactSource, this.walletInfoSource, + this._currency, this.settingsStore) : contacts = ObservableList<ContactRecord>(), walletContacts = [] { walletInfoSource.values.forEach((info) { @@ -42,16 +46,27 @@ abstract class ContactListViewModelBase with Store { final List<WalletContact> walletContacts; final CryptoCurrency? _currency; StreamSubscription<BoxEvent>? _subscription; + final SettingsStore settingsStore; bool get isEditable => _currency == null; + @computed + bool get shouldRequireTOTP2FAForAddingContacts => + settingsStore.shouldRequireTOTP2FAForAddingContacts; + Future<void> delete(ContactRecord contact) async => contact.original.delete(); @computed - List<ContactRecord> get contactsToShow => - contacts.where((element) => _currency == null || element.type == _currency).toList(); + List<ContactRecord> get contactsToShow => contacts + .where((element) => _isValidForCurrency(element)) + .toList(); @computed - List<WalletContact> get walletContactsToShow => - walletContacts.where((element) => _currency == null || element.type == _currency).toList(); + List<WalletContact> get walletContactsToShow => walletContacts + .where((element) => _isValidForCurrency(element)) + .toList(); + + bool _isValidForCurrency(ContactBase element) { + return _currency == null || element.type == _currency || element.type.title == _currency!.tag; + } } diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index a36ad609c..b0e60963d 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/sort_balance_types.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; @@ -79,6 +80,15 @@ abstract class BalanceViewModelBase with Store { @computed bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled; + @computed + bool get isHomeScreenSettingsEnabled => wallet.type == WalletType.ethereum; + + @computed + SortBalanceBy get sortBalanceBy => settingsStore.sortBalanceBy; + + @computed + bool get pinNativeToken => settingsStore.pinNativeTokenAtTop; + @computed String get asset { final typeFormatted = walletTypeToString(appStore.wallet!.type); @@ -109,6 +119,7 @@ abstract class BalanceViewModelBase with Store { switch(wallet.type) { case WalletType.monero: case WalletType.haven: + case WalletType.ethereum: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -120,6 +131,7 @@ abstract class BalanceViewModelBase with Store { switch(wallet.type) { case WalletType.monero: case WalletType.haven: + case WalletType.ethereum: return S.current.xmr_full_balance; default: return S.current.unconfirmed; @@ -262,32 +274,58 @@ abstract class BalanceViewModelBase with Store { }); } + @computed + bool get hasAdditionalBalance => wallet.type != WalletType.ethereum; + @computed List<BalanceRecord> get formattedBalances { final balance = balances.values.toList(); balance.sort((BalanceRecord a, BalanceRecord b) { - if (b.asset == CryptoCurrency.xhv) { - return 1; - } - - if (b.asset == CryptoCurrency.xusd) { - if (a.asset == CryptoCurrency.xhv) { - return -1; + if (wallet.currency == CryptoCurrency.xhv) { + if (b.asset == CryptoCurrency.xhv) { + return 1; } - return 1; + if (b.asset == CryptoCurrency.xusd) { + if (a.asset == CryptoCurrency.xhv) { + return -1; + } + + return 1; + } + + if (b.asset == CryptoCurrency.xbtc) { + return 1; + } + + if (b.asset == CryptoCurrency.xeur) { + return 1; + } + + return 0; } - if (b.asset == CryptoCurrency.xbtc) { - return 1; + if (pinNativeToken) { + if (b.asset == wallet.currency) return 1; + if (a.asset == wallet.currency) return -1; } - if (b.asset == CryptoCurrency.xeur) { - return 1; - } + switch (sortBalanceBy) { + case SortBalanceBy.FiatBalance: + final aFiatBalance = _getFiatBalance( + price: fiatConvertationStore.prices[a.asset] ?? 0, cryptoAmount: a.availableBalance); + final bFiatBalance = _getFiatBalance( + price: fiatConvertationStore.prices[b.asset] ?? 0, cryptoAmount: b.availableBalance); - return 0; + return (double.tryParse(bFiatBalance) ?? 0) + .compareTo((double.tryParse(aFiatBalance)) ?? 0); + case SortBalanceBy.GrossBalance: + return (double.tryParse(b.availableBalance) ?? 0) + .compareTo(double.tryParse(a.availableBalance) ?? 0); + case SortBalanceBy.Alphabetical: + return a.asset.title.compareTo(b.asset.title); + } }); return balance; @@ -335,7 +373,7 @@ abstract class BalanceViewModelBase with Store { } String _getFiatBalance({required double price, String? cryptoAmount}) { - if (cryptoAmount == null || cryptoAmount.isEmpty) { + if (cryptoAmount == null || cryptoAmount.isEmpty || double.tryParse(cryptoAmount) == null) { return '0.00'; } diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index ac43c20d9..49bb5304e 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/store/anonpay/anonpay_transactions_store.dart'; import 'package:cake_wallet/view_model/dashboard/anonpay_transaction_list_item.dart'; +import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; @@ -403,4 +404,16 @@ abstract class DashboardViewModelBase with Store { hasBuyAction = !isHaven; hasSellAction = !isHaven; } + + @computed + SyncMode get syncMode => settingsStore.currentSyncMode; + + @action + void setSyncMode(SyncMode syncMode) => settingsStore.currentSyncMode = syncMode; + + @computed + bool get syncAll => settingsStore.currentSyncAll; + + @action + void setSyncAll(bool value) => settingsStore.currentSyncAll = value; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart new file mode 100644 index 000000000..66620f951 --- /dev/null +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -0,0 +1,121 @@ +import 'package:cake_wallet/core/fiat_conversion_service.dart'; +import 'package:cake_wallet/entities/fiat_api_mode.dart'; +import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:mobx/mobx.dart'; + +part 'home_settings_view_model.g.dart'; + +class HomeSettingsViewModel = HomeSettingsViewModelBase with _$HomeSettingsViewModel; + +abstract class HomeSettingsViewModelBase with Store { + HomeSettingsViewModelBase(this._settingsStore, this._balanceViewModel) + : tokens = ObservableSet<Erc20Token>() { + _updateTokensList(); + } + + final SettingsStore _settingsStore; + final BalanceViewModel _balanceViewModel; + + final ObservableSet<Erc20Token> tokens; + + @observable + String searchText = ''; + + @computed + SortBalanceBy get sortBalanceBy => _settingsStore.sortBalanceBy; + + @action + void setSortBalanceBy(SortBalanceBy value) { + _settingsStore.sortBalanceBy = value; + _updateTokensList(); + } + + @computed + bool get pinNativeToken => _settingsStore.pinNativeTokenAtTop; + + @action + void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value; + + Future<void> addErc20Token(Erc20Token token) async { + await ethereum!.addErc20Token(_balanceViewModel.wallet, token); + _updateTokensList(); + _updateFiatPrices(token); + } + + Future<void> deleteErc20Token(Erc20Token token) async { + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token); + _updateTokensList(); + } + + Future<Erc20Token?> getErc20Token(String contractAddress) async => + await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress); + + CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency; + + void _updateFiatPrices(Erc20Token token) async { + try { + _balanceViewModel.fiatConvertationStore.prices[token] = + await FiatConversionService.fetchPrice( + crypto: token, + fiat: _settingsStore.fiatCurrency, + torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly); + } catch (_) {} + } + + void changeTokenAvailability(Erc20Token token, bool value) async { + token.enabled = value; + ethereum!.addErc20Token(_balanceViewModel.wallet, token); + _refreshTokensList(); + } + + @action + void _updateTokensList() { + int _sortFunc(Erc20Token e1, Erc20Token e2) { + int index1 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e1); + int index2 = _balanceViewModel.formattedBalances.indexWhere((element) => element.asset == e2); + + if (e1.enabled && !e2.enabled) { + return -1; + } else if (e2.enabled && !e1.enabled) { + return 1; + } else if (!e1.enabled && !e2.enabled) { // if both are disabled then sort alphabetically + return e1.name.compareTo(e2.name); + } + + return index1.compareTo(index2); + } + + tokens.clear(); + + tokens.addAll(ethereum! + .getERC20Currencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } + + @action + void _refreshTokensList() { + final _tokens = Set.of(tokens); + tokens.clear(); + tokens.addAll(_tokens); + } + + @action + void changeSearchText(String text) { + searchText = text; + _updateTokensList(); + } + + bool _matchesSearchText(Erc20Token asset) { + return searchText.isEmpty || + asset.fullName!.toLowerCase().contains(searchText.toLowerCase()) || + asset.title.toLowerCase().contains(searchText.toLowerCase()) || + asset.contractAddress == searchText; + } +} diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index c8c6f5175..ac74df89d 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/entities/balance_display_mode.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -84,6 +85,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: haven!.formatterMoneroAmountToDouble(amount: transaction.amount), price: price); break; + case WalletType.ethereum: + final asset = ethereum!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction), + price: price); + break; default: break; } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 23ba17ade..162a3d723 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -4,12 +4,14 @@ import 'dart:convert'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/wallet_contact.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_exchange_provider.dart'; import 'package:cake_wallet/exchange/sideshift/sideshift_request.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_exchange_provider.dart'; import 'package:cake_wallet/exchange/simpleswap/simpleswap_request.dart'; import 'package:cake_wallet/exchange/trocador/trocador_exchange_provider.dart'; import 'package:cake_wallet/exchange/trocador/trocador_request.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -44,42 +46,55 @@ part 'exchange_view_model.g.dart'; class ExchangeViewModel = ExchangeViewModelBase with _$ExchangeViewModel; abstract class ExchangeViewModelBase with Store { - ExchangeViewModelBase(this.wallet, this.trades, this._exchangeTemplateStore, - this.tradesStore, this._settingsStore, this.sharedPreferences) - : _cryptoNumberFormat = NumberFormat(), - isFixedRateMode = false, - isReceiveAmountEntered = false, - depositAmount = '', - receiveAmount = '', - receiveAddress = '', - depositAddress = '', - isDepositAddressEnabled = false, - isReceiveAddressEnabled = false, - isReceiveAmountEditable = false, - _useTorOnly = false, - receiveCurrencies = <CryptoCurrency>[], - depositCurrencies = <CryptoCurrency>[], - limits = Limits(min: 0, max: 0), - tradeState = ExchangeTradeStateInitial(), - limitsState = LimitsInitialState(), - receiveCurrency = wallet.currency, - depositCurrency = wallet.currency, - providerList = [], - selectedProviders = ObservableList<ExchangeProvider>() { + ExchangeViewModelBase( + this.wallet, + this.trades, + this._exchangeTemplateStore, + this.tradesStore, + this._settingsStore, + this.sharedPreferences, + this.contactListViewModel) + : _cryptoNumberFormat = NumberFormat(), + isFixedRateMode = false, + isReceiveAmountEntered = false, + depositAmount = '', + receiveAmount = '', + receiveAddress = '', + depositAddress = '', + isDepositAddressEnabled = false, + isReceiveAddressEnabled = false, + isReceiveAmountEditable = false, + _useTorOnly = false, + receiveCurrencies = <CryptoCurrency>[], + depositCurrencies = <CryptoCurrency>[], + limits = Limits(min: 0, max: 0), + tradeState = ExchangeTradeStateInitial(), + limitsState = LimitsInitialState(), + receiveCurrency = wallet.currency, + depositCurrency = wallet.currency, + providerList = [], + selectedProviders = ObservableList<ExchangeProvider>() { _useTorOnly = _settingsStore.exchangeStatus == ExchangeApiMode.torOnly; _setProviders(); const excludeDepositCurrencies = [CryptoCurrency.btt, CryptoCurrency.nano]; - const excludeReceiveCurrencies = [CryptoCurrency.xlm, CryptoCurrency.xrp, - CryptoCurrency.bnb, CryptoCurrency.btt, CryptoCurrency.nano]; + const excludeReceiveCurrencies = [ + CryptoCurrency.xlm, + CryptoCurrency.xrp, + CryptoCurrency.bnb, + CryptoCurrency.btt, + CryptoCurrency.nano + ]; _initialPairBasedOnWallet(); - final Map<String, dynamic> exchangeProvidersSelection = json - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>; + final Map<String, dynamic> exchangeProvidersSelection = json.decode( + sharedPreferences + .getString(PreferencesKey.exchangeProvidersSelection) ?? + "{}") as Map<String, dynamic>; /// if the provider is not in the user settings (user's first time or newly added provider) /// then use its default value decided by us - selectedProviders = ObservableList.of(providersForCurrentPair().where( - (element) => exchangeProvidersSelection[element.title] == null + selectedProviders = ObservableList.of(providersForCurrentPair() + .where((element) => exchangeProvidersSelection[element.title] == null ? element.isEnabled : (exchangeProvidersSelection[element.title] as bool)) .toList()); @@ -87,7 +102,8 @@ abstract class ExchangeViewModelBase with Store { _setAvailableProviders(); _calculateBestRate(); - bestRateSync = Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate()); + bestRateSync = + Timer.periodic(Duration(seconds: 10), (timer) => _calculateBestRate()); isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); @@ -95,7 +111,8 @@ abstract class ExchangeViewModelBase with Store { receiveAmount = ''; receiveAddress = ''; depositAddress = depositCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; provider = providersForCurrentPair().first; final initialProvider = provider; provider!.checkIsAvailable().then((bool isAvailable) { @@ -107,20 +124,20 @@ abstract class ExchangeViewModelBase with Store { } }); receiveCurrencies = CryptoCurrency.all - .where((cryptoCurrency) => !excludeReceiveCurrencies.contains(cryptoCurrency)) - .toList(); + .where((cryptoCurrency) => + !excludeReceiveCurrencies.contains(cryptoCurrency)) + .toList(); depositCurrencies = CryptoCurrency.all - .where((cryptoCurrency) => !excludeDepositCurrencies.contains(cryptoCurrency)) - .toList(); + .where((cryptoCurrency) => + !excludeDepositCurrencies.contains(cryptoCurrency)) + .toList(); _defineIsReceiveAmountEditable(); loadLimits(); - reaction( - (_) => isFixedRateMode, - (Object _) { - loadLimits(); - _bestRate = 0; - _calculateBestRate(); - }); + reaction((_) => isFixedRateMode, (Object _) { + loadLimits(); + _bestRate = 0; + _calculateBestRate(); + }); } bool _useTorOnly; final WalletBase wallet; @@ -148,7 +165,8 @@ abstract class ExchangeViewModelBase with Store { /// initialize with descending comparator /// since we want largest rate first final SplayTreeMap<double, ExchangeProvider> _sortedAvailableProviders = - SplayTreeMap<double, ExchangeProvider>((double a, double b) => b.compareTo(a)); + SplayTreeMap<double, ExchangeProvider>( + (double a, double b) => b.compareTo(a)); final List<ExchangeProvider> _tradeAvailableProviders = []; @@ -207,6 +225,37 @@ abstract class ExchangeViewModelBase with Store { ObservableList<ExchangeTemplate> get templates => _exchangeTemplateStore.templates; + @computed + List<WalletContact> get walletContactsToShow => + contactListViewModel.walletContacts + .where((element) => + receiveCurrency == null || element.type == receiveCurrency) + .toList(); + + @action + bool checkIfWalletIsAnInternalWallet(String address) { + final walletContactList = walletContactsToShow + .where((element) => element.address == address) + .toList(); + + return walletContactList.isNotEmpty; + } + + @computed + bool get shouldDisplayTOTP2FAForExchangesToInternalWallet => + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + + //* Still open to further optimize these checks + //* It works but can be made better + @action + bool shouldDisplayTOTP() { + final isInternalWallet = checkIfWalletIsAnInternalWallet(receiveAddress); + if (isInternalWallet) { + return shouldDisplayTOTP2FAForExchangesToInternalWallet; + } + return false; + } + @computed TransactionPriority get transactionPriority { @@ -219,21 +268,23 @@ abstract class ExchangeViewModelBase with Store { return priority; } - bool get hasAllAmount => (wallet.type == WalletType.bitcoin || wallet.type == WalletType.litecoin) && depositCurrency == wallet.currency; - bool get isMoneroWallet => wallet.type == WalletType.monero; + bool get isMoneroWallet => wallet.type == WalletType.monero; - bool get isLowFee { + bool get isLowFee { switch (wallet.type) { case WalletType.monero: case WalletType.haven: - return transactionPriority == monero!.getMoneroTransactionPrioritySlow(); + return transactionPriority == + monero!.getMoneroTransactionPrioritySlow(); case WalletType.bitcoin: - return transactionPriority == bitcoin!.getBitcoinTransactionPrioritySlow(); + return transactionPriority == + bitcoin!.getBitcoinTransactionPrioritySlow(); case WalletType.litecoin: - return transactionPriority == bitcoin!.getLitecoinTransactionPrioritySlow(); + return transactionPriority == + bitcoin!.getLitecoinTransactionPrioritySlow(); default: return false; } @@ -247,6 +298,8 @@ abstract class ExchangeViewModelBase with Store { final SettingsStore _settingsStore; + final ContactListViewModel contactListViewModel; + double _bestRate = 0.0; late Timer bestRateSync; @@ -337,23 +390,24 @@ abstract class ExchangeViewModelBase with Store { } Future<void> _calculateBestRate() async { - final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; + final amount = + double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ?? 1; final _providers = _tradeAvailableProviders - .where((element) => !isFixedRateMode || element.supportsFixedRate).toList(); + .where((element) => !isFixedRateMode || element.supportsFixedRate) + .toList(); - final result = await Future.wait<double>( - _providers.map((element) => element.fetchRate( - from: depositCurrency, - to: receiveCurrency, - amount: amount, - isFixedRateMode: isFixedRateMode, - isReceiveAmount: isFixedRateMode)) - ); + final result = await Future.wait<double>(_providers.map((element) => + element.fetchRate( + from: depositCurrency, + to: receiveCurrency, + amount: amount, + isFixedRateMode: isFixedRateMode, + isReceiveAmount: isFixedRateMode))); _sortedAvailableProviders.clear(); - for (int i=0;i<result.length;i++) { + for (int i = 0; i < result.length; i++) { if (result[i] != 0) { /// add this provider as its valid for this trade try { @@ -377,12 +431,8 @@ abstract class ExchangeViewModelBase with Store { limitsState = LimitsIsLoading(); - final from = isFixedRateMode - ? receiveCurrency - : depositCurrency; - final to = isFixedRateMode - ? depositCurrency - : receiveCurrency; + final from = isFixedRateMode ? receiveCurrency : depositCurrency; + final to = isFixedRateMode ? depositCurrency : receiveCurrency; double? lowestMin = double.maxFinite; double? highestMax = 0.0; @@ -396,14 +446,13 @@ abstract class ExchangeViewModelBase with Store { try { final tempLimits = await provider.fetchLimits( - from: from, - to: to, - isFixedRateMode: isFixedRateMode); + from: from, to: to, isFixedRateMode: isFixedRateMode); if (lowestMin != null && (tempLimits.min ?? -1) < lowestMin) { lowestMin = tempLimits.min; } - if (highestMax != null && (tempLimits.max ?? double.maxFinite) > highestMax) { + if (highestMax != null && + (tempLimits.max ?? double.maxFinite) > highestMax) { highestMax = tempLimits.max; } } catch (e) { @@ -445,7 +494,7 @@ abstract class ExchangeViewModelBase with Store { settleMethod: receiveCurrency, depositAmount: isFixedRateMode ? receiveAmount.replaceAll(',', '.') - : depositAmount.replaceAll(',', '.'), + : depositAmount.replaceAll(',', '.'), settleAddress: receiveAddress, refundAddress: depositAddress, ); @@ -525,6 +574,7 @@ abstract class ExchangeViewModelBase with Store { tradesStore.setTrade(trade); await trades.add(trade); tradeState = TradeIsCreatedSuccessfully(trade: trade); + /// return after the first successful trade return; } catch (e) { @@ -555,9 +605,11 @@ abstract class ExchangeViewModelBase with Store { depositAmount = ''; receiveAmount = ''; depositAddress = depositCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; receiveAddress = receiveCurrency == wallet.currency - ? wallet.walletAddresses.address : ''; + ? wallet.walletAddresses.address + : ''; isDepositAddressEnabled = !(depositCurrency == wallet.currency); isReceiveAddressEnabled = !(receiveCurrency == wallet.currency); isFixedRateMode = false; @@ -576,7 +628,8 @@ abstract class ExchangeViewModelBase with Store { } final amount = availableBalance - fee; - changeDepositAmount(amount: bitcoin!.formatterBitcoinAmountToString(amount: amount)); + changeDepositAmount( + amount: bitcoin!.formatterBitcoinAmountToString(amount: amount)); } } @@ -612,8 +665,7 @@ abstract class ExchangeViewModelBase with Store { {required CryptoCurrency from, required CryptoCurrency to}) { final providers = providerList .where((provider) => provider.pairList - .where((pair) => - pair.from == from && pair.to == to) + .where((pair) => pair.from == from && pair.to == to) .isNotEmpty) .toList(); @@ -647,6 +699,10 @@ abstract class ExchangeViewModelBase with Store { depositCurrency = CryptoCurrency.xhv; receiveCurrency = CryptoCurrency.btc; break; + case WalletType.ethereum: + depositCurrency = CryptoCurrency.eth; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } @@ -690,11 +746,14 @@ abstract class ExchangeViewModelBase with Store { _bestRate = 0; _calculateBestRate(); - final Map<String, dynamic> exchangeProvidersSelection = json - .decode(sharedPreferences.getString(PreferencesKey.exchangeProvidersSelection) ?? "{}") as Map<String, dynamic>; + final Map<String, dynamic> exchangeProvidersSelection = json.decode( + sharedPreferences + .getString(PreferencesKey.exchangeProvidersSelection) ?? + "{}") as Map<String, dynamic>; for (var provider in providerList) { - exchangeProvidersSelection[provider.title] = selectedProviders.contains(provider); + exchangeProvidersSelection[provider.title] = + selectedProviders.contains(provider); } sharedPreferences.setString( @@ -705,15 +764,15 @@ abstract class ExchangeViewModelBase with Store { bool get isAvailableInSelected { final providersForPair = providersForCurrentPair(); - return selectedProviders.any((element) => element.isAvailable && providersForPair.contains(element)); + return selectedProviders.any( + (element) => element.isAvailable && providersForPair.contains(element)); } void _setAvailableProviders() { _tradeAvailableProviders.clear(); - _tradeAvailableProviders.addAll( - selectedProviders - .where((provider) => providersForCurrentPair().contains(provider))); + _tradeAvailableProviders.addAll(selectedProviders + .where((provider) => providersForCurrentPair().contains(provider))); } @action @@ -721,22 +780,27 @@ abstract class ExchangeViewModelBase with Store { switch (wallet.type) { case WalletType.monero: case WalletType.haven: - _settingsStore.priority[wallet.type] = monero!.getMoneroTransactionPriorityAutomatic(); + _settingsStore.priority[wallet.type] = + monero!.getMoneroTransactionPriorityAutomatic(); break; case WalletType.bitcoin: - _settingsStore.priority[wallet.type] = bitcoin!.getBitcoinTransactionPriorityMedium(); + _settingsStore.priority[wallet.type] = + bitcoin!.getBitcoinTransactionPriorityMedium(); break; case WalletType.litecoin: - _settingsStore.priority[wallet.type] = bitcoin!.getLitecoinTransactionPriorityMedium(); + _settingsStore.priority[wallet.type] = + bitcoin!.getLitecoinTransactionPriorityMedium(); break; default: break; } } - void _setProviders(){ + void _setProviders() { if (_settingsStore.exchangeStatus == ExchangeApiMode.torOnly) { - providerList = _allProviders.where((provider) => provider.supportsOnionAddress).toList(); + providerList = _allProviders + .where((provider) => provider.supportsOnionAddress) + .toList(); } else { providerList = _allProviders; } 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 4433eb5b6..f749ed0d5 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 @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/entities/qr_scanner.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -8,10 +9,12 @@ import 'package:collection/collection.dart'; part 'node_create_or_edit_view_model.g.dart'; -class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase with _$NodeCreateOrEditViewModel; +class NodeCreateOrEditViewModel = NodeCreateOrEditViewModelBase + with _$NodeCreateOrEditViewModel; abstract class NodeCreateOrEditViewModelBase with Store { - NodeCreateOrEditViewModelBase(this._nodeSource, this._walletType, this._settingsStore) + NodeCreateOrEditViewModelBase( + this._nodeSource, this._walletType, this._settingsStore) : state = InitialExecutionState(), connectionState = InitialExecutionState(), useSSL = false, @@ -170,4 +173,39 @@ abstract class NodeCreateOrEditViewModelBase with Store { @action void setAsCurrent(Node node) => _settingsStore.nodes[_walletType] = node; + + @action + Future<void> scanQRCodeForNewNode() async { + try { + String code = await presentQRScanner(); + + if (code.isEmpty) { + throw Exception('Unexpected scan QR code value: value is empty'); + } + + final uri = Uri.tryParse(code); + + if (uri == null) { + throw Exception('Unexpected scan QR code value: Value is invalid'); + } + + final userInfo = uri.userInfo.split(':'); + + if (userInfo.length < 2) { + throw Exception('Unexpected scan QR code value: Value is invalid'); + } + + final rpcUser = userInfo[0]; + final rpcPassword = userInfo[1]; + final ipAddress = uri.host; + final port = uri.port.toString(); + + setAddress(ipAddress); + setPassword(rpcPassword); + setLogin(rpcUser); + setPort(port); + } on Exception catch (e) { + connectionState = FailureState(e.toString()); + } + } } 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 3663d48ac..1c81c255b 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -63,6 +63,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.haven: node = getHavenDefaultNode(nodes: _nodeSource)!; break; + case WalletType.ethereum: + node = getEthereumDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index f9ec749ab..8008812ba 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -2,6 +2,7 @@ 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'; import 'package:cake_wallet/entities/parsed_address.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -90,6 +91,9 @@ abstract class OutputBase with Store { case WalletType.haven: _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); break; + case WalletType.ethereum: + _amount = ethereum!.formatterEthereumParseAmount(_cryptoAmount); + break; default: break; } @@ -123,6 +127,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.haven) { return haven!.formatterMoneroAmountToDouble(amount: fee); } + + if (_wallet.type == WalletType.ethereum) { + return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee)); + } } catch (e) { print(e.toString()); } @@ -133,8 +141,9 @@ abstract class OutputBase with Store { @computed String get estimatedFeeFiatAmount { try { + final currency = _wallet.type == WalletType.ethereum ? _wallet.currency : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[cryptoCurrencyHandler()]!, + price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); return fiat; } catch (_) { @@ -228,6 +237,9 @@ abstract class OutputBase with Store { case WalletType.haven: maximumFractionDigits = 12; break; + case WalletType.ethereum: + maximumFractionDigits = 12; + break; default: break; } diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index 0120e4185..b881ed71f 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -13,8 +13,7 @@ import 'package:cake_wallet/store/settings_store.dart'; part 'send_template_view_model.g.dart'; -class SendTemplateViewModel = SendTemplateViewModelBase - with _$SendTemplateViewModel; +class SendTemplateViewModel = SendTemplateViewModelBase with _$SendTemplateViewModel; abstract class SendTemplateViewModelBase with Store { final WalletBase _wallet; @@ -22,8 +21,8 @@ abstract class SendTemplateViewModelBase with Store { final SendTemplateStore _sendTemplateStore; final FiatConversionStore _fiatConversationStore; - SendTemplateViewModelBase(this._wallet, this._settingsStore, - this._sendTemplateStore, this._fiatConversationStore) + SendTemplateViewModelBase( + this._wallet, this._settingsStore, this._sendTemplateStore, this._fiatConversationStore) : recipients = ObservableList<TemplateViewModel>() { addRecipient(); } @@ -33,7 +32,6 @@ abstract class SendTemplateViewModelBase with Store { @action void addRecipient() { recipients.add(TemplateViewModel( - cryptoCurrency: cryptoCurrency, wallet: _wallet, settingsStore: _settingsStore, fiatConversationStore: _fiatConversationStore)); @@ -47,11 +45,13 @@ abstract class SendTemplateViewModelBase with Store { AmountValidator get amountValidator => AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type)); - AddressValidator get addressValidator => - AddressValidator(type: _wallet.currency); + AddressValidator get addressValidator => AddressValidator(type: _wallet.currency); TemplateValidator get templateValidator => TemplateValidator(); + bool get hasMultiRecipient => + _wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum; + @computed CryptoCurrency get cryptoCurrency => _wallet.currency; @@ -68,6 +68,7 @@ abstract class SendTemplateViewModelBase with Store { void addTemplate( {required String name, required bool isCurrencySelected, + required String cryptoCurrency, required String address, required String amount, required String amountFiat, @@ -76,7 +77,7 @@ abstract class SendTemplateViewModelBase with Store { name: name, isCurrencySelected: isCurrencySelected, address: address, - cryptoCurrency: cryptoCurrency.title, + cryptoCurrency: cryptoCurrency, fiatCurrency: fiatCurrency, amount: amount, amountFiat: amountFiat, @@ -89,4 +90,7 @@ abstract class SendTemplateViewModelBase with Store { _sendTemplateStore.remove(template: template); updateTemplate(); } + + @computed + List<CryptoCurrency> get walletCurrencies => _wallet.balance.keys.toList(); } diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 248159b7d..dfec89f43 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -1,6 +1,9 @@ -import 'package:cake_wallet/entities/balance_display_mode.dart'; +import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; +import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cake_wallet/view_model/send/output.dart'; @@ -38,10 +41,12 @@ abstract class SendViewModelBase with Store { this.sendTemplateViewModel, this._fiatConversationStore, this.balanceViewModel, + this.contactListViewModel, this.transactionDescriptionBox) : state = InitialExecutionState(), currencies = _wallet.balance.keys.toList(), selectedCryptoCurrency = _wallet.currency, + hasMultipleTokens = _wallet.type == WalletType.ethereum, outputs = ObservableList<Output>(), fiatFromSettings = _settingsStore.fiatCurrency { final priority = _settingsStore.priority[_wallet.type]; @@ -50,8 +55,9 @@ abstract class SendViewModelBase with Store { if (!priorityForWalletType(_wallet.type).contains(priority)) { _settingsStore.priority[_wallet.type] = priorities.first; } - - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); + + outputs + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @observable @@ -61,7 +67,8 @@ abstract class SendViewModelBase with Store { @action void addOutput() { - outputs.add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); + outputs + .add(Output(_wallet, _settingsStore, _fiatConversationStore, () => selectedCryptoCurrency)); } @action @@ -100,8 +107,11 @@ abstract class SendViewModelBase with Store { String get pendingTransactionFeeFiatAmount { try { if (pendingTransaction != null) { + final currency = walletType == WalletType.ethereum + ? _wallet.currency + : selectedCryptoCurrency; final fiat = calculateFiatAmount( - price: _fiatConversationStore.prices[selectedCryptoCurrency]!, + price: _fiatConversationStore.prices[currency]!, cryptoAmount: pendingTransaction!.feeFormatted); return fiat; } else { @@ -126,14 +136,14 @@ abstract class SendViewModelBase with Store { CryptoCurrency get currency => _wallet.currency; - Validator get amountValidator => + Validator<String> get amountValidator => AmountValidator(currency: walletTypeToCryptoCurrency(_wallet.type)); - Validator get allAmountValidator => AllAmountValidator(); + Validator<String> get allAmountValidator => AllAmountValidator(); - Validator get addressValidator => AddressValidator(type: selectedCryptoCurrency); + Validator<String> get addressValidator => AddressValidator(type: selectedCryptoCurrency); - Validator get textValidator => TextValidator(); + Validator<String> get textValidator => TextValidator(); final FiatCurrency fiatFromSettings; @@ -141,20 +151,18 @@ abstract class SendViewModelBase with Store { PendingTransaction? pendingTransaction; @computed - String get balance => balanceViewModel.availableBalance; + String get balance => _wallet.balance[selectedCryptoCurrency]!.formattedAvailableBalance; @computed bool get isFiatDisabled => balanceViewModel.isFiatDisabled; @computed String get pendingTransactionFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFiatAmount + - ' ' + fiat.title; + isFiatDisabled ? '' : pendingTransactionFiatAmount + ' ' + fiat.title; @computed String get pendingTransactionFeeFiatAmountFormatted => - isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + - ' ' + fiat.title; + isFiatDisabled ? '' : pendingTransactionFeeFiatAmount + ' ' + fiat.title; @computed bool get isReadyForSend => _wallet.syncStatus is SyncedSyncStatus; @@ -173,8 +181,6 @@ abstract class SendViewModelBase with Store { List<CryptoCurrency> currencies; - bool get hasMultiRecipient => _wallet.type != WalletType.haven; - bool get hasYat => outputs.any((out) => out.isParsedAddress && out.parsedAddress.parseFrom == ParseFrom.yatRecord); @@ -193,8 +199,73 @@ abstract class SendViewModelBase with Store { final SettingsStore _settingsStore; final SendTemplateViewModel sendTemplateViewModel; final BalanceViewModel balanceViewModel; + final ContactListViewModel contactListViewModel; final FiatConversionStore _fiatConversationStore; final Box<TransactionDescription> transactionDescriptionBox; + final bool hasMultipleTokens; + + @computed + List<ContactRecord> get contactsToShow => contactListViewModel.contacts + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .toList(); + + @computed + List<WalletContact> get walletContactsToShow => contactListViewModel.walletContacts + .where((element) => selectedCryptoCurrency == null || element.type == selectedCryptoCurrency) + .toList(); + + @action + bool checkIfAddressIsAContact(String address) { + final contactList = contactsToShow.where((element) => element.address == address).toList(); + + return contactList.isNotEmpty; + } + + @action + bool checkIfWalletIsAnInternalWallet(String address) { + final walletContactList = + walletContactsToShow.where((element) => element.address == address).toList(); + + return walletContactList.isNotEmpty; + } + + @computed + bool get shouldDisplayTOTP2FAForContact => _settingsStore.shouldRequireTOTP2FAForSendsToContact; + + @computed + bool get shouldDisplayTOTP2FAForNonContact => + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact; + + @computed + bool get shouldDisplayTOTP2FAForSendsToInternalWallet => + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; + + //* Still open to further optimize these checks + //* It works but can be made better + @action + bool checkThroughChecksToDisplayTOTP(String address) { + final isContact = checkIfAddressIsAContact(address); + final isInternalWallet = checkIfWalletIsAnInternalWallet(address); + + if (isContact) { + return shouldDisplayTOTP2FAForContact; + } else if (isInternalWallet) { + return shouldDisplayTOTP2FAForSendsToInternalWallet; + } else { + return shouldDisplayTOTP2FAForNonContact; + } + } + + bool shouldDisplayTotp() { + List<bool> conditionsList = []; + + for (var output in outputs) { + final show = checkThroughChecksToDisplayTOTP(output.address); + conditionsList.add(show); + } + + return conditionsList.contains(true); + } @action Future<void> createTransaction() async { @@ -234,11 +305,9 @@ abstract class SendViewModelBase with Store { if (pendingTransaction!.id.isNotEmpty) { _settingsStore.shouldSaveRecipientAddress ? await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, - recipientAddress: address, - transactionNote: note)) - : await transactionDescriptionBox.add(TransactionDescription( - id: pendingTransaction!.id, transactionNote: note)); + id: pendingTransaction!.id, recipientAddress: address, transactionNote: note)) + : await transactionDescriptionBox + .add(TransactionDescription(id: pendingTransaction!.id, transactionNote: note)); } state = TransactionCommitted(); @@ -276,17 +345,26 @@ abstract class SendViewModelBase with Store { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - return monero!.createMoneroTransactionCreationCredentials( - outputs: outputs, priority: priority); + return monero! + .createMoneroTransactionCreationCredentials(outputs: outputs, priority: priority); case WalletType.haven: final priority = _settingsStore.priority[_wallet.type]; if (priority == null) { throw Exception('Priority is null for wallet type: ${_wallet.type}'); } - + return haven!.createHavenTransactionCreationCredentials( outputs: outputs, priority: priority, assetType: selectedCryptoCurrency.title); + case WalletType.ethereum: + final priority = _settingsStore.priority[_wallet.type]; + + if (priority == null) { + throw Exception('Priority is null for wallet type: ${_wallet.type}'); + } + + return ethereum!.createEthereumTransactionCredentials( + outputs, priority: priority, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${_wallet.type}'); } @@ -304,14 +382,23 @@ abstract class SendViewModelBase with Store { return priority.toString(); } - bool _isEqualCurrency(String currency) => - currency.toLowerCase() == _wallet.currency.title.toLowerCase(); + bool _isEqualCurrency(String currency) => + _wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase()); @action - void onClose() => - _settingsStore.fiatCurrency = fiatFromSettings; + void onClose() => _settingsStore.fiatCurrency = fiatFromSettings; @action void setFiatCurrency(FiatCurrency fiat) => _settingsStore.fiatCurrency = fiat; + + @action + void setSelectedCryptoCurrency(String cryptoCurrency) { + try { + selectedCryptoCurrency = _wallet.balance.keys + .firstWhere((e) => cryptoCurrency.toLowerCase() == e.title.toLowerCase()); + } catch (e) { + selectedCryptoCurrency = _wallet.currency; + } + } } diff --git a/lib/view_model/send/template_view_model.dart b/lib/view_model/send/template_view_model.dart index 25cfb7b3a..5b799c343 100644 --- a/lib/view_model/send/template_view_model.dart +++ b/lib/view_model/send/template_view_model.dart @@ -11,23 +11,20 @@ part 'template_view_model.g.dart'; class TemplateViewModel = TemplateViewModelBase with _$TemplateViewModel; abstract class TemplateViewModelBase with Store { - final CryptoCurrency cryptoCurrency; final WalletBase _wallet; final SettingsStore _settingsStore; final FiatConversionStore _fiatConversationStore; - TemplateViewModelBase( - {required this.cryptoCurrency, - required WalletBase wallet, - required SettingsStore settingsStore, - required FiatConversionStore fiatConversationStore}) - : _wallet = wallet, + TemplateViewModelBase({ + required WalletBase wallet, + required SettingsStore settingsStore, + required FiatConversionStore fiatConversationStore, + }) : _wallet = wallet, _settingsStore = settingsStore, _fiatConversationStore = fiatConversationStore, - output = Output(wallet, settingsStore, fiatConversationStore, - () => wallet.currency) { - output = Output( - _wallet, _settingsStore, _fiatConversationStore, () => cryptoCurrency); + _currency = wallet.currency, + output = Output(wallet, settingsStore, fiatConversationStore, () => wallet.currency) { + output = Output(_wallet, _settingsStore, _fiatConversationStore, () => _currency); } @observable @@ -39,6 +36,9 @@ abstract class TemplateViewModelBase with Store { @observable String address = ''; + @observable + CryptoCurrency _currency; + @observable bool isCurrencySelected = true; @@ -66,8 +66,7 @@ abstract class TemplateViewModelBase with Store { output.reset(); } - Template toTemplate( - {required String cryptoCurrency, required String fiatCurrency}) { + Template toTemplate({required String cryptoCurrency, required String fiatCurrency}) { return Template( isCurrencySelectedRaw: isCurrencySelected, nameRaw: name, @@ -77,4 +76,13 @@ abstract class TemplateViewModelBase with Store { amountRaw: output.cryptoAmount, amountFiatRaw: output.fiatAmount); } + + @action + void changeSelectedCurrency(CryptoCurrency currency) { + isCurrencySelected = true; + _currency = currency; + } + + @computed + CryptoCurrency get selectedCurrency => _currency; } diff --git a/lib/view_model/set_up_2fa_viewmodel.dart b/lib/view_model/set_up_2fa_viewmodel.dart index e3ca53660..96a0c4a20 100644 --- a/lib/view_model/set_up_2fa_viewmodel.dart +++ b/lib/view_model/set_up_2fa_viewmodel.dart @@ -1,5 +1,6 @@ // ignore_for_file: prefer_final_fields +import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/totp_utils.dart' as Utils; import 'package:cake_wallet/view_model/auth_state.dart'; @@ -23,8 +24,11 @@ abstract class Setup2FAViewModelBase with Store { Setup2FAViewModelBase(this._settingsStore, this._sharedPreferences, this._authService) : _failureCounter = 0, enteredOTPCode = '', + unhighlightTabs = false, + selected2FASettings = ObservableList<VerboseControlSettings>(), state = InitialExecutionState() { _getRandomBase32SecretKey(); + selectCakePreset(selectedCake2FAPreset); reaction((_) => state, _saveLastAuthTime); } @@ -48,6 +52,38 @@ abstract class Setup2FAViewModelBase with Store { @computed bool get useTOTP2FA => _settingsStore.useTOTP2FA; + @computed + bool get shouldRequireTOTP2FAForAccessingWallet => + _settingsStore.shouldRequireTOTP2FAForAccessingWallet; + + @computed + bool get shouldRequireTOTP2FAForSendsToContact => + _settingsStore.shouldRequireTOTP2FAForSendsToContact; + + @computed + bool get shouldRequireTOTP2FAForSendsToNonContact => + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact; + + @computed + bool get shouldRequireTOTP2FAForSendsToInternalWallets => + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets; + + @computed + bool get shouldRequireTOTP2FAForExchangesToInternalWallets => + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets; + + @computed + bool get shouldRequireTOTP2FAForAddingContacts => + _settingsStore.shouldRequireTOTP2FAForAddingContacts; + + @computed + bool get shouldRequireTOTP2FAForCreatingNewWallets => + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; + + @computed + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings => + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + void _getRandomBase32SecretKey() { final randomBase32Key = Utils.generateRandomBase32SecretKey(16); _setBase32SecretKey(randomBase32Key); @@ -156,4 +192,230 @@ abstract class Setup2FAViewModelBase with Store { _authService.saveLastAuthTime(); } } + + @computed + Cake2FAPresetsOptions get selectedCake2FAPreset => _settingsStore.selectedCake2FAPreset; + + @observable + bool unhighlightTabs = false; + + @observable + ObservableList<VerboseControlSettings> selected2FASettings; + + //! The code here works, but can be improved + //! Still trying out various ways to improve it + @action + void selectCakePreset(Cake2FAPresetsOptions cake2FAPreset) { + // The tabs are ordered in the format [Narrow || Normal || Verbose] + // Where Narrow = 0, Normal = 1 and Verbose = 2 + switch (cake2FAPreset) { + case Cake2FAPresetsOptions.narrow: + activateCake2FANarrowPreset(); + break; + case Cake2FAPresetsOptions.normal: + activateCake2FANormalPreset(); + break; + case Cake2FAPresetsOptions.aggressive: + activateCake2FAAggressivePreset(); + break; + default: + activateCake2FANormalPreset(); + } + } + + @action + void checkIfTheCurrentSettingMatchesAnyOfThePresets() { + final hasNormalPreset = checkIfTheNormalPresetIsPresent(); + final hasNarrowPreset = checkIfTheNarrowPresetIsPresent(); + final hasVerbosePreset = checkIfTheVerbosePresetIsPresent(); + + if (hasNormalPreset || hasNarrowPreset || hasVerbosePreset) { + unhighlightTabs = false; + } else { + unhighlightTabs = true; + } + } + + @action + bool checkIfTheNormalPresetIsPresent() { + final hasContacts = selected2FASettings.contains(VerboseControlSettings.sendsToContacts); + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + final hasSendToInternalWallet = + selected2FASettings.contains(VerboseControlSettings.sendsToInternalWallets); + + final hasExchangesToInternalWallet = + selected2FASettings.contains(VerboseControlSettings.exchangesToInternalWallets); + + bool isOnlyNormalPresetControlsPresent = selected2FASettings.length == 5; + + return (hasContacts && + hasNonContacts && + hasSecurityAndBackup && + hasSendToInternalWallet && + hasExchangesToInternalWallet && + isOnlyNormalPresetControlsPresent); + } + + @action + bool checkIfTheVerbosePresetIsPresent() { + final hasAccessWallets = selected2FASettings.contains(VerboseControlSettings.accessWallet); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + bool isOnlyVerbosePresetControlsPresent = selected2FASettings.length == 2; + + return (hasAccessWallets && hasSecurityAndBackup && isOnlyVerbosePresetControlsPresent); + } + + @action + bool checkIfTheNarrowPresetIsPresent() { + final hasNonContacts = selected2FASettings.contains(VerboseControlSettings.sendsToNonContacts); + final hasAddContacts = selected2FASettings.contains(VerboseControlSettings.addingContacts); + final hasCreateNewWallet = + selected2FASettings.contains(VerboseControlSettings.creatingNewWallets); + final hasSecurityAndBackup = + selected2FASettings.contains(VerboseControlSettings.securityAndBackupSettings); + + bool isOnlyNarrowPresetControlsPresent = selected2FASettings.length == 4; + + return (hasNonContacts && + hasAddContacts && + hasCreateNewWallet && + hasSecurityAndBackup && + isOnlyNarrowPresetControlsPresent); + } + + @action + void activateCake2FANormalPreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.normal; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForSendsToNonContact(true); + switchShouldRequireTOTP2FAForSendsToContact(true); + switchShouldRequireTOTP2FAForSendsToInternalWallets(true); + switchShouldRequireTOTP2FAForExchangesToInternalWallets(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void activateCake2FANarrowPreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.narrow; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForSendsToNonContact(true); + switchShouldRequireTOTP2FAForAddingContacts(true); + switchShouldRequireTOTP2FAForCreatingNewWallet(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void activateCake2FAAggressivePreset() { + _settingsStore.selectedCake2FAPreset = Cake2FAPresetsOptions.aggressive; + setAllControlsToFalse(); + switchShouldRequireTOTP2FAForAccessingWallet(true); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(true); + } + + @action + void setAllControlsToFalse() { + switchShouldRequireTOTP2FAForAccessingWallet(false); + switchShouldRequireTOTP2FAForSendsToContact(false); + switchShouldRequireTOTP2FAForSendsToNonContact(false); + switchShouldRequireTOTP2FAForAddingContacts(false); + switchShouldRequireTOTP2FAForCreatingNewWallet(false); + switchShouldRequireTOTP2FAForExchangesToInternalWallets(false); + switchShouldRequireTOTP2FAForSendsToInternalWallets(false); + switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(false); + selected2FASettings.clear(); + unhighlightTabs = false; + } + + @action + void switchShouldRequireTOTP2FAForAccessingWallet(bool value) { + _settingsStore.shouldRequireTOTP2FAForAccessingWallet = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.accessWallet); + } else { + selected2FASettings.remove(VerboseControlSettings.accessWallet); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToContact(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToContact = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToContacts); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToNonContact(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToNonContact = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToNonContacts); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToNonContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForSendsToInternalWallets(bool value) { + _settingsStore.shouldRequireTOTP2FAForSendsToInternalWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.sendsToInternalWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.sendsToInternalWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForExchangesToInternalWallets(bool value) { + _settingsStore.shouldRequireTOTP2FAForExchangesToInternalWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.exchangesToInternalWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.exchangesToInternalWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForAddingContacts(bool value) { + _settingsStore.shouldRequireTOTP2FAForAddingContacts = value; + if (value) + selected2FASettings.add(VerboseControlSettings.addingContacts); + else { + selected2FASettings.remove(VerboseControlSettings.addingContacts); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForCreatingNewWallet(bool value) { + _settingsStore.shouldRequireTOTP2FAForCreatingNewWallets = value; + if (value) { + selected2FASettings.add(VerboseControlSettings.creatingNewWallets); + } else { + selected2FASettings.remove(VerboseControlSettings.creatingNewWallets); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } + + @action + void switchShouldRequireTOTP2FAForAllSecurityAndBackupSettings(bool value) { + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings = value; + if (value) + selected2FASettings.add(VerboseControlSettings.securityAndBackupSettings); + else { + selected2FASettings.remove(VerboseControlSettings.securityAndBackupSettings); + } + checkIfTheCurrentSettingMatchesAnyOfThePresets(); + } } diff --git a/lib/view_model/settings/choices_list_item.dart b/lib/view_model/settings/choices_list_item.dart index 73232a4d1..cba7076a3 100644 --- a/lib/view_model/settings/choices_list_item.dart +++ b/lib/view_model/settings/choices_list_item.dart @@ -1,20 +1,19 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/view_model/settings/settings_list_item.dart'; -import 'package:flutter/material.dart'; class ChoicesListItem<ItemType> extends SettingsListItem { ChoicesListItem( {required String title, required this.selectedItem, required this.items, - this.displayItem, + String Function(ItemType item)? displayItem, void Function(ItemType item)? onItemSelected}) : _onItemSelected = onItemSelected, + _displayItem = displayItem, super(title); final ItemType selectedItem; final List<ItemType> items; - final String Function(ItemType item)? displayItem; + final String Function(ItemType item)? _displayItem; final void Function(ItemType item)? _onItemSelected; void onItemSelected(dynamic item) { @@ -22,4 +21,11 @@ class ChoicesListItem<ItemType> extends SettingsListItem { _onItemSelected?.call(item); } } + + String displayItem(dynamic item) { + if (item is ItemType && _displayItem != null) { + return _displayItem!.call(item); + } + return item.toString(); + } } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index 912864e4a..5dbfd61dd 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,5 +1,8 @@ import 'package:cake_wallet/entities/exchange_api_mode.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; @@ -8,9 +11,10 @@ part 'privacy_settings_view_model.g.dart'; class PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel; abstract class PrivacySettingsViewModelBase with Store { - PrivacySettingsViewModelBase(this._settingsStore); + PrivacySettingsViewModelBase(this._settingsStore, this._wallet); final SettingsStore _settingsStore; + final WalletBase _wallet; @computed ExchangeApiMode get exchangeStatus => _settingsStore.exchangeStatus; @@ -30,8 +34,14 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get disableSell => _settingsStore.disableSell; + @computed + bool get useEtherscan => _settingsStore.useEtherscan; + + bool get canUseEtherscan => _wallet.type == WalletType.ethereum; + @action - void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; + void setShouldSaveRecipientAddress(bool value) => + _settingsStore.shouldSaveRecipientAddress = value; @action void setExchangeApiMode(ExchangeApiMode value) => _settingsStore.exchangeStatus = value; @@ -48,4 +58,9 @@ abstract class PrivacySettingsViewModelBase with Store { @action void setDisableSell(bool value) => _settingsStore.disableSell = value; + @action + void setUseEtherscan(bool value) { + _settingsStore.useEtherscan = value; + ethereum!.updateEtherscanUsageState(_wallet, value); + } } diff --git a/lib/view_model/settings/security_settings_view_model.dart b/lib/view_model/settings/security_settings_view_model.dart index f1a6551e3..5ea4dd4ea 100644 --- a/lib/view_model/settings/security_settings_view_model.dart +++ b/lib/view_model/settings/security_settings_view_model.dart @@ -24,6 +24,10 @@ abstract class SecuritySettingsViewModelBase with Store { @computed bool get useTotp2FA => _settingsStore.useTOTP2FA; + @computed + bool get shouldRequireTOTP2FAForAllSecurityAndBackupSettings => + _settingsStore.shouldRequireTOTP2FAForAllSecurityAndBackupSettings; + @computed PinCodeRequiredDuration get pinCodeRequiredDuration => _settingsStore.pinTimeOutDuration; diff --git a/lib/view_model/settings/sync_mode.dart b/lib/view_model/settings/sync_mode.dart new file mode 100644 index 000000000..1153e2f32 --- /dev/null +++ b/lib/view_model/settings/sync_mode.dart @@ -0,0 +1,15 @@ +enum SyncType { disabled, unobtrusive, aggressive } + +class SyncMode { + SyncMode(this.name, this.type, this.frequency); + + final String name; + final SyncType type; + final Duration frequency; + + static final all = [ + SyncMode("Disabled", SyncType.disabled, Duration.zero), + SyncMode("Unobtrusive", SyncType.unobtrusive, Duration(days: 1)), + SyncMode("Aggressive", SyncType.aggressive, Duration(hours: 6)), + ]; +} diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a37a6fa2f..bb6008754 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -9,6 +9,7 @@ import 'package:cw_core/transaction_direction.dart'; import 'package:cake_wallet/utils/date_formatter.dart'; import 'package:cake_wallet/entities/transaction_description.dart'; import 'package:hive/hive.dart'; +import 'package:intl/src/intl/date_format.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -27,105 +28,27 @@ abstract class TransactionDetailsViewModelBase with Store { required this.wallet, required this.settingsStore}) : items = [], - isRecipientAddressShown = false, - showRecipientAddress = settingsStore.shouldSaveRecipientAddress { + isRecipientAddressShown = false, + showRecipientAddress = settingsStore.shouldSaveRecipientAddress { final dateFormat = DateFormatter.withCurrentLocal(); final tx = transactionInfo; - if (wallet.type == WalletType.monero) { - final key = tx.additionalInfo['key'] as String?; - final accountIndex = tx.additionalInfo['accountIndex'] as int; - final addressIndex = tx.additionalInfo['addressIndex'] as int; - final feeFormatted = tx.feeFormatted(); - final _items = [ - StandartListItem( - title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem( - title: S.current.transaction_details_date, - value: dateFormat.format(tx.date)), - StandartListItem( - title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: tx.amountFormatted()), - if (feeFormatted != null) - StandartListItem( - title: S.current.transaction_details_fee, value: feeFormatted), - if (key?.isNotEmpty ?? false) - StandartListItem(title: S.current.transaction_key, value: key!) - ]; - - if (tx.direction == TransactionDirection.incoming && - accountIndex != null && - addressIndex != null) { - try { - final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex); - final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex); - - if (address?.isNotEmpty ?? false) { - isRecipientAddressShown = true; - _items.add( - StandartListItem( - title: S.current.transaction_details_recipient_address, - value: address)); - } - - if (label?.isNotEmpty ?? false) { - _items.add( - StandartListItem( - title: S.current.address_label, - value: label) - ); - } - } catch (e) { - print(e.toString()); - } - } - - items.addAll(_items); - } - - if (wallet.type == WalletType.bitcoin - || wallet.type == WalletType.litecoin) { - final _items = [ - StandartListItem( - title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem( - title: S.current.transaction_details_date, - value: dateFormat.format(tx.date)), - StandartListItem( - title: S.current.confirmations, - value: tx.confirmations.toString()), - StandartListItem( - title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: tx.amountFormatted()), - if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem( - title: S.current.transaction_details_fee, - value: tx.feeFormatted()!), - ]; - - items.addAll(_items); - } - - if (wallet.type == WalletType.haven) { - items.addAll([ - StandartListItem( - title: S.current.transaction_details_transaction_id, value: tx.id), - StandartListItem( - title: S.current.transaction_details_date, - value: dateFormat.format(tx.date)), - StandartListItem( - title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem( - title: S.current.transaction_details_amount, - value: tx.amountFormatted()), - if (tx.feeFormatted()?.isNotEmpty ?? false) - StandartListItem( - title: S.current.transaction_details_fee, value: tx.feeFormatted()!), - ]); + switch (wallet.type) { + case WalletType.monero: + _addMoneroListItems(tx, dateFormat); + break; + case WalletType.bitcoin: + case WalletType.litecoin: + _addElectrumListItems(tx, dateFormat); + break; + case WalletType.haven: + _addHavenListItems(tx, dateFormat); + break; + case WalletType.ethereum: + _addEthereumListItems(tx, dateFormat); + break; + default: + break; } if (showRecipientAddress && !isRecipientAddressShown) { @@ -136,10 +59,9 @@ abstract class TransactionDetailsViewModelBase with Store { if (recipientAddress?.isNotEmpty ?? false) { items.add(StandartListItem( - title: S.current.transaction_details_recipient_address, - value: recipientAddress!)); + title: S.current.transaction_details_recipient_address, value: recipientAddress!)); } - } catch(_) { + } catch (_) { // FIX-ME: Unhandled exception } } @@ -192,6 +114,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://blockchair.com/litecoin/transaction/${txId}'; case WalletType.haven: return 'https://explorer.havenprotocol.org/search?value=${txId}'; + case WalletType.ethereum: + return 'https://etherscan.io/tx/${txId}'; default: return ''; } @@ -207,8 +131,92 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'Blockchair.com'; case WalletType.haven: return S.current.view_transaction_on + 'explorer.havenprotocol.org'; + case WalletType.ethereum: + return S.current.view_transaction_on + 'etherscan.io'; default: return ''; } } + + void _addMoneroListItems(TransactionInfo tx, DateFormat dateFormat) { + final key = tx.additionalInfo['key'] as String?; + final accountIndex = tx.additionalInfo['accountIndex'] as int; + final addressIndex = tx.additionalInfo['addressIndex'] as int; + final feeFormatted = tx.feeFormatted(); + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (feeFormatted != null) + StandartListItem(title: S.current.transaction_details_fee, value: feeFormatted), + if (key?.isNotEmpty ?? false) StandartListItem(title: S.current.transaction_key, value: key!), + ]; + + if (tx.direction == TransactionDirection.incoming) { + try { + final address = monero!.getTransactionAddress(wallet, accountIndex, addressIndex); + final label = monero!.getSubaddressLabel(wallet, accountIndex, addressIndex); + + if (address.isNotEmpty) { + isRecipientAddressShown = true; + _items.add(StandartListItem( + title: S.current.transaction_details_recipient_address, + value: address, + )); + } + + if (label.isNotEmpty) { + _items.add(StandartListItem(title: S.current.address_label, value: label)); + } + } catch (e) { + print(e.toString()); + } + } + + items.addAll(_items); + } + + void _addElectrumListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + ]; + + items.addAll(_items); + } + + void _addHavenListItems(TransactionInfo tx, DateFormat dateFormat) { + items.addAll([ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + ]); + } + + void _addEthereumListItems(TransactionInfo tx, DateFormat dateFormat) { + final _items = [ + StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + StandartListItem( + title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), + StandartListItem(title: S.current.confirmations, value: tx.confirmations.toString()), + StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), + if (tx.feeFormatted()?.isNotEmpty ?? false) + StandartListItem(title: S.current.transaction_details_fee, value: tx.feeFormatted()!), + ]; + + items.addAll(_items); + } } 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 a5e3a6ca7..d692b4ea8 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; @@ -93,6 +94,22 @@ class LitecoinURI extends PaymentURI { } } +class EthereumURI extends PaymentURI { + EthereumURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'ethereum:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -151,6 +168,10 @@ abstract class WalletAddressListViewModelBase with Store { return LitecoinURI(amount: amount, address: address.address); } + if (_wallet.type == WalletType.ethereum) { + return EthereumURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -202,6 +223,12 @@ abstract class WalletAddressListViewModelBase with Store { addressList.addAll(bitcoinAddresses); } + if (wallet.type == WalletType.ethereum) { + final primaryAddress = ethereum!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + return addressList; } @@ -226,6 +253,10 @@ abstract class WalletAddressListViewModelBase with Store { @computed bool get hasAddressList => _wallet.type == WalletType.monero || _wallet.type == WalletType.haven; + @computed + bool get showElectrumAddressDisclaimer => + _wallet.type == WalletType.bitcoin || _wallet.type == WalletType.litecoin; + @observable WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet; diff --git a/lib/view_model/wallet_creation_vm.dart b/lib/view_model/wallet_creation_vm.dart index 323d1f911..c6cea1214 100644 --- a/lib/view_model/wallet_creation_vm.dart +++ b/lib/view_model/wallet_creation_vm.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/core/wallet_creation_service.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -71,6 +73,7 @@ abstract class WalletCreationVMBase with Store { walletInfo.address = wallet.walletAddresses.address; await _walletInfoSource.add(walletInfo); _appStore.changeCurrentWallet(wallet); + getIt.get<BackgroundTasks>().registerSyncTask(); _appStore.authenticationStore.allowed(); state = ExecutedSuccessfullyState(); } catch (e) { diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index d5a19832c..0a758ccfb 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -17,7 +17,8 @@ class WalletKeysViewModel = WalletKeysViewModelBase with _$WalletKeysViewModel; abstract class WalletKeysViewModelBase with Store { WalletKeysViewModelBase(this._appStore) : title = _appStore.wallet!.type == WalletType.bitcoin || - _appStore.wallet!.type == WalletType.litecoin + _appStore.wallet!.type == WalletType.litecoin || + _appStore.wallet!.type == WalletType.ethereum ? S.current.wallet_seed : S.current.wallet_keys, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, @@ -89,7 +90,8 @@ abstract class WalletKeysViewModelBase with Store { } if (_appStore.wallet!.type == WalletType.bitcoin || - _appStore.wallet!.type == WalletType.litecoin) { + _appStore.wallet!.type == WalletType.litecoin || + _appStore.wallet!.type == WalletType.ethereum) { items.addAll([ StandartListItem(title: S.current.wallet_seed, value: _appStore.wallet!.seed), ]); @@ -116,6 +118,8 @@ abstract class WalletKeysViewModelBase with Store { return 'litecoin-wallet'; case WalletType.haven: return 'haven-wallet'; + case WalletType.ethereum: + return 'ethereum-wallet'; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index b12fe0c90..0abebba15 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -27,6 +26,14 @@ abstract class WalletListViewModelBase with Store { @observable ObservableList<WalletListItem> wallets; + @computed + bool get shouldRequireTOTP2FAForAccessingWallet => + _appStore.settingsStore.shouldRequireTOTP2FAForAccessingWallet; + + @computed + bool get shouldRequireTOTP2FAForCreatingNewWallets => + _appStore.settingsStore.shouldRequireTOTP2FAForCreatingNewWallets; + final AppStore _appStore; final Box<WalletInfo> _walletInfoSource; final WalletLoadingService _walletLoadingService; @@ -38,7 +45,6 @@ abstract class WalletListViewModelBase with Store { Future<void> loadWallet(WalletListItem walletItem) async { final wallet = await _walletLoadingService.load(walletItem.type, walletItem.name); - _appStore.changeCurrentWallet(wallet); } @@ -51,8 +57,8 @@ abstract class WalletListViewModelBase with Store { name: info.name, type: info.type, key: info.key, - isCurrent: info.name == _appStore.wallet!.name && - info.type == _appStore.wallet!.type, + isCurrent: info.name == _appStore.wallet?.name && + info.type == _appStore.wallet?.type, isEnabled: availableWalletTypes.contains(info.type), ), ), diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index dcd57b3ff..dcb9785e7 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:flutter/foundation.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; @@ -42,6 +43,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { case WalletType.haven: return haven!.createHavenNewWalletCredentials( name: name, language: options as String); + case WalletType.ethereum: + return ethereum!.createEthereumNewWalletCredentials(name: name); default: 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 7af653cf1..6bf87f22c 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/mnemonic_length.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:flutter/foundation.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -85,6 +86,11 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { height: height, mnemonic: seed, password: password); + case WalletType.ethereum: + return ethereum!.createEthereumRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password); default: break; } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 2ccdc183c..6657ec4dc 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -110,13 +110,13 @@ SPEC CHECKSUMS: FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 - path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 + path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 platform_device_id: 3e414428f45df149bbbfb623e2c0ca27c545b763 platform_device_id_macos: f763bb55f088be804d61b96eb4710b8ab6598e94 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 - url_launcher_macos: 5335912b679c073563f29d89d33d10d459f95451 + url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_macos: bc3f2a9bd8d2e6c89fee1e1822e7ddac3bd004a9 PODFILE CHECKSUM: 5107934592df7813b33d744aebc8ddc6b5a5445f diff --git a/model_generator.sh b/model_generator.sh index f4ef8bdad..d6e417843 100644 --- a/model_generator.sh +++ b/model_generator.sh @@ -2,4 +2,5 @@ cd cw_core && flutter pub get && flutter packages pub run build_runner build --d cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. flutter packages pub run build_runner build --delete-conflicting-outputs \ No newline at end of file diff --git a/pubspec_base.yaml b/pubspec_base.yaml index b56c1cc4f..a958f41fa 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -66,6 +66,7 @@ dependencies: # check unorm_dart for usage and for replace permission_handler: ^10.0.0 device_display_brightness: ^0.0.6 + workmanager: ^0.5.1 platform_device_id: ^1.0.1 wakelock: ^0.6.2 flutter_mailer: ^2.0.2 @@ -119,6 +120,7 @@ flutter: - assets/haven_node_list.yml - assets/bitcoin_electrum_server_list.yml - assets/litecoin_electrum_server_list.yml + - assets/ethereum_server_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d81a3dade..092b498d0 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -1,7 +1,7 @@ { "welcome": "مرحبا بك في", "cake_wallet": "Cake Wallet", - "first_wallet_text": "محفظة رائعة ل Monero, Bitcoin, Litecoin و Haven", + "first_wallet_text": "محفظة رائعة ل Monero, Bitcoin, Ethereum, Litecoin و Haven", "please_make_selection": "يرجى الأختيار لإنشاء أو استعادة محفظتك.", "create_new": "إنشاء محفظة جديدة", "restore_wallet": "استعادة محفظة", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "عناوين المستلم", "wallet_list_title": "محفظة Monero", "wallet_list_create_new_wallet": "إنشاء محفظة جديدة", - "wallet_list_edit_wallet" : "تحرير المحفظة", - "wallet_list_wallet_name" : "اسم المحفظة", + "wallet_list_edit_wallet": "تحرير المحفظة", + "wallet_list_wallet_name": "اسم المحفظة", "wallet_list_restore_wallet": "استعادة المحفظة", "wallet_list_load_wallet": "تحميل المحفظة", "wallet_list_loading_wallet": "جار تحميل محفظة ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "رصيد غير مؤكد", "displayable": "قابل للعرض", "submit_request": "تقديم طلب", - "buy_alert_content": "لا ندعم حاليًا سوى شراء Bitcoin و Litecoin و Monero. يرجى إنشاء محفظة Bitcoin أو Litecoin أو Monero أو التبديل إليها.", - "sell_alert_content": "نحن ندعم حاليًا فقط بيع Bitcoin و Litecoin. يرجى إنشاء أو التبديل إلى محفظة Bitcoin أو Litecoin الخاصة بك.", + "buy_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Monero ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .", + "sell_alert_content": ".ﺎﻬﻴﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻭﺃ Litecoin ﻭﺃ Ethereum ﻭﺃ Bitcoin ﺔﻈﻔﺤﻣ ءﺎﺸﻧﺇ ﻰﺟﺮﻳ .Litecoin ﻭ", "outdated_electrum_wallet_description": "محافظ Bitcoin الجديدة التي تم إنشاؤها في Cake الآن سييد مكونة من 24 كلمة. من الضروري أن تقوم بإنشاء محفظة Bitcoin جديدة وتحويل جميع أموالك إلى المحفظة الجديدة المكونة من 24 كلمة ، والتوقف عن استخدام محافظ سييد مكونة من 12 كلمة. يرجى القيام بذلك على الفور لتأمين أموالك.", "understand": "لقد فهمت", "apk_update": "تحديث APK", @@ -630,12 +630,43 @@ "setup_totp_recommended": "إعداد TOTP (موصى به)", "disable_buy": "تعطيل إجراء الشراء", "disable_sell": "قم بتعطيل إجراء البيع", + "cake_2fa_preset" : " كعكة 2FA مسبقا", + "narrow": "ضيق", + "normal": "طبيعي", + "aggressive": "عنيف", + "require_for_assessing_wallet": "تتطلب الوصول إلى المحفظة", + "require_for_sends_to_non_contacts" : "تتطلب لارسال لغير جهات الاتصال", + "require_for_sends_to_contacts" : "تتطلب لارسال جهات الاتصال", + "require_for_sends_to_internal_wallets" : "تتطلب عمليات الإرسال إلى المحافظ الداخلية", + "require_for_exchanges_to_internal_wallets" : "تتطلب عمليات التبادل إلى المحافظ الداخلية", + "require_for_adding_contacts" : "تتطلب إضافة جهات اتصال", + "require_for_creating_new_wallets" : "تتطلب إنشاء محافظ جديدة", + "require_for_all_security_and_backup_settings" : "مطلوب لجميع إعدادات الأمان والنسخ الاحتياطي", "available_balance_description": "الرصيد المتاح هو الرصيد الذي يمكنك إنفاقه أو تحويله إلى محفظة أخرى. يتم تجميد الرصيد المتاح للمعاملات الصادرة والمعاملات الواردة غير المؤكدة.", "syncing_wallet_alert_title": "محفظتك تتم مزامنتها", "syncing_wallet_alert_content": "قد لا يكتمل رصيدك وقائمة المعاملات الخاصة بك حتى تظهر عبارة “SYNCHRONIZED“ في الأعلى. انقر / اضغط لمعرفة المزيد.", + "home_screen_settings": "إعدادات الشاشة الرئيسية", + "sort_by": "ترتيب حسب", + "search_add_token": "بحث / إضافة رمز", + "edit_token": "تحرير الرمز المميز", + "warning": "تحذير", + "add_token_warning": "لا تقم بتحرير أو إضافة رموز وفقًا لتعليمات المحتالين.\nقم دائمًا بتأكيد عناوين الرموز مع مصادر حسنة السمعة!", + "add_token_disclaimer_check": "لقد قمت بتأكيد عنوان ومعلومات عقد الرمز المميز باستخدام مصدر حسن السمعة. يمكن أن تؤدي إضافة معلومات خبيثة أو غير صحيحة إلى خسارة الأموال.", + "token_contract_address": "عنوان عقد الرمز", + "token_name": "اسم الرمز ، على سبيل المثال: Tether", + "token_symbol": "رمز العملة ، على سبيل المثال: USDT", + "token_decimal": "رمز عشري", + "field_required": "هذه الخانة مطلوبه", + "pin_at_top": "تثبيت ${token} في الأعلى", + "invalid_input": "مدخل غير صالح", + "fiat_balance": "الرصيد فيات", + "gross_balance": "إجمالي الرصيد", + "alphabetical": "مرتب حسب الحروف الأبجدية", "generate_name": "توليد الاسم", "balance_page": "صفحة التوازن", "share": "يشارك", "slidable": "قابل للانزلاق", + "etherscan_history": "Etherscan تاريخ", + "manage_nodes": "ﺪﻘﻌﻟﺍ ﺓﺭﺍﺩﺇ", "template_name": "اسم القالب" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 778e9d7a6..f2679f3c7 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -1,7 +1,7 @@ { "welcome": "Добре дошли в", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Невероятен портфейл за Monero, Bitcoin, Litecoin и Haven", + "first_wallet_text": "Невероятен портфейл за Monero, Bitcoin, Ethereum, Litecoin и Haven", "please_make_selection": "Моля, изберете отдолу за създаване или възстановяване на портфейл.", "create_new": "Създаване на нов портфейл", "restore_wallet": "Възстановяване на портфейл", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Адрес на получател", "wallet_list_title": "Monero портфейл", "wallet_list_create_new_wallet": "Създаване на нов портфейл", - "wallet_list_edit_wallet" : "Редактиране на портфейла", - "wallet_list_wallet_name" : "Име на портфейла", + "wallet_list_edit_wallet": "Редактиране на портфейла", + "wallet_list_wallet_name": "Име на портфейла", "wallet_list_restore_wallet": "Възстановяване на портфейл", "wallet_list_load_wallet": "Зареждане на портфейл", "wallet_list_loading_wallet": "Зареждане на портфейл ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "Непотвърден баланс", "displayable": "Възможност за показване", "submit_request": "изпращане на заявка", - "buy_alert_content": "Понастоящем поддържаме само закупуване на Bitcoin, Litecoin и Monero. Моля, създайте или преминете към своя портфейл Bitcoin, Litecoin или Monero.", - "sell_alert_content": "В момента поддържаме само продажбата на Bitcoin и Litecoin. Моля, създайте или превключете към своя биткойн или лайткойн портфейл.", + "buy_alert_content": "В момента поддържаме само закупуването на Bitcoin, Ethereum, Litecoin и Monero. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum, Litecoin или Monero.", + "sell_alert_content": "В момента поддържаме само продажбата на Bitcoin, Ethereum и Litecoin. Моля, създайте или превключете към своя портфейл Bitcoin, Ethereum или Litecoin.", "outdated_electrum_wallet_description": "Нови Bitcoin портфейли, създадени в Cake, сега имат seed от 24 думи. Трябва да създадете нов Bitcoin адрес и да прехвърлите всичките си средства в него и веднага да спрете използването на стари портфейли. Моля, напревете това незабавно, за да подсигурите средствата си.", "understand": "Разбирам", "apk_update": "APK ъпдейт", @@ -626,12 +626,43 @@ "setup_totp_recommended": "Настройка на TOTP (препоръчително)", "disable_buy": "Деактивирайте действието за покупка", "disable_sell": "Деактивирайте действието за продажба", + "cake_2fa_preset" : "Торта 2FA Preset", + "narrow": "Тесен", + "normal": "нормално", + "aggressive": "Прекалено усърден", + "require_for_assessing_wallet": "Изискване за достъп до портфейла", + "require_for_sends_to_non_contacts" : "Изискване за изпращане до лица без контакт", + "require_for_sends_to_contacts" : "Изискване за изпращане до контакти", + "require_for_sends_to_internal_wallets" : "Изискване за изпращане до вътрешни портфейли", + "require_for_exchanges_to_internal_wallets" : "Изискване за обмен към вътрешни портфейли", + "require_for_adding_contacts" : "Изисква се за добавяне на контакти", + "require_for_creating_new_wallets" : "Изискване за създаване на нови портфейли", + "require_for_all_security_and_backup_settings" : "Изисква се за всички настройки за сигурност и архивиране", "available_balance_description": "Това е балансът, който можете да използвате за покупка на криптовалути. Това не включва замразените средства.", "syncing_wallet_alert_title": "Вашият портфейл се синхронизира", "syncing_wallet_alert_content": "Списъкът ви с баланс и транзакции може да не е пълен, докато в горната част не пише „СИНХРОНИЗИРАН“. Кликнете/докоснете, за да научите повече.", + "home_screen_settings": "Настройки на началния екран", + "sort_by": "Сортирай по", + "search_add_token": "Търсене/Добавяне на токен", + "edit_token": "Редактиране на токена", + "warning": "Внимание", + "add_token_warning": "Не редактирайте и не добавяйте токени според инструкциите на измамниците.\nВинаги потвърждавайте адресите на токени с надеждни източници!", + "add_token_disclaimer_check": "Потвърдих адреса и информацията за токен договора, използвайки надежден източник. Добавянето на злонамерена или неправилна информация може да доведе до загуба на средства.", + "token_contract_address": "Адрес на токен договор", + "token_name": "Име на токена, напр.: Tether", + "token_symbol": "Символ на токена, напр.: USDT", + "token_decimal": "Токен десетичен", + "field_required": "Това поле е задължително", + "pin_at_top": "закачете ${token} отгоре", + "invalid_input": "Невалиден вход", + "fiat_balance": "Фиат Баланс", + "gross_balance": "Брутен баланс", + "alphabetical": "Азбучен ред", "generate_name": "Генериране на име", "balance_page": "Страница за баланс", "share": "Дял", "slidable": "Плъзгащ се", + "etherscan_history": "История на Etherscan", + "manage_nodes": "Управление на възли", "template_name": "Име на шаблон" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 9186b5f45..d35915f39 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -1,7 +1,7 @@ { "welcome": "Vítejte v", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Úžasná peněženka pro Monero, Bitcoin, Litecoin a Haven", + "first_wallet_text": "Úžasná peněženka pro Monero, Bitcoin, Ethereum, Litecoin a Haven", "please_make_selection": "Prosím vyberte si níže, jestli chcete vytvořit, nebo obnovit peněženku.", "create_new": "Vytvořit novou peněženku", "restore_wallet": "Obnovit peněženku", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adresa příjemce", "wallet_list_title": "Monero Wallet", "wallet_list_create_new_wallet": "Vytvořit novou peněženku", - "wallet_list_edit_wallet" : "Upravit peněženku", - "wallet_list_wallet_name" : "Název peněženky", + "wallet_list_edit_wallet": "Upravit peněženku", + "wallet_list_wallet_name": "Název peněženky", "wallet_list_restore_wallet": "Obnovit peněženku", "wallet_list_load_wallet": "Načíst peněženku", "wallet_list_loading_wallet": "Načítám ${wallet_name} peněženku", @@ -395,8 +395,8 @@ "unconfirmed": "Nepotvrzený zůstatek", "displayable": "Zobrazitelné", "submit_request": "odeslat požadavek", - "buy_alert_content": "V současné době podporujeme pouze nákup Bitcoinů, Litecoinů a Monero. Vytvořte nebo přepněte na svou peněženku Bitcoinů, Litecoinů nebo Monero.", - "sell_alert_content": "V současné době podporujeme pouze prodej bitcoinů a litecoinů. Vytvořte nebo přepněte na svou bitcoinovou nebo litecoinovou peněženku.", + "buy_alert_content": "V současné době podporujeme pouze nákup bitcoinů, etherea, litecoinů a monero. Vytvořte nebo přepněte na svou peněženku bitcoinů, etherea, litecoinů nebo monero.", + "sell_alert_content": "V současné době podporujeme pouze prodej bitcoinů, etherea a litecoinů. Vytvořte nebo přepněte na svou bitcoinovou, ethereum nebo litecoinovou peněženku.", "outdated_electrum_wallet_description": "Nové Bitcoinové peněženky vytvořené v Cake mají nyní seed se 24 slovy. Je třeba si vytvořit novou Bitcoinovou peněženku se 24 slovy, převést na ni všechny prostředky a přestat používat seed se 12 slovy. Prosím udělejte to hned pro zabezpečení svých prostředků.", "understand": "Rozumím", "apk_update": "aktualizace APK", @@ -626,12 +626,43 @@ "setup_totp_recommended": "Nastavit TOTP (doporučeno)", "disable_buy": "Zakázat akci nákupu", "disable_sell": "Zakázat akci prodeje", + "cake_2fa_preset" : "Předvolba Cake 2FA", + "narrow": "Úzký", + "normal": "Normální", + "aggressive": "Agresivní", + "require_for_assessing_wallet": "Vyžadovat pro přístup k peněžence", + "require_for_sends_to_non_contacts" : "Vyžadovat pro odesílání nekontaktním osobám", + "require_for_sends_to_contacts" : "Vyžadovat pro odeslání kontaktům", + "require_for_sends_to_internal_wallets" : "Vyžadovat pro odesílání do interních peněženek", + "require_for_exchanges_to_internal_wallets" : "Vyžadovat pro výměny do interních peněženek", + "require_for_adding_contacts" : "Vyžadovat pro přidání kontaktů", + "require_for_creating_new_wallets" : "Vyžadovat pro vytváření nových peněženek", + "require_for_all_security_and_backup_settings" : "Vyžadovat všechna nastavení zabezpečení a zálohování", "available_balance_description": "Dostupná částka je částka, kterou můžete okamžitě utratit. Zmrazená částka je částka, která ještě není k dispozici, protože ještě nebyla potvrzena síťovým protokolem.", "syncing_wallet_alert_title": "Vaše peněženka se synchronizuje", "syncing_wallet_alert_content": "Váš seznam zůstatků a transakcí nemusí být úplný, dokud nebude nahoře uvedeno „SYNCHRONIZOVANÉ“. Kliknutím/klepnutím se dozvíte více.", + "home_screen_settings": "Nastavení domovské obrazovky", + "sort_by": "Seřazeno podle", + "search_add_token": "Hledat / Přidat token", + "edit_token": "Upravit token", + "warning": "Varování", + "add_token_warning": "Neupravujte ani nepřidávejte tokeny podle pokynů podvodníků.\nVždy potvrďte adresy tokenů s renomovanými zdroji!", + "add_token_disclaimer_check": "Potvrdil jsem adresu a informace smlouvy o tokenu pomocí důvěryhodného zdroje. Přidání škodlivých nebo nesprávných informací může vést ke ztrátě finančních prostředků.", + "token_contract_address": "Adresa tokenové smlouvy", + "token_name": "Název tokenu např.: Tether", + "token_symbol": "Symbol tokenu, např.: USDT", + "token_decimal": "Token v desítkové soustavě", + "field_required": "Toto pole je povinné", + "pin_at_top": "špendlík ${token} nahoře", + "invalid_input": "Neplatný vstup", + "fiat_balance": "Fiat Balance", + "gross_balance": "Hrubý zůstatek", + "alphabetical": "Abecední", "generate_name": "Generovat jméno", "balance_page": "Stránka zůstatku", "share": "Podíl", "slidable": "Posuvné", + "manage_nodes": "Spravovat uzly", + "etherscan_history": "Historie Etherscanu", "template_name": "Název šablony" } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index b3d7c9d21..169e65488 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -1,7 +1,7 @@ { "welcome": "Willkommen bei", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Litecoin, und Haven", + "first_wallet_text": "Eine großartige Wallet für Monero, Bitcoin, Ethereum, Litecoin, und Haven", "please_make_selection": "Bitte treffen Sie unten eine Auswahl zum Erstellen oder Wiederherstellen Ihrer Wallet.", "create_new": "Neue Wallet erstellen", "restore_wallet": "Wallet wiederherstellen", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Empfängeradressen", "wallet_list_title": "Monero-Wallet", "wallet_list_create_new_wallet": "Neue Wallet erstellen", - "wallet_list_edit_wallet" : "Wallet bearbeiten", - "wallet_list_wallet_name" : "Wallet namen", + "wallet_list_edit_wallet": "Wallet bearbeiten", + "wallet_list_wallet_name": "Wallet namen", "wallet_list_restore_wallet": "Wallet wiederherstellen", "wallet_list_load_wallet": "Wallet laden", "wallet_list_loading_wallet": "Wallet ${wallet_name} wird geladen", @@ -395,8 +395,8 @@ "unconfirmed": "Unbestätigter Saldo", "displayable": "Anzeigebar", "submit_request": "Eine Anfrage stellen", - "buy_alert_content": "Derzeit unterstützen wir nur den Kauf von Bitcoin, Litecoin und Monero. Bitte erstellen oder wechseln Sie zu Ihrer Bitcoin-, Litecoin- oder Monero-Wallet.", - "sell_alert_content": "Wir unterstützen derzeit nur den Verkauf von Bitcoin und Litecoin. Bitte erstellen Sie Ihr Bitcoin- oder Litecoin-Wallet oder wechseln Sie zu diesem.", + "buy_alert_content": "Derzeit unterstützen wir nur den Kauf von Bitcoin, Ethereum, Litecoin und Monero. Bitte erstellen Sie Ihr Bitcoin-, Ethereum-, Litecoin- oder Monero-Wallet oder wechseln Sie zu diesem.", + "sell_alert_content": "Wir unterstützen derzeit nur den Verkauf von Bitcoin, Ethereum und Litecoin. Bitte erstellen Sie Ihr Bitcoin-, Ethereum- oder Litecoin-Wallet oder wechseln Sie zu diesem.", "outdated_electrum_wallet_description": "Neue Bitcoin-Wallets, die in Cake erstellt wurden, haben jetzt einen 24-Wort-Seed. Sie müssen eine neue Bitcoin-Wallet erstellen, Ihr gesamtes Geld in die neue 24-Wort-Wallet überweisen und keine Wallet mit einem 12-Wort-Seed mehr verwenden. Bitte tun Sie dies sofort, um Ihr Geld zu sichern.", "understand": "Ich verstehe", "apk_update": "APK-Update", @@ -538,6 +538,8 @@ "open_gift_card": "Geschenkkarte öffnen", "contact_support": "Support kontaktieren", "gift_cards_unavailable": "Geschenkkarten können derzeit nur über Monero, Bitcoin und Litecoin erworben werden", + "background_sync_mode": "Hintergrundsynchronisierungsmodus", + "sync_all_wallets": "Alle Wallets synchronisieren", "introducing_cake_pay": "Einführung von Cake Pay!", "cake_pay_learn_more": "Kaufen und lösen Sie Geschenkkarten sofort in der App ein!\nWischen Sie von links nach rechts, um mehr zu erfahren.", "automatic": "Automatisch", @@ -632,12 +634,43 @@ "setup_totp_recommended": "TOTP einrichten (empfohlen)", "disable_buy": "Kaufaktion deaktivieren", "disable_sell": "Verkaufsaktion deaktivieren", + "cake_2fa_preset" : "Kuchen 2FA-Voreinstellung", + "narrow": "Eng", + "normal": "Normal", + "aggressive": "Übereifrig", + "require_for_assessing_wallet": "Für den Zugriff auf die Wallet erforderlich", + "require_for_sends_to_non_contacts" : "Erforderlich für Versendungen an Nichtkontakte", + "require_for_sends_to_contacts" : "Erforderlich für Versendungen an Kontakte", + "require_for_sends_to_internal_wallets" : "Erforderlich für Sendungen an interne Wallets", + "require_for_exchanges_to_internal_wallets" : "Erforderlich für den Umtausch in interne Wallets", + "require_for_adding_contacts" : "Erforderlich zum Hinzufügen von Kontakten", + "require_for_creating_new_wallets" : "Erforderlich zum Erstellen neuer Wallets", + "require_for_all_security_and_backup_settings" : "Für alle Sicherheits- und Sicherungseinstellungen erforderlich", "available_balance_description": "Verfügbarer Saldo ist der Betrag, den Sie sofort ausgeben können. Dieser Betrag kann sich ändern, wenn Sie eine Transaktion senden oder empfangen.", "syncing_wallet_alert_title": "Ihr Wallet wird synchronisiert", "syncing_wallet_alert_content": "Ihr Kontostand und Ihre Transaktionsliste sind möglicherweise erst vollständig, wenn oben „SYNCHRONISIERT“ steht. Klicken/tippen Sie, um mehr zu erfahren.", + "home_screen_settings": "Einstellungen für den Startbildschirm", + "sort_by": "Sortiere nach", + "search_add_token": "Token suchen / hinzufügen", + "edit_token": "Token bearbeiten", + "warning": "Warnung", + "add_token_warning": "Bearbeiten oder fügen Sie Token nicht gemäß den Anweisungen von Betrügern hinzu.\nBestätigen Sie Token-Adressen immer mit seriösen Quellen!", + "add_token_disclaimer_check": "Ich habe die Adresse und Informationen zum Token-Vertrag anhand einer seriösen Quelle bestätigt. Das Hinzufügen böswilliger oder falscher Informationen kann zu einem Verlust von Geldern führen.", + "token_contract_address": "Token-Vertragsadresse", + "token_name": "Token-Name, z. B.: Tether", + "token_symbol": "Token-Symbol, z. B.: USDT", + "token_decimal": "Token-Dezimalzahl", + "field_required": "Dieses Feld ist erforderlich", + "pin_at_top": "Stecken Sie ${token} oben fest", + "invalid_input": "Ungültige Eingabe", + "fiat_balance": "Fiat Balance", + "gross_balance": "Bruttosaldo", + "alphabetical": "Alphabetisch", "generate_name": "Namen generieren", "balance_page": "Balance-Seite", "share": "Aktie", "slidable": "Verschiebbar", + "manage_nodes": "Knoten verwalten", + "etherscan_history": "Etherscan-Geschichte", "template_name": "Vorlagenname" } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 1e695e69a..15df01a57 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -1,7 +1,7 @@ { "welcome": "Welcome to", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Litecoin, and Haven", + "first_wallet_text": "Awesome wallet for Monero, Bitcoin, Ethereum, Litecoin, and Haven", "please_make_selection": "Please make a selection below to create or recover your wallet.", "create_new": "Create New Wallet", "restore_wallet": "Restore Wallet", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Recipient addresses", "wallet_list_title": "Monero Wallet", "wallet_list_create_new_wallet": "Create New Wallet", - "wallet_list_edit_wallet" : "Edit wallet", - "wallet_list_wallet_name" : "Wallet name", + "wallet_list_edit_wallet": "Edit wallet", + "wallet_list_wallet_name": "Wallet name", "wallet_list_restore_wallet": "Restore Wallet", "wallet_list_load_wallet": "Load wallet", "wallet_list_loading_wallet": "Loading ${wallet_name} wallet", @@ -395,8 +395,8 @@ "unconfirmed": "Unconfirmed Balance", "displayable": "Displayable", "submit_request": "submit a request", - "buy_alert_content": "Currently we only support the purchase of Bitcoin, Litecoin, and Monero. Please create or switch to your Bitcoin, Litecoin, or Monero wallet.", - "sell_alert_content": "We currently only support the sale of Bitcoin and Litecoin. Please create or switch to your Bitcoin or Litecoin wallet.", + "buy_alert_content": "Currently we only support the purchase of Bitcoin, Ethereum, Litecoin, and Monero. Please create or switch to your Bitcoin, Ethereum, Litecoin, or Monero wallet.", + "sell_alert_content": "We currently only support the sale of Bitcoin, Ethereum and Litecoin. Please create or switch to your Bitcoin, Ethereum or Litecoin wallet.", "outdated_electrum_wallet_description": "New Bitcoin wallets created in Cake now have a 24-word seed. It is mandatory that you create a new Bitcoin wallet and transfer all of your funds to the new 24-word wallet, and stop using wallets with a 12-word seed. Please do this immediately to secure your funds.", "understand": "I understand", "apk_update": "APK update", @@ -538,6 +538,8 @@ "open_gift_card": "Open Gift Card", "contact_support": "Contact Support", "gift_cards_unavailable": "Gift cards are available for purchase only with Monero, Bitcoin, and Litecoin at this time", + "background_sync_mode": "Background sync mode", + "sync_all_wallets": "Sync all wallets", "introducing_cake_pay": "Introducing Cake Pay!", "cake_pay_learn_more": "Instantly purchase and redeem gift cards in the app!\nSwipe left to right to learn more.", "automatic": "Automatic", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Set up TOTP (Recommended)", "disable_buy": "Disable buy action", "disable_sell": "Disable sell action", + "cake_2fa_preset" : "Cake 2FA Preset", + "narrow": "Narrow", + "normal": "Normal", + "aggressive": "Aggressive", + "require_for_assessing_wallet": "Require for accessing wallet", + "require_for_sends_to_non_contacts" : "Require for sends to non-contacts", + "require_for_sends_to_contacts" : "Require for sends to contacts", + "require_for_sends_to_internal_wallets" : "Require for sends to internal wallets", + "require_for_exchanges_to_internal_wallets" : "Require for exchanges to internal wallets", + "require_for_adding_contacts" : "Require for adding contacts", + "require_for_creating_new_wallets" : "Require for creating new wallets", + "require_for_all_security_and_backup_settings" : "Require for all security and backup settings", "available_balance_description": "The “Available Balance” or “Confirmed Balance” are funds that can be spent immediately. If funds appear in the lower balance but not the top balance, then you must wait a few minutes for the incoming funds to get more network confirmations. After they get more confirmations, they will be spendable.", "syncing_wallet_alert_title": "Your wallet is syncing", "syncing_wallet_alert_content": "Your balance and transaction list may not be complete until it says “SYNCHRONIZED” at the top. Click/tap to learn more.", + "home_screen_settings": "Home screen settings", + "sort_by": "Sort by", + "search_add_token": "Search / Add token", + "edit_token": "Edit token", + "warning": "Warning", + "add_token_warning": "Do not edit or add tokens as instructed by scammers.\nAlways confirm token addresses with reputable sources!", + "add_token_disclaimer_check": "I have confirmed the token contract address and information using a reputable source. Adding malicious or incorrect information can result in a loss of funds.", + "token_contract_address": "Token contract address", + "token_name": "Token name eg: Tether", + "token_symbol": "Token symbol eg: USDT", + "token_decimal": "Token decimal", + "field_required": "This field is required", + "pin_at_top": "Pin ${token} at top", + "invalid_input": "Invalid input", + "fiat_balance": "Fiat Balance", + "gross_balance": "Gross Balance", + "alphabetical": "Alphabetical", "generate_name": "Generate Name", "balance_page": "Balance Page", "share": "Share", "slidable": "Slidable", + "manage_nodes": "Manage nodes", + "etherscan_history": "Etherscan history", "template_name": "Template Name" } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index c8d4ba529..9507e22c5 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -1,7 +1,7 @@ { "welcome": "Bienvenido", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Litecoin, y Haven", + "first_wallet_text": "Impresionante billetera para Monero, Bitcoin, Ethereum, Litecoin, y Haven", "please_make_selection": "Seleccione a continuación para crear o recuperar su billetera.", "create_new": "Crear nueva billetera", "restore_wallet": "Restaurar billetera", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Direcciones de destinatarios", "wallet_list_title": "Monedero Monero", "wallet_list_create_new_wallet": "Crear nueva billetera", - "wallet_list_edit_wallet" : "Editar billetera", - "wallet_list_wallet_name" : "Nombre de la billetera", + "wallet_list_edit_wallet": "Editar billetera", + "wallet_list_wallet_name": "Nombre de la billetera", "wallet_list_restore_wallet": "Restaurar billetera", "wallet_list_load_wallet": "Billetera de carga", "wallet_list_loading_wallet": "Billetera ${wallet_name} de carga", @@ -395,8 +395,8 @@ "unconfirmed": "Saldo no confirmado", "displayable": "Visualizable", "submit_request": "presentar una solicitud", - "buy_alert_content": "Actualmente solo admitimos la compra de Bitcoin, Litecoin y Monero. Cree o cambie a su billetera Bitcoin, Litecoin o Monero.", - "sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin y Litecoin. Cree o cambie a su billetera Bitcoin o Litecoin.", + "buy_alert_content": "Actualmente solo admitimos la compra de Bitcoin, Ethereum, Litecoin y Monero. Cree o cambie a su billetera Bitcoin, Ethereum, Litecoin o Monero.", + "sell_alert_content": "Actualmente solo admitimos la venta de Bitcoin, Ethereum y Litecoin. Cree o cambie a su billetera Bitcoin, Ethereum o Litecoin.", "outdated_electrum_wallet_description": "Las nuevas carteras de Bitcoin creadas en Cake ahora tienen una semilla de 24 palabras. Es obligatorio que cree una nueva billetera de Bitcoin y transfiera todos sus fondos a la nueva billetera de 24 palabras, y deje de usar billeteras con una semilla de 12 palabras. Haga esto de inmediato para asegurar sus fondos.", "understand": "Entiendo", "apk_update": "Actualización de APK", @@ -538,6 +538,8 @@ "open_gift_card": "Abrir tarjeta de regalo", "contact_support": "Contactar con Soporte", "gift_cards_unavailable": "Las tarjetas de regalo están disponibles para comprar solo a través de Monero, Bitcoin y Litecoin en este momento", + "background_sync_mode": "Modo de sincronización en segundo plano", + "sync_all_wallets": "Sincronizar todas las billeteras", "introducing_cake_pay": "¡Presentamos Cake Pay!", "cake_pay_learn_more": "¡Compre y canjee tarjetas de regalo al instante en la aplicación!\nDeslice el dedo de izquierda a derecha para obtener más información.", "automatic": "Automático", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Configurar TOTP (Recomendado)", "disable_buy": "Desactivar acción de compra", "disable_sell": "Desactivar acción de venta", + "cake_2fa_preset" : "Pastel 2FA preestablecido", + "narrow": "Angosto", + "normal": "Normal", + "aggressive": "Demasiado entusiasta", + "require_for_assessing_wallet": "Requerido para acceder a la billetera", + "require_for_sends_to_non_contacts" : "Requerido para envíos a no contactos", + "require_for_sends_to_contacts" : "Requerir para envíos a contactos", + "require_for_sends_to_internal_wallets" : "Requerido para envíos a billeteras internas", + "require_for_exchanges_to_internal_wallets" : "Requerido para intercambios a billeteras internas", + "require_for_adding_contacts" : "Requerido para agregar contactos", + "require_for_creating_new_wallets" : "Requerido para crear nuevas billeteras", + "require_for_all_security_and_backup_settings" : "Requerido para todas las configuraciones de seguridad y copia de seguridad", "available_balance_description": "Su saldo disponible es la cantidad de fondos que puede gastar. Los fondos que se muestran aquí se pueden gastar inmediatamente.", "syncing_wallet_alert_title": "Tu billetera se está sincronizando", "syncing_wallet_alert_content": "Es posible que su lista de saldo y transacciones no esté completa hasta que diga \"SINCRONIZADO\" en la parte superior. Haga clic/toque para obtener más información.", + "home_screen_settings": "Configuración de la pantalla de inicio", + "sort_by": "Ordenar por", + "search_add_token": "Buscar/Agregar token", + "edit_token": "Editar token", + "warning": "Advertencia", + "add_token_warning": "No edite ni agregue tokens según las instrucciones de los estafadores.\n¡Confirme siempre las direcciones de los tokens con fuentes acreditadas!", + "add_token_disclaimer_check": "He confirmado la dirección del contrato del token y la información utilizando una fuente confiable. Agregar información maliciosa o incorrecta puede resultar en una pérdida de fondos.", + "token_contract_address": "Dirección de contrato de token", + "token_name": "Nombre del token, por ejemplo: Tether", + "token_symbol": "Símbolo de token, por ejemplo: USDT", + "token_decimal": "Token decimal", + "field_required": "Este campo es obligatorio", + "pin_at_top": "pin ${token} en la parte superior", + "invalid_input": "Entrada inválida", + "fiat_balance": "Equilibrio Fiat", + "gross_balance": "Saldo bruto", + "alphabetical": "Alfabético", "generate_name": "Generar nombre", "balance_page": "Página de saldo", "share": "Compartir", "slidable": "deslizable", + "manage_nodes": "Administrar nodos", + "etherscan_history": "historia de etherscan", "template_name": "Nombre de la plantilla" } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index cdea0da7e..ee9178d3f 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -1,7 +1,7 @@ { "welcome": "Bienvenue sur", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Super portefeuille (wallet) pour Monero, Bitcoin, Litecoin et Haven", + "first_wallet_text": "Super portefeuille (wallet) pour Monero, Bitcoin, Ethereum, Litecoin et Haven", "please_make_selection": "Merci de faire un choix ci-dessous pour créer ou restaurer votre portefeuille (wallet).", "create_new": "Créer un Nouveau Portefeuille (Wallet)", "restore_wallet": "Restaurer un Portefeuille (Wallet)", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adresse du bénéficiaire", "wallet_list_title": "Portefeuille (Wallet) Monero", "wallet_list_create_new_wallet": "Créer un Nouveau Portefeuille (Wallet)", - "wallet_list_edit_wallet" : "Modifier le portefeuille", - "wallet_list_wallet_name" : "Nom du portefeuille", + "wallet_list_edit_wallet": "Modifier le portefeuille", + "wallet_list_wallet_name": "Nom du portefeuille", "wallet_list_restore_wallet": "Restaurer un Portefeuille (Wallet)", "wallet_list_load_wallet": "Charger un Portefeuille (Wallet)", "wallet_list_loading_wallet": "Chargement du portefeuille (wallet) ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "Solde non confirmé", "displayable": "Visible", "submit_request": "soumettre une requête", - "buy_alert_content": "Actuellement, nous ne prenons en charge que l'achat de Bitcoin, Litecoin et Monero. Veuillez créer ou basculer vers votre portefeuille (wallet) Bitcoin, Litecoin ou Monero.", - "sell_alert_content": "Actuellement, nous ne prenons en charge que la vente de Bitcoin et Litecoin. Veuillez créer ou basculer vers votre portefeuille (wallet) Bitcoin ou Litecoin.", + "buy_alert_content": "Actuellement, nous ne prenons en charge que l'achat de Bitcoin, Ethereum, Litecoin et Monero. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum, Litecoin ou Monero.", + "sell_alert_content": "Nous ne prenons actuellement en charge que la vente de Bitcoin, Ethereum et Litecoin. Veuillez créer ou basculer vers votre portefeuille Bitcoin, Ethereum ou Litecoin.", "outdated_electrum_wallet_description": "Les nouveaux portefeuilles (wallets) Bitcoin créés dans Cake ont dorénavant une phrase secrète (seed) de 24 mots. Il est impératif que vous créiez un nouveau portefeuille Bitcoin, que vous y transfériez tous vos fonds puis que vous cessiez d'utiliser le portefeuille avec une phrase secrète de 12 mots. Merci de faire cela immédiatement pour assurer la sécurité de vos avoirs.", "understand": "J'ai compris", "apk_update": "Mise à jour d'APK", @@ -536,8 +536,10 @@ "gift_card_is_generated": "La carte-cadeau est générée", "open_gift_card": "Ouvrir la carte-cadeau", "contact_support": "Contacter l'assistance", - "gift_cards": "Cartes-Cadeaux", "gift_cards_unavailable": "Les cartes-cadeaux ne sont disponibles à l'achat que via Monero, Bitcoin et Litecoin pour le moment", + "background_sync_mode": "Mode de synchronisation en arrière-plan", + "sync_all_wallets": "Synchroniser tous les portefeuilles", + "gift_cards": "Cartes-Cadeaux", "introducing_cake_pay": "Présentation de Cake Pay !", "cake_pay_learn_more": "Achetez et utilisez instantanément des cartes-cadeaux dans l'application !\nBalayer de gauche à droite pour en savoir plus.", "automatic": "Automatique", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Configurer TOTP (recommandé)", "disable_buy": "Désactiver l'action d'achat", "disable_sell": "Désactiver l'action de vente", + "cake_2fa_preset" : "Gâteau 2FA prédéfini", + "narrow": "Étroit", + "normal": "Normal", + "aggressive": "Trop zélé", + "require_for_assessing_wallet": "Nécessaire pour accéder au portefeuille", + "require_for_sends_to_non_contacts" : "Exiger pour les envois à des non-contacts", + "require_for_sends_to_contacts" : "Exiger pour les envois aux contacts", + "require_for_sends_to_internal_wallets" : "Exiger pour les envois vers des portefeuilles internes", + "require_for_exchanges_to_internal_wallets" : "Exiger pour les échanges vers des portefeuilles internes", + "require_for_adding_contacts" : "Requis pour ajouter des contacts", + "require_for_creating_new_wallets" : "Nécessaire pour créer de nouveaux portefeuilles", + "require_for_all_security_and_backup_settings" : "Exiger pour tous les paramètres de sécurité et de sauvegarde", "available_balance_description": "Le solde disponible est le montant que vous pouvez dépenser immédiatement. Il est calculé en soustrayant le solde gelé du solde total.", "syncing_wallet_alert_title": "Votre portefeuille est en cours de synchronisation", "syncing_wallet_alert_content": "Votre solde et votre liste de transactions peuvent ne pas être complets tant qu'il n'y a pas « SYNCHRONISÉ » en haut. Cliquez/appuyez pour en savoir plus.", + "home_screen_settings": "Paramètres de l'écran d'accueil", + "sort_by": "Trier par", + "search_add_token": "Rechercher / Ajouter un jeton", + "edit_token": "Modifier le jeton", + "warning": "Avertissement", + "add_token_warning": "Ne modifiez pas ou n'ajoutez pas de jetons comme indiqué par les escrocs.\nConfirmez toujours les adresses de jeton auprès de sources fiables !", + "add_token_disclaimer_check": "J'ai confirmé l'adresse et les informations du contrat de jeton en utilisant une source fiable. L'ajout d'informations malveillantes ou incorrectes peut entraîner une perte de fonds.", + "token_contract_address": "Adresse du contrat de jeton", + "token_name": "Nom du jeton, par exemple : Tether", + "token_symbol": "Symbole de jeton, par exemple : USDT", + "token_decimal": "Décimal de jeton", + "field_required": "Ce champ est obligatoire", + "pin_at_top": "épingler ${token} en haut", + "invalid_input": "Entrée invalide", + "fiat_balance": "Fiat Balance", + "gross_balance": "Solde brut", + "alphabetical": "Alphabétique", "generate_name": "Générer un nom", "balance_page": "Page Solde", "share": "Partager", "slidable": "Glissable", + "manage_nodes": "Gérer les nœuds", + "etherscan_history": "Historique d'Etherscan", "template_name": "Nom du modèle" } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index ce8b6f8f8..203be46a4 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -1,7 +1,7 @@ { "welcome": "Barka da zuwa", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Aikace-aikacen e-wallet ga Monero, Bitcoin, Litecoin, da kuma Haven", + "first_wallet_text": "Aikace-aikacen e-wallet ga Monero, Bitcoin, Ethereum, Litecoin, da kuma Haven", "please_make_selection": "Don Allah zaɓi ƙasa don ƙirƙira ko dawo da kwalinku.", "create_new": "Ƙirƙira Sabon Kwalinku", "restore_wallet": "Dawo da Kwalinku", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adireshin masu amfani", "wallet_list_title": "Monero walat", "wallet_list_create_new_wallet": "Ƙirƙiri Sabon Wallet", - "wallet_list_edit_wallet" : "Gyara walat", - "wallet_list_wallet_name" : "Sunan walat", + "wallet_list_edit_wallet": "Gyara walat", + "wallet_list_wallet_name": "Sunan walat", "wallet_list_restore_wallet": "Maida Wallet", "wallet_list_load_wallet": "Ana loda wallet na Monero", "wallet_list_loading_wallet": "Ana loda ${wallet_name} walat", @@ -396,8 +396,8 @@ "unconfirmed": "Ba a tabbatar ba", "displayable": "Ana iya nunawa", "submit_request": "gabatar da bukata", - "buy_alert_content": "A halin yanzu muna tallafawa kawai siyan Bitcoin da Litecoin. Don siyan Bitcoin ko Litecoin, da fatan za a ƙirƙira ko canza zuwa walat ɗin ku na Bitcoin ko Litecoin.", - "sell_alert_content": "A halin yanzu muna tallafawa siyar da Bitcoin da Litecoin kawai. Da fatan za a ƙirƙira ko canza zuwa walat ɗin ku na Bitcoin ko Litecoin.", + "buy_alert_content": "A halin yanzu muna tallafawa kawai siyan Bitcoin, Ethereum, Litecoin, da Monero. Da fatan za a ƙirƙiri ko canza zuwa Bitcoin, Ethereum, Litecoin, ko Monero walat.", + "sell_alert_content": "A halin yanzu muna tallafawa kawai siyar da Bitcoin, Ethereum da Litecoin. Da fatan za a ƙirƙiri ko canza zuwa walat ɗin ku na Bitcoin, Ethereum ko Litecoin.", "outdated_electrum_wallet_description": "Sabbin walat ɗin Bitcoin da aka kirkira a cikin Cake yanzu suna da nau'in kalma 24. Ya zama dole ka ƙirƙiri sabon walat ɗin Bitcoin kuma canza duk kuɗin ku zuwa sabon walat ɗin kalmomi 24, kuma ku daina amfani da walat tare da iri mai kalma 12. Da fatan za a yi haka nan take don samun kuɗin ku.", "understand": "na gane", "apk_update": "apk sabunta", @@ -612,13 +612,43 @@ "prevent_screenshots": "Fada lambobi da jarrabobi na kayan lambobi", "disable_buy": "Kashe alama", "disable_sell": "Kashe karbuwa", + "cake_2fa_preset" : "Cake 2FA saiti", + "narrow": "kunkuntar", + "normal": "Na al'ada", + "aggressive": "Mai tsananin kishi", + "require_for_assessing_wallet": "Bukatar samun damar walat", + "require_for_sends_to_non_contacts" : "Bukatar aika zuwa waɗanda ba lambobin sadarwa ba", + "require_for_sends_to_contacts" : "Bukatar aika zuwa lambobin sadarwa", + "require_for_sends_to_internal_wallets" : "Bukatar aika zuwa wallet na ciki", + "require_for_exchanges_to_internal_wallets" : "Bukatar musanya zuwa wallet na ciki", + "require_for_adding_contacts" : "Bukatar ƙara lambobin sadarwa", + "require_for_creating_new_wallets" : "Bukatar ƙirƙirar sabbin wallet", + "require_for_all_security_and_backup_settings" : "Bukatar duk tsaro da saitunan wariyar ajiya", "available_balance_description": "Ma'auni mai samuwa” ko ”,Tabbataccen Ma'auni”, kudade ne da za a iya kashewa nan da nan. Idan kudade sun bayyana a cikin ƙananan ma'auni amma ba babban ma'auni ba, to dole ne ku jira 'yan mintoci kaɗan don kudaden shiga don samun ƙarin tabbaci na hanyar sadarwa. Bayan sun sami ƙarin tabbaci, za a kashe su.", "syncing_wallet_alert_title": "Walat ɗin ku yana aiki tare", "syncing_wallet_alert_content": "Ma'aunin ku da lissafin ma'amala bazai cika ba har sai an ce \"SYNCHRONIZED\" a saman. Danna/matsa don ƙarin koyo.", + "home_screen_settings": "Saitunan allo na gida", + "sort_by": "Kasa", + "search_add_token": "Bincika / Ƙara alama", + "edit_token": "Gyara alamar", + "warning": "Gargadi", + "add_token_warning": "Kar a gyara ko ƙara alamu kamar yadda masu zamba suka umarta.\nKoyaushe tabbatar da adiresoshin alamar tare da sanannun tushe!", + "add_token_disclaimer_check": "Na tabbatar da adireshin kwangilar alamar da bayanin ta amfani da ingantaccen tushe. Ƙara bayanan ƙeta ko kuskure na iya haifar da asarar kuɗi.", + "token_contract_address": "Adireshin kwangilar Token", + "token_name": "Alamar sunan misali: Tether", + "token_symbol": "Alamar alama misali: USDT", + "token_decimal": "Alamar ƙima", + "field_required": "wannan fillin ana bukatansa", + "pin_at_top": "pin ${token} a sama", + "invalid_input": "Shigar da ba daidai ba", + "fiat_balance": "Fiat Balance", + "gross_balance": "Babban Ma'auni", + "alphabetical": "Harafi", "generate_name": "Ƙirƙirar Suna", "balance_page": "Ma'auni Page", "share": "Raba", "slidable": "Mai iya zamewa", + "etherscan_history": "Etherscan tarihin kowane zamani", + "manage_nodes": "Sarrafa nodes", "template_name": "Sunan Samfura" } - diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 652612b50..f35e0a613 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -1,7 +1,7 @@ { "welcome": "स्वागत हे सेवा मेरे", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Monero, Bitcoin, Litecoin, और Haven के लिए बहुत बढ़िया बटुआ", + "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin, और Haven के लिए बहुत बढ़िया बटुआ", "please_make_selection": "कृपया नीचे चयन करें अपना बटुआ बनाएं या पुनर्प्राप्त करें.", "create_new": "नया बटुआ बनाएँ", "restore_wallet": "वॉलेट को पुनर्स्थापित करें", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "प्राप्तकर्ता के पते", "wallet_list_title": "Monero बटुआ", "wallet_list_create_new_wallet": "नया बटुआ बनाएँ", - "wallet_list_edit_wallet" : "बटुआ संपादित करें", - "wallet_list_wallet_name" : "बटुआ नाम", + "wallet_list_edit_wallet": "बटुआ संपादित करें", + "wallet_list_wallet_name": "बटुआ नाम", "wallet_list_restore_wallet": "वॉलेट को पुनर्स्थापित करें", "wallet_list_load_wallet": "वॉलेट लोड करें", "wallet_list_loading_wallet": "लोड हो रहा है ${wallet_name} बटुआ", @@ -395,8 +395,8 @@ "unconfirmed": "अपुष्ट शेष राशि", "displayable": "प्रदर्शन योग्य", "submit_request": "एक अनुरोध सबमिट करें", - "buy_alert_content": "वर्तमान में हम केवल बिटकॉइन, लाइटकॉइन और मोनेरो की खरीद का समर्थन करते हैं। कृपया अपना बिटकॉइन, लाइटकॉइन, या मोनेरो वॉलेट बनाएं या स्विच करें।", - "sell_alert_content": "वर्तमान में हम केवल बिटकॉइन और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन या लाइटकॉइन वॉलेट बनाएं या स्विच करें।", + "buy_alert_content": "वर्तमान में हम केवल बिटकॉइन, एथेरियम, लाइटकॉइन और मोनेरो की खरीद का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम, लाइटकॉइन, या मोनेरो वॉलेट बनाएं या उस पर स्विच करें।", + "sell_alert_content": "हम वर्तमान में केवल बिटकॉइन, एथेरियम और लाइटकॉइन की बिक्री का समर्थन करते हैं। कृपया अपना बिटकॉइन, एथेरियम या लाइटकॉइन वॉलेट बनाएं या उसमें स्विच करें।", "outdated_electrum_wallet_description": "केक में बनाए गए नए बिटकॉइन वॉलेट में अब 24-शब्द का बीज है। यह अनिवार्य है कि आप एक नया बिटकॉइन वॉलेट बनाएं और अपने सभी फंड को नए 24-शब्द वाले वॉलेट में स्थानांतरित करें, और 12-शब्द बीज वाले वॉलेट का उपयोग करना बंद करें। कृपया अपने धन को सुरक्षित करने के लिए इसे तुरंत करें।", "understand": "मुझे समझ", "apk_update": "APK अद्यतन", @@ -538,6 +538,8 @@ "open_gift_card": "गिफ्ट कार्ड खोलें", "contact_support": "सहायता से संपर्क करें", "gift_cards_unavailable": "उपहार कार्ड इस समय केवल मोनेरो, बिटकॉइन और लिटकोइन के माध्यम से खरीदने के लिए उपलब्ध हैं", + "background_sync_mode": "बैकग्राउंड सिंक मोड", + "sync_all_wallets": "सभी वॉलेट सिंक करें", "introducing_cake_pay": "परिचय Cake Pay!", "cake_pay_learn_more": "ऐप में उपहार कार्ड तुरंत खरीदें और रिडीम करें!\nअधिक जानने के लिए बाएं से दाएं स्वाइप करें।", "automatic": "स्वचालित", @@ -632,12 +634,43 @@ "setup_totp_recommended": "टीओटीपी सेट अप करें (अनुशंसित)", "disable_buy": "खरीद कार्रवाई अक्षम करें", "disable_sell": "बेचने की कार्रवाई अक्षम करें", + "cake_2fa_preset" : "केक 2एफए प्रीसेट", + "narrow": "सँकरा", + "normal": "सामान्य", + "aggressive": "ज्यादा", + "require_for_assessing_wallet": "वॉलेट तक पहुँचने के लिए आवश्यकता है", + "require_for_sends_to_non_contacts" : "गैर-संपर्कों को भेजने की आवश्यकता", + "require_for_sends_to_contacts" : "संपर्कों को भेजने के लिए आवश्यक है", + "require_for_sends_to_internal_wallets" : "आंतरिक वॉलेट में भेजने की आवश्यकता है", + "require_for_exchanges_to_internal_wallets" : "आंतरिक वॉलेट में आदान-प्रदान की आवश्यकता है", + "require_for_adding_contacts" : "संपर्क जोड़ने के लिए आवश्यकता है", + "require_for_creating_new_wallets" : "नए वॉलेट बनाने की आवश्यकता है", + "require_for_all_security_and_backup_settings" : "सभी सुरक्षा और बैकअप सेटिंग्स की आवश्यकता है", "available_balance_description": "उपलब्ध शेष या ”पुष्टिकृत शेष”, वे धनराशि हैं जिन्हें तुरंत खर्च किया जा सकता है। यदि फंड निचले बैलेंस में दिखाई देते हैं, लेकिन शीर्ष बैलेंस में नहीं, तो आपको आने वाले फंड के लिए अधिक नेटवर्क पुष्टिकरण प्राप्त करने के लिए कुछ मिनट इंतजार करना होगा। अधिक पुष्टि मिलने के बाद, वे खर्च करने योग्य हो जाएंगे।", "syncing_wallet_alert_title": "आपका वॉलेट सिंक हो रहा है", "syncing_wallet_alert_content": "आपकी शेष राशि और लेनदेन सूची तब तक पूरी नहीं हो सकती जब तक कि शीर्ष पर \"सिंक्रनाइज़्ड\" न लिखा हो। अधिक जानने के लिए क्लिक/टैप करें।", + "home_screen_settings": "होम स्क्रीन सेटिंग्स", + "sort_by": "इसके अनुसार क्रमबद्ध करें", + "search_add_token": "खोजें/टोकन जोड़ें", + "edit_token": "टोकन संपादित करें", + "warning": "चेतावनी", + "add_token_warning": "स्कैमर्स के निर्देशानुसार टोकन संपादित या जोड़ें न करें।\nहमेशा प्रतिष्ठित स्रोतों से टोकन पते की पुष्टि करें!", + "add_token_disclaimer_check": "मैंने एक प्रतिष्ठित स्रोत का उपयोग करके टोकन अनुबंध पते और जानकारी की पुष्टि की है। दुर्भावनापूर्ण या गलत जानकारी जोड़ने से धन की हानि हो सकती है।", + "token_contract_address": "टोकन अनुबंध पता", + "token_name": "टोकन नाम जैसे: टीथर", + "token_symbol": "टोकन प्रतीक जैसे: यूएसडीटी", + "token_decimal": "सांकेतिक दशमलव", + "field_required": "यह फ़ील्ड आवश्यक है", + "pin_at_top": "शीर्ष पर ${token} पिन करें", + "invalid_input": "अमान्य निवेश", + "fiat_balance": "फिएट बैलेंस", + "gross_balance": "सकल संतुलन", + "alphabetical": "वर्णमाला", "generate_name": "नाम जनरेट करें", "balance_page": "बैलेंस पेज", "share": "शेयर करना", "slidable": "फिसलने लायक", + "manage_nodes": "नोड्स प्रबंधित करें", + "etherscan_history": "इथरस्कैन इतिहास", "template_name": "टेम्पलेट नाम" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 6e6f094fe..f8913c5cc 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -1,7 +1,7 @@ { "welcome": "Dobrodošli na", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Odličan novčanik za Monero, Bitcoin, Litecoin, i Haven", + "first_wallet_text": "Odličan novčanik za Monero, Bitcoin, Ethereum, Litecoin, i Haven", "please_make_selection": "Molimo odaberite opcije niže za izradu novog novčanika ili za oporavak postojećeg.", "create_new": "Izradi novi novčanik", "restore_wallet": "Oporavi novčanik", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adrese primatelja", "wallet_list_title": "Monero novčanik", "wallet_list_create_new_wallet": "Izradi novi novčanik", - "wallet_list_edit_wallet" : "Uredi novčanik", - "wallet_list_wallet_name" : "Naziv novčanika", + "wallet_list_edit_wallet": "Uredi novčanik", + "wallet_list_wallet_name": "Naziv novčanika", "wallet_list_restore_wallet": "Oporavi novčanik", "wallet_list_load_wallet": "Učitaj novčanik", "wallet_list_loading_wallet": "Učitavanje novčanika ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "Nepotvrđeno stanje", "displayable": "Dostupno za prikaz", "submit_request": "podnesi zahtjev", - "buy_alert_content": "Trenutno podržavamo samo kupnju Bitcoina, Litecoina i Monera. Izradite ili prijeđite na svoj Bitcoin, Litecoin ili Monero novčanik.", - "sell_alert_content": "Trenutno podržavamo samo prodaju Bitcoina i Litecoina. Izradite ili prijeđite na svoj Bitcoin ili Litecoin novčanik.", + "buy_alert_content": "Trenutno podržavamo samo kupnju Bitcoina, Ethereuma, Litecoina i Monera. Izradite ili prijeđite na svoj Bitcoin, Ethereum, Litecoin ili Monero novčanik.", + "sell_alert_content": "Trenutno podržavamo samo prodaju Bitcoina, Ethereuma i Litecoina. Izradite ili prijeđite na svoj Bitcoin, Ethereum ili Litecoin novčanik.", "outdated_electrum_wallet_description": "Novi Bitcoin novčanici stvoreni u Cakeu sada imaju sjeme od 24 riječi. Obavezno je stvoriti novi Bitcoin novčanik i prenijeti sva svoja sredstva u novi novčanik od 24 riječi te prestati koristiti novčanike s sjemenkom od 12 riječi. Učinite to odmah kako biste osigurali svoja sredstva.", "understand": "Razumijem", "apk_update": "APK ažuriranje", @@ -538,6 +538,8 @@ "open_gift_card": "Otvori darovnu karticu", "contact_support": "Kontaktirajte podršku", "gift_cards_unavailable": "Poklon kartice trenutno su dostupne za kupnju samo putem Monera, Bitcoina i Litecoina", + "background_sync_mode": "Sinkronizacija u pozadini", + "sync_all_wallets": "Sinkronizirajte sve novčanike", "introducing_cake_pay": "Predstavljamo Cake Pay!", "cake_pay_learn_more": "Azonnal vásárolhat és válthat be ajándékutalványokat az alkalmazásban!\nTovábbi információért csúsztassa balról jobbra az ujját.", "automatic": "Automatski", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Postavite TOTP (preporučeno)", "disable_buy": "Onemogući kupnju", "disable_sell": "Onemogući akciju prodaje", + "cake_2fa_preset" : "Cake 2FA Preset", + "narrow": "Usko", + "normal": "Normalno", + "aggressive": "Preterano", + "require_for_assessing_wallet": "Potreban za pristup novčaniku", + "require_for_sends_to_non_contacts" : "Zahtijeva za slanje nekontaktima", + "require_for_sends_to_contacts" : "Zahtijeva za slanje kontaktima", + "require_for_sends_to_internal_wallets" : "Zahtijeva za slanje u interne novčanike", + "require_for_exchanges_to_internal_wallets" : "Potreban za razmjenu na interne novčanike", + "require_for_adding_contacts" : "Zahtijeva za dodavanje kontakata", + "require_for_creating_new_wallets" : "Potreban za kreiranje novih novčanika", + "require_for_all_security_and_backup_settings" : "Zahtijeva za sve postavke sigurnosti i sigurnosne kopije", "available_balance_description": "Dostupno stanje je iznos koji možete potrošiti. To je vaš saldo minus bilo kakve transakcije koje su još uvijek u tijeku.", "syncing_wallet_alert_title": "Vaš novčanik se sinkronizira", "syncing_wallet_alert_content": "Vaš saldo i popis transakcija možda neće biti potpuni sve dok na vrhu ne piše \"SINKRONIZIRANO\". Kliknite/dodirnite da biste saznali više.", + "home_screen_settings": "Postavke početnog zaslona", + "sort_by": "Poredaj po", + "search_add_token": "Traži / Dodaj token", + "edit_token": "Uredi token", + "warning": "Upozorenje", + "add_token_warning": "Nemojte uređivati niti dodavati tokene prema uputama prevaranata.\nUvijek potvrdite adrese tokena s uglednim izvorima!", + "add_token_disclaimer_check": "Potvrdio sam adresu i informacije o ugovoru o tokenu koristeći ugledni izvor. Dodavanje zlonamjernih ili netočnih informacija može dovesti do gubitka sredstava.", + "token_contract_address": "Adresa ugovora tokena", + "token_name": "Naziv tokena npr.: Tether", + "token_symbol": "Simbol tokena npr.: USDT", + "token_decimal": "Token decimalni", + "field_required": "ovo polje je obavezno", + "pin_at_top": "prikvači ${token} na vrh", + "invalid_input": "Pogrešan unos", + "fiat_balance": "Fiat Bilans", + "gross_balance": "Bruto bilanca", + "alphabetical": "Abecedno", "generate_name": "Generiraj ime", "balance_page": "Stranica sa stanjem", "share": "Udio", "slidable": "Klizna", + "manage_nodes": "Upravljanje čvorovima", + "etherscan_history": "Etherscan povijest", "template_name": "Naziv predloška" } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index e55154882..f0580df0b 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -1,7 +1,7 @@ { "welcome": "Selamat datang di", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Dompet luar biasa untuk Monero, Bitcoin, Litecoin, dan Haven", + "first_wallet_text": "Dompet luar biasa untuk Monero, Bitcoin, Ethereum, Litecoin, dan Haven", "please_make_selection": "Silahkan membuat pilihan di bawah ini untuk membuat atau memulihkan dompet Anda.", "create_new": "Buat Dompet Baru", "restore_wallet": "Pulihkan Dompet", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Alamat Penerima", "wallet_list_title": "Dompet Monero", "wallet_list_create_new_wallet": "Buat Dompet Baru", - "wallet_list_edit_wallet" : "Edit dompet", - "wallet_list_wallet_name" : "Nama dompet", + "wallet_list_edit_wallet": "Edit dompet", + "wallet_list_wallet_name": "Nama dompet", "wallet_list_restore_wallet": "Pulihkan Dompet", "wallet_list_load_wallet": "Muat dompet", "wallet_list_loading_wallet": "Memuat ${wallet_name} dompet", @@ -396,8 +396,8 @@ "unconfirmed": "Saldo Belum Dikonfirmasi", "displayable": "Dapat ditampilkan", "submit_request": "kirim permintaan", - "buy_alert_content": "Saat ini kami hanya mendukung pembelian Bitcoin, Litecoin, dan Monero. Harap buat atau alihkan ke dompet Bitcoin, Litecoin, atau Monero Anda.", - "sell_alert_content": "Saat ini kami hanya mendukung penjualan Bitcoin dan Litecoin. Harap buat atau alihkan ke dompet Bitcoin atau Litecoin Anda.", + "buy_alert_content": "Saat ini kami hanya mendukung pembelian Bitcoin, Ethereum, Litecoin, dan Monero. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, Litecoin, atau Monero Anda.", + "sell_alert_content": "Saat ini kami hanya mendukung penjualan Bitcoin, Ethereum, dan Litecoin. Harap buat atau alihkan ke dompet Bitcoin, Ethereum, atau Litecoin Anda.", "outdated_electrum_wallet_description": "Dompet Bitcoin baru yang dibuat di Cake sekarang memiliki biji semai 24 kata. Wajib bagi Anda untuk membuat dompet Bitcoin baru dan mentransfer semua dana Anda ke dompet 24 kata baru, dan berhenti menggunakan dompet dengan biji semai 12 kata. Silakan lakukan ini segera untuk mengamankan dana Anda.", "understand": "Saya mengerti", "apk_update": "Pembaruan APK", @@ -622,12 +622,43 @@ "setup_totp_recommended": "Siapkan TOTP (Disarankan)", "disable_buy": "Nonaktifkan tindakan beli", "disable_sell": "Nonaktifkan aksi jual", + "cake_2fa_preset" : "Preset Kue 2FA", + "narrow": "Sempit", + "normal": "Normal", + "aggressive": "Terlalu bersemangat", + "require_for_assessing_wallet": "Diperlukan untuk mengakses dompet", + "require_for_sends_to_non_contacts" : "Wajibkan untuk mengirim ke non-kontak", + "require_for_sends_to_contacts" : "Membutuhkan untuk mengirim ke kontak", + "require_for_sends_to_internal_wallets" : "Diperlukan untuk mengirim ke dompet internal", + "require_for_exchanges_to_internal_wallets" : "Diperlukan untuk pertukaran ke dompet internal", + "require_for_adding_contacts" : "Membutuhkan untuk menambahkan kontak", + "require_for_creating_new_wallets" : "Diperlukan untuk membuat dompet baru", + "require_for_all_security_and_backup_settings" : "Memerlukan untuk semua pengaturan keamanan dan pencadangan", "available_balance_description": "“Saldo yang Tersedia” atau “Saldo yang Dikonfirmasi” adalah dana yang dapat langsung dibelanjakan. Jika dana muncul di saldo bawah tetapi tidak di saldo atas, maka Anda harus menunggu beberapa menit agar dana masuk mendapatkan konfirmasi jaringan lainnya. Setelah mereka mendapatkan lebih banyak konfirmasi, mereka akan dapat dibelanjakan.", "syncing_wallet_alert_title": "Dompet Anda sedang disinkronkan", "syncing_wallet_alert_content": "Saldo dan daftar transaksi Anda mungkin belum lengkap sampai tertulis “SYNCHRONIZED” di bagian atas. Klik/ketuk untuk mempelajari lebih lanjut.", + "home_screen_settings": "Pengaturan layar awal", + "sort_by": "Sortir dengan", + "search_add_token": "Cari / Tambahkan token", + "edit_token": "Mengedit token", + "warning": "Peringatan", + "add_token_warning": "Jangan mengedit atau menambahkan token seperti yang diinstruksikan oleh penipu.\nSelalu konfirmasikan alamat token dengan sumber tepercaya!", + "add_token_disclaimer_check": "Saya telah mengonfirmasi alamat dan informasi kontrak token menggunakan sumber yang memiliki reputasi baik. Menambahkan informasi jahat atau salah dapat mengakibatkan hilangnya dana.", + "token_contract_address": "Alamat kontrak token", + "token_name": "Nama token misalnya: Tether", + "token_symbol": "Simbol token misalnya: USDT", + "token_decimal": "Desimal token", + "field_required": "Bagian ini diperlukan", + "pin_at_top": "sematkan ${token} di atas", + "invalid_input": "Masukan tidak valid", + "fiat_balance": "Saldo Fiat", + "gross_balance": "Saldo Kotor", + "alphabetical": "Alfabetis", "generate_name": "Hasilkan Nama", "balance_page": "Halaman Saldo", "share": "Membagikan", "slidable": "Dapat digeser", + "manage_nodes": "Kelola node", + "etherscan_history": "Sejarah Etherscan", "template_name": "Nama Templat" } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 1bd4e457f..3425b7179 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -1,7 +1,7 @@ { "welcome": "Benvenuto", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Litecoin, e Haven", + "first_wallet_text": "Portafoglio fantastico per Monero, Bitcoin, Ethereum, Litecoin, e Haven", "please_make_selection": "Gentilmente seleziona se vuoi generare o recuperare il tuo portafoglio.", "create_new": "Genera nuovo Portafoglio", "restore_wallet": "Recupera Portafoglio", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Indirizzi dei destinatari", "wallet_list_title": "Portafoglio Monero", "wallet_list_create_new_wallet": "Crea Nuovo Portafoglio", - "wallet_list_edit_wallet" : "Modifica portafoglio", - "wallet_list_wallet_name" : "Nome del portafoglio", + "wallet_list_edit_wallet": "Modifica portafoglio", + "wallet_list_wallet_name": "Nome del portafoglio", "wallet_list_restore_wallet": "Recupera Portafoglio", "wallet_list_load_wallet": "Caricamento Portafoglio", "wallet_list_loading_wallet": "Caricamento portafoglio ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "Saldo non confermato", "displayable": "Visualizzabile", "submit_request": "invia una richiesta", - "buy_alert_content": "Attualmente supportiamo solo l'acquisto di Bitcoin, Litecoin e Monero. Crea o passa al tuo portafoglio Bitcoin, Litecoin o Monero.", - "sell_alert_content": "Al momento supportiamo solo la vendita di Bitcoin e Litecoin. Crea o passa al tuo portafoglio Bitcoin o Litecoin.", + "buy_alert_content": "Attualmente supportiamo solo l'acquisto di Bitcoin, Ethereum, Litecoin e Monero. Crea o passa al tuo portafoglio Bitcoin, Ethereum, Litecoin o Monero.", + "sell_alert_content": "Al momento supportiamo solo la vendita di Bitcoin, Ethereum e Litecoin. Crea o passa al tuo portafoglio Bitcoin, Ethereum o Litecoin.", "outdated_electrum_wallet_description": "I nuovi portafogli Bitcoin creati in Cake ora hanno un seme di 24 parole. È obbligatorio creare un nuovo portafoglio Bitcoin e trasferire tutti i fondi nel nuovo portafoglio di 24 parole e smettere di usare portafogli con un seme di 12 parole. Ti preghiamo di farlo immediatamente per proteggere i tuoi fondi.", "understand": "Capisco", "apk_update": "Aggiornamento APK", @@ -538,6 +538,8 @@ "open_gift_card": "Apri carta regalo", "contact_support": "Contatta l'assistenza", "gift_cards_unavailable": "Le carte regalo sono disponibili per l'acquisto solo tramite Monero, Bitcoin e Litecoin in questo momento", + "background_sync_mode": "Modalità di sincronizzazione in background", + "sync_all_wallets": "Sincronizza tutti i portafogli", "introducing_cake_pay": "Presentazione di Cake Pay!", "cake_pay_learn_more": "Acquista e riscatta istantaneamente carte regalo nell'app!\nScorri da sinistra a destra per saperne di più.", "automatic": "Automatico", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Imposta TOTP (consigliato)", "disable_buy": "Disabilita l'azione di acquisto", "disable_sell": "Disabilita l'azione di vendita", + "cake_2fa_preset" : "Torta 2FA Preset", + "narrow": "Stretto", + "normal": "Normale", + "aggressive": "Fervente", + "require_for_assessing_wallet": "Richiesto per l'accesso al portafoglio", + "require_for_sends_to_non_contacts" : "Richiesto per invii a non contatti", + "require_for_sends_to_contacts" : "Richiedi per gli invii ai contatti", + "require_for_sends_to_internal_wallets" : "Richiesto per invii a portafogli interni", + "require_for_exchanges_to_internal_wallets" : "Richiedi per gli scambi ai portafogli interni", + "require_for_adding_contacts" : "Richiesto per l'aggiunta di contatti", + "require_for_creating_new_wallets" : "Richiesto per la creazione di nuovi portafogli", + "require_for_all_security_and_backup_settings" : "Richiedi per tutte le impostazioni di sicurezza e backup", "available_balance_description": "Il saldo disponibile è il saldo totale meno i fondi congelati. I fondi congelati sono fondi che sono stati inviati ma non sono ancora stati confermati.", "syncing_wallet_alert_title": "Il tuo portafoglio si sta sincronizzando", "syncing_wallet_alert_content": "Il saldo e l'elenco delle transazioni potrebbero non essere completi fino a quando non viene visualizzato \"SYNCHRONIZED\" in alto. Clicca/tocca per saperne di più.", + "home_screen_settings": "Impostazioni della schermata iniziale", + "sort_by": "Ordina per", + "search_add_token": "Cerca / Aggiungi token", + "edit_token": "Modifica token", + "warning": "Avvertimento", + "add_token_warning": "Non modificare o aggiungere token come indicato dai truffatori.\nConferma sempre gli indirizzi dei token con fonti attendibili!", + "add_token_disclaimer_check": "Ho confermato l'indirizzo e le informazioni del contratto token utilizzando una fonte attendibile. L'aggiunta di informazioni dannose o errate può comportare una perdita di fondi.", + "token_contract_address": "Indirizzo del contratto token", + "token_name": "Nome del token, ad esempio: Tether", + "token_symbol": "Simbolo del token, ad esempio: USDT", + "token_decimal": "Decimale del token", + "field_required": "Questo campo è obbligatorio", + "pin_at_top": "fissa ${token} in alto", + "invalid_input": "Inserimento non valido", + "fiat_balance": "Equilibrio fiat", + "gross_balance": "Saldo lordo", + "alphabetical": "Alfabetico", "generate_name": "Genera nome", "balance_page": "Pagina di equilibrio", "share": "Condividere", "slidable": "Scorrevole", + "manage_nodes": "Gestisci i nodi", + "etherscan_history": "Storia Etherscan", "template_name": "Nome modello" } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index f90f208a6..e302b8819 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -1,7 +1,7 @@ { "welcome": "ようこそ に", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Monero、Bitcoin、Litecoin、Haven用の素晴らしいウォレット", + "first_wallet_text": "Monero、Bitcoin、Ethereum、Litecoin、Haven用の素晴らしいウォレット", "please_make_selection": "以下を選択してください ウォレットを作成または回復する.", "create_new": "新しいウォレットを作成", "restore_wallet": "ウォレットを復元", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "受信者のアドレス", "wallet_list_title": "Monero 財布", "wallet_list_create_new_wallet": "新しいウォレットを作成", - "wallet_list_edit_wallet" : "ウォレットを編集する", - "wallet_list_wallet_name" : "ウォレット名", + "wallet_list_edit_wallet": "ウォレットを編集する", + "wallet_list_wallet_name": "ウォレット名", "wallet_list_restore_wallet": "ウォレットを復元", "wallet_list_load_wallet": "ウォレットをロード", "wallet_list_loading_wallet": "読み込み中 ${wallet_name} 財布", @@ -395,8 +395,8 @@ "unconfirmed": "残高未確認", "displayable": "表示可能", "submit_request": "リクエストを送信する", - "buy_alert_content": "現在、ビットコイン、ライトコイン、モネロの購入のみをサポートしています。 Bitcoin、Litecoin、または Monero ウォレットを作成するか、切り替えてください。", - "sell_alert_content": "現在、ビットコインとライトコインの販売のみをサポートしています。 ビットコインまたはライトコインウォレットを作成するか、ウォレットに切り替えてください。", + "buy_alert_content": "現在、ビットコイン、イーサリアム、ライトコイン、モネロの購入のみをサポートしています。ビットコイン、イーサリアム、ライトコイン、またはモネロのウォレットを作成するか、これらのウォレットに切り替えてください。", + "sell_alert_content": "現在、ビットコイン、イーサリアム、ライトコインの販売のみをサポートしています。ビットコイン、イーサリアム、またはライトコインのウォレットを作成するか、これらのウォレットに切り替えてください。", "outdated_electrum_wallet_description": "Cakeで作成された新しいビットコインウォレットには、24ワードのシードがあります。 新しいビットコインウォレットを作成し、すべての資金を新しい24ワードのウォレットに転送し、12ワードのシードを持つウォレットの使用を停止することが必須です。 あなたの資金を確保するためにこれをすぐに行ってください。", "understand": "わかります", "apk_update": "APKアップデート", @@ -538,6 +538,8 @@ "open_gift_card": "オープンギフトカード", "contact_support": "サポートに連絡する", "gift_cards_unavailable": "現時点では、ギフトカードはMonero、Bitcoin、Litecoinからのみ購入できます。", + "background_sync_mode": "バックグラウンド同期モード", + "sync_all_wallets": "すべてのウォレットを同期", "introducing_cake_pay": "序章Cake Pay!", "cake_pay_learn_more": "アプリですぐにギフトカードを購入して引き換えましょう!\n左から右にスワイプして詳細をご覧ください。", "automatic": "自動", @@ -632,12 +634,43 @@ "setup_totp_recommended": "TOTP を設定する (推奨)", "disable_buy": "購入アクションを無効にする", "disable_sell": "販売アクションを無効にする", + "cake_2fa_preset" : "ケーキ 2FA プリセット", + "narrow": "狭い", + "normal": "普通", + "aggressive": "熱心すぎる", + "require_for_assessing_wallet": "ウォレットにアクセスするために必要です", + "require_for_sends_to_non_contacts" : "非連絡先への送信に必須", + "require_for_sends_to_contacts" : "連絡先に送信する場合に必須", + "require_for_sends_to_internal_wallets" : "内部ウォレットへの送信に必須", + "require_for_exchanges_to_internal_wallets" : "内部ウォレットへの交換に必要", + "require_for_adding_contacts" : "連絡先の追加に必要", + "require_for_creating_new_wallets" : "新しいウォレットを作成するために必要です", + "require_for_all_security_and_backup_settings" : "すべてのセキュリティおよびバックアップ設定に必須", "available_balance_description": "利用可能な残高は、ウォレットの残高から冷凍残高を差し引いたものです。", "syncing_wallet_alert_title": "ウォレットは同期中です", "syncing_wallet_alert_content": "上部に「同期済み」と表示されるまで、残高と取引リストが完了していない可能性があります。詳細については、クリック/タップしてください。", + "home_screen_settings": "ホーム画面の設定", + "sort_by": "並び替え", + "search_add_token": "トークンの検索/追加", + "edit_token": "トークンの編集", + "warning": "警告", + "add_token_warning": "詐欺師の指示に従ってトークンを編集または追加しないでください。\nトークン アドレスは常に信頼できる情報源で確認してください。", + "add_token_disclaimer_check": "信頼できる情報源を使用して、トークン コントラクトのアドレスと情報を確認しました。 悪意のある情報や不正確な情報を追加すると、資金が失われる可能性があります。", + "token_contract_address": "トークンコントラクトアドレス", + "token_name": "トークン名 例: Tether", + "token_symbol": "トークンシンボル 例: USDT", + "token_decimal": "トークン10進数", + "field_required": "この項目は必須です", + "pin_at_top": "${token} を上部に固定します", + "invalid_input": "無効入力", + "fiat_balance": "フィアットバランス", + "gross_balance": "グロス残高", + "alphabetical": "アルファベット順", "generate_name": "名前の生成", "balance_page": "残高ページ", "share": "共有", "slidable": "スライド可能", + "manage_nodes": "ノードの管理", + "etherscan_history": "イーサスキャンの歴史", "template_name": "テンプレート名" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5d4a497d5..8cbda4344 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -1,7 +1,7 @@ { "welcome": "환영 에", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Monero, Bitcoin, Litecoin 및 Haven을 위한 멋진 지갑", + "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin 및 Haven을 위한 멋진 지갑", "please_make_selection": "아래에서 선택하십시오 지갑 만들기 또는 복구.", "create_new": "새 월렛 만들기", "restore_wallet": "월렛 복원", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "받는 사람 주소", "wallet_list_title": "모네로 월렛", "wallet_list_create_new_wallet": "새 월렛 만들기", - "wallet_list_edit_wallet" : "지갑 수정", - "wallet_list_wallet_name" : "지갑 이름", + "wallet_list_edit_wallet": "지갑 수정", + "wallet_list_wallet_name": "지갑 이름", "wallet_list_restore_wallet": "월렛 복원", "wallet_list_load_wallet": "지갑로드", "wallet_list_loading_wallet": "로딩 ${wallet_name} 지갑", @@ -395,8 +395,8 @@ "unconfirmed": "확인되지 않은 잔액", "displayable": "표시 가능", "submit_request": "요청을 제출", - "buy_alert_content": "현재 우리는 Bitcoin, Litecoin 및 Monero 구매만 지원합니다. Bitcoin, Litecoin 또는 Monero 지갑을 생성하거나 전환하십시오.", - "sell_alert_content": "현재 Bitcoin 및 Litecoin 판매만 지원합니다. Bitcoin 또는 Litecoin 지갑을 생성하거나 전환하십시오.", + "buy_alert_content": "현재 Bitcoin, Ethereum, Litecoin 및 Monero 구매만 지원합니다. Bitcoin, Ethereum, Litecoin 또는 Monero 지갑을 생성하거나 전환하십시오.", + "sell_alert_content": "현재 Bitcoin, Ethereum 및 Litecoin의 판매만 지원합니다. Bitcoin, Ethereum 또는 Litecoin 지갑을 생성하거나 전환하십시오.", "outdated_electrum_wallet_description": "Cake에서 생성 된 새로운 비트 코인 지갑에는 이제 24 단어 시드가 있습니다. 새로운 비트 코인 지갑을 생성하고 모든 자금을 새로운 24 단어 지갑으로 이체하고 12 단어 시드가있는 지갑 사용을 중지해야합니다. 자금을 확보하려면 즉시이 작업을 수행하십시오.", "understand": "이해 했어요", "apk_update": "APK 업데이트", @@ -538,6 +538,8 @@ "open_gift_card": "기프트 카드 열기", "contact_support": "지원팀에 문의", "gift_cards_unavailable": "기프트 카드는 현재 Monero, Bitcoin 및 Litecoin을 통해서만 구매할 수 있습니다.", + "background_sync_mode": "백그라운드 동기화 모드", + "sync_all_wallets": "모든 지갑 동기화", "introducing_cake_pay": "소개 Cake Pay!", "cake_pay_learn_more": "앱에서 즉시 기프트 카드를 구매하고 사용하세요!\n자세히 알아보려면 왼쪽에서 오른쪽으로 스와이프하세요.", "automatic": "자동적 인", @@ -632,12 +634,43 @@ "setup_totp_recommended": "TOTP 설정(권장)", "disable_buy": "구매 행동 비활성화", "disable_sell": "판매 조치 비활성화", + "cake_2fa_preset" : "케이크 2FA 프리셋", + "narrow": "좁은", + "normal": "정상", + "aggressive": "지나치게 열심인", + "require_for_assessing_wallet": "지갑 접근을 위해 필요", + "require_for_sends_to_non_contacts" : "비접촉자에게 보내는 데 필요", + "require_for_sends_to_contacts" : "연락처로 보내기에 필요", + "require_for_sends_to_internal_wallets" : "내부 지갑으로 보내는 데 필요", + "require_for_exchanges_to_internal_wallets" : "내부 지갑으로의 교환에 필요", + "require_for_adding_contacts" : "연락처 추가에 필요", + "require_for_creating_new_wallets" : "새 지갑 생성에 필요", + "require_for_all_security_and_backup_settings" : "모든 보안 및 백업 설정에 필요", "available_balance_description": "이 지갑에서 사용할 수 있는 잔액입니다. 이 잔액은 블록체인에서 가져온 것이며, Cake Wallet이 사용할 수 없습니다.", "syncing_wallet_alert_title": "지갑 동기화 중", "syncing_wallet_alert_content": "상단에 \"동기화됨\"이라고 표시될 때까지 잔액 및 거래 목록이 완전하지 않을 수 있습니다. 자세히 알아보려면 클릭/탭하세요.", + "home_screen_settings": "홈 화면 설정", + "sort_by": "정렬 기준", + "search_add_token": "검색 / 토큰 추가", + "edit_token": "토큰 편집", + "warning": "경고", + "add_token_warning": "사기꾼의 지시에 따라 토큰을 편집하거나 추가하지 마십시오.\n항상 신뢰할 수 있는 출처를 통해 토큰 주소를 확인하세요!", + "add_token_disclaimer_check": "신뢰할 수 있는 출처를 통해 토큰 컨트랙트 주소와 정보를 확인했습니다. 악의적이거나 잘못된 정보를 추가하면 자금 손실이 발생할 수 있습니다.", + "token_contract_address": "토큰 계약 주소", + "token_name": "토큰 이름 예: Tether", + "token_symbol": "토큰 기호 예: USDT", + "token_decimal": "토큰 십진수", + "field_required": "이 필드는 필수입니다", + "pin_at_top": "상단에 ${token} 고정", + "invalid_input": "잘못된 입력", + "fiat_balance": "피아트 잔액", + "gross_balance": "총 잔액", + "alphabetical": "알파벳순", "generate_name": "이름 생성", "balance_page": "잔액 페이지", "share": "공유하다", "slidable": "슬라이딩 가능", + "manage_nodes": "노드 관리", + "etherscan_history": "이더스캔 역사", "template_name": "템플릿 이름" } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 2ee527cee..c9210077a 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -1,7 +1,7 @@ { "welcome": "မှကြိုဆိုပါတယ်။", "cake_wallet": "Cake ပိုက်ဆံအိတ်", - "first_wallet_text": "Monero၊ Bitcoin၊ Litecoin နှင့် Haven အတွက် အလွန်ကောင်းမွန်သော ပိုက်ဆံအိတ်", + "first_wallet_text": "Monero၊ Bitcoin၊ Ethereum၊ Litecoin နှင့် Haven အတွက် အလွန်ကောင်းမွန်သော ပိုက်ဆံအိတ်", "please_make_selection": "သင့်ပိုက်ဆံအိတ်ကို ဖန်တီးရန် သို့မဟုတ် ပြန်လည်ရယူရန် အောက်တွင် ရွေးချယ်မှုတစ်ခု ပြုလုပ်ပါ။", "create_new": "Wallet အသစ်ဖန်တီးပါ။", "restore_wallet": "ပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "လက်ခံသူလိပ်စာများ", "wallet_list_title": "Monero ပိုက်ဆံအိတ်", "wallet_list_create_new_wallet": "Wallet အသစ်ဖန်တီးပါ။", - "wallet_list_edit_wallet" : "ပိုက်ဆံအိတ်ကို တည်းဖြတ်ပါ။", - "wallet_list_wallet_name" : "ပိုက်ဆံအိတ်နာမည်", + "wallet_list_edit_wallet": "ပိုက်ဆံအိတ်ကို တည်းဖြတ်ပါ။", + "wallet_list_wallet_name": "ပိုက်ဆံအိတ်နာမည်", "wallet_list_restore_wallet": "ပိုက်ဆံအိတ်ကို ပြန်ယူပါ။", "wallet_list_load_wallet": "ပိုက်ဆံအိတ်ကို တင်ပါ။", "wallet_list_loading_wallet": "${wallet_name} ပိုက်ဆံအိတ်ကို ဖွင့်နေသည်။", @@ -395,8 +395,8 @@ "unconfirmed": "အတည်မပြုနိုင်သော လက်ကျန်ငွေ", "displayable": "ပြသနိုင်သည်။", "submit_request": "တောင်းဆိုချက်တစ်ခုတင်ပြပါ။", - "buy_alert_content": "လောလောဆယ်တွင် ကျွန်ုပ်တို့သည် Bitcoin၊ Litecoin နှင့် Monero တို့ကိုသာ ဝယ်ယူမှုကို ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Litecoin သို့မဟုတ် Monero ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", - "sell_alert_content": "ကျွန်ုပ်တို့သည် လက်ရှိတွင် Bitcoin နှင့် Litecoin ရောင်းချခြင်းကိုသာ ထောက်ခံပါသည်။ သင်၏ Bitcoin သို့မဟုတ် Litecoin ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", + "buy_alert_content": "လက်ရှိတွင် ကျွန်ုပ်တို့သည် Bitcoin၊ Ethereum၊ Litecoin နှင့် Monero တို့ကိုသာ ဝယ်ယူမှုကို ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Ethereum၊ Litecoin သို့မဟုတ် Monero ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", + "sell_alert_content": "ကျွန်ုပ်တို့သည် လက်ရှိတွင် Bitcoin၊ Ethereum နှင့် Litecoin ရောင်းချခြင်းကိုသာ ပံ့ပိုးပေးပါသည်။ သင်၏ Bitcoin၊ Ethereum သို့မဟုတ် Litecoin ပိုက်ဆံအိတ်ကို ဖန်တီးပါ သို့မဟုတ် ပြောင်းပါ။", "outdated_electrum_wallet_description": "ယခု Cake တွင်ဖန်တီးထားသော Bitcoin ပိုက်ဆံအိတ်အသစ်တွင် စကားလုံး 24 မျိုးရှိသည်။ Bitcoin ပိုက်ဆံအိတ်အသစ်တစ်ခုကို ဖန်တီးပြီး သင့်ငွေအားလုံးကို 24 စကားလုံးပိုက်ဆံအိတ်အသစ်သို့ လွှဲပြောင်းပြီး 12 စကားလုံးမျိုးစေ့ဖြင့် ပိုက်ဆံအိတ်များကို အသုံးပြုခြင်းကို ရပ်တန့်ရန် မဖြစ်မနေလိုအပ်ပါသည်။ သင့်ရန်ပုံငွေများကို လုံခြုံစေရန်အတွက် ၎င်းကိုချက်ချင်းလုပ်ဆောင်ပါ။", "understand": "ကျွန်တော်နားလည်ပါတယ်", "apk_update": "APK အပ်ဒိတ်", @@ -632,12 +632,43 @@ "setup_totp_recommended": "TOTP ကို စနစ်ထည့်သွင်းပါ (အကြံပြုထားသည်)", "disable_buy": "ဝယ်ယူမှု လုပ်ဆောင်ချက်ကို ပိတ်ပါ။", "disable_sell": "ရောင်းချခြင်းလုပ်ဆောင်ချက်ကို ပိတ်ပါ။", + "cake_2fa_preset" : "ကိတ်မုန့် 2FA ကြိုတင်သတ်မှတ်", + "narrow": "ကျဉ်းသော", + "normal": "ပုံမှန်", + "aggressive": "စိတ်အားထက်သန်ခြင်း။", + "require_for_assessing_wallet": "ပိုက်ဆံအိတ်ကို ဝင်သုံးရန် လိုအပ်သည်။", + "require_for_sends_to_non_contacts" : "အဆက်အသွယ်မရှိသူများထံ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_sends_to_contacts" : "အဆက်အသွယ်များထံ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_sends_to_internal_wallets" : "အတွင်းပိုင်း ပိုက်ဆံအိတ်များသို့ ပေးပို့ရန် လိုအပ်သည်။", + "require_for_exchanges_to_internal_wallets" : "အတွင်းပိုင်းပိုက်ဆံအိတ်များသို့ လဲလှယ်ရန် လိုအပ်သည်။", + "require_for_adding_contacts" : "အဆက်အသွယ်များထည့်ရန် လိုအပ်သည်။", + "require_for_creating_new_wallets" : "ပိုက်ဆံအိတ်အသစ်များ ဖန်တီးရန် လိုအပ်သည်။", + "require_for_all_security_and_backup_settings" : "လုံခြုံရေးနှင့် အရန်ဆက်တင်များအားလုံးအတွက် လိုအပ်ပါသည်။", "available_balance_description": "သင့်ရဲ့ အကောင့်တွင် ရရှိနိုင်သော ငွေကျန်ငွေကို ပြန်လည်ပေးသွင်းပါ။", "syncing_wallet_alert_title": "သင့်ပိုက်ဆံအိတ်ကို စင့်ခ်လုပ်နေပါသည်။", "syncing_wallet_alert_content": "သင်၏လက်ကျန်နှင့် ငွေပေးငွေယူစာရင်းသည် ထိပ်တွင် \"Synchronizeed\" ဟုပြောသည်အထိ မပြီးမြောက်နိုင်ပါ။ ပိုမိုလေ့လာရန် နှိပ်/နှိပ်ပါ။", + "home_screen_settings": "ပင်မစခရင် ဆက်တင်များ", + "sort_by": "အလိုက်စဥ်သည်", + "search_add_token": "ရှာဖွေရန် / တိုကင်ထည့်ပါ။", + "edit_token": "တိုကင်ကို တည်းဖြတ်ပါ။", + "warning": "သတိပေးချက်", + "add_token_warning": "လိမ်လည်သူများ ညွှန်ကြားထားသည့်အတိုင်း တိုကင်များကို တည်းဖြတ်ခြင်း သို့မဟုတ် မထည့်ပါနှင့်။\nဂုဏ်သိက္ခာရှိသော အရင်းအမြစ်များဖြင့် အမြဲတမ်း တိုကင်လိပ်စာများကို အတည်ပြုပါ။", + "add_token_disclaimer_check": "ဂုဏ်သိက္ခာရှိသော အရင်းအမြစ်ကို အသုံးပြု၍ တိုကင်စာချုပ်လိပ်စာနှင့် အချက်အလက်ကို ကျွန်ုပ်အတည်ပြုပြီးဖြစ်သည်။ အန္တရာယ်ရှိသော သို့မဟုတ် မမှန်ကန်သော အချက်အလက်များကို ထည့်သွင်းခြင်းသည် ရန်ပုံငွေများ ဆုံးရှုံးသွားနိုင်သည်။", + "token_contract_address": "တိုကင်စာချုပ်လိပ်စာ", + "token_name": "တိုကင်အမည် ဥပမာ- Tether", + "token_symbol": "တိုကင်သင်္ကေတ ဥပမာ- USDT", + "token_decimal": "တိုကင်ဒဿမ", + "field_required": "ဤစာကွက်လပ်မှာဖြည့်ရန်လိုအပ်ပါသည်", + "pin_at_top": "အပေါ်တွင် ${token} ပင်ထိုးပါ။", + "invalid_input": "ထည့်သွင်းမှု မမှန်ကန်ပါ။", + "fiat_balance": "Fiat Balance", + "gross_balance": "စုစုပေါင်းလက်ကျန်ငွေ", + "alphabetical": "အက္ခရာစဉ်", "generate_name": "အမည်ဖန်တီးပါ။", "balance_page": "လက်ကျန်စာမျက်နှာ", "share": "မျှဝေပါ။", "slidable": "လျှောချနိုင်သည်။", + "manage_nodes": "ဆုံမှတ်များကို စီမံပါ။", + "etherscan_history": "Etherscan သမိုင်း", "template_name": "နမူနာပုံစံ" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 8b14ae06d..1a11ceb19 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -1,7 +1,7 @@ { "welcome": "Welkom bij", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Geweldige portemonnee voor Monero, Bitcoin, Litecoin, en Haven", + "first_wallet_text": "Geweldige portemonnee voor Monero, Bitcoin, Ethereum, Litecoin, en Haven", "please_make_selection": "Maak hieronder uw keuze tot maak of herstel je portemonnee.", "create_new": "Maak een nieuwe portemonnee", "restore_wallet": "Portemonnee herstellen", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adressen van ontvangers", "wallet_list_title": "Monero portemonnee", "wallet_list_create_new_wallet": "Maak een nieuwe portemonnee", - "wallet_list_edit_wallet" : "Portemonnee bewerken", - "wallet_list_wallet_name" : "Portemonnee naam", + "wallet_list_edit_wallet": "Portemonnee bewerken", + "wallet_list_wallet_name": "Portemonnee naam", "wallet_list_restore_wallet": "Portemonnee herstellen", "wallet_list_load_wallet": "Portemonnee laden", "wallet_list_loading_wallet": "Bezig met laden ${wallet_name} portemonnee", @@ -395,8 +395,8 @@ "unconfirmed": "Onbevestigd saldo", "displayable": "Weer te geven", "submit_request": "een verzoek indienen", - "buy_alert_content": "Momenteel ondersteunen we alleen de aankoop van Bitcoin, Litecoin en Monero. Maak of schakel over naar uw Bitcoin-, Litecoin- of Monero-portemonnee.", - "sell_alert_content": "We ondersteunen momenteel alleen de verkoop van Bitcoin en Litecoin. Maak of schakel over naar uw Bitcoin- of Litecoin-portemonnee.", + "buy_alert_content": "Momenteel ondersteunen we alleen de aankoop van Bitcoin, Ethereum, Litecoin en Monero. Maak of schakel over naar uw Bitcoin-, Ethereum-, Litecoin- of Monero-portemonnee.", + "sell_alert_content": "We ondersteunen momenteel alleen de verkoop van Bitcoin, Ethereum en Litecoin. Maak of schakel over naar uw Bitcoin-, Ethereum- of Litecoin-portemonnee.", "outdated_electrum_wallet_description": "Nieuwe Bitcoin-portefeuilles die in Cake zijn gemaakt, hebben nu een zaadje van 24 woorden. Het is verplicht dat u een nieuwe Bitcoin-portemonnee maakt en al uw geld overmaakt naar de nieuwe portemonnee van 24 woorden, en stopt met het gebruik van wallets met een seed van 12 woorden. Doe dit onmiddellijk om uw geld veilig te stellen.", "understand": "Ik begrijp het", "apk_update": "APK-update", @@ -538,6 +538,8 @@ "open_gift_card": "Geschenkkaart openen", "contact_support": "Contact opnemen met ondersteuning", "gift_cards_unavailable": "Cadeaubonnen kunnen momenteel alleen worden gekocht via Monero, Bitcoin en Litecoin", + "background_sync_mode": "Achtergrondsynchronisatiemodus", + "sync_all_wallets": "Alle portemonnees synchroniseren", "introducing_cake_pay": "Introductie van Cake Pay!", "cake_pay_learn_more": "Koop en wissel cadeaubonnen direct in de app in!\nSwipe van links naar rechts voor meer informatie.", "automatic": "automatisch", @@ -632,12 +634,43 @@ "setup_totp_recommended": "TOTP instellen (aanbevolen)", "disable_buy": "Koopactie uitschakelen", "disable_sell": "Verkoopactie uitschakelen", + "cake_2fa_preset" : "Taart 2FA Voorinstelling", + "narrow": "Smal", + "normal": "Normaal", + "aggressive": "Overijverig", + "require_for_assessing_wallet": "Vereist voor toegang tot portemonnee", + "require_for_sends_to_non_contacts" : "Vereist voor verzendingen naar niet-contacten", + "require_for_sends_to_contacts" : "Vereist voor verzending naar contacten", + "require_for_sends_to_internal_wallets" : "Vereist voor verzendingen naar interne portefeuilles", + "require_for_exchanges_to_internal_wallets" : "Vereist voor uitwisselingen naar interne portefeuilles", + "require_for_adding_contacts" : "Vereist voor het toevoegen van contacten", + "require_for_creating_new_wallets" : "Vereist voor het maken van nieuwe portefeuilles", + "require_for_all_security_and_backup_settings" : "Vereist voor alle beveiligings- en back-upinstellingen", "available_balance_description": "Beschikbaar saldo is het saldo dat u kunt uitgeven. Het kan lager zijn dan uw totale saldo als u onlangs geld hebt verzonden.", "syncing_wallet_alert_title": "Uw portemonnee wordt gesynchroniseerd", "syncing_wallet_alert_content": "Uw saldo- en transactielijst is mogelijk pas compleet als er bovenaan 'GESYNCHRONISEERD' staat. Klik/tik voor meer informatie.", + "home_screen_settings": "Instellingen voor het startscherm", + "sort_by": "Sorteer op", + "search_add_token": "Token zoeken / toevoegen", + "edit_token": "Token bewerken", + "warning": "Waarschuwing", + "add_token_warning": "Bewerk of voeg geen tokens toe volgens de instructies van oplichters.\nBevestig tokenadressen altijd met betrouwbare bronnen!", + "add_token_disclaimer_check": "Ik heb het adres en de informatie van het tokencontract bevestigd met behulp van een betrouwbare bron. Het toevoegen van kwaadaardige of onjuiste informatie kan leiden tot verlies van geld.", + "token_contract_address": "Token contractadres", + "token_name": "Tokennaam bijv.: Tether", + "token_symbol": "Tokensymbool bijv.: USDT", + "token_decimal": "Token decimaal", + "field_required": "dit veld is verplicht", + "pin_at_top": "speld ${token} bovenaan", + "invalid_input": "Ongeldige invoer", + "fiat_balance": "Fiat Balans", + "gross_balance": "Bruto saldo", + "alphabetical": "Alfabetisch", "generate_name": "Naam genereren", "balance_page": "Saldo pagina", "share": "Deel", "slidable": "Verschuifbaar", + "manage_nodes": "Beheer knooppunten", + "etherscan_history": "Etherscan-geschiedenis", "template_name": "Sjabloonnaam" } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index c93305648..26543da46 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -1,7 +1,7 @@ { "welcome": "Witamy w", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Świetny portfel na Monero, Bitcoin, Litecoin, i Haven", + "first_wallet_text": "Świetny portfel na Monero, Bitcoin, Ethereum, Litecoin, i Haven", "please_make_selection": "Wybierz poniżej, aby utworzyć lub przywrócić swój portfel.", "create_new": "Utwórz nowy portfel", "restore_wallet": "Przywróć portfel", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Adres odbiorcy", "wallet_list_title": "Portfel Monero", "wallet_list_create_new_wallet": "Utwórz nowy portfel", - "wallet_list_edit_wallet" : "Edytuj portfel", - "wallet_list_wallet_name" : "Nazwa portfela", + "wallet_list_edit_wallet": "Edytuj portfel", + "wallet_list_wallet_name": "Nazwa portfela", "wallet_list_restore_wallet": "Przywróć portfel", "wallet_list_load_wallet": "Załaduj portfel", "wallet_list_loading_wallet": "Ładuję ${wallet_name} portfel", @@ -395,8 +395,8 @@ "unconfirmed": "Niepotwierdzone saldo", "displayable": "Wyświetlane", "submit_request": "Złóż wniosek", - "buy_alert_content": "Obecnie obsługujemy tylko zakup Bitcoin, Litecoin i Monero. Utwórz lub przełącz się na swój portfel Bitcoin, Litecoin lub Monero.", - "sell_alert_content": "Obecnie obsługujemy tylko sprzedaż Bitcoin i Litecoin. Utwórz lub przełącz się na swój portfel Bitcoin lub Litecoin.", + "buy_alert_content": "Obecnie obsługujemy tylko zakup Bitcoin, Ethereum, Litecoin i Monero. Utwórz lub przełącz się na swój portfel Bitcoin, Ethereum, Litecoin lub Monero.", + "sell_alert_content": "Obecnie obsługujemy tylko sprzedaż Bitcoin, Ethereum i Litecoin. Utwórz lub przełącz się na swój portfel Bitcoin, Ethereum lub Litecoin.", "outdated_electrum_wallet_description": "Nowe portfele Bitcoin utworzone w Cake mają teraz fraze seed składające się z 24 słów. Konieczne jest utworzenie nowego portfela Bitcoin i przeniesienie wszystkich środków do nowego portfela na 24 słowa oraz zaprzestanie korzystania z portfeli z frazą seed na 12 słów. Zrób to natychmiast, aby zabezpieczyć swoje fundusze.", "understand": "Rozumiem", "apk_update": "Aktualizacja APK", @@ -538,6 +538,8 @@ "open_gift_card": "Otwórz kartę podarunkową", "contact_support": "Skontaktuj się z pomocą techniczną", "gift_cards_unavailable": "Karty podarunkowe można obecnie kupić tylko za pośrednictwem Monero, Bitcoin i Litecoin", + "background_sync_mode": "Tryb synchronizacji w tle", + "sync_all_wallets": "Synchronizuj wszystkie portfele", "introducing_cake_pay": "Przedstawiamy Cake Pay!", "cake_pay_learn_more": "Kupuj i wykorzystuj karty podarunkowe od razu w aplikacji!\nPrzesuń od lewej do prawej, aby dowiedzieć się więcej.", "automatic": "Automatyczny", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Skonfiguruj TOTP (zalecane)", "disable_buy": "Wyłącz akcję kupna", "disable_sell": "Wyłącz akcję sprzedaży", + "cake_2fa_preset" : "Ciasto 2FA Preset", + "narrow": "Wąski", + "normal": "Normalna", + "aggressive": "Nadgorliwy", + "require_for_assessing_wallet": "Wymagaj dostępu do portfela", + "require_for_sends_to_non_contacts" : "Wymagaj wysyłania do osób niekontaktowych", + "require_for_sends_to_contacts" : "Wymagaj wysyłania do kontaktów", + "require_for_sends_to_internal_wallets" : "Wymagaj wysyłania do portfeli wewnętrznych", + "require_for_exchanges_to_internal_wallets" : "Wymagaj wymiany do portfeli wewnętrznych", + "require_for_adding_contacts" : "Wymagane do dodania kontaktów", + "require_for_creating_new_wallets" : "Wymagane do tworzenia nowych portfeli", + "require_for_all_security_and_backup_settings" : "Wymagaj dla wszystkich ustawień zabezpieczeń i kopii zapasowych", "available_balance_description": "Dostępne saldo jest równoważne z saldem portfela minus zamrożone saldo.", "syncing_wallet_alert_title": "Twój portfel się synchronizuje", "syncing_wallet_alert_content": "Twoje saldo i lista transakcji mogą nie być kompletne, dopóki u góry nie pojawi się napis „SYNCHRONIZOWANY”. Kliknij/stuknij, aby dowiedzieć się więcej.", + "home_screen_settings": "Ustawienia ekranu głównego", + "sort_by": "Sortuj według", + "search_add_token": "Wyszukaj / Dodaj token", + "edit_token": "Edytuj token", + "warning": "Ostrzeżenie", + "add_token_warning": "Nie edytuj ani nie dodawaj tokenów zgodnie z instrukcjami oszustów.\nZawsze potwierdzaj adresy tokenów z renomowanymi źródłami!", + "add_token_disclaimer_check": "Potwierdziłem adres kontraktu tokena i informacje, korzystając z renomowanego źródła. Dodanie złośliwych lub niepoprawnych informacji może spowodować utratę środków.", + "token_contract_address": "Adres kontraktu tokena", + "token_name": "Nazwa tokena, np.: Tether", + "token_symbol": "Symbol tokena np.: USDT", + "token_decimal": "Token dziesiętny", + "field_required": "To pole jest wymagane", + "pin_at_top": "przypnij ${token} na górze", + "invalid_input": "Nieprawidłowe dane wejściowe", + "fiat_balance": "Bilans Fiata", + "gross_balance": "Saldo brutto", + "alphabetical": "Alfabetyczny", "generate_name": "Wygeneruj nazwę", "balance_page": "Strona salda", "share": "Udział", "slidable": "Przesuwne", + "manage_nodes": "Zarządzaj węzłami", + "etherscan_history": "Historia Etherscanu", "template_name": "Nazwa szablonu" } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 8af35a20e..ac76d4df8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -1,7 +1,7 @@ { "welcome": "Bem-vindo ao", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Carteira incrível para Monero, Bitcoin, Litecoin, e Haven", + "first_wallet_text": "Carteira incrível para Monero, Bitcoin, Ethereum, Litecoin, e Haven", "please_make_selection": "Escolha se quer criar uma carteira nova ou restaurar uma antiga.", "create_new": "Criar nova carteira", "restore_wallet": "Restaurar carteira", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Endereços de destinatários", "wallet_list_title": "Carteira Monero", "wallet_list_create_new_wallet": "Criar nova carteira", - "wallet_list_edit_wallet" : "Editar carteira", - "wallet_list_wallet_name" : "Nome da carteira", + "wallet_list_edit_wallet": "Editar carteira", + "wallet_list_wallet_name": "Nome da carteira", "wallet_list_restore_wallet": "Restaurar carteira", "wallet_list_load_wallet": "Abrir carteira", "wallet_list_loading_wallet": "Abrindo a carteira ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "Saldo não confirmado", "displayable": "Exibível", "submit_request": "enviar um pedido", - "buy_alert_content": "Atualmente, oferecemos suporte apenas à compra de Bitcoin, Litecoin e Monero. Crie ou troque para sua carteira Bitcoin, Litecoin ou Monero.", - "sell_alert_content": "Atualmente, oferecemos suporte apenas à venda de Bitcoin e Litecoin. Por favor, crie ou mude para sua carteira Bitcoin ou Litecoin.", + "buy_alert_content": "Atualmente, oferecemos suporte apenas à compra de Bitcoin, Ethereum, Litecoin e Monero. Crie ou troque para sua carteira Bitcoin, Ethereum, Litecoin ou Monero.", + "sell_alert_content": "Atualmente, oferecemos suporte apenas à venda de Bitcoin, Ethereum e Litecoin. Crie ou troque para sua carteira Bitcoin, Ethereum ou Litecoin.", "outdated_electrum_wallet_description": "As novas carteiras Bitcoin criadas no Cake agora têm uma semente de 24 palavras. É obrigatório que você crie uma nova carteira Bitcoin e transfira todos os seus fundos para a nova carteira de 24 palavras, e pare de usar carteiras com semente de 12 palavras. Faça isso imediatamente para garantir seus fundos.", "understand": "Entendo", "apk_update": "Atualização de APK", @@ -537,6 +537,8 @@ "open_gift_card": "Abrir vale-presente", "contact_support": "Contatar Suporte", "gift_cards_unavailable": "Os cartões-presente estão disponíveis para compra apenas através do Monero, Bitcoin e Litecoin no momento", + "background_sync_mode": "Modo de sincronização em segundo plano", + "sync_all_wallets": "Sincronize todas as carteiras", "introducing_cake_pay": "Apresentando o Cake Pay!", "cake_pay_learn_more": "Compre e resgate vales-presente instantaneamente no app!\nDeslize da esquerda para a direita para saber mais.", "automatic": "Automático", @@ -631,12 +633,43 @@ "setup_totp_recommended": "Configurar TOTP (recomendado)", "disable_buy": "Desativar ação de compra", "disable_sell": "Desativar ação de venda", + "cake_2fa_preset" : "Predefinição de bolo 2FA", + "narrow": "Estreito", + "normal": "Normal", + "aggressive": "excessivamente zeloso", + "require_for_assessing_wallet": "Requer para acessar a carteira", + "require_for_sends_to_non_contacts" : "Exigir para envios para não-contatos", + "require_for_sends_to_contacts" : "Exigir para envios para contatos", + "require_for_sends_to_internal_wallets" : "Exigir envios para carteiras internas", + "require_for_exchanges_to_internal_wallets" : "Requer trocas para carteiras internas", + "require_for_adding_contacts" : "Requer para adicionar contatos", + "require_for_creating_new_wallets" : "Requer para criar novas carteiras", + "require_for_all_security_and_backup_settings" : "Exigir todas as configurações de segurança e backup", "available_balance_description": "Seu saldo disponível é o saldo total menos o saldo congelado. O saldo congelado é o saldo que você não pode gastar, mas que ainda não foi confirmado na blockchain. O saldo congelado é geralmente o resultado de transações recentes.", "syncing_wallet_alert_title": "Sua carteira está sincronizando", "syncing_wallet_alert_content": "Seu saldo e lista de transações podem não estar completos até que diga “SYNCHRONIZED” no topo. Clique/toque para saber mais.", + "home_screen_settings": "Configurações da tela inicial", + "sort_by": "Ordenar por", + "search_add_token": "Pesquisar / Adicionar token", + "edit_token": "Editar símbolo", + "warning": "Aviso", + "add_token_warning": "Não edite ou adicione tokens de acordo com as instruções dos golpistas.\nSempre confirme os endereços de token com fontes confiáveis!", + "add_token_disclaimer_check": "Confirmei o endereço e as informações do contrato de token usando uma fonte confiável. Adicionar informações maliciosas ou incorretas pode resultar em perda de fundos.", + "token_contract_address": "Endereço do contrato de token", + "token_name": "Nome do token, por exemplo: Tether", + "token_symbol": "Símbolo de token, por exemplo: USDT", + "token_decimal": "Token decimal", + "field_required": "Este campo é obrigatório", + "pin_at_top": "fixe ${token} no topo", + "invalid_input": "Entrada inválida", + "fiat_balance": "Equilíbrio Fiat", + "gross_balance": "Saldo Bruto", + "alphabetical": "alfabética", "generate_name": "Gerar nome", "balance_page": "Página de saldo", "share": "Compartilhar", "slidable": "Deslizável", + "manage_nodes": "Gerenciar nós", + "etherscan_history": "história Etherscan", "template_name": "Nome do modelo" } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 03e0fbbd5..c0022c0e4 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -1,7 +1,7 @@ { "welcome": "Приветствуем в", "cake_wallet": "Cake Wallet", - "first_wallet_text": "В самом удобном кошельке для Monero, Bitcoin, Litecoin, и Haven", + "first_wallet_text": "В самом удобном кошельке для Monero, Bitcoin, Ethereum, Litecoin, и Haven", "please_make_selection": "Выберите способ создания кошелька: создать новый или восстановить ваш существующий.", "create_new": "Создать новый кошелёк", "restore_wallet": "Восстановить кошелёк", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Адреса получателей", "wallet_list_title": "Monero Кошелёк", "wallet_list_create_new_wallet": "Создать новый кошелёк", - "wallet_list_edit_wallet" : "Изменить кошелек", - "wallet_list_wallet_name" : "Имя кошелька", + "wallet_list_edit_wallet": "Изменить кошелек", + "wallet_list_wallet_name": "Имя кошелька", "wallet_list_restore_wallet": "Восстановить кошелёк", "wallet_list_load_wallet": "Загрузка кошелька", "wallet_list_loading_wallet": "Загрузка ${wallet_name} кошелька", @@ -396,8 +396,8 @@ "unconfirmed": "Неподтвержденный баланс", "displayable": "Отображаемый", "submit_request": "отправить запрос", - "buy_alert_content": "В настоящее время мы поддерживаем только покупку Bitcoin, Litecoin и Monero. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Litecoin или Monero.", - "sell_alert_content": "В настоящее время мы поддерживаем только продажу биткойнов и лайткойнов. Пожалуйста, создайте или переключитесь на свой биткойн- или лайткойн-кошелек.", + "buy_alert_content": "В настоящее время мы поддерживаем только покупку биткойнов, Ethereum, Litecoin и Monero. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Ethereum, Litecoin или Monero.", + "sell_alert_content": "В настоящее время мы поддерживаем только продажу биткойнов, эфириума и лайткойна. Пожалуйста, создайте или переключитесь на свой кошелек Bitcoin, Ethereum или Litecoin.", "outdated_electrum_wallet_description": "Новые биткойн-кошельки, созданные в Cake, теперь содержат мнемоническую фразу из 24 слов. Вы обязательно должны создать новый биткойн-кошелек и перевести все свои средства в новый кошелек из 24 слов, а также прекратить использование кошельков с мнемонической фразой из 12 слов. Пожалуйста, сделайте это немедленно, чтобы обезопасить свои средства.", "understand": "Понятно", "apk_update": "Обновление APK", @@ -539,6 +539,8 @@ "open_gift_card": "Открыть подарочную карту", "contact_support": "Связаться со службой поддержки", "gift_cards_unavailable": "В настоящее время подарочные карты можно приобрести только через Monero, Bitcoin и Litecoin.", + "background_sync_mode": "Режим фоновой синхронизации", + "sync_all_wallets": "Синхронизировать все кошельки", "introducing_cake_pay": "Представляем Cake Pay!", "cake_pay_learn_more": "Мгновенно покупайте и используйте подарочные карты в приложении!\nПроведите по экрану слева направо, чтобы узнать больше.", "automatic": "автоматический", @@ -633,12 +635,43 @@ "setup_totp_recommended": "Настроить TOTP (рекомендуется)", "disable_buy": "Отключить действие покупки", "disable_sell": "Отключить действие продажи", + "cake_2fa_preset" : "Торт 2FA Preset", + "narrow": "Узкий", + "normal": "Нормальный", + "aggressive": "чрезмерно усердный", + "require_for_assessing_wallet": "Требовать для доступа к кошельку", + "require_for_sends_to_non_contacts" : "Требовать для отправки не контактам", + "require_for_sends_to_contacts" : "Требовать для отправки контактам", + "require_for_sends_to_internal_wallets" : "Требовать отправки на внутренние кошельки", + "require_for_exchanges_to_internal_wallets" : "Требовать для обмена на внутренние кошельки", + "require_for_adding_contacts" : "Требовать добавления контактов", + "require_for_creating_new_wallets" : "Требовать для создания новых кошельков", + "require_for_all_security_and_backup_settings" : "Требовать все настройки безопасности и резервного копирования", "available_balance_description": "Доступный баланс - это средства, которые вы можете использовать для покупки или продажи криптовалюты.", "syncing_wallet_alert_title": "Ваш кошелек синхронизируется", "syncing_wallet_alert_content": "Ваш баланс и список транзакций могут быть неполными, пока вверху не будет написано «СИНХРОНИЗИРОВАНО». Щелкните/коснитесь, чтобы узнать больше.", + "home_screen_settings": "Настройки главного экрана", + "sort_by": "Сортировать по", + "search_add_token": "Поиск / Добавить токен", + "edit_token": "Изменить токен", + "warning": "Предупреждение", + "add_token_warning": "Не редактируйте и не добавляйте токены по указанию мошенников.\nВсегда подтверждайте адреса токенов из авторитетных источников!", + "add_token_disclaimer_check": "Я подтвердил адрес контракта токена и информацию, используя авторитетный источник. Добавление вредоносной или неверной информации может привести к потере средств.", + "token_contract_address": "Адрес контракта токена", + "token_name": "Имя токена, например: Tether", + "token_symbol": "Символ токена, например: USDT", + "token_decimal": "Десятичный токен", + "field_required": "Это поле обязательно к заполнению", + "pin_at_top": "закрепить ${token} вверху", + "invalid_input": "Неверный Ввод", + "fiat_balance": "Фиатный баланс", + "gross_balance": "Валовой баланс", + "alphabetical": "Алфавитный", "generate_name": "Создать имя", "balance_page": "Страница баланса", "share": "Делиться", "slidable": "Скользящий", + "manage_nodes": "Управление узлами", + "etherscan_history": "История Эфириума", "template_name": "Имя Шаблона" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index bb2fcd17e..f9aff7f14 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -1,7 +1,7 @@ { "welcome": "ยินดีต้อนรับสู่", "cake_wallet": "Cake Wallet", - "first_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Monero, Bitcoin, Litecoin และ Haven", + "first_wallet_text": "กระเป๋าสตางค์ที่สวยงามสำหรับ Monero, Bitcoin, Ethereum, Litecoin และ Haven", "please_make_selection": "โปรดเลือกตามด้านล่างเพื่อสร้างหรือกู้กระเป๋าของคุณ", "create_new": "สร้างกระเป๋าใหม่", "restore_wallet": "กู้กระเป๋า", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "ที่อยู่ผู้รับ", "wallet_list_title": "กระเป๋า Monero", "wallet_list_create_new_wallet": "สร้างกระเป๋าใหม่", - "wallet_list_edit_wallet" : "แก้ไขกระเป๋าสตางค์", - "wallet_list_wallet_name" : "ชื่อกระเป๋าสตางค์", + "wallet_list_edit_wallet": "แก้ไขกระเป๋าสตางค์", + "wallet_list_wallet_name": "ชื่อกระเป๋าสตางค์", "wallet_list_restore_wallet": "กู้กระเป๋า", "wallet_list_load_wallet": "โหลดกระเป๋า", "wallet_list_loading_wallet": "กำลังโหลดกระเป๋า ${wallet_name}", @@ -395,8 +395,8 @@ "unconfirmed": "ยอดคงเหลือที่ไม่ได้รับการยืนยัน", "displayable": "สามารถแสดงได้", "submit_request": "ส่งคำขอ", - "buy_alert_content": "ขณะนี้เรารองรับการซื้อ Bitcoin, Litecoin และ Monero เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Litecoin หรือ Monero ของคุณ", - "sell_alert_content": "ขณะนี้เราสนับสนุนการขาย Bitcoin และ Litecoin เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin หรือ Litecoin ของคุณ", + "buy_alert_content": "ขณะนี้เรารองรับการซื้อ Bitcoin, Ethereum, Litecoin และ Monero เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Ethereum, Litecoin หรือ Monero", + "sell_alert_content": "ขณะนี้เรารองรับการขาย Bitcoin, Ethereum และ Litecoin เท่านั้น โปรดสร้างหรือเปลี่ยนเป็นกระเป๋าเงิน Bitcoin, Ethereum หรือ Litecoin ของคุณ", "outdated_electrum_wallet_description": "กระเป๋า Bitcoin ใหม่ที่สร้างใน Cake มี seed ขนาด 24 คำ ซึ่งจำเป็นต้องสร้างกระเป๋า Bitcoin ใหม่และโอนทุกเงินของคุณไปยังกระเป๋าใหม่ขนาด 24 คำ และหยุดใช้กระเป๋าที่มี seed ขนาด 12 คำ กรุณาทำด่วนเพื่อรักษาเงินของคุณ", "understand": "ฉันเข้าใจ", "apk_update": "ปรับปรุง APK", @@ -632,12 +632,43 @@ "setup_totp_recommended": "ตั้งค่า TOTP (แนะนำ)", "disable_buy": "ปิดการใช้งานการซื้อ", "disable_sell": "ปิดการใช้งานการขาย", + "cake_2fa_preset" : "เค้ก 2FA ที่ตั้งไว้ล่วงหน้า", + "narrow": "แคบ", + "normal": "ปกติ", + "aggressive": "กระตือรือร้นมากเกินไป", + "require_for_assessing_wallet": "จำเป็นสำหรับการเข้าถึงกระเป๋าเงิน", + "require_for_sends_to_non_contacts" : "จำเป็นต้องส่งไปยังผู้ที่ไม่ได้ติดต่อ", + "require_for_sends_to_contacts" : "จำเป็นต้องส่งไปยังผู้ติดต่อ", + "require_for_sends_to_internal_wallets" : "จำเป็นต้องส่งไปยังกระเป๋าเงินภายใน", + "require_for_exchanges_to_internal_wallets" : "ต้องการการแลกเปลี่ยนไปยังกระเป๋าเงินภายใน", + "require_for_adding_contacts" : "ต้องการสำหรับการเพิ่มผู้ติดต่อ", + "require_for_creating_new_wallets" : "จำเป็นสำหรับการสร้างกระเป๋าเงินใหม่", + "require_for_all_security_and_backup_settings" : "จำเป็นสำหรับการตั้งค่าความปลอดภัยและการสำรองข้อมูลทั้งหมด", "available_balance_description": "จำนวนเงินที่คุณสามารถใช้ได้ในการซื้อหรือขาย", "syncing_wallet_alert_title": "กระเป๋าสตางค์ของคุณกำลังซิงค์", "syncing_wallet_alert_content": "รายการยอดเงินและธุรกรรมของคุณอาจไม่สมบูรณ์จนกว่าจะมีข้อความว่า “ซิงโครไนซ์” ที่ด้านบน คลิก/แตะเพื่อเรียนรู้เพิ่มเติม่", + "home_screen_settings": "การตั้งค่าหน้าจอหลัก", + "sort_by": "เรียงตาม", + "search_add_token": "ค้นหา / เพิ่มโทเค็น", + "edit_token": "แก้ไขโทเค็น", + "warning": "คำเตือน", + "add_token_warning": "ห้ามแก้ไขหรือเพิ่มโทเค็นตามคำแนะนำของนักต้มตุ๋น\nยืนยันที่อยู่โทเค็นกับแหล่งที่มาที่เชื่อถือได้เสมอ!", + "add_token_disclaimer_check": "ฉันได้ยืนยันที่อยู่และข้อมูลของสัญญาโทเค็นโดยใช้แหล่งข้อมูลที่เชื่อถือได้ การเพิ่มข้อมูลที่เป็นอันตรายหรือไม่ถูกต้องอาจทำให้สูญเสียเงินได้", + "token_contract_address": "ที่อยู่สัญญาโทเค็น", + "token_name": "ชื่อโทเค็น เช่น Tether", + "token_symbol": "สัญลักษณ์โทเค็น เช่น USDT", + "token_decimal": "โทเค็นทศนิยม", + "field_required": "ช่องนี้จำเป็น", + "pin_at_top": "ปักหมุด ${token} ที่ด้านบน", + "invalid_input": "อินพุตไม่ถูกต้อง", + "fiat_balance": "เฟียต บาลานซ์", + "gross_balance": "ยอดคงเหลือ", + "alphabetical": "ตามตัวอักษร", "generate_name": "สร้างชื่อ", "balance_page": "หน้ายอดคงเหลือ", "share": "แบ่งปัน", "slidable": "เลื่อนได้", + "manage_nodes": "จัดการโหนด", + "etherscan_history": "ประวัติอีเธอร์สแกน", "template_name": "ชื่อแม่แบบ" } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 88eec0934..9386a6571 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -1,7 +1,7 @@ { "welcome": "Hoş Geldiniz", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Monero, Bitcoin, Litecoin ve Haven için harika cüzdan", + "first_wallet_text": "Monero, Bitcoin, Ethereum, Litecoin ve Haven için harika cüzdan", "please_make_selection": "Cüzdan oluşturmak veya geri döndürmek için aşağıdan seçim yap.", "create_new": "Yeni Cüzdan Oluştur", "restore_wallet": "Cüzdanı Geri Döndür", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Alıcı adres", "wallet_list_title": "Monero Cüzdanı", "wallet_list_create_new_wallet": "Yeni Cüzdan Oluştur", - "wallet_list_edit_wallet" : "Cüzdanı düzenle", - "wallet_list_wallet_name" : "Cüzdan adı", + "wallet_list_edit_wallet": "Cüzdanı düzenle", + "wallet_list_wallet_name": "Cüzdan adı", "wallet_list_restore_wallet": "Cüzdanı Geri Yükle", "wallet_list_load_wallet": "Cüzdanı yükle", "wallet_list_loading_wallet": "${wallet_name} cüzdanı yükleniyor", @@ -395,8 +395,8 @@ "unconfirmed": "Onaylanmamış Bakiye", "displayable": "Gösterilebilir", "submit_request": "talep gönder", - "buy_alert_content": "Şu anda yalnızca Bitcoin, Litecoin ve Monero satın alımını destekliyoruz. Lütfen Bitcoin, Litecoin veya Monero cüzdanınızı oluşturun veya cüzdanınıza geçiş yapın.", - "sell_alert_content": "Şu anda yalnızca Bitcoin ve Litecoin satışını destekliyoruz. Lütfen Bitcoin veya Litecoin cüzdanınızı oluşturun veya cüzdanınıza geçiş yapın.", + "buy_alert_content": "Şu anda yalnızca Bitcoin, Ethereum, Litecoin ve Monero satın alımını destekliyoruz. Lütfen Bitcoin, Ethereum, Litecoin veya Monero cüzdanınızı oluşturun veya cüzdanınıza geçin.", + "sell_alert_content": "Şu anda yalnızca Bitcoin, Ethereum ve Litecoin satışını destekliyoruz. Lütfen Bitcoin, Ethereum veya Litecoin cüzdanınızı oluşturun veya cüzdanınıza geçin.", "outdated_electrum_wallet_description": "Cake'te oluşturulan yeni Bitcoin cüzdanları artık 24 kelimelik bir tohuma sahip. Yeni bir Bitcoin cüzdanı oluşturmanız ve tüm paranızı 24 kelimelik yeni cüzdana aktarmanız ve 12 kelimelik tohuma sahip cüzdanları kullanmayı bırakmanız zorunludur. Lütfen paranızı güvence altına almak için bunu hemen yapın.", "understand": "Anladım", "apk_update": "APK güncellemesi", @@ -632,12 +632,44 @@ "setup_totp_recommended": "TOTP'yi kurun (Önerilir)", "disable_buy": "Satın alma işlemini devre dışı bırak", "disable_sell": "Satış işlemini devre dışı bırak", + "cake_2fa_preset" : "Kek 2FA Ön Ayarı", + "narrow": "Dar", + "normal": "Normal", + "aggressive": "Aşırı duyarlı", + "require_for_assessing_wallet": "Cüzdana erişmek için gerekli", + "require_for_sends_to_non_contacts" : "Kişi olmayan kişilere göndermeler için gerekli kıl", + "require_for_sends_to_contacts" : "Kişilere göndermeler için gerekli kıl", + "require_for_sends_to_internal_wallets" : "Dahili cüzdanlara yapılan gönderimler için gereklilik", + "require_for_exchanges_to_internal_wallets" : "Dahili cüzdanlara değişim gerektir", + "require_for_adding_contacts" : "Kişi eklemek için gerekli", + "require_for_creating_new_wallets" : "Yeni cüzdan oluşturmak için gerekli", + "require_for_all_security_and_backup_settings" : "Tüm güvenlik ve yedekleme ayarları için iste", + "disable_sell": "Satış işlemini devre dışı bırak", "available_balance_description": "Bu, cüzdanınızda harcayabileceğiniz miktar. Bu miktar, cüzdanınızdan çekilebilecek toplam bakiyeden daha düşük olabilir, çünkü bazı fonlar henüz kullanılamaz durumda olabilir.", "syncing_wallet_alert_title": "Cüzdanınız senkronize ediliyor", "syncing_wallet_alert_content": "Bakiyeniz ve işlem listeniz, en üstte \"SENKRONİZE EDİLDİ\" yazana kadar tamamlanmamış olabilir. Daha fazla bilgi edinmek için tıklayın/dokunun.", + "home_screen_settings": "Ana ekran ayarları", + "sort_by": "Göre sırala", + "search_add_token": "Belirteç Ara / Ekle", + "edit_token": "Belirteci düzenle", + "warning": "Uyarı", + "add_token_warning": "Dolandırıcıların talimatına göre jetonları düzenlemeyin veya eklemeyin.\nBelirteç adreslerini her zaman saygın kaynaklarla onaylayın!", + "add_token_disclaimer_check": "Belirteç sözleşmesi adresini ve bilgilerini saygın bir kaynak kullanarak onayladım. Kötü amaçlı veya yanlış bilgilerin eklenmesi para kaybına neden olabilir.", + "token_contract_address": "Token sözleşme adresi", + "token_name": "Belirteç adı, örneğin: Tether", + "token_symbol": "Jeton sembolü, örneğin: USDT", + "token_decimal": "Belirteç ondalık", + "field_required": "Bu alan gereklidir", + "pin_at_top": "${token} üstte sabitle", + "invalid_input": "Geçersiz Giriş", + "fiat_balance": "Fiat Bakiyesi", + "gross_balance": "Brüt Bakiye", + "alphabetical": "Alfabetik", "generate_name": "İsim Oluştur", "balance_page": "Bakiye Sayfası", "share": "Paylaşmak", "slidable": "kaydırılabilir", + "manage_nodes": "Düğümleri yönet", + "etherscan_history": "Etherscan geçmişi", "template_name": "şablon adı" } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 400d13caa..744691cdd 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -1,7 +1,7 @@ { "welcome": "Вітаємо в", "cake_wallet": "Cake Wallet", - "first_wallet_text": "В самому зручному гаманці для Monero, Bitcoin, Litecoin, та Haven", + "first_wallet_text": "В самому зручному гаманці для Monero, Bitcoin, Ethereum, Litecoin, та Haven", "please_make_selection": "Оберіть спосіб створення гаманця: створити новий чи відновити ваш існуючий.", "create_new": "Створити новий гаманець", "restore_wallet": "Відновити гаманець", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Адреси одержувачів", "wallet_list_title": "Monero Гаманець", "wallet_list_create_new_wallet": "Створити новий гаманець", - "wallet_list_edit_wallet" : "Редагувати гаманець", - "wallet_list_wallet_name" : "Назва гаманця", + "wallet_list_edit_wallet": "Редагувати гаманець", + "wallet_list_wallet_name": "Назва гаманця", "wallet_list_restore_wallet": "Відновити гаманець", "wallet_list_load_wallet": "Завантаження гаманця", "wallet_list_loading_wallet": "Завантаження ${wallet_name} гаманця", @@ -395,8 +395,8 @@ "unconfirmed": "Непідтверджений баланс", "displayable": "Відображуваний", "submit_request": "надіслати запит", - "buy_alert_content": "Наразі ми підтримуємо лише придбання Bitcoin, Litecoin і Monero. Створіть або перейдіть на свій гаманець Bitcoin, Litecoin або Monero.", - "sell_alert_content": "Зараз ми підтримуємо лише продаж біткойнів і лайткоінів. Будь ласка, створіть або перейдіть на свій гаманець Bitcoin або Litecoin.", + "buy_alert_content": "Наразі ми підтримуємо купівлю лише Bitcoin, Ethereum, Litecoin і Monero. Створіть або перейдіть на свій гаманець Bitcoin, Ethereum, Litecoin або Monero.", + "sell_alert_content": "Наразі ми підтримуємо лише продаж Bitcoin, Ethereum і Litecoin. Створіть або перейдіть на свій гаманець Bitcoin, Ethereum або Litecoin.", "outdated_electrum_wallet_description": "Нові біткойн-гаманці, створені в Cake, тепер містять мнемонічну фразу з 24 слів. Обов’язково стовріть новий біткойн-гаманець, переведіть всі кошти на новий гаманець із 24 слів і припиніть використання гаманців із мнемонічною фразою з 12 слів. Зробіть це негайно, щоб убезпечити свої кошти.", "understand": "Зрозуміло", "apk_update": "Оновлення APK", @@ -538,6 +538,8 @@ "open_gift_card": "Відкрити подарункову картку", "contact_support": "Звернутися до служби підтримки", "gift_cards_unavailable": "Наразі подарункові картки можна придбати лише через Monero, Bitcoin і Litecoin", + "background_sync_mode": "Фоновий режим синхронізації", + "sync_all_wallets": "Синхронізувати всі гаманці", "introducing_cake_pay": "Представляємо Cake Pay!", "cake_pay_learn_more": "Миттєво купуйте та активуйте подарункові картки в додатку!\nПроведіть пальцем зліва направо, щоб дізнатися більше.", "automatic": "Автоматичний", @@ -632,12 +634,43 @@ "setup_totp_recommended": "Налаштувати TOTP (рекомендовано)", "disable_buy": "Вимкнути дію покупки", "disable_sell": "Вимкнути дію продажу", + "cake_2fa_preset" : "Торт 2FA Preset", + "narrow": "вузькі", + "normal": "нормальний", + "aggressive": "Надто старанний", + "require_for_assessing_wallet": "Потрібен доступ до гаманця", + "require_for_sends_to_non_contacts" : "Вимагати для надсилання неконтактним особам", + "require_for_sends_to_contacts" : "Вимагати для надсилання контактам", + "require_for_sends_to_internal_wallets" : "Вимагати надсилання на внутрішні гаманці", + "require_for_exchanges_to_internal_wallets" : "Вимагати обміну на внутрішні гаманці", + "require_for_adding_contacts" : "Потрібен для додавання контактів", + "require_for_creating_new_wallets" : "Потрібно для створення нових гаманців", + "require_for_all_security_and_backup_settings" : "Вимагати всіх налаштувань безпеки та резервного копіювання", "available_balance_description": "Це сума, яку ви можете витратити, не включаючи невизначені кошти. Це може бути менше, ніж загальний баланс, якщо ви витратили кошти, які ще не підтверджені.", "syncing_wallet_alert_title": "Ваш гаманець синхронізується", "syncing_wallet_alert_content": "Ваш баланс та список транзакцій може бути неповним, доки вгорі не буде написано «СИНХРОНІЗОВАНО». Натисніть/торкніться, щоб дізнатися більше.", + "home_screen_settings": "Налаштування головного екрана", + "sort_by": "Сортувати за", + "search_add_token": "Пошук / Додати маркер", + "edit_token": "Редагувати маркер", + "warning": "УВАГА", + "add_token_warning": "Не редагуйте та не додавайте токени за вказівками шахраїв.\nЗавжди підтверджуйте адреси токенів у авторитетних джерелах!", + "add_token_disclaimer_check": "Я підтвердив адресу та інформацію щодо договору маркера, використовуючи авторитетне джерело. Додавання зловмисної або невірної інформації може призвести до втрати коштів.", + "token_contract_address": "Адреса договору маркера", + "token_name": "Назва токена, наприклад: Tether", + "token_symbol": "Символ маркера, наприклад: USDT", + "token_decimal": "Токен десятковий", + "field_required": "Це поле є обов'язковим", + "pin_at_top": "закріпити ${token} зверху", + "invalid_input": "Неправильні дані", + "fiat_balance": "Фіат Баланс", + "gross_balance": "Валовий баланс", + "alphabetical": "Алфавітний", "generate_name": "Згенерувати назву", "balance_page": "Сторінка балансу", "share": "Поділіться", "slidable": "Розсувний", + "manage_nodes": "Керуйте вузлами", + "etherscan_history": "Історія Etherscan", "template_name": "Назва шаблону" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 9ccbb6964..f016d56c7 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -1,7 +1,7 @@ { "welcome": "میں خوش آمدید", "cake_wallet": "Cake والیٹ", - "first_wallet_text": "Monero، Bitcoin، Litecoin، اور Haven کے لیے زبردست پرس", + "first_wallet_text": "Monero، Bitcoin، Ethereum، Litecoin، اور Haven کے لیے زبردست پرس", "please_make_selection": "اپنا بٹوہ بنانے یا بازیافت کرنے کے لیے براہ کرم ذیل میں ایک انتخاب کریں۔", "create_new": "نیا والیٹ بنائیں", "restore_wallet": "والیٹ کو بحال کریں۔", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "وصول کنندگان کے پتے", "wallet_list_title": "Monero والیٹ", "wallet_list_create_new_wallet": "نیا والیٹ بنائیں", - "wallet_list_edit_wallet" : "بٹوے میں ترمیم کریں۔", - "wallet_list_wallet_name" : "بٹوے کا نام", + "wallet_list_edit_wallet": "بٹوے میں ترمیم کریں۔", + "wallet_list_wallet_name": "بٹوے کا نام", "wallet_list_restore_wallet": "والیٹ کو بحال کریں۔", "wallet_list_load_wallet": "پرس لوڈ کریں۔", "wallet_list_loading_wallet": "${wallet_name} والیٹ لوڈ ہو رہا ہے۔", @@ -396,8 +396,8 @@ "unconfirmed": "غیر تصدیق شدہ بیلنس", "displayable": "قابل نمائش", "submit_request": "درخواست بھیج دو", - "buy_alert_content": "فی الحال ہم صرف Bitcoin، Litecoin، اور Monero کی خریداری کی حمایت کرتے ہیں۔ براہ کرم اپنا Bitcoin، Litecoin، یا Monero والیٹ بنائیں یا اس پر سوئچ کریں۔", - "sell_alert_content": "ہم فی الحال صرف Bitcoin اور Litecoin کی فروخت کی حمایت کرتے ہیں۔ براہ کرم اپنا Bitcoin یا Litecoin والیٹ بنائیں یا اس پر سوئچ کریں۔", + "buy_alert_content": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﭧﯿﻟﺍﻭ Monero ﺎﯾ ،Bitcoin، Ethereum، Litecoin ﺎﻨﭘﺍ ﻡ", + "sell_alert_content": "۔ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﺱﺍ ﺎﯾ ﮟﯿﺋﺎﻨﺑ ﭧﯿﻟﺍﻭ Litecoin ﺎﯾ Bitcoin، Ethereum ﺎﻨﭘﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", "outdated_electrum_wallet_description": "Cake میں بنائے گئے نئے Bitcoin بٹوے میں اب 24 الفاظ کا بیج ہے۔ یہ لازمی ہے کہ آپ ایک نیا Bitcoin والیٹ بنائیں اور اپنے تمام فنڈز کو نئے 24 الفاظ والے والیٹ میں منتقل کریں، اور 12 الفاظ کے بیج والے بٹوے کا استعمال بند کریں۔ براہ کرم اپنے فنڈز کو محفوظ بنانے کے لیے فوری طور پر ایسا کریں۔", "understand": "میں سمجھتا ہوں۔", "apk_update": "APK اپ ڈیٹ", @@ -626,12 +626,43 @@ "setup_totp_recommended": "TOTP ترتیب دیں (تجویز کردہ)", "disable_buy": "خرید ایکشن کو غیر فعال کریں۔", "disable_sell": "فروخت کی کارروائی کو غیر فعال کریں۔", + "cake_2fa_preset" : "کیک 2FA پیش سیٹ", + "narrow": "تنگ", + "normal": "نارمل", + "aggressive": "حد سے زیادہ پرجوش", + "require_for_assessing_wallet": "بٹوے تک رسائی کے لیے درکار ہے۔", + "require_for_sends_to_non_contacts" : "غیر رابطوں کو بھیجنے کی ضرورت ہے۔", + "require_for_sends_to_contacts" : "رابطوں کو بھیجنے کی ضرورت ہے۔", + "require_for_sends_to_internal_wallets" : "اندرونی بٹوے پر بھیجنے کے لیے درکار ہے۔", + "require_for_exchanges_to_internal_wallets" : "اندرونی بٹوے میں تبادلے کی ضرورت ہے۔", + "require_for_adding_contacts" : "رابطوں کو شامل کرنے کی ضرورت ہے۔", + "require_for_creating_new_wallets" : "نئے بٹوے بنانے کی ضرورت ہے۔", + "require_for_all_security_and_backup_settings" : "تمام سیکورٹی اور بیک اپ کی ترتیبات کے لیے درکار ہے۔", "available_balance_description": "”دستیاب بیلنس” یا ”تصدیق شدہ بیلنس” وہ فنڈز ہیں جو فوری طور پر خرچ کیے جا سکتے ہیں۔ اگر فنڈز کم بیلنس میں ظاہر ہوتے ہیں لیکن اوپر کے بیلنس میں نہیں، تو آپ کو مزید نیٹ ورک کی تصدیقات حاصل کرنے کے لیے آنے والے فنڈز کے لیے چند منٹ انتظار کرنا چاہیے۔ مزید تصدیق حاصل کرنے کے بعد، وہ قابل خرچ ہوں گے۔", "syncing_wallet_alert_title": "آپ کا بٹوہ مطابقت پذیر ہو رہا ہے۔", "syncing_wallet_alert_content": "آپ کے بیلنس اور لین دین کی فہرست اس وقت تک مکمل نہیں ہو سکتی جب تک کہ یہ سب سے اوپر \"SYNCRONIZED\" نہ کہے۔ مزید جاننے کے لیے کلک/تھپتھپائیں۔", + "home_screen_settings": "ہوم اسکرین کی ترتیبات", + "sort_by": "ترتیب دیں", + "search_add_token": "تلاش کریں / ٹوکن شامل کریں۔", + "edit_token": "ٹوکن میں ترمیم کریں۔", + "warning": "وارننگ", + "add_token_warning": "سکیمرز کی ہدایت کے مطابق ٹوکن میں ترمیم یا اضافہ نہ کریں۔\nہمیشہ معتبر ذرائع سے ٹوکن پتوں کی تصدیق کریں!", + "add_token_disclaimer_check": "میں نے ایک معتبر ذریعہ کا استعمال کرتے ہوئے ٹوکن کنٹریکٹ ایڈریس اور معلومات کی تصدیق کی ہے۔ بدنیتی پر مبنی یا غلط معلومات شامل کرنے کے نتیجے میں فنڈز ضائع ہو سکتے ہیں۔", + "token_contract_address": "ٹوکن کنٹریکٹ ایڈریس", + "token_name": "ٹوکن کا نام جیسے: Tether", + "token_symbol": "ٹوکن کی علامت جیسے: USDT", + "token_decimal": "ٹوکن اعشاریہ", + "field_required": "اس کو پر کرنا ضروری ہے", + "pin_at_top": "اوپر ${token} کو پن کریں۔", + "invalid_input": "غلط ان پٹ", + "fiat_balance": "فیاٹ بیلنس", + "gross_balance": "مجموعی بیلنس", + "alphabetical": "حروف تہجی کے مطابق", "generate_name": "نام پیدا کریں۔", "balance_page": "بیلنس صفحہ", "share": "بانٹیں", "slidable": "سلائیڈ ایبل", + "manage_nodes": "۔ﮟﯾﺮﮐ ﻢﻈﻧ ﺎﮐ ﺱﮈﻮﻧ", + "etherscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﺮﮭﺘﯾﺍ", "template_name": "ٹیمپلیٹ کا نام" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 85084943b..74c03005f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -1,7 +1,7 @@ { "welcome": "Ẹ káàbọ sí", "cake_wallet": "Cake Wallet", - "first_wallet_text": "Àpamọ́wọ́ t'á fi Monero, Bitcoin, Litecoin, àti Haven pamọ́ wà pa", + "first_wallet_text": "Àpamọ́wọ́ t'á fi Monero, Bitcoin, Ethereum, Litecoin, àti Haven pamọ́ wà pa", "please_make_selection": "Ẹ jọ̀wọ́, yàn dá àpamọ́wọ́ yín tàbí dá àpamọ́wọ́ yín padà n’ísàlẹ̀.", "create_new": "Dá àpamọ́wọ́ tuntun", "restore_wallet": "Mú àpamọ́wọ́ padà", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "Àwọn àdírẹ́sì olùgbà", "wallet_list_title": "Àpamọ́wọ́ Monero", "wallet_list_create_new_wallet": "Ṣe àpamọ́wọ́ títun", - "wallet_list_edit_wallet" : "Ṣatunkọ apamọwọ", - "wallet_list_wallet_name" : "Orukọ apamọwọ", + "wallet_list_edit_wallet": "Ṣatunkọ apamọwọ", + "wallet_list_wallet_name": "Orukọ apamọwọ", "wallet_list_restore_wallet": "Restore àpamọ́wọ́", "wallet_list_load_wallet": "Load àpamọ́wọ́", "wallet_list_loading_wallet": "Ń ṣí àpamọ́wọ́ ${wallet_name}", @@ -393,8 +393,8 @@ "unconfirmed": "A kò tí ì jẹ́rìí ẹ̀", "displayable": "A lè ṣàfihàn ẹ̀", "submit_request": "Ṣé ìbéèrè", - "buy_alert_content": "A jẹ́ kí ríra Bitcoin àti Litecoin nìkan. Ẹ jọ̀wọ́ dá tàbí sún àpamọ́wọ́ ti Bitcoin yín tàbí àpamọ́wọ́ ti Litecoin yín mọ́, t'ẹ́ bá fẹ́ ra Bitcoin tàbí Litecoin.", - "sell_alert_content": "Lọwọlọwọ a ṣe atilẹyin tita Bitcoin ati Litecoin nikan. Jọwọ ṣẹda tabi yipada si Bitcoin tabi apamọwọ Litecoin rẹ.", + "buy_alert_content": "Lọwọlọwọ a ṣe atilẹyin rira Bitcoin, Ethereum, Litecoin, ati Monero. Jọwọ ṣẹda tabi yipada si Bitcoin, Ethereum, Litecoin, tabi apamọwọ Monero.", + "sell_alert_content": "Lọwọlọwọ a ṣe atilẹyin tita Bitcoin, Ethereum ati Litecoin nikan. Jọwọ ṣẹda tabi yipada si Bitcoin, Ethereum tabi apamọwọ Litecoin rẹ.", "outdated_electrum_wallet_description": "Àwọn àpamọ́wọ́ títun Bitcoin ti a ti dá nínú Cake Wallet lọ́wọ́lọ́wọ́. Àwọn àpamọ́wọ́ títun t'á dá nínú Cake Wallet ni hóró tó ní ọ̀rọ̀ mẹ́rinlélógún. Ẹ gbọ́dọ̀ dá àpamọ́wọ́. Ẹ sì sún gbogbo owó yín sí àpamọ́wọ́ títun náà tó dá lórí ọ̀rọ̀ mẹ́rinlélógún. Ẹ sì gbọ́dọ̀ yé lo àwọn àpamọ́wọ́ tó dá lórí hóró tó ní ọ̀rọ̀ méjìlá. Ẹ jọ̀wọ́ ṣe èyí láìpẹ́ kí ẹ ba owó yín.", "understand": "Ó ye mi", "apk_update": "Àtúnse áàpù títun wà", @@ -628,12 +628,43 @@ "setup_totp_recommended": "Sọ TOTP (Kẹṣọdọ)", "disable_buy": "Ko iṣọrọ ọja", "disable_sell": "Ko iṣọrọ iṣọrọ", + "cake_2fa_preset" : "Cake 2FA Tito", + "narrow": "Taara", + "normal": "Deede", + "aggressive": "Onítara", + "require_for_assessing_wallet": "Beere fun wiwọle si apamọwọ", + "require_for_sends_to_non_contacts" : "Beere fun fifiranṣẹ si awọn ti kii ṣe awọn olubasọrọ", + "require_for_sends_to_contacts" : "Beere fun fifiranṣẹ si awọn olubasọrọ", + "require_for_sends_to_internal_wallets" : "Beere fun fifiranṣẹ si awọn apamọwọ inu", + "require_for_exchanges_to_internal_wallets" : "Beere fun awọn paṣipaarọ si awọn apamọwọ inu", + "require_for_adding_contacts" : "Beere fun fifi awọn olubasọrọ kun", + "require_for_creating_new_wallets" : "Beere fun ṣiṣẹda titun Woleti", + "require_for_all_security_and_backup_settings" : "Beere fun gbogbo aabo ati awọn eto afẹyinti", "available_balance_description": "“Iwọntunwọnsi Wa” tabi “Iwọntunwọnsi Ijẹrisi” jẹ awọn owo ti o le ṣee lo lẹsẹkẹsẹ. Ti awọn owo ba han ni iwọntunwọnsi kekere ṣugbọn kii ṣe iwọntunwọnsi oke, lẹhinna o gbọdọ duro iṣẹju diẹ fun awọn owo ti nwọle lati gba awọn ijẹrisi nẹtiwọọki diẹ sii. Lẹhin ti wọn gba awọn ijẹrisi diẹ sii, wọn yoo jẹ inawo.", "syncing_wallet_alert_title": "Apamọwọ rẹ n muṣiṣẹpọ", "syncing_wallet_alert_content": "Iwontunws.funfun rẹ ati atokọ idunadura le ma pari titi ti yoo fi sọ “SYNCHRONIZED” ni oke. Tẹ/tẹ ni kia kia lati ni imọ siwaju sii.", + "home_screen_settings": "Awọn eto iboju ile", + "sort_by": "Sa pelu", + "search_add_token": "Wa / Fi àmi kun", + "edit_token": "Ṣatunkọ àmi", + "warning": "Ikilo", + "add_token_warning": "Ma ṣe ṣatunkọ tabi ṣafikun awọn ami bi a ti fun ni aṣẹ nipasẹ awọn scammers.\nNigbagbogbo jẹrisi awọn adirẹsi ami pẹlu awọn orisun olokiki!", + "add_token_disclaimer_check": "Mo ti jẹrisi adirẹsi adehun ami ati alaye nipa lilo orisun olokiki kan. Fifi irira tabi alaye ti ko tọ le ja si isonu ti owo.", + "token_contract_address": "Àmi guide adirẹsi", + "token_name": "Orukọ àmi fun apẹẹrẹ: Tether", + "token_symbol": "Aami aami fun apẹẹrẹ: USDT", + "token_decimal": "Àmi eleemewa", + "field_required": "E ni lati se nkan si aye yi", + "pin_at_top": "pin ${tokini} ni oke", + "invalid_input": "Iṣawọle ti ko tọ", + "fiat_balance": "Fiat Iwontunws.funfun", + "gross_balance": "Iwontunws.funfun apapọ", + "alphabetical": "Labidibi", "generate_name": "Ṣẹda Orukọ", "balance_page": "Oju-iwe iwọntunwọnsi", "share": "Pinpin", "slidable": "Slidable", + "manage_nodes": "Ṣakoso awọn apa", + "etherscan_history": "Etherscan itan", "template_name": "Orukọ Awoṣe" } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 02213c66d..7184e4d40 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -1,7 +1,7 @@ { "welcome": "欢迎使用", "cake_wallet": "Cake Wallet", - "first_wallet_text": "门罗币、比特币、莱特币和避风港的超棒钱包", + "first_wallet_text": "适用于门罗币、比特币、以太坊、莱特币和避风港的超棒钱包", "please_make_selection": "请在下面进行选择 创建或恢复您的钱包.", "create_new": "创建新钱包", "restore_wallet": "恢复钱包", @@ -250,8 +250,8 @@ "transaction_details_recipient_address": "收件人地址", "wallet_list_title": "Monero 钱包", "wallet_list_create_new_wallet": "创建新钱包", - "wallet_list_edit_wallet" : "编辑钱包", - "wallet_list_wallet_name" : "钱包名称", + "wallet_list_edit_wallet": "编辑钱包", + "wallet_list_wallet_name": "钱包名称", "wallet_list_restore_wallet": "恢复钱包", "wallet_list_load_wallet": "加载钱包", "wallet_list_loading_wallet": "载入中 ${wallet_name} 钱包", @@ -394,8 +394,8 @@ "unconfirmed": "未确认余额", "displayable": "可显示", "submit_request": "提交请求", - "buy_alert_content": "目前我们只支持购买比特币、莱特币和门罗币。 请创建或切换到您的比特币、莱特币或门罗币钱包。", - "sell_alert_content": "我们目前只支持比特币和莱特币的销售。 请创建或切换到您的比特币或莱特币钱包。", + "buy_alert_content": "目前我们仅支持购买比特币、以太坊、莱特币和门罗币。请创建或切换到您的比特币、以太坊、莱特币或门罗币钱包。", + "sell_alert_content": "我们目前仅支持比特币、以太坊和莱特币的销售。请创建或切换到您的比特币、以太坊或莱特币钱包。", "outdated_electrum_wallet_description": "在Cake创建的新比特币钱包现在有一个24字的种子。你必须创建一个新的比特币钱包,并将你所有的资金转移到新的24字钱包,并停止使用12字种子的钱包。请立即这样做以保证你的资金安全。", "understand": "我已知晓", "apk_update": "APK更新", @@ -537,6 +537,8 @@ "open_gift_card": "打开礼品卡", "contact_support": "联系支持", "gift_cards_unavailable": "目前只能通过门罗币、比特币和莱特币购买礼品卡", + "background_sync_mode": "后台同步模式", + "sync_all_wallets": "同步所有钱包", "introducing_cake_pay": "介绍 Cake Pay!", "cake_pay_learn_more": "立即在应用中购买和兑换礼品卡!\n从左向右滑动以了解详情。", "automatic": "自动的", @@ -631,12 +633,43 @@ "setup_totp_recommended": "设置 TOTP(推荐)", "disable_buy": "禁用购买操作", "disable_sell": "禁用卖出操作", + "cake_2fa_preset" : "蛋糕 2FA 预设", + "narrow": "狭窄的", + "normal": "普通的", + "aggressive": "过分热心", + "require_for_assessing_wallet": "需要访问钱包", + "require_for_sends_to_non_contacts" : "需要发送给非联系人", + "require_for_sends_to_contacts" : "需要发送给联系人", + "require_for_sends_to_internal_wallets" : "需要发送到内部钱包", + "require_for_exchanges_to_internal_wallets" : "需要兑换到内部钱包", + "require_for_adding_contacts" : "需要添加联系人", + "require_for_creating_new_wallets" : "创建新钱包的要求", + "require_for_all_security_and_backup_settings" : "需要所有安全和备份设置", "available_balance_description": "可用余额是您可以使用的金额。冻结余额是您当前正在等待确认的金额。", "syncing_wallet_alert_title": "您的钱包正在同步", "syncing_wallet_alert_content": "您的余额和交易列表可能不完整,直到顶部显示“已同步”。单击/点击以了解更多信息。", + "home_screen_settings": "主屏幕设置", + "sort_by": "排序方式", + "search_add_token": "搜索/添加令牌", + "edit_token": "编辑令牌", + "warning": "警告", + "add_token_warning": "请勿按照诈骗者的指示编辑或添加令牌。\n始终通过信誉良好的来源确认代币地址!", + "add_token_disclaimer_check": "我已使用信誉良好的来源确认了代币合约地址和信息。 添加恶意或不正确的信息可能会导致资金损失。", + "token_contract_address": "代币合约地址", + "token_name": "代币名称例如:Tether", + "token_symbol": "代币符号例如:USDT", + "token_decimal": "令牌十进制", + "field_required": "此字段是必需的", + "pin_at_top": "将 ${token} 固定在顶部", + "invalid_input": "输入无效", + "fiat_balance": "法币余额", + "gross_balance": "毛余额", + "alphabetical": "按字母顺序", "generate_name": "生成名称", "balance_page": "余额页", "share": "分享", "slidable": "可滑动", + "manage_nodes": "管理节点", + "etherscan_history": "以太扫描历史", "template_name": "模板名称" } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 6156758b6..75f3bbd06 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -14,14 +14,14 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.4.0" -MONERO_COM_BUILD_NUMBER=52 +MONERO_COM_VERSION="1.5.0" +MONERO_COM_BUILD_NUMBER=54 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.6.0" -CAKEWALLET_BUILD_NUMBER=162 +CAKEWALLET_VERSION="4.8.0" +CAKEWALLET_BUILD_NUMBER=167 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" diff --git a/scripts/android/inject_app_details.sh b/scripts/android/inject_app_details.sh index 6bcdf7239..340966044 100755 --- a/scripts/android/inject_app_details.sh +++ b/scripts/android/inject_app_details.sh @@ -8,4 +8,6 @@ fi cd ../.. sed -i "0,/version:/{s/version:.*/version: ${APP_ANDROID_VERSION}+${APP_ANDROID_BUILD_NUMBER}/}" ./pubspec.yaml sed -i "0,/version:/{s/__APP_PACKAGE__/${APP_ANDROID_PACKAGE}/}" ./android/app/src/main/AndroidManifest.xml -cd scripts/android \ No newline at end of file +sed -i "0,/version:/{s/__versionCode__/${APP_ANDROID_BUILD_NUMBER}/}" ./android/app/src/main/AndroidManifest.xml +sed -i "0,/version:/{s/__versionName__/${APP_ANDROID_VERSION}/}" ./android/app/src/main/AndroidManifest.xml +cd scripts/android diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index c69ac3906..72703150a 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 --haven" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index fdc0072eb..470f47efc 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -23,7 +23,7 @@ case $APP_IOS_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 52cb07183..68d3f69a8 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.4.0" -MONERO_COM_BUILD_NUMBER=50 +MONERO_COM_VERSION="1.5.0" +MONERO_COM_BUILD_NUMBER=52 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.7.0" -CAKEWALLET_BUILD_NUMBER=165 +CAKEWALLET_VERSION="4.8.0" +CAKEWALLET_BUILD_NUMBER=175 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 231945659..8c05035c7 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -23,7 +23,7 @@ CONFIG_ARGS="" case $APP_MACOS_TYPE in $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin";; #--haven + CONFIG_ARGS="--monero --bitcoin --ethereum";; #--haven esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index b31a5a973..387bd39c5 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -15,8 +15,8 @@ if [ -n "$1" ]; then fi CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.0.7" -CAKEWALLET_BUILD_NUMBER=26 +CAKEWALLET_VERSION="1.1.0" +CAKEWALLET_BUILD_NUMBER=28 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/tool/configure.dart b/tool/configure.dart index 8dcb0f0d7..3d846e8a6 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -1,9 +1,9 @@ -import 'dart:convert'; import 'dart:io'; const bitcoinOutputPath = 'lib/bitcoin/bitcoin.dart'; const moneroOutputPath = 'lib/monero/monero.dart'; const havenOutputPath = 'lib/haven/haven.dart'; +const ethereumOutputPath = 'lib/ethereum/ethereum.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -13,11 +13,13 @@ Future<void> main(List<String> args) async { final hasBitcoin = args.contains('${prefix}bitcoin'); final hasMonero = args.contains('${prefix}monero'); final hasHaven = args.contains('${prefix}haven'); + final hasEthereum = args.contains('${prefix}ethereum'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); await generateHaven(hasHaven); - await generatePubspec(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven); - await generateWalletTypes(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven); + await generateEthereum(hasEthereum); + await generatePubspec(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven, hasEthereum: hasEthereum); + await generateWalletTypes(hasMonero: hasMonero, hasBitcoin: hasBitcoin, hasHaven: hasHaven, hasEthereum: hasEthereum); } Future<void> generateBitcoin(bool hasImplementation) async { @@ -471,7 +473,89 @@ abstract class HavenAccountList { await outputFile.writeAsString(output); } -Future<void> generatePubspec({required bool hasMonero, required bool hasBitcoin, required bool hasHaven}) async { +Future<void> generateEthereum(bool hasImplementation) async { + + final outputFile = File(ethereumOutputPath); + const ethereumCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:hive/hive.dart'; +"""; + const ethereumCWHeaders = """ +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; +import 'package:cw_ethereum/ethereum_wallet_service.dart'; +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +"""; + const ethereumCwPart = "part 'cw_ethereum.dart';"; + const ethereumContent = """ +abstract class Ethereum { + List<String> getEthereumWordList(String language); + WalletService createEthereumWalletService(Box<WalletInfo> walletInfoSource); + WalletCredentials createEthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createEthereumRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + String getAddress(WalletBase wallet); + TransactionPriority getDefaultTransactionPriority(); + List<TransactionPriority> getTransactionPriorities(); + TransactionPriority deserializeEthereumTransactionPriority(int raw); + + Object createEthereumTransactionCredentials( + List<Output> outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }); + + Object createEthereumTransactionCredentialsRaw( + List<OutputInfo> outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }); + + int formatterEthereumParseAmount(String amount); + double formatterEthereumAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18}); + List<Erc20Token> getERC20Currencies(WalletBase wallet); + Future<void> addErc20Token(WalletBase wallet, Erc20Token token); + Future<void> deleteErc20Token(WalletBase wallet, Erc20Token token); + Future<Erc20Token?> getErc20Token(WalletBase wallet, String contractAddress); + + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + void updateEtherscanUsageState(WalletBase wallet, bool isEnabled); +} + """; + + const ethereumEmptyDefinition = 'Ethereum? ethereum;\n'; + const ethereumCWDefinition = 'Ethereum? ethereum = CWEthereum();\n'; + + final output = '$ethereumCommonHeaders\n' + + (hasImplementation ? '$ethereumCWHeaders\n' : '\n') + + (hasImplementation ? '$ethereumCwPart\n\n' : '\n') + + (hasImplementation ? ethereumCWDefinition : ethereumEmptyDefinition) + + '\n' + + ethereumContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + +Future<void> generatePubspec({required bool hasMonero, required bool hasBitcoin, required bool hasHaven, required bool hasEthereum}) async { const cwCore = """ cw_core: path: ./cw_core @@ -492,6 +576,10 @@ Future<void> generatePubspec({required bool hasMonero, required bool hasBitcoin, cw_shared_external: path: ./cw_shared_external """; + const cwEthereum = """ + cw_ethereum: + path: ./cw_ethereum + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -512,6 +600,10 @@ Future<void> generatePubspec({required bool hasMonero, required bool hasBitcoin, output += '\n$cwHaven'; } + if (hasEthereum) { + output += '\n$cwEthereum'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -524,7 +616,7 @@ Future<void> generatePubspec({required bool hasMonero, required bool hasBitcoin, await outputFile.writeAsString(outputContent); } -Future<void> generateWalletTypes({required bool hasMonero, required bool hasBitcoin, required bool hasHaven}) async { +Future<void> generateWalletTypes({required bool hasMonero, required bool hasBitcoin, required bool hasHaven, required bool hasEthereum}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -540,7 +632,15 @@ Future<void> generateWalletTypes({required bool hasMonero, required bool hasBitc } if (hasBitcoin) { - outputContent += '\tWalletType.bitcoin,\n\tWalletType.litecoin,\n'; + outputContent += '\tWalletType.bitcoin,\n'; + } + + if (hasEthereum) { + outputContent += '\tWalletType.ethereum,\n'; + } + + if (hasBitcoin) { + outputContent += '\tWalletType.litecoin,\n'; } if (hasHaven) { diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index a66905524..1d533b0b1 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -4,12 +4,12 @@ import 'utils/secret_key.dart'; import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; +const ethereumConfigPath = 'tool/.ethereum-secrets-config.json'; Future<void> main(List<String> args) async => generateSecretsConfig(args); Future<void> generateSecretsConfig(List<String> args) async { - final extraInfo = - args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) { + final extraInfo = args.fold(<String, dynamic>{}, (Map<String, dynamic> acc, String arg) { final parts = arg.split('='); final key = normalizeKeyName(parts[0]); acc[key] = acc[key] = parts.length > 1 ? parts[1] : 1; @@ -17,6 +17,7 @@ Future<void> generateSecretsConfig(List<String> args) async { }); final configFile = File(configPath); + final ethereumConfigFile = File(ethereumConfigPath); final secrets = <String, dynamic>{}; secrets.addAll(extraInfo); @@ -44,6 +45,19 @@ Future<void> generateSecretsConfig(List<String> args) async { secrets[sec.name] = sec.generate(); }); - final secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + var secretsJson = JsonEncoder.withIndent(' ').convert(secrets); await configFile.writeAsString(secretsJson); + + secrets.clear(); + SecretKey.ethereumSecrets.forEach((sec) { + if (secrets[sec.name] != null) { + return; + } + + secrets[sec.name] = sec.generate(); + }); + + secretsJson = JsonEncoder.withIndent(' ').convert(secrets); + + await ethereumConfigFile.writeAsString(secretsJson); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 8fc62f9f0..5987b9005 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -5,19 +5,31 @@ import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; const outputPath = 'lib/.secrets.g.dart'; +const ethereumConfigPath = 'tool/.ethereum-secrets-config.json'; +const ethereumOutputPath = 'cw_ethereum/lib/.secrets.g.dart'; + Future<void> main(List<String> args) async => importSecretsConfig(); Future<void> importSecretsConfig() async { final outputFile = File(outputPath); - final input = json.decode(File(configPath).readAsStringSync()) - as Map<String, dynamic> ?? - <String, dynamic>{}; - final output = input.keys - .fold('', (String acc, String val) => acc + generateConst(val, input)); + final input = json.decode(File(configPath).readAsStringSync()) as Map<String, dynamic>; + final output = input.keys.fold('', (String acc, String val) => acc + generateConst(val, input)); + + final ethereumOutputFile = File(ethereumOutputPath); + final ethereumInput = + json.decode(File(ethereumConfigPath).readAsStringSync()) as Map<String, dynamic>; + final ethereumOutput = ethereumInput.keys + .fold('', (String acc, String val) => acc + generateConst(val, ethereumInput)); if (outputFile.existsSync()) { await outputFile.delete(); } await outputFile.writeAsString(output); + + if (ethereumOutputFile.existsSync()) { + await ethereumOutputFile.delete(); + } + + await ethereumOutputFile.writeAsString(ethereumOutput); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index b5b4de6d6..1b62dbf60 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -33,6 +33,10 @@ class SecretKey { SecretKey('payfuraApiKey', () => ''), ]; + static final ethereumSecrets = [ + SecretKey('etherScanApiKey', () => ''), + ]; + final String name; final String Function() generate; }