diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 7b2b611d3..8fef4fdb6 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -97,6 +97,7 @@ jobs: cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_polygon && 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 @@ -131,6 +132,7 @@ jobs: 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 + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart echo "const chatwootWebsiteToken = '${{ secrets.CHATWOOT_WEBSITE_TOKEN }}';" >> lib/.secrets.g.dart echo "const exolixApiKey = '${{ secrets.EXOLIX_API_KEY }}';" >> lib/.secrets.g.dart echo "const robinhoodApplicationId = '${{ secrets.ROBINHOOD_APPLICATION_ID }}';" >> lib/.secrets.g.dart diff --git a/.gitignore b/.gitignore index c735d4058..0a883dd18 100644 --- a/.gitignore +++ b/.gitignore @@ -126,6 +126,7 @@ lib/haven/haven.dart lib/ethereum/ethereum.dart lib/bitcoin_cash/bitcoin_cash.dart lib/nano/nano.dart +lib/polygon/polygon.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 910149f60..f32482e22 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -62,6 +62,9 @@ + + + (other is Erc20Token && other.contractAddress == contractAddress) || diff --git a/cw_core/lib/get_height_by_date.dart b/cw_core/lib/get_height_by_date.dart index a680e6c25..6f3ccaf68 100644 --- a/cw_core/lib/get_height_by_date.dart +++ b/cw_core/lib/get_height_by_date.dart @@ -118,7 +118,10 @@ final dates = { "2023-6": 2898234, "2023-7": 2919771, "2023-8": 2942045, - "2023-9": 2964280 + "2023-9": 2964280, + "2023-10": 2985937, + "2023-11": 3008178, + "2023-12": 3029759 }; int getMoneroHeigthByDate({required DateTime date}) { diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index 484325f91..bd96e1395 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -6,7 +6,7 @@ import 'package:hive/hive.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:http/io_client.dart' as ioc; -import 'package:tor/tor.dart'; +// import 'package:tor/tor.dart'; part 'node.g.dart'; @@ -88,6 +88,8 @@ class Node extends HiveObject with Keyable { } else { return Uri.http(uriRaw, ''); } + case WalletType.polygon: + return Uri.https(uriRaw, ''); default: throw Exception('Unexpected type ${type.toString()} for Node uri'); } @@ -146,6 +148,8 @@ class Node extends HiveObject with Keyable { case WalletType.nano: case WalletType.banano: return requestNanoNode(); + case WalletType.polygon: + return requestElectrumServer(); default: return false; } @@ -210,14 +214,17 @@ class Node extends HiveObject with Keyable { } Future requestNodeWithProxy() async { - if (!isValidProxyAddress && !Tor.instance.enabled) { + if (!isValidProxyAddress/* && !Tor.instance.enabled*/) { return false; } String? proxy = socksProxyAddress; - if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { - proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + // if ((proxy?.isEmpty ?? true) && Tor.instance.enabled) { + // proxy = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + // } + if (proxy == null) { + return false; } final proxyAddress = proxy!.split(':')[0]; final proxyPort = int.parse(proxy.split(':')[1]); diff --git a/cw_bitcoin/lib/file.dart b/cw_core/lib/utils/file.dart similarity index 66% rename from cw_bitcoin/lib/file.dart rename to cw_core/lib/utils/file.dart index 8fd236ec3..0b1c5cffd 100644 --- a/cw_bitcoin/lib/file.dart +++ b/cw_core/lib/utils/file.dart @@ -2,17 +2,8 @@ import 'dart:io'; import 'package:cw_core/key.dart'; import 'package:encrypt/encrypt.dart' as encrypt; -Future 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 write({required String path, required String password, required String data}) async => + writeData(path: path, password: password, data: data); Future writeData( {required String path, diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index debf92e11..20f0bdb19 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -13,6 +13,7 @@ const walletTypes = [ WalletType.bitcoinCash, WalletType.nano, WalletType.banano, + WalletType.polygon, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -44,6 +45,8 @@ enum WalletType { @HiveField(8) bitcoinCash, + @HiveField(9) + polygon } int serializeToInt(WalletType type) { @@ -64,6 +67,8 @@ int serializeToInt(WalletType type) { return 6; case WalletType.bitcoinCash: return 7; + case WalletType.polygon: + return 8; default: return -1; } @@ -87,6 +92,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.banano; case 7: return WalletType.bitcoinCash; + case 8: + return WalletType.polygon; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -110,6 +117,8 @@ String walletTypeToString(WalletType type) { return 'Nano'; case WalletType.banano: return 'Banano'; + case WalletType.polygon: + return 'Polygon'; default: return ''; } @@ -133,6 +142,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Nano (XNO)'; case WalletType.banano: return 'Banano (BAN)'; + case WalletType.polygon: + return 'Polygon (MATIC)'; default: return ''; } @@ -156,7 +167,10 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type) { return CryptoCurrency.nano; case WalletType.banano: return CryptoCurrency.banano; + case WalletType.polygon: + return CryptoCurrency.maticpoly; default: - throw Exception('Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); + throw Exception( + 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); } } diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index 533b578ad..04a840d4e 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -20,10 +20,10 @@ dependencies: intl: ^0.18.0 encrypt: ^5.0.1 socks5_proxy: ^1.0.4 - tor: - git: - url: https://github.com/cake-tech/tor.git - ref: main +# tor: +# git: +# url: https://github.com/cake-tech/tor.git +# ref: main dev_dependencies: flutter_test: diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index f0c7381e8..5e408bb9e 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -15,12 +15,12 @@ import 'package:cw_ethereum/ethereum_transaction_priority.dart'; import 'package:cw_ethereum/.secrets.g.dart' as secrets; class EthereumClient { - final _httpClient = Client(); + final httpClient = Client(); Web3Client? _client; bool connect(Node node) { try { - _client = Web3Client(node.uri.toString(), _httpClient); + _client = Web3Client(node.uri.toString(), httpClient); return true; } catch (e) { @@ -74,9 +74,11 @@ class EthereumClient { required int exponent, String? contractAddress, }) async { - assert(currency == CryptoCurrency.eth || contractAddress != null); + assert(currency == CryptoCurrency.eth || + currency == CryptoCurrency.maticpoly || + contractAddress != null); - bool _isEthereum = currency == CryptoCurrency.eth; + bool _isEVMCompatibleChain = currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; final price = _client!.getGasPrice(); @@ -84,19 +86,23 @@ class EthereumClient { from: privateKey.address, to: EthereumAddress.fromHex(toAddress), maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), - value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + value: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), ); - final signedTransaction = await _client!.signTransaction(privateKey, transaction); + final chainId = _getChainIdForCurrency(currency); + + final signedTransaction = + await _client!.signTransaction(privateKey, transaction, chainId: chainId); final Function _sendTransaction; - if (_isEthereum) { + if (_isEVMCompatibleChain) { _sendTransaction = () async => await sendTransaction(signedTransaction); } else { final erc20 = ERC20( client: _client!, address: EthereumAddress.fromHex(contractAddress!), + chainId: chainId, ); _sendTransaction = () async { @@ -118,6 +124,16 @@ class EthereumClient { ); } + int _getChainIdForCurrency(CryptoCurrency currency) { + switch (currency) { + case CryptoCurrency.maticpoly: + return 137; + case CryptoCurrency.eth: + default: + return 1; + } + } + Future sendTransaction(Uint8List signedTransaction) async => await _client!.sendRawTransaction(prependTransactionType(0x02, signedTransaction)); @@ -198,7 +214,7 @@ I/flutter ( 4474): Gas Used: 53000 Future> fetchTransactions(String address, {String? contractAddress}) async { try { - final response = await _httpClient.get(Uri.https("api.etherscan.io", "/api", { + final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { "module": "account", "action": contractAddress != null ? "tokentx" : "txlist", if (contractAddress != null) "contractaddress": contractAddress, diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart index a0649ba25..f0deae931 100644 --- a/cw_ethereum/lib/ethereum_transaction_info.dart +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:cw_core/format_amount.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -34,8 +36,10 @@ class EthereumTransactionInfo extends TransactionInfo { final String? to; @override - String amountFormatted() => - '${formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString())} $tokenSymbol'; + String amountFormatted() { + final amount = formatAmount((ethAmount / BigInt.from(10).pow(exponent)).toString()); + return '${amount.substring(0, min(10, amount.length))} $tokenSymbol'; + } @override String fiatAmount() => _fiatAmount ?? ''; @@ -44,7 +48,10 @@ class EthereumTransactionInfo extends TransactionInfo { void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); @override - String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} ETH'; + String feeFormatted() { + final amount = (ethFee / BigInt.from(10).pow(18)).toString(); + return '${amount.substring(0, min(10, amount.length))} ETH'; + } factory EthereumTransactionInfo.fromJson(Map data) { return EthereumTransactionInfo( diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_ethereum/lib/ethereum_transaction_model.dart index c1260795a..3b5f724fc 100644 --- a/cw_ethereum/lib/ethereum_transaction_model.dart +++ b/cw_ethereum/lib/ethereum_transaction_model.dart @@ -1,3 +1,4 @@ +//! Model used for in parsing transactions fetched using etherscan class EthereumTransactionModel { final DateTime date; final String hash; diff --git a/cw_ethereum/lib/pending_ethereum_transaction.dart b/cw_ethereum/lib/pending_ethereum_transaction.dart index 35b0123cc..d47630fd6 100644 --- a/cw_ethereum/lib/pending_ethereum_transaction.dart +++ b/cw_ethereum/lib/pending_ethereum_transaction.dart @@ -21,8 +21,8 @@ class PendingEthereumTransaction with PendingTransaction { @override String get amountFormatted { - final _amount = BigInt.parse(amount) / BigInt.from(pow(10, exponent)); - return _amount.toStringAsFixed(min(15, _amount.toString().length)); + final _amount = (BigInt.parse(amount) / BigInt.from(pow(10, exponent))).toString(); + return _amount.substring(0, min(10, _amount.length)); } @override @@ -30,8 +30,8 @@ class PendingEthereumTransaction with PendingTransaction { @override String get feeFormatted { - final _fee = fee / BigInt.from(pow(10, 18)); - return _fee.toStringAsFixed(min(15, _fee.toString().length)); + final _fee = (fee / BigInt.from(pow(10, 18))).toString(); + return _fee.substring(0, min(10, _fee.length)); } @override diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 7ad873647..6162375b2 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -385,6 +385,9 @@ extern "C" (uint64_t)restoreHeight, std::string(spendKey)); + // Cache Raw to support Polyseed + wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); + int status; std::string errorString; @@ -396,9 +399,6 @@ extern "C" return false; } - // Cache Raw to support Polyseed - wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); - change_current_wallet(wallet); return true; } diff --git a/cw_monero/lib/api/wallet_manager.dart b/cw_monero/lib/api/wallet_manager.dart index 260b420c5..0aa694e9a 100644 --- a/cw_monero/lib/api/wallet_manager.dart +++ b/cw_monero/lib/api/wallet_manager.dart @@ -1,14 +1,16 @@ import 'dart:ffi'; -import 'package:ffi/ffi.dart'; -import 'package:flutter/foundation.dart'; + import 'package:cw_monero/api/convert_utf8_to_string.dart'; -import 'package:cw_monero/api/signatures.dart'; -import 'package:cw_monero/api/types.dart'; -import 'package:cw_monero/api/monero_api.dart'; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_creation_exception.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_keys_exception.dart'; import 'package:cw_monero/api/exceptions/wallet_restore_from_seed_exception.dart'; +import 'package:cw_monero/api/monero_api.dart'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:cw_monero/api/wallet.dart'; +import 'package:ffi/ffi.dart'; +import 'package:flutter/foundation.dart'; final createWalletNative = moneroApi .lookup>('create_wallet') @@ -175,6 +177,8 @@ void restoreWalletFromSpendKeySync( calloc.free(languagePointer); calloc.free(spendKeyPointer); + storeSync(); + if (!isWalletRestored) { throw WalletRestoreFromKeysException( message: convertUTF8ToString(pointer: errorMessagePointer)); diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 077ab4e54..a6b3227b2 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -77,8 +77,12 @@ class MoneroWalletService extends WalletService< final polyseed = Polyseed.create(); final lang = PolyseedLang.getByEnglishName(credentials.language); + final heightOverride = + getMoneroHeigthByDate(date: DateTime.now().subtract(Duration(days: 2))); + return _restoreFromPolyseed( - path, credentials.password!, polyseed, credentials.walletInfo!, lang); + path, credentials.password!, polyseed, credentials.walletInfo!, lang, + overrideHeight: heightOverride); } await monero_wallet_manager.createWallet( @@ -268,10 +272,11 @@ class MoneroWalletService extends WalletService< Future _restoreFromPolyseed(String path, String password, Polyseed polyseed, WalletInfo walletInfo, PolyseedLang lang, - {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO}) async { - final height = getMoneroHeigthByDate( + {PolyseedCoin coin = PolyseedCoin.POLYSEED_MONERO, int? overrideHeight}) async { + final height = overrideHeight ?? getMoneroHeigthByDate( date: DateTime.fromMillisecondsSinceEpoch(polyseed.birthday * 1000)); final spendKey = keyToHexString(polyseed.generateKey(coin, 32)); + final seed = polyseed.encode(lang, coin); walletInfo.isRecovery = true; walletInfo.restoreHeight = height; @@ -279,10 +284,11 @@ class MoneroWalletService extends WalletService< await monero_wallet_manager.restoreFromSpendKey( path: path, password: password, - seed: polyseed.encode(lang, coin), + seed: seed, language: lang.nameEnglish, restoreHeight: height, spendKey: spendKey); + final wallet = MoneroWallet( walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); diff --git a/cw_monero/macos/Classes/monero_api.cpp b/cw_monero/macos/Classes/monero_api.cpp index a5ca13822..6fabc29fd 100644 --- a/cw_monero/macos/Classes/monero_api.cpp +++ b/cw_monero/macos/Classes/monero_api.cpp @@ -385,6 +385,9 @@ extern "C" (uint64_t)restoreHeight, std::string(spendKey)); + // Cache Raw to support Polyseed + wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); + int status; std::string errorString; @@ -396,9 +399,6 @@ extern "C" return false; } - // Cache Raw to support Polyseed - wallet->setCacheAttribute("cakewallet.seed", std::string(seed)); - change_current_wallet(wallet); return true; } diff --git a/cw_polygon/.gitignore b/cw_polygon/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_polygon/.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_polygon/.metadata b/cw_polygon/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_polygon/.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: f468f3366c26a5092eb964a230ce7892fda8f2f8 + channel: stable + +project_type: package diff --git a/cw_polygon/CHANGELOG.md b/cw_polygon/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_polygon/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_polygon/LICENSE b/cw_polygon/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_polygon/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_polygon/README.md b/cw_polygon/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_polygon/README.md @@ -0,0 +1,39 @@ + + +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_polygon/analysis_options.yaml b/cw_polygon/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_polygon/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_polygon/lib/cw_polygon.dart b/cw_polygon/lib/cw_polygon.dart new file mode 100644 index 000000000..5d4e447d1 --- /dev/null +++ b/cw_polygon/lib/cw_polygon.dart @@ -0,0 +1,7 @@ +library cw_polygon; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_polygon/lib/default_erc20_tokens.dart b/cw_polygon/lib/default_erc20_tokens.dart new file mode 100644 index 000000000..132c52e4c --- /dev/null +++ b/cw_polygon/lib/default_erc20_tokens.dart @@ -0,0 +1,86 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; + +class DefaultPolygonErc20Tokens { + final List _defaultTokens = [ + Erc20Token( + name: "Wrapped Ether", + symbol: "WETH", + contractAddress: "0x7ceB23fD6bC0adD59E62ac25578270cFf1b9f619", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Tether USD (PoS)", + symbol: "USDT", + contractAddress: "0xc2132D05D31c914a87C6611C10748AEb04B58e8F", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin", + symbol: "USDC", + contractAddress: "0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359", + decimal: 6, + enabled: true, + ), + Erc20Token( + name: "USD Coin (POS)", + symbol: "USDC.e", + contractAddress: "0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174", + decimal: 6, + enabled: false, + ), + Erc20Token( + name: "Avalanche Token", + symbol: "AVAX", + contractAddress: "0x2C89bbc92BD86F8075d1DEcc58C7F4E0107f286b", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Wrapped BTC (PoS)", + symbol: "WBTC", + contractAddress: "0x1BFD67037B42Cf73acF2047067bd4F2C47D9BfD6", + decimal: 8, + enabled: false, + ), + Erc20Token( + name: "Dai (PoS)", + symbol: "DAI", + contractAddress: "0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063", + decimal: 18, + enabled: true, + ), + Erc20Token( + name: "SHIBA INU (PoS)", + symbol: "SHIB", + contractAddress: "0x6f8a06447Ff6FcF75d803135a7de15CE88C1d4ec", + decimal: 18, + enabled: false, + ), + Erc20Token( + name: "Uniswap (PoS)", + symbol: "UNI", + contractAddress: "0xb33EaAd8d922B1083446DC23f610c2567fB5180f", + decimal: 18, + enabled: false, + ), + ]; + + List get initialPolygonErc20Tokens => _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_polygon/lib/pending_polygon_transaction.dart b/cw_polygon/lib/pending_polygon_transaction.dart new file mode 100644 index 000000000..50f1f0638 --- /dev/null +++ b/cw_polygon/lib/pending_polygon_transaction.dart @@ -0,0 +1,19 @@ +import 'dart:typed_data'; + +import 'package:cw_ethereum/pending_ethereum_transaction.dart'; + +class PendingPolygonTransaction extends PendingEthereumTransaction { + PendingPolygonTransaction({ + required Function sendTransaction, + required Uint8List signedTransaction, + required BigInt fee, + required String amount, + required int exponent, + }) : super( + amount: amount, + sendTransaction: sendTransaction, + signedTransaction: signedTransaction, + fee: fee, + exponent: exponent, + ); +} diff --git a/cw_polygon/lib/polygon_client.dart b/cw_polygon/lib/polygon_client.dart new file mode 100644 index 000000000..86c2253af --- /dev/null +++ b/cw_polygon/lib/polygon_client.dart @@ -0,0 +1,34 @@ +import 'dart:convert'; + +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_polygon/polygon_transaction_model.dart'; +import 'package:cw_ethereum/.secrets.g.dart' as secrets; + +class PolygonClient extends EthereumClient { + @override + Future> fetchTransactions(String address, + {String? contractAddress}) async { + try { + final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", { + "module": "account", + "action": contractAddress != null ? "tokentx" : "txlist", + if (contractAddress != null) "contractaddress": contractAddress, + "address": address, + "apikey": secrets.polygonScanApiKey, + })); + + final jsonResponse = json.decode(response.body) as Map; + + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => PolygonTransactionModel.fromJson(e as Map)) + .toList(); + } + + return []; + } catch (e) { + print(e); + return []; + } + } +} diff --git a/cw_polygon/lib/polygon_exceptions.dart b/cw_polygon/lib/polygon_exceptions.dart new file mode 100644 index 000000000..2d08106b6 --- /dev/null +++ b/cw_polygon/lib/polygon_exceptions.dart @@ -0,0 +1,6 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_ethereum/ethereum_exceptions.dart'; + +class PolygonTransactionCreationException extends EthereumTransactionCreationException { + PolygonTransactionCreationException(CryptoCurrency currency) : super(currency); +} diff --git a/cw_polygon/lib/polygon_formatter.dart b/cw_polygon/lib/polygon_formatter.dart new file mode 100644 index 000000000..f016db7ab --- /dev/null +++ b/cw_polygon/lib/polygon_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const polygonAmountLength = 12; +const polygonAmountDivider = 1000000000000; +final polygonAmountFormat = NumberFormat() + ..maximumFractionDigits = polygonAmountLength + ..minimumFractionDigits = 1; + +class PolygonFormatter { + static int parsePolygonAmount(String amount) { + try { + return (double.parse(amount) * polygonAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parsePolygonAmountToDouble(int amount) { + try { + return amount / polygonAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_polygon/lib/polygon_mnemonics_exception.dart b/cw_polygon/lib/polygon_mnemonics_exception.dart new file mode 100644 index 000000000..c1a2fcc84 --- /dev/null +++ b/cw_polygon/lib/polygon_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class PolygonMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Polygon mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_polygon/lib/polygon_transaction_credentials.dart b/cw_polygon/lib/polygon_transaction_credentials.dart new file mode 100644 index 000000000..6611e15da --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_credentials.dart @@ -0,0 +1,18 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; +import 'package:cw_polygon/polygon_transaction_priority.dart'; + +class PolygonTransactionCredentials extends EthereumTransactionCredentials { + PolygonTransactionCredentials( + List outputs, { + required PolygonTransactionPriority? priority, + required CryptoCurrency currency, + final int? feeRate, + }) : super( + outputs, + currency: currency, + priority: priority, + feeRate: feeRate, + ); +} diff --git a/cw_polygon/lib/polygon_transaction_history.dart b/cw_polygon/lib/polygon_transaction_history.dart new file mode 100644 index 000000000..a06b8be4a --- /dev/null +++ b/cw_polygon/lib/polygon_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:cw_polygon/polygon_transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'polygon_transaction_history.g.dart'; + +const transactionsHistoryFileName = 'polygon_transactions.json'; + +class PolygonTransactionHistory = PolygonTransactionHistoryBase with _$PolygonTransactionHistory; + +abstract class PolygonTransactionHistoryBase extends TransactionHistoryBase + with Store { + PolygonTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + final WalletInfo walletInfo; + String _password; + + Future init() async => await _load(); + + @override + Future 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 saving polygon transaction history: ${e.toString()}'); + print(s); + } + } + + @override + void addOne(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _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; + } + + Future _load() async { + try { + final content = await _read(); + final txs = content['transactions'] as Map? ?? {}; + + txs.entries.forEach((entry) { + final val = entry.value; + + if (val is Map) { + final tx = PolygonTransactionInfo.fromJson(val); + _update(tx); + } + }); + } catch (e) { + print(e); + } + } + + void _update(PolygonTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_polygon/lib/polygon_transaction_info.dart b/cw_polygon/lib/polygon_transaction_info.dart new file mode 100644 index 000000000..f1976a601 --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_info.dart @@ -0,0 +1,49 @@ +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_ethereum/ethereum_transaction_info.dart'; + +class PolygonTransactionInfo extends EthereumTransactionInfo { + PolygonTransactionInfo({ + required String id, + required int height, + required BigInt ethAmount, + int exponent = 18, + required TransactionDirection direction, + required DateTime date, + required bool isPending, + required BigInt ethFee, + required int confirmations, + String tokenSymbol = "MATIC", + required String? to, + }) : super( + confirmations: confirmations, + id: id, + height: height, + ethAmount: ethAmount, + exponent: exponent, + direction: direction, + date: date, + isPending: isPending, + ethFee: ethFee, + to: to, + tokenSymbol: tokenSymbol, + ); + + factory PolygonTransactionInfo.fromJson(Map data) { + return PolygonTransactionInfo( + 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, + to: data['to'], + ); + } + + @override + String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} MATIC'; +} diff --git a/cw_polygon/lib/polygon_transaction_model.dart b/cw_polygon/lib/polygon_transaction_model.dart new file mode 100644 index 000000000..704d674e5 --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_model.dart @@ -0,0 +1,49 @@ +import 'package:cw_ethereum/ethereum_transaction_model.dart'; + +class PolygonTransactionModel extends EthereumTransactionModel { + PolygonTransactionModel({ + required DateTime date, + required String hash, + required String from, + required String to, + required BigInt amount, + required int gasUsed, + required BigInt gasPrice, + required String contractAddress, + required int confirmations, + required int blockNumber, + required String? tokenSymbol, + required int? tokenDecimal, + required bool isError, + }) : super( + amount: amount, + date: date, + hash: hash, + from: from, + to: to, + gasPrice: gasPrice, + gasUsed: gasUsed, + confirmations: confirmations, + contractAddress: contractAddress, + blockNumber: blockNumber, + tokenDecimal: tokenDecimal, + tokenSymbol: tokenSymbol, + isError: isError, + ); + + factory PolygonTransactionModel.fromJson(Map json) => PolygonTransactionModel( + 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"] ?? "MATIC", + tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), + isError: json["isError"] == "1", + ); +} diff --git a/cw_polygon/lib/polygon_transaction_priority.dart b/cw_polygon/lib/polygon_transaction_priority.dart new file mode 100644 index 000000000..dba1dab55 --- /dev/null +++ b/cw_polygon/lib/polygon_transaction_priority.dart @@ -0,0 +1,51 @@ +import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + +class PolygonTransactionPriority extends EthereumTransactionPriority { + const PolygonTransactionPriority({required String title, required int raw, required int tip}) + : super(title: title, raw: raw, tip: tip); + + static const List all = [fast, medium, slow]; + static const PolygonTransactionPriority slow = + PolygonTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const PolygonTransactionPriority medium = + PolygonTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const PolygonTransactionPriority fast = + PolygonTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static PolygonTransactionPriority 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 PolygonTransactionPriority deserialize'); + } + } + + @override + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case PolygonTransactionPriority.slow: + label = 'Slow'; + break; + case PolygonTransactionPriority.medium: + label = 'Medium'; + break; + case PolygonTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_polygon/lib/polygon_wallet.dart b/cw_polygon/lib/polygon_wallet.dart new file mode 100644 index 000000000..66e6797c6 --- /dev/null +++ b/cw_polygon/lib/polygon_wallet.dart @@ -0,0 +1,540 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/cake_hive.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/erc20_balance.dart'; +import 'package:cw_ethereum/ethereum_formatter.dart'; +import 'package:cw_ethereum/ethereum_transaction_model.dart'; +import 'package:cw_ethereum/file.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_polygon/default_erc20_tokens.dart'; +import 'package:cw_polygon/polygon_client.dart'; +import 'package:cw_polygon/polygon_exceptions.dart'; +import 'package:cw_polygon/polygon_formatter.dart'; +import 'package:cw_polygon/polygon_transaction_credentials.dart'; +import 'package:cw_polygon/polygon_transaction_history.dart'; +import 'package:cw_polygon/polygon_transaction_info.dart'; +import 'package:cw_polygon/polygon_transaction_model.dart'; +import 'package:cw_polygon/polygon_transaction_priority.dart'; +import 'package:cw_polygon/polygon_wallet_addresses.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/crypto.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:bip32/bip32.dart' as bip32; + +part 'polygon_wallet.g.dart'; + +class PolygonWallet = PolygonWalletBase with _$PolygonWallet; + +abstract class PolygonWalletBase extends WalletBase with Store { + PolygonWalletBase({ + required WalletInfo walletInfo, + String? mnemonic, + String? privateKey, + required String password, + ERC20Balance? initialBalance, + }) : syncStatus = NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _isTransactionUpdating = false, + _client = PolygonClient(), + walletAddresses = PolygonWalletAddresses(walletInfo), + balance = ObservableMap.of({ + CryptoCurrency.maticpoly: initialBalance ?? ERC20Balance(BigInt.zero) + }), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = + PolygonTransactionHistory(walletInfo: walletInfo, password: password); + + if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { + CakeHive.registerAdapter(Erc20TokenAdapter()); + } + + _sharedPrefs.complete(SharedPreferences.getInstance()); + } + + final String? _mnemonic; + final String? _hexPrivateKey; + final String _password; + + late final Box polygonErc20TokensBox; + + late final EthPrivateKey _polygonPrivateKey; + + EthPrivateKey get polygonPrivateKey => _polygonPrivateKey; + + late PolygonClient _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 balance; + + Completer _sharedPrefs = Completer(); + + Future init() async { + polygonErc20TokensBox = + await CakeHive.openBox(Erc20Token.polygonBoxName); + await walletAddresses.init(); + await transactionHistory.init(); + _polygonPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + walletAddresses.address = _polygonPrivateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is PolygonTransactionPriority) { + final priorityFee = + EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); + return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); + } + + return 0; + } catch (e) { + return 0; + } + } + + @override + Future changePassword(String password) { + throw UnimplementedError("changePassword"); + } + + @override + void close() { + _client.stop(); + _transactionsUpdateTimer?.cancel(); + } + + @action + @override + Future connectToNode({required Node node}) async { + try { + syncStatus = ConnectingSyncStatus(); + + final isConnected = _client.connect(node); + + if (!isConnected) { + throw Exception("Polygon Node connection failed"); + } + + _client.setListeners(_polygonPrivateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as PolygonTransactionCredentials; + final outputs = _credentials.outputs; + final hasMultiDestination = outputs.length > 1; + + final CryptoCurrency transactionCurrency = balance.keys + .firstWhere((element) => element.title == _credentials.currency.title); + + final _erc20Balance = balance[transactionCurrency]!; + BigInt totalAmount = BigInt.zero; + int exponent = + transactionCurrency is Erc20Token ? transactionCurrency.decimal : 18; + num amountToPolygonMultiplier = pow(10, exponent); + + // so far this can not be made with Polygon as Polygon does not support multiple recipients + if (hasMultiDestination) { + if (outputs.any( + (item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw PolygonTransactionCreationException(transactionCurrency); + } + + final totalOriginalAmount = PolygonFormatter.parsePolygonAmountToDouble( + outputs.fold( + 0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = + BigInt.from(totalOriginalAmount * amountToPolygonMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw PolygonTransactionCreationException(transactionCurrency); + } + } else { + final output = outputs.first; + // since the fees are taken from Ethereum + // then no need to subtract the fees from the amount if send all + final BigInt allAmount; + if (transactionCurrency is Erc20Token) { + allAmount = _erc20Balance.balance; + } else { + allAmount = _erc20Balance.balance - + BigInt.from(calculateEstimatedFee(_credentials.priority!, null)); + } + final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( + output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToPolygonMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw PolygonTransactionCreationException(transactionCurrency); + } + } + + final pendingPolygonTransaction = await _client.signTransaction( + privateKey: _polygonPrivateKey, + toAddress: _credentials.outputs.first.isParsedAddress + ? _credentials.outputs.first.extractedAddress! + : _credentials.outputs.first.address, + amount: totalAmount.toString(), + gas: _estimatedGas!, + priority: _credentials.priority!, + currency: transactionCurrency, + exponent: exponent, + contractAddress: transactionCurrency is Erc20Token + ? transactionCurrency.contractAddress + : null, + ); + + return pendingPolygonTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + bool isPolygonScanEnabled = (await _sharedPrefs.future).getBool("use_polygonscan") ?? true; + if (!isPolygonScanEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _polygonPrivateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List>> polygonErc20TokensTransactions = + []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + polygonErc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + ) as Future>); + } + } + + final tokensTransaction = await Future.wait(polygonErc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = PolygonTransactionInfo( + 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 ?? "MATIC", + to: transactionModel.to, + ); + } + + return result; + } + + @override + Object get keys => throw UnimplementedError("keys"); + + @override + Future rescan({required int height}) { + throw UnimplementedError("rescan"); + } + + @override + Future 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; + + @override + String get privateKey => HEX.encode(_polygonPrivateKey.privateKey); + + @action + @override + Future 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 makePath() async => + pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + static Future 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 privateKey = data['private_key'] as String?; + final balance = ERC20Balance.fromJSON(data['balance'] as String) ?? + ERC20Balance(BigInt.zero); + + return PolygonWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + ); + } + + Future _updateBalance() async { + balance[currency] = await _fetchMaticBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchMaticBalance() async { + final balance = await _client.getBalance(_polygonPrivateKey.address); + return ERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in polygonErc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _polygonPrivateKey.address, + token.contractAddress, + ); + } else { + balance.remove(token); + } + } catch (_) {} + } + } + + Future getPrivateKey( + {String? mnemonic, String? privateKey, required String password}) async { + assert(mnemonic != null || privateKey != null); + + if (privateKey != null) { + return EthPrivateKey.fromHex(privateKey); + } + + final seed = bip39.mnemonicToSeed(mnemonic!); + + final root = bip32.BIP32.fromSeed(seed); + + const _hdPathPolygon = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$_hdPathPolygon/$index"); + + return EthPrivateKey.fromHex( + HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => polygonErc20TokensBox.values.toList(); + + Future 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 polygonErc20TokensBox.put(_token.contractAddress, _token); + + if (_token.enabled) { + balance[_token] = await _client.fetchERC20Balances( + _polygonPrivateKey.address, + _token.contractAddress, + ); + } else { + balance.remove(_token); + } + } + + Future deleteErc20Token(Erc20Token token) async { + await token.delete(); + + balance.remove(token); + _updateBalance(); + } + + Future getErc20Token(String contractAddress) async => + await _client.getErc20Token(contractAddress); + + void _onNewTransaction() { + _updateBalance(); + _updateTransactions(); + } + + void addInitialTokens() { + final initialErc20Tokens = + DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; + + for (var token in initialErc20Tokens) { + polygonErc20TokensBox.put(token.contractAddress, token); + } + } + + @override + Future 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 updatePolygonScanUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } + + @override + String signMessage(String message, {String? address = null}) => bytesToHex( + _polygonPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); + + Web3Client? getWeb3Client() => _client.getWeb3Client(); +} diff --git a/cw_polygon/lib/polygon_wallet_addresses.dart b/cw_polygon/lib/polygon_wallet_addresses.dart new file mode 100644 index 000000000..0a6a407c7 --- /dev/null +++ b/cw_polygon/lib/polygon_wallet_addresses.dart @@ -0,0 +1,5 @@ +import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; + +class PolygonWalletAddresses extends EthereumWalletAddresses { + PolygonWalletAddresses(super.walletInfo); +} diff --git a/cw_polygon/lib/polygon_wallet_creation_credentials.dart b/cw_polygon/lib/polygon_wallet_creation_credentials.dart new file mode 100644 index 000000000..74c7c5ed7 --- /dev/null +++ b/cw_polygon/lib/polygon_wallet_creation_credentials.dart @@ -0,0 +1,28 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +class PolygonNewWalletCredentials extends WalletCredentials { + PolygonNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class PolygonRestoreWalletFromSeedCredentials extends WalletCredentials { + PolygonRestoreWalletFromSeedCredentials( + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class PolygonRestoreWalletFromPrivateKey extends WalletCredentials { + PolygonRestoreWalletFromPrivateKey( + {required String name, + required String password, + required this.privateKey, + WalletInfo? walletInfo}) + : super(name: name, password: password, walletInfo: walletInfo); + + final String privateKey; +} diff --git a/cw_polygon/lib/polygon_wallet_service.dart b/cw_polygon/lib/polygon_wallet_service.dart new file mode 100644 index 000000000..dafe3bab0 --- /dev/null +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -0,0 +1,123 @@ +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_polygon/polygon_wallet.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:hive/hive.dart'; +import 'polygon_wallet_creation_credentials.dart'; +import 'package:collection/collection.dart'; + +class PolygonWalletService extends WalletService { + PolygonWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + Future create(PolygonNewWalletCredentials credentials) async { + final strength = (credentials.seedPhraseLength == 12) + ? 128 + : (credentials.seedPhraseLength == 24) + ? 256 + : 128; + + final mnemonic = bip39.generateMnemonic(strength: strength); + final wallet = PolygonWallet( + walletInfo: credentials.walletInfo!, + mnemonic: mnemonic, + password: credentials.password!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + WalletType getType() => WalletType.polygon; + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @override + Future openWallet(String name, String password) async { + final walletInfo = + walletInfoSource.values.firstWhere((info) => info.id == WalletBase.idFor(name, getType())); + final wallet = await PolygonWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + + await wallet.init(); + await wallet.save(); + + return wallet; + } + + @override + Future remove(String wallet) async { + File(await pathForWalletDir(name: wallet, type: getType())).delete(recursive: true); + final walletInfo = walletInfoSource.values + .firstWhereOrNull((info) => info.id == WalletBase.idFor(wallet, getType()))!; + await walletInfoSource.delete(walletInfo.key); + } + + @override + Future restoreFromKeys(PolygonRestoreWalletFromPrivateKey credentials) async { + final wallet = PolygonWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future restoreFromSeed(PolygonRestoreWalletFromSeedCredentials credentials) async { + if (!bip39.validateMnemonic(credentials.mnemonic)) { + throw EthereumMnemonicIsIncorrectException(); + } + + final wallet = PolygonWallet( + password: credentials.password!, + mnemonic: credentials.mnemonic, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; + } + + @override + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await PolygonWalletBase.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_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml new file mode 100644 index 000000000..e99e6dbbb --- /dev/null +++ b/cw_polygon/pubspec.yaml @@ -0,0 +1,73 @@ +name: cw_polygon +description: A new Flutter package project. +version: 0.0.1 +publish_to: none +author: Cake Wallet +homepage: https://cakewallet.com + +environment: + sdk: '>=3.0.6 <4.0.0' + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + cw_core: + path: ../cw_core + cw_ethereum: + path: ../cw_ethereum + mobx: ^2.0.7+4 + intl: ^0.18.0 + bip39: ^1.0.6 + hive: ^2.2.3 + collection: ^1.17.1 + web3dart: ^2.7.1 + bip32: ^2.0.0 + hex: ^0.2.0 + shared_preferences: ^2.0.15 + + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + 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_polygon/test/cw_polygon_test.dart b/cw_polygon/test/cw_polygon_test.dart new file mode 100644 index 000000000..554e28795 --- /dev/null +++ b/cw_polygon/test/cw_polygon_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_polygon/cw_polygon.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/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 6cea7a730..4f7036498 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -160,6 +160,26 @@ bitcoincash-wallet + + CFBundleTypeRole + Editor + CFBundleURLName + polygon + CFBundleURLSchemes + + polygon + + + + CFBundleTypeRole + Viewer + CFBundleURLName + polygon-wallet + CFBundleURLSchemes + + polygon-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index fcb881943..1bc0d1ae2 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -271,6 +271,8 @@ class AddressValidator extends TextValidator { '|([^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}'; + case CryptoCurrency.maticpoly: + return '0x[0-9a-zA-Z]{42}'; case CryptoCurrency.nano: return 'nano_[0-9a-zA-Z]{60}'; case CryptoCurrency.banano: diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 479aa3b82..8a37175b4 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -16,7 +16,7 @@ Future _fetchPrice(Map args) async { final Map queryParams = { 'interval_count': '1', - 'base': crypto, + 'base': crypto.split(".").first, 'quote': fiat, 'key': secrets.fiatApiKey, }; diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index 95ccf89ac..8f65159e1 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -3,6 +3,7 @@ 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'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/nano/nano.dart'; @@ -34,6 +35,8 @@ class SeedValidator extends Validator { case WalletType.nano: case WalletType.banano: return nano!.getNanoWordList(language); + case WalletType.polygon: + return polygon!.getPolygonWordList(language); default: return []; } diff --git a/lib/core/wallet_connect/evm_chain_service.dart b/lib/core/wallet_connect/evm_chain_service.dart index dc22e3dda..b9849fdac 100644 --- a/lib/core/wallet_connect/evm_chain_service.dart +++ b/lib/core/wallet_connect/evm_chain_service.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/core/wallet_connect/eth_transaction_model.dart'; import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; @@ -14,7 +15,6 @@ import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_widget import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; import 'package:cake_wallet/src/screens/wallet_connect/utils/string_parsing.dart'; import 'package:convert/convert.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:eth_sig_util/eth_sig_util.dart'; import 'package:eth_sig_util/util/utils.dart'; import 'package:http/http.dart' as http; @@ -46,13 +46,12 @@ class EvmChainServiceImpl implements ChainService { required this.wcKeyService, required this.bottomSheetService, required this.wallet, - Web3Client? ethClient, - }) : ethClient = ethClient ?? + Web3Client? web3Client, + }) : ethClient = web3Client ?? Web3Client( - appStore.settingsStore.getCurrentNode(WalletType.ethereum).uri.toString(), + appStore.settingsStore.getCurrentNode(appStore.wallet!.type).uri.toString(), http.Client(), ) { - for (final String event in getEvents()) { wallet.registerEventEmitter(chainId: getChainId(), event: event); } @@ -138,7 +137,8 @@ class EvmChainServiceImpl implements ChainService { try { // Load the private key - final List keys = wcKeyService.getKeysForChain(getChainId()); + final List keys = wcKeyService + .getKeysForChain(getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type)); final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); @@ -176,13 +176,15 @@ class EvmChainServiceImpl implements ChainService { try { // Load the private key - final List keys = wcKeyService.getKeysForChain(getChainId()); + final List keys = wcKeyService + .getKeysForChain(getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type)); final EthPrivateKey credentials = EthPrivateKey.fromHex(keys[0].privateKey); final String signature = hex.encode( credentials.signPersonalMessageToUint8List( Uint8List.fromList(utf8.encode(message)), + chainId: getChainIdBasedOnWalletType(appStore.wallet!.type), ), ); log(signature); @@ -212,7 +214,8 @@ class EvmChainServiceImpl implements ChainService { } // Load the private key - final List keys = wcKeyService.getKeysForChain(getChainId()); + final List keys = wcKeyService + .getKeysForChain(getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type)); final Credentials credentials = EthPrivateKey.fromHex(keys[0].privateKey); @@ -232,7 +235,11 @@ class EvmChainServiceImpl implements ChainService { ); try { - final result = await ethClient.sendTransaction(credentials, transaction); + final result = await ethClient.sendTransaction( + credentials, + transaction, + chainId: getChainIdBasedOnWalletType(appStore.wallet!.type), + ); log('Result: $result'); @@ -267,7 +274,8 @@ class EvmChainServiceImpl implements ChainService { return authError; } - final List keys = wcKeyService.getKeysForChain(getChainId()); + final List keys = wcKeyService + .getKeysForChain(getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type)); return EthSigUtil.signTypedData( privateKey: keys[0].privateKey, diff --git a/lib/core/wallet_connect/wallet_connect_key_service.dart b/lib/core/wallet_connect/wallet_connect_key_service.dart index 2e61ebb99..bb0c9d14c 100644 --- a/lib/core/wallet_connect/wallet_connect_key_service.dart +++ b/lib/core/wallet_connect/wallet_connect_key_service.dart @@ -1,9 +1,11 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; abstract class WalletConnectKeyService { /// Returns a list of all the keys. @@ -32,16 +34,36 @@ class KeyServiceImpl implements WalletConnectKeyService { 'eip155:42161', 'eip155:80001', ], - privateKey: ethereum!.getPrivateKey(wallet), - publicKey: ethereum!.getPublicKey(wallet), + privateKey: _getPrivateKeyForWallet(wallet), + publicKey: _getPublicKeyForWallet(wallet), ), - ]; late final WalletBase, TransactionInfo> wallet; late final List _keys; + static String _getPrivateKeyForWallet(WalletBase wallet) { + switch (wallet.type) { + case WalletType.ethereum: + return ethereum!.getPrivateKey(wallet); + case WalletType.polygon: + return polygon!.getPrivateKey(wallet); + default: + return ''; + } + } + + static String _getPublicKeyForWallet(WalletBase wallet) { + switch (wallet.type) { + case WalletType.ethereum: + return ethereum!.getPublicKey(wallet); + case WalletType.polygon: + return polygon!.getPublicKey(wallet); + default: + return ''; + } + } @override List getChains() { final List chainIds = []; diff --git a/lib/core/wallet_connect/web3wallet_service.dart b/lib/core/wallet_connect/web3wallet_service.dart index c69692c9d..7a1dac6cc 100644 --- a/lib/core/wallet_connect/web3wallet_service.dart +++ b/lib/core/wallet_connect/web3wallet_service.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/wallet_connect/models/auth_request_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/chain_key_model.dart'; import 'package:cake_wallet/core/wallet_connect/models/session_request_model.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/connection_request_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/modals/web3_request_modal.dart'; @@ -164,8 +165,10 @@ abstract class Web3WalletServiceBase with Store { void _onSessionProposal(SessionProposalEvent? args) async { if (args != null) { + final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type); final Widget modalWidget = Web3RequestModal( child: ConnectionRequestWidget( + chaindIdNamespace: chaindIdNamespace, wallet: _web3Wallet, sessionProposal: SessionRequestModel(request: args.params), ), @@ -232,12 +235,13 @@ abstract class Web3WalletServiceBase with Store { @action Future _onAuthRequest(AuthRequest? args) async { if (args != null) { - List chainKeys = walletKeyService.getKeysForChain('eip155:1'); + final chaindIdNamespace = getChainNameSpaceAndIdBasedOnWalletType(appStore.wallet!.type); + List chainKeys = walletKeyService.getKeysForChain(chaindIdNamespace); // Create the message to be signed - final String iss = 'did:pkh:eip155:1:${chainKeys.first.publicKey}'; - + final String iss = 'did:pkh:$chaindIdNamespace:${chainKeys.first.publicKey}'; final Widget modalWidget = Web3RequestModal( child: ConnectionRequestWidget( + chaindIdNamespace: chaindIdNamespace, wallet: _web3Wallet, authRequest: AuthRequestModel(iss: iss, request: args), ), diff --git a/lib/di.dart b/lib/di.dart index 362935fbc..deadf5675 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -20,6 +20,8 @@ import 'package:cake_wallet/nano/nano.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'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/anonpay_details/anonpay_details_page.dart'; import 'package:cake_wallet/src/screens/buy/buy_options_page.dart'; @@ -753,7 +755,7 @@ Future setup({ final wallet = getIt.get().wallet; return ConnectionSyncPage( getIt.get(), - wallet?.type == WalletType.ethereum ? getIt.get() : null, + isEVMCompatibleChain(wallet!.type) ? getIt.get() : null, ); }); @@ -855,6 +857,8 @@ Future setup({ .createBitcoinCashWalletService(_walletInfoSource, _unspentCoinsInfoSource!); case WalletType.nano: return nano!.createNanoWalletService(_walletInfoSource); + case WalletType.polygon: + return polygon!.createPolygonWalletService(_walletInfoSource); default: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index a65742e99..45803899e 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -1,5 +1,6 @@ import 'dart:io' show Directory, File, Platform; import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/entities/encrypt.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cw_core/pathForWallet.dart'; import 'package:cake_wallet/entities/secret_store_key.dart'; @@ -26,6 +27,7 @@ const cakeWalletBitcoinElectrumUri = 'electrum.cakewallet.com:50002'; const cakeWalletLitecoinElectrumUri = 'ltc-electrum.cakewallet.com:50002'; const havenDefaultNodeUri = 'nodes.havenprotocol.org:443'; const ethereumDefaultNodeUri = 'ethereum.publicnode.com'; +const polygonDefaultNodeUri = 'polygon-bor.publicnode.com'; const cakeWalletBitcoinCashDefaultNodeUri = 'bitcoincash.stackwallet.com:50002'; const nanoDefaultNodeUri = 'rpc.nano.to'; const nanoDefaultPowNodeUri = 'rpc.nano.to'; @@ -64,6 +66,8 @@ Future defaultSettingsMigration( final migrationVersions = List.generate(migrationVersionsLength, (i) => currentVersion + (i + 1)); + /// When you add a new case, increase the initialMigrationVersion parameter in the main.dart file. + /// This ensures that this switch case runs the newly added case. await Future.forEach(migrationVersions, (int version) async { try { switch (version) { @@ -82,8 +86,7 @@ Future defaultSettingsMigration( sharedPreferences: sharedPreferences, nodes: nodes); await changeLitecoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); - await changeHavenCurrentNodeToDefault( - sharedPreferences: sharedPreferences, nodes: nodes); + await changeHavenCurrentNodeToDefault(sharedPreferences: sharedPreferences, nodes: nodes); await changeBitcoinCashCurrentNodeToDefault( sharedPreferences: sharedPreferences, nodes: nodes); @@ -175,6 +178,14 @@ Future defaultSettingsMigration( await changeBitcoinCurrentElectrumServerToDefault( sharedPreferences: sharedPreferences, nodes: nodes); break; + case 24: + await addPolygonNodeList(nodes: nodes); + await changePolygonCurrentNodeToDefault( + sharedPreferences: sharedPreferences, nodes: nodes); + break; + case 25: + await rewriteSecureStoragePin(secureStorage: secureStorage); + break; default: break; @@ -329,6 +340,11 @@ Node? getEthereumDefaultNode({required Box nodes}) { nodes.values.firstWhereOrNull((node) => node.type == WalletType.ethereum); } +Node? getPolygonDefaultNode({required Box nodes}) { + return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == polygonDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.polygon); +} + Node? getNanoDefaultNode({required Box nodes}) { return nodes.values.firstWhereOrNull((Node node) => node.uriRaw == nanoDefaultNodeUri) ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.nano); @@ -340,9 +356,9 @@ Node? getNanoDefaultPowNode({required Box nodes}) { } Node? getBitcoinCashDefaultElectrumServer({required Box nodes}) { - return nodes.values.firstWhereOrNull( - (Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) - ?? nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); + return nodes.values + .firstWhereOrNull((Node node) => node.uriRaw == cakeWalletBitcoinCashDefaultNodeUri) ?? + nodes.values.firstWhereOrNull((node) => node.type == WalletType.bitcoinCash); } Node getMoneroDefaultNode({required Box nodes}) { @@ -364,6 +380,37 @@ Node getMoneroDefaultNode({required Box nodes}) { } } +Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorage}) async { + // the bug only affects ios/mac: + if (!Platform.isIOS && !Platform.isMacOS) { + return; + } + + // first, get the encoded pin: + final keyForPinCode = generateStoreKeyFor(key: SecretStoreKey.pinCodePassword); + String? encodedPin; + try { + encodedPin = await secureStorage.read(key: keyForPinCode); + } catch (e) { + // either we don't have a pin, or we can't read it (maybe even because of the bug!) + // the only option here is to abort the migration or we risk losing the pin and locking the user out + return; + } + + if (encodedPin == null) { + return; + } + + // ensure we overwrite by deleting the old key first: + await secureStorage.delete(key: keyForPinCode); + await secureStorage.write( + key: keyForPinCode, + value: encodedPin, + iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), + mOptions: MacOsOptions(accessibility: KeychainAccessibility.first_unlock), + ); +} + Future changeBitcoinCurrentElectrumServerToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final server = getBitcoinDefaultElectrumServer(nodes: nodes); @@ -381,8 +428,7 @@ Future changeLitecoinCurrentElectrumServerToDefault( } Future changeBitcoinCashCurrentNodeToDefault( - {required SharedPreferences sharedPreferences, - required Box nodes}) async { + {required SharedPreferences sharedPreferences, required Box nodes}) async { final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); final serverId = server?.key as int ?? 0; @@ -538,34 +584,33 @@ Future checkCurrentNodes( final currentMoneroNodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey); final currentBitcoinElectrumSeverId = sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey); - final currentLitecoinElectrumSeverId = sharedPreferences - .getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); - final currentHavenNodeId = sharedPreferences - .getInt(PreferencesKey.currentHavenNodeIdKey); - final currentEthereumNodeId = sharedPreferences - .getInt(PreferencesKey.currentEthereumNodeIdKey); - final currentNanoNodeId = sharedPreferences - .getInt(PreferencesKey.currentNanoNodeIdKey); - final currentNanoPowNodeId = sharedPreferences - .getInt(PreferencesKey.currentNanoPowNodeIdKey); - final currentBitcoinCashNodeId = sharedPreferences - .getInt(PreferencesKey.currentBitcoinCashNodeIdKey); - final currentMoneroNode = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentMoneroNodeId); - final currentBitcoinElectrumServer = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentBitcoinElectrumSeverId); - final currentLitecoinElectrumServer = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentLitecoinElectrumSeverId); - final currentHavenNodeServer = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentHavenNodeId); - final currentEthereumNodeServer = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentEthereumNodeId); + final currentLitecoinElectrumSeverId = + sharedPreferences.getInt(PreferencesKey.currentLitecoinElectrumSererIdKey); + final currentHavenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); + final currentEthereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); + final currentPolygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); + final currentNanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); + final currentNanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); + final currentBitcoinCashNodeId = + sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); + final currentMoneroNode = + nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); + final currentBitcoinElectrumServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinElectrumSeverId); + final currentLitecoinElectrumServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentLitecoinElectrumSeverId); + final currentHavenNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentHavenNodeId); + final currentEthereumNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentEthereumNodeId); + final currentPolygonNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentPolygonNodeId); final currentNanoNodeServer = - nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); + nodeSource.values.firstWhereOrNull((node) => node.key == currentNanoNodeId); final currentNanoPowNodeServer = - powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); - final currentBitcoinCashNodeServer = nodeSource.values.firstWhereOrNull( - (node) => node.key == currentBitcoinCashNodeId); + powNodeSource.values.firstWhereOrNull((node) => node.key == currentNanoPowNodeId); + final currentBitcoinCashNodeServer = + nodeSource.values.firstWhereOrNull((node) => node.key == currentBitcoinCashNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); await nodeSource.add(newCakeWalletNode); @@ -617,8 +662,13 @@ Future checkCurrentNodes( if (currentBitcoinCashNodeServer == null) { final node = Node(uri: cakeWalletBitcoinCashDefaultNodeUri, type: WalletType.bitcoinCash); await nodeSource.add(node); - await sharedPreferences.setInt( - PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); + await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, node.key as int); + } + + if (currentPolygonNodeServer == null) { + final node = Node(uri: polygonDefaultNodeUri, type: WalletType.polygon); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); } } @@ -714,3 +764,20 @@ Future changeNanoCurrentPowNodeToDefault( final nodeId = node?.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentNanoPowNodeIdKey, nodeId); } + +Future addPolygonNodeList({required Box nodes}) async { + final nodeList = await loadDefaultPolygonNodes(); + for (var node in nodeList) { + if (nodes.values.firstWhereOrNull((element) => element.uriRaw == node.uriRaw) == null) { + await nodes.add(node); + } + } +} + +Future changePolygonCurrentNodeToDefault( + {required SharedPreferences sharedPreferences, required Box nodes}) async { + final node = getPolygonDefaultNode(nodes: nodes); + final nodeId = node?.key as int? ?? 0; + + await sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, nodeId); +} diff --git a/lib/entities/ens_record.dart b/lib/entities/ens_record.dart index 8cf62d79b..b2ce51806 100644 --- a/lib/entities/ens_record.dart +++ b/lib/entities/ens_record.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:ens_dart/ens_dart.dart'; @@ -12,6 +13,10 @@ class EnsRecord { if (wallet != null && wallet.type == WalletType.ethereum) { _client = ethereum!.getWeb3Client(wallet); } + + if (wallet != null && wallet.type == WalletType.polygon) { + _client = polygon!.getWeb3Client(wallet); + } if (_client == null) { _client = Web3Client("https://ethereum.publicnode.com", Client()); @@ -31,6 +36,7 @@ class EnsRecord { case WalletType.haven: return await ens.withName(name).getCoinAddress(CoinType.XHV); case WalletType.ethereum: + case WalletType.polygon: default: return (await ens.withName(name).getAddress()).hex; } diff --git a/lib/entities/main_actions.dart b/lib/entities/main_actions.dart index 81a835d99..8a73da2d7 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -21,7 +21,8 @@ class MainActions { final bool Function(DashboardViewModel viewModel)? isEnabled; final bool Function(DashboardViewModel viewModel)? canShow; - final Future Function(BuildContext context, DashboardViewModel viewModel) onTap; + final Future Function( + BuildContext context, DashboardViewModel viewModel) onTap; MainActions._({ required this.name, @@ -54,6 +55,7 @@ class MainActions { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: + case WalletType.polygon: case WalletType.bitcoinCash: switch (defaultBuyProvider) { case BuyProviderType.AskEachTime: @@ -161,6 +163,7 @@ class MainActions { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: + case WalletType.polygon: case WalletType.bitcoinCash: if (viewModel.isEnabledSellAction) { final moonPaySellProvider = MoonPaySellProvider(); diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 53facf18c..6d1cf299d 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -133,6 +133,22 @@ Future> loadDefaultNanoPowNodes() async { return nodes; } +Future> loadDefaultPolygonNodes() async { + final nodesRaw = await rootBundle.loadString('assets/polygon_node_list.yml'); + final loadedNodes = loadYaml(nodesRaw) as YamlList; + final nodes = []; + + for (final raw in loadedNodes) { + if (raw is Map) { + final node = Node.fromMap(Map.from(raw)); + node.type = WalletType.polygon; + nodes.add(node); + } + } + + return nodes; +} + Future resetToDefault(Box nodeSource) async { final moneroNodes = await loadDefaultNodes(); final bitcoinElectrumServerList = await loadBitcoinElectrumServerList(); @@ -141,6 +157,8 @@ Future resetToDefault(Box nodeSource) async { final havenNodes = await loadDefaultHavenNodes(); final ethereumNodes = await loadDefaultEthereumNodes(); final nanoNodes = await loadDefaultNanoNodes(); + final polygonNodes = await loadDefaultPolygonNodes(); + final nodes = moneroNodes + bitcoinElectrumServerList + @@ -148,7 +166,8 @@ Future resetToDefault(Box nodeSource) async { havenNodes + ethereumNodes + bitcoinCashElectrumServerList + - nanoNodes; + nanoNodes + + polygonNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index e6a43c409..072419a75 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -6,6 +6,7 @@ class PreferencesKey { static const currentLitecoinElectrumSererIdKey = 'current_node_id_ltc'; static const currentHavenNodeIdKey = 'current_node_id_xhv'; static const currentEthereumNodeIdKey = 'current_node_id_eth'; + static const currentPolygonNodeIdKey = 'current_node_id_matic'; static const currentNanoNodeIdKey = 'current_node_id_nano'; static const currentNanoPowNodeIdKey = 'current_node_id_nano_pow'; static const currentBananoNodeIdKey = 'current_node_id_banano'; @@ -38,6 +39,7 @@ class PreferencesKey { static const havenTransactionPriority = 'current_fee_priority_haven'; static const litecoinTransactionPriority = 'current_fee_priority_litecoin'; static const ethereumTransactionPriority = 'current_fee_priority_ethereum'; + static const polygonTransactionPriority = 'current_fee_priority_polygon'; static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash'; static const shouldShowReceiveWarning = 'should_show_receive_warning'; static const shouldShowYatPopup = 'should_show_yat_popup'; @@ -51,6 +53,7 @@ class PreferencesKey { static const sortBalanceBy = 'sort_balance_by'; static const pinNativeTokenAtTop = 'pin_native_token_at_top'; static const useEtherscan = 'use_etherscan'; + static const usePolygonScan = 'use_polygonscan'; static const defaultNanoRep = 'default_nano_representative'; static const defaultBananoRep = 'default_banano_representative'; static const lookupsTwitter = 'looks_up_twitter'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index bf6f8157d..70b072c55 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/haven/haven.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -24,6 +25,8 @@ List priorityForWalletType(WalletType type) { case WalletType.nano: case WalletType.banano: return []; + case WalletType.polygon: + return polygon!.getTransactionPriorities(); default: return []; } diff --git a/lib/main.dart b/lib/main.dart index 6807e9185..165db1ddd 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,7 +163,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 23); + initialMigrationVersion: 25); } Future initialSetup( diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart new file mode 100644 index 000000000..066b29d43 --- /dev/null +++ b/lib/polygon/cw_polygon.dart @@ -0,0 +1,156 @@ +part of 'polygon.dart'; + +class CWPolygon extends Polygon { + @override + List getPolygonWordList(String language) => EthereumMnemonics.englishWordlist; + + WalletService createPolygonWalletService(Box walletInfoSource) => + PolygonWalletService(walletInfoSource); + + @override + WalletCredentials createPolygonNewWalletCredentials({ + required String name, + WalletInfo? walletInfo, + }) => + PolygonNewWalletCredentials(name: name, walletInfo: walletInfo); + + @override + WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ + required String name, + required String mnemonic, + required String password, + }) => + PolygonRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + + @override + WalletCredentials createPolygonRestoreWalletFromPrivateKey({ + required String name, + required String privateKey, + required String password, + }) => + PolygonRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + + @override + String getAddress(WalletBase wallet) => (wallet as PolygonWallet).walletAddresses.address; + + @override + String getPrivateKey(WalletBase wallet) { + final privateKeyHolder = (wallet as PolygonWallet).polygonPrivateKey; + String stringKey = bytesToHex(privateKeyHolder.privateKey); + return stringKey; + } + + @override + String getPublicKey(WalletBase wallet) { + final privateKeyInUnitInt = (wallet as PolygonWallet).polygonPrivateKey; + final publicKey = privateKeyInUnitInt.address.hex; + return publicKey; + } + + @override + TransactionPriority getDefaultTransactionPriority() => PolygonTransactionPriority.medium; + + @override + TransactionPriority getPolygonTransactionPrioritySlow() => PolygonTransactionPriority.slow; + + @override + List getTransactionPriorities() => PolygonTransactionPriority.all; + + @override + TransactionPriority deserializePolygonTransactionPriority(int raw) => + PolygonTransactionPriority.deserialize(raw: raw); + + Object createPolygonTransactionCredentials( + List outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }) => + PolygonTransactionCredentials( + 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 PolygonTransactionPriority, + currency: currency, + feeRate: feeRate, + ); + + Object createPolygonTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }) => + PolygonTransactionCredentials( + outputs, + priority: priority as PolygonTransactionPriority?, + currency: currency, + feeRate: feeRate, + ); + + @override + int formatterPolygonParseAmount(String amount) => PolygonFormatter.parsePolygonAmount(amount); + + @override + double formatterPolygonAmountToDouble( + {TransactionInfo? transaction, BigInt? amount, int exponent = 18}) { + assert(transaction != null || amount != null); + + if (transaction != null) { + transaction as PolygonTransactionInfo; + return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); + } else { + return (amount!) / BigInt.from(10).pow(exponent); + } + } + + @override + List getERC20Currencies(WalletBase wallet) { + final polygonWallet = wallet as PolygonWallet; + return polygonWallet.erc20Currencies; + } + + @override + Future addErc20Token(WalletBase wallet, Erc20Token token) async => + await (wallet as PolygonWallet).addErc20Token(token); + + @override + Future deleteErc20Token(WalletBase wallet, Erc20Token token) async => + await (wallet as PolygonWallet).deleteErc20Token(token); + + @override + Future getErc20Token(WalletBase wallet, String contractAddress) async { + final polygonWallet = wallet as PolygonWallet; + return await polygonWallet.getErc20Token(contractAddress); + } + + @override + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { + transaction as PolygonTransactionInfo; + if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) { + return CryptoCurrency.maticpoly; + } + + wallet as PolygonWallet; + return wallet.erc20Currencies.firstWhere( + (element) => transaction.tokenSymbol.toLowerCase() == element.symbol.toLowerCase()); + } + + @override + void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) { + (wallet as PolygonWallet).updatePolygonScanUsageState(isEnabled); + } + + @override + Web3Client? getWeb3Client(WalletBase wallet) { + return (wallet as PolygonWallet).getWeb3Client(); + } +} diff --git a/lib/reactions/fiat_rate_update.dart b/lib/reactions/fiat_rate_update.dart index 141401e6a..2b757ad44 100644 --- a/lib/reactions/fiat_rate_update.dart +++ b/lib/reactions/fiat_rate_update.dart @@ -3,9 +3,11 @@ 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/polygon/polygon.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'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; @@ -33,10 +35,18 @@ Future startFiatRateUpdate( torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); } + Iterable? currencies; if (appStore.wallet!.type == WalletType.ethereum) { - final currencies = - ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + currencies = + ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } + if (appStore.wallet!.type == WalletType.polygon) { + currencies = + polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } + + if (currencies != null) { for (final currency in currencies) { () async { fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 5f956dc1a..42fbd182e 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -2,7 +2,8 @@ import 'package:cake_wallet/entities/auto_generate_subaddress_status.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/nano/nano.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_info.dart'; @@ -107,10 +108,17 @@ void startCurrentWalletChangeReaction( fiat: settingsStore.fiatCurrency, torOnly: settingsStore.fiatApiMode == FiatApiMode.torOnly); + Iterable? currencies; if (wallet.type == WalletType.ethereum) { - final currencies = + currencies = ethereum!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } + if (wallet.type == WalletType.polygon) { + currencies = + polygon!.getERC20Currencies(appStore.wallet!).where((element) => element.enabled); + } + if (currencies != null) { for (final currency in currencies) { () async { fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice( diff --git a/lib/reactions/wallet_connect.dart b/lib/reactions/wallet_connect.dart new file mode 100644 index 000000000..d0ff37267 --- /dev/null +++ b/lib/reactions/wallet_connect.dart @@ -0,0 +1,46 @@ +import 'package:cake_wallet/core/wallet_connect/evm_chain_id.dart'; +import 'package:cw_core/wallet_type.dart'; + +bool isEVMCompatibleChain(WalletType walletType) { + switch (walletType) { + case WalletType.polygon: + case WalletType.ethereum: + return true; + default: + return false; + } +} + +String getChainNameSpaceAndIdBasedOnWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.ethereum: + return EVMChainId.ethereum.chain(); + case WalletType.polygon: + return EVMChainId.polygon.chain(); + default: + return ''; + } +} + +int getChainIdBasedOnWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.polygon: + return 137; + + // For now, we return eth chain Id as the default, we'll modify as we add more wallets + case WalletType.ethereum: + default: + return 1; + } +} + +String getChainNameBasedOnWalletType(WalletType walletType) { + switch (walletType) { + case WalletType.ethereum: + return 'eth'; + case WalletType.polygon: + return 'polygon'; + default: + return ''; + } +} 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 1aa7f6c4a..59c31aa62 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 @@ -33,6 +33,7 @@ class _DesktopWalletSelectionDropDownState extends State { this.ethereumIcon = Image.asset('assets/images/eth_icon.png'), this.nanoIcon = Image.asset('assets/images/nano_icon.png'), this.bananoIcon = Image.asset('assets/images/nano_icon.png'), - this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'); + this.bitcoinCashIcon = Image.asset('assets/images/bch_icon.png'), + this.polygonIcon = Image.asset('assets/images/matic_icon.png'); final largeScreen = 731; @@ -54,6 +55,8 @@ class MenuWidgetState extends State { Image bitcoinCashIcon; Image nanoIcon; Image bananoIcon; + Image polygonIcon; + @override void initState() { @@ -219,6 +222,8 @@ class MenuWidgetState extends State { return nanoIcon; case WalletType.banano: return bananoIcon; + case WalletType.polygon: + return polygonIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/new_wallet/new_wallet_page.dart b/lib/src/screens/new_wallet/new_wallet_page.dart index d86d51e93..5c9533ace 100644 --- a/lib/src/screens/new_wallet/new_wallet_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_page.dart @@ -1,4 +1,6 @@ import 'package:cake_wallet/entities/generate_name.dart'; +import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/main.dart'; import 'package:cake_wallet/routes.dart'; @@ -88,6 +90,8 @@ class _WalletNameFormState extends State { }); } }); + + _setSeedType(SeedType.defaultSeedType); super.initState(); } @@ -183,25 +187,38 @@ class _WalletNameFormState extends State { ), ), ), + if (_walletNewVM.hasLanguageSelector) ...[ - Padding( - padding: EdgeInsets.only(top: 40), - child: Text( - S.of(context).seed_language_choose, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 16.0, - fontWeight: FontWeight.w500, - color: Theme.of(context).extension()!.titleColor), + if (_walletNewVM.hasSeedType) ...[ + Observer( + builder: (BuildContext build) => Padding( + padding: EdgeInsets.only(top: 24), + child: SelectButton( + text: widget._settingsStore.moneroSeedType.title, + onTap: () async { + await showPopUp( + context: context, + builder: (_) => Picker( + items: SeedType.all, + selectedAtIndex: isPolyseed ? 1 : 0, + onItemSelected: _setSeedType, + isSeparated: false, + ), + ); + }, + ), + ), ), - ), + ], Observer( builder: (BuildContext build) => Padding( - padding: EdgeInsets.only(top: 24), + padding: EdgeInsets.only(top: 10), child: SeedLanguageSelector( key: _languageSelectorKey, initialSelected: defaultSeedLanguage, - seedType: widget._settingsStore.moneroSeedType, + seedType: _walletNewVM.hasSeedType + ? widget._settingsStore.moneroSeedType + : SeedType.legacy, ), ), ) @@ -231,7 +248,7 @@ class _WalletNameFormState extends State { Navigator.of(context) .pushNamed(Routes.advancedPrivacySettings, arguments: _walletNewVM.type); }, - child: Text(S.of(context).advanced_privacy_settings), + child: Text(S.of(context).advanced_settings), ), ], )), @@ -253,11 +270,17 @@ class _WalletNameFormState extends State { buttonAction: () => Navigator.of(context).pop()); }); } else { - final isPolyseed = widget._settingsStore.moneroSeedType == SeedType.polyseed; _walletNewVM.create( options: _walletNewVM.hasLanguageSelector ? [_languageSelectorKey.currentState!.selected, isPolyseed] : null); } } + + bool get isPolyseed => widget._settingsStore.moneroSeedType == SeedType.polyseed; + + void _setSeedType(SeedType item) { + widget._settingsStore.moneroSeedType = item; + _languageSelectorKey.currentState?.selected = defaultSeedLanguage; // Reset Seed language + } } diff --git a/lib/src/screens/receive/widgets/currency_input_field.dart b/lib/src/screens/receive/widgets/currency_input_field.dart index 1241b2ba7..84b2a7bca 100644 --- a/lib/src/screens/receive/widgets/currency_input_field.dart +++ b/lib/src/screens/receive/widgets/currency_input_field.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/src/widgets/base_text_form_field.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:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -23,6 +24,13 @@ class CurrencyInputField extends StatelessWidget { final TextEditingController controller; final bool isLight; + String get _currencyName { + if (selectedCurrency is CryptoCurrency) { + return (selectedCurrency as CryptoCurrency).title.toUpperCase(); + } + return selectedCurrency.name.toUpperCase(); + } + @override Widget build(BuildContext context) { final arrowBottomPurple = Image.asset( @@ -74,7 +82,7 @@ class CurrencyInputField extends StatelessWidget { child: arrowBottomPurple, ), Text( - selectedCurrency.name.toUpperCase(), + _currencyName, style: TextStyle( fontWeight: FontWeight.w600, fontSize: 16, @@ -83,7 +91,7 @@ class CurrencyInputField extends StatelessWidget { ), if (selectedCurrency.tag != null) Padding( - padding: const EdgeInsets.only(right: 3.0), + padding: const EdgeInsets.symmetric(horizontal: 3.0), child: Container( decoration: BoxDecoration( color: Theme.of(context).extension()!.textFieldButtonColor, diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index ac8413212..f31657743 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -124,25 +124,30 @@ class WalletRestoreFromSeedFormState extends State { onSeedChange: onSeedChange), if (widget.type == WalletType.monero) GestureDetector( - onTap: () async { - await showPopUp( - context: context, - builder: (_) => Picker( - items: SeedType.all, - selectedAtIndex: isPolyseed ? 1 : 0, - mainAxisAlignment: MainAxisAlignment.start, - onItemSelected: _changeSeedType, - isSeparated: false, - )); - }, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.only(top: 20.0), - child: IgnorePointer( - child: BaseTextFormField( - controller: seedTypeController, - enableInteractiveSelection: false, - readOnly: true)))), + onTap: () async { + await showPopUp( + context: context, + builder: (_) => Picker( + items: SeedType.all, + selectedAtIndex: isPolyseed ? 1 : 0, + mainAxisAlignment: MainAxisAlignment.start, + onItemSelected: _changeSeedType, + isSeparated: false, + )); + }, + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 20.0), + child: IgnorePointer( + child: BaseTextFormField( + controller: seedTypeController, + enableInteractiveSelection: false, + readOnly: true, + suffixIcon: expandIcon, + ), + ), + ), + ), if (widget.displayLanguageSelector) GestureDetector( onTap: () async { @@ -154,14 +159,19 @@ class WalletRestoreFromSeedFormState extends State { seedType: isPolyseed ? SeedType.polyseed : SeedType.legacy, )); }, - child: Container( - color: Colors.transparent, - padding: EdgeInsets.only(top: 20.0), - child: IgnorePointer( - child: BaseTextFormField( - controller: languageController, - enableInteractiveSelection: false, - readOnly: true)))), + child: Container( + color: Colors.transparent, + padding: EdgeInsets.only(top: 20.0), + child: IgnorePointer( + child: BaseTextFormField( + controller: languageController, + enableInteractiveSelection: false, + readOnly: true, + suffixIcon: expandIcon, + ), + ), + ), + ), if (!isPolyseed && widget.displayBlockHeightSelector) BlockchainHeightWidget( focusNode: widget.blockHeightFocusNode, @@ -171,6 +181,17 @@ class WalletRestoreFromSeedFormState extends State { ])); } + Widget get expandIcon => Container( + padding: EdgeInsets.all(18), + width: 24, + height: 24, + child: Image.asset( + 'assets/images/arrow_bottom_purple_icon.png', + height: 8, + color: Theme.of(context).hintColor, + ), + ); + void _changeLanguage(String language) { final setLang = isPolyseed ? "POLYSEED_$language" : language; setState(() { diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index ccac6f34c..445858f30 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -210,7 +210,7 @@ class WalletRestorePage extends BasePage { Navigator.of(context).pushNamed(Routes.advancedPrivacySettings, arguments: walletRestoreViewModel.type); }, - child: Text(S.of(context).advanced_privacy_settings), + child: Text(S.of(context).advanced_settings), ), ], ), diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index ca86cdccc..67cd689b1 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -2,9 +2,9 @@ import 'dart:async'; import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/totp_request_details.dart'; import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/device_info.dart'; import 'package:cake_wallet/utils/payment_request.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/auth/auth_page.dart'; @@ -169,7 +169,7 @@ class RootState extends State with WidgetsBindingObserver { ); launchUri = null; } else if (isWalletConnectLink) { - if (widget.appStore.wallet!.type == WalletType.ethereum) { + if (isEVMCompatibleChain(widget.appStore.wallet!.type)) { widget.navigatorKey.currentState?.pushNamed( Routes.walletConnectConnectionsListing, arguments: launchUri, @@ -179,7 +179,7 @@ class RootState extends State with WidgetsBindingObserver { _nonETHWalletErrorToast(S.current.switchToETHWallet); } } - + launchUri = null; return WillPopScope( onWillPop: () async => false, @@ -205,7 +205,7 @@ class RootState extends State with WidgetsBindingObserver { String? _getRouteToGo() { if (isWalletConnectLink) { - if (widget.appStore.wallet!.type != WalletType.ethereum) { + if (isEVMCompatibleChain(widget.appStore.wallet!.type)) { _nonETHWalletErrorToast(S.current.switchToETHWallet); return null; } diff --git a/lib/src/screens/seed/pre_seed_page.dart b/lib/src/screens/seed/pre_seed_page.dart index 947099983..a45d81fc0 100644 --- a/lib/src/screens/seed/pre_seed_page.dart +++ b/lib/src/screens/seed/pre_seed_page.dart @@ -13,7 +13,8 @@ class PreSeedPage extends BasePage { PreSeedPage(this.type, this.advancedPrivacySettingsViewModel) : imageLight = Image.asset('assets/images/pre_seed_light.png'), imageDark = Image.asset('assets/images/pre_seed_dark.png'), - seedPhraseLength = advancedPrivacySettingsViewModel.seedPhraseLength.value { + seedPhraseLength = + advancedPrivacySettingsViewModel.seedPhraseLength.value { wordsCount = _wordsCount(type, seedPhraseLength); } @@ -40,14 +41,14 @@ class PreSeedPage extends BasePage { alignment: Alignment.center, padding: EdgeInsets.all(24), child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: BoxConstraints( + maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ ConstrainedBox( constraints: BoxConstraints( - maxHeight: MediaQuery.of(context).size.height * 0.3 - ), + maxHeight: MediaQuery.of(context).size.height * 0.3), child: AspectRatio(aspectRatio: 1, child: image), ), Padding( @@ -58,12 +59,14 @@ class PreSeedPage extends BasePage { style: TextStyle( fontSize: 14, fontWeight: FontWeight.normal, - color: Theme.of(context).extension()!.secondaryTextColor), + color: Theme.of(context) + .extension()! + .secondaryTextColor), ), ), PrimaryButton( - onPressed: () => - Navigator.of(context).popAndPushNamed(Routes.seed, arguments: true), + onPressed: () => Navigator.of(context) + .popAndPushNamed(Routes.seed, arguments: true), text: S.of(context).pre_seed_button_text, color: Theme.of(context).primaryColor, textColor: Colors.white) @@ -79,6 +82,7 @@ class PreSeedPage extends BasePage { return 25; case WalletType.ethereum: case WalletType.bitcoinCash: + case WalletType.polygon: return seedPhraseLength; default: return 24; diff --git a/lib/src/screens/settings/connection_sync_page.dart b/lib/src/screens/settings/connection_sync_page.dart index 50881dd57..40a04b0db 100644 --- a/lib/src/screens/settings/connection_sync_page.dart +++ b/lib/src/screens/settings/connection_sync_page.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; 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'; @@ -8,7 +9,6 @@ import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/generated/i18n.dart'; @@ -85,7 +85,7 @@ class ConnectionSyncPage extends BasePage { ); }, ), - if (dashboardViewModel.wallet.type == WalletType.ethereum) ...[ + if (isEVMCompatibleChain(dashboardViewModel.wallet.type)) ...[ WalletConnectTile( onTap: () => Navigator.of(context).pushNamed(Routes.walletConnectConnectionsListing), ), @@ -101,6 +101,7 @@ class ConnectionSyncPage extends BasePage { ); } + Future _presentReconnectAlert(BuildContext context) async { await showPopUp( context: context, diff --git a/lib/src/screens/settings/privacy_page.dart b/lib/src/screens/settings/privacy_page.dart index b1927648a..dcc41254e 100644 --- a/lib/src/screens/settings/privacy_page.dart +++ b/lib/src/screens/settings/privacy_page.dart @@ -87,6 +87,14 @@ class PrivacyPage extends BasePage { onValueChange: (BuildContext _, bool value) { _privacySettingsViewModel.setUseEtherscan(value); }), + if (_privacySettingsViewModel.canUsePolygonScan) + SettingsSwitcherCell( + title: S.current.polygonscan_history, + value: _privacySettingsViewModel.usePolygonScan, + onValueChange: (BuildContext _, bool value) { + _privacySettingsViewModel.setUsePolygonScan(value); + }, + ), SettingsCellWithArrow( title: S.current.domain_looks_up, handler: (context) => Navigator.of(context).pushNamed(Routes.domainLookupsPage), diff --git a/lib/src/screens/settings/tor_page.dart b/lib/src/screens/settings/tor_page.dart index d1d5c7e83..ae1ef1677 100644 --- a/lib/src/screens/settings/tor_page.dart +++ b/lib/src/screens/settings/tor_page.dart @@ -4,7 +4,7 @@ import 'dart:io'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:flutter/material.dart'; -import 'package:tor/tor.dart'; +// import 'package:tor/tor.dart'; class TorPage extends BasePage { final AppStore appStore; @@ -40,44 +40,44 @@ class _TorPageBodyState extends State { connecting = true; // Update flag }); - await Tor.init(); - - // Start the proxy - await Tor.instance.start(); - - // Toggle started flag. - setState(() { - torEnabled = Tor.instance.enabled; // Update flag - connecting = false; - }); - - final node = widget.appStore.settingsStore.getCurrentNode(widget.appStore.wallet!.type); - if (node.socksProxyAddress?.isEmpty ?? true) { - node.socksProxyAddress = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; - } - widget.appStore.wallet!.connectToNode(node: node); + // await Tor.init(); + // + // // Start the proxy + // await Tor.instance.start(); + // + // // Toggle started flag. + // setState(() { + // torEnabled = Tor.instance.enabled; // Update flag + // connecting = false; + // }); + // + // final node = widget.appStore.settingsStore.getCurrentNode(widget.appStore.wallet!.type); + // if (node.socksProxyAddress?.isEmpty ?? true) { + // node.socksProxyAddress = "${InternetAddress.loopbackIPv4.address}:${Tor.instance.port}"; + // } + // widget.appStore.wallet!.connectToNode(node: node); print('Done awaiting; tor should be running'); } Future endTor() async { - // Start the proxy - Tor.instance.disable(); - - // Toggle started flag. - setState(() { - torEnabled = Tor.instance.enabled; // Update flag - }); - - print('Done awaiting; tor should be stopped'); - } - - @override - void initState() { - super.initState(); - - torEnabled = Tor.instance.enabled; + // // Start the proxy + // Tor.instance.disable(); + // + // // Toggle started flag. + // setState(() { + // torEnabled = Tor.instance.enabled; // Update flag + // }); + // + // print('Done awaiting; tor should be stopped'); } + // + // @override + // void initState() { + // super.initState(); + // + // torEnabled = Tor.instance.enabled; + // } @override void dispose() { diff --git a/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart index c73c4bfa8..179bd6b93 100644 --- a/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart +++ b/lib/src/screens/wallet_connect/widgets/connection_request_widget.dart @@ -14,12 +14,14 @@ import 'connection_widget.dart'; class ConnectionRequestWidget extends StatefulWidget { const ConnectionRequestWidget({ required this.wallet, + required this.chaindIdNamespace, this.authRequest, this.sessionProposal, Key? key, }) : super(key: key); final Web3Wallet wallet; + final String chaindIdNamespace; final AuthRequestModel? authRequest; final SessionRequestModel? sessionProposal; @@ -52,23 +54,26 @@ class _ConnectionRequestWidgetState extends State { return _ConnectionMetadataDisplayWidget( metadata: metadata, + wallet: widget.wallet, authRequest: widget.authRequest, sessionProposal: widget.sessionProposal, - wallet: widget.wallet, + chaindIdNamespace: widget.chaindIdNamespace, ); } } class _ConnectionMetadataDisplayWidget extends StatelessWidget { const _ConnectionMetadataDisplayWidget({ - required this.metadata, required this.wallet, - this.authRequest, + required this.metadata, required this.sessionProposal, + required this.chaindIdNamespace, + this.authRequest, }); final ConnectionMetadata? metadata; final Web3Wallet wallet; + final String chaindIdNamespace; final AuthRequestModel? authRequest; final SessionRequestModel? sessionProposal; @@ -114,7 +119,11 @@ class _ConnectionMetadataDisplayWidget extends StatelessWidget { const SizedBox(height: 8), Visibility( visible: authRequest != null, - child: _AuthRequestWidget(wallet: wallet, authRequest: authRequest), + child: _AuthRequestWidget( + wallet: wallet, + authRequest: authRequest, + chaindIdNamespace: chaindIdNamespace, + ), //If authRequest is null, sessionProposal is not null. replacement: _SessionProposalWidget(sessionProposal: sessionProposal!), @@ -126,16 +135,21 @@ class _ConnectionMetadataDisplayWidget extends StatelessWidget { } class _AuthRequestWidget extends StatelessWidget { - const _AuthRequestWidget({required this.wallet, this.authRequest}); + const _AuthRequestWidget({ + required this.wallet, + required this.chaindIdNamespace, + this.authRequest, + }); final Web3Wallet wallet; + final String chaindIdNamespace; final AuthRequestModel? authRequest; @override Widget build(BuildContext context) { final model = ConnectionModel( text: wallet.formatAuthMessage( - iss: 'did:pkh:eip155:1:${authRequest!.iss}', + iss: 'did:pkh:$chaindIdNamespace:${authRequest!.iss}', cacaoPayload: CacaoRequestPayload.fromPayloadParams( authRequest!.request.payloadParams, ), diff --git a/lib/src/screens/wallet_list/wallet_list_page.dart b/lib/src/screens/wallet_list/wallet_list_page.dart index da5df874a..53a9b3eca 100644 --- a/lib/src/screens/wallet_list/wallet_list_page.dart +++ b/lib/src/screens/wallet_list/wallet_list_page.dart @@ -49,6 +49,7 @@ class WalletListBodyState extends State { final ethereumIcon = Image.asset('assets/images/eth_icon.png', height: 24, width: 24); final bitcoinCashIcon = Image.asset('assets/images/bch_icon.png', height: 24, width: 24); final nanoIcon = Image.asset('assets/images/nano_icon.png', height: 24, width: 24); + final polygonIcon = Image.asset('assets/images/matic_icon.png', height: 24, width: 24); final scrollController = ScrollController(); final double tileHeight = 60; Flushbar? _progressBar; @@ -256,6 +257,8 @@ class WalletListBodyState extends State { return bitcoinCashIcon; case WalletType.nano: return nanoIcon; + case WalletType.polygon: + return polygonIcon; default: return nonWalletTypeIcon; } diff --git a/lib/src/widgets/seed_language_selector.dart b/lib/src/widgets/seed_language_selector.dart index 07638bb2f..dde78c58c 100644 --- a/lib/src/widgets/seed_language_selector.dart +++ b/lib/src/widgets/seed_language_selector.dart @@ -1,19 +1,20 @@ import 'package:cake_wallet/entities/seed_type.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; import 'package:cake_wallet/src/widgets/seed_language_picker.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; class SeedLanguageSelector extends StatefulWidget { - SeedLanguageSelector({Key? key, required this.initialSelected, this.seedType = SeedType.defaultSeedType}) + SeedLanguageSelector( + {Key? key, required this.initialSelected, this.seedType = SeedType.defaultSeedType}) : super(key: key); final String initialSelected; final SeedType seedType; @override - SeedLanguageSelectorState createState() => - SeedLanguageSelectorState(selected: initialSelected); + SeedLanguageSelectorState createState() => SeedLanguageSelectorState(selected: initialSelected); } class SeedLanguageSelectorState extends State { @@ -25,15 +26,17 @@ class SeedLanguageSelectorState extends State { Widget build(BuildContext context) { return SelectButton( image: null, - text: seedLanguages.firstWhere((e) => e.name == selected).nameLocalized, + text: + "${seedLanguages.firstWhere((e) => e.name == selected).nameLocalized} (${S.of(context).seed_language})", onTap: () async { await showPopUp( - context: context, - builder: (_) => SeedLanguagePicker( - selected: this.selected, - seedType: widget.seedType, - onItemSelected: (String selected) => - setState(() => this.selected = selected))); + context: context, + builder: (_) => SeedLanguagePicker( + selected: this.selected, + seedType: widget.seedType, + onItemSelected: (String selected) => setState(() => this.selected = selected), + ), + ); }, ); } diff --git a/lib/store/app_store.dart b/lib/store/app_store.dart index e814ff44b..46ac2cf1a 100644 --- a/lib/store/app_store.dart +++ b/lib/store/app_store.dart @@ -1,8 +1,8 @@ import 'package:cake_wallet/core/wallet_connect/web3wallet_service.dart'; import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/wallet_base.dart'; @@ -44,7 +44,7 @@ abstract class AppStoreBase with Store { this.wallet = wallet; this.wallet!.setExceptionHandler(ExceptionHandler.onError); - if (wallet.type == WalletType.ethereum) { + if (isEVMCompatibleChain(wallet.type)) { getIt.get().init(); } } diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 336f87706..22cf4f51d 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -13,6 +13,7 @@ import 'package:cake_wallet/entities/preferences_key.dart'; import 'package:cake_wallet/entities/seed_phrase_length.dart'; import 'package:cake_wallet/entities/seed_type.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/exchange/provider/trocador_exchange_provider.dart'; import 'package:cake_wallet/view_model/settings/sync_mode.dart'; import 'package:cake_wallet/utils/device_info.dart'; @@ -90,6 +91,7 @@ abstract class SettingsStoreBase with Store { required this.sortBalanceBy, required this.pinNativeTokenAtTop, required this.useEtherscan, + required this.usePolygonScan, required this.defaultNanoRep, required this.defaultBananoRep, required this.lookupsTwitter, @@ -103,6 +105,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialLitecoinTransactionPriority, TransactionPriority? initialEthereumTransactionPriority, + TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -168,6 +171,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum] = initialEthereumTransactionPriority; } + if (initialPolygonTransactionPriority != null) { + priority[WalletType.polygon] = initialPolygonTransactionPriority; + } + if (initialBitcoinCashTransactionPriority != null) { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } @@ -205,6 +212,9 @@ abstract class SettingsStoreBase with Store { case WalletType.bitcoinCash: key = PreferencesKey.bitcoinCashTransactionPriority; break; + case WalletType.polygon: + key = PreferencesKey.polygonTransactionPriority; + break; default: key = null; } @@ -253,8 +263,8 @@ abstract class SettingsStoreBase with Store { reaction( (_) => moneroSeedType, - (SeedType moneroSeedType) => sharedPreferences.setInt( - PreferencesKey.moneroSeedType, moneroSeedType.raw)); + (SeedType moneroSeedType) => + sharedPreferences.setInt(PreferencesKey.moneroSeedType, moneroSeedType.raw)); reaction( (_) => fiatApiMode, @@ -350,9 +360,9 @@ abstract class SettingsStoreBase with Store { sharedPreferences.setString(PreferencesKey.currentLanguageCode, languageCode)); reaction( - (_) => seedPhraseLength, - (SeedPhraseLength seedPhraseWordCount) => - sharedPreferences.setInt(PreferencesKey.currentSeedPhraseLength, seedPhraseWordCount.value)); + (_) => seedPhraseLength, + (SeedPhraseLength seedPhraseWordCount) => sharedPreferences.setInt( + PreferencesKey.currentSeedPhraseLength, seedPhraseWordCount.value)); reaction( (_) => pinTimeOutDuration, @@ -396,6 +406,11 @@ abstract class SettingsStoreBase with Store { (bool useEtherscan) => _sharedPreferences.setBool(PreferencesKey.useEtherscan, useEtherscan)); + reaction( + (_) => usePolygonScan, + (bool usePolygonScan) => + _sharedPreferences.setBool(PreferencesKey.usePolygonScan, usePolygonScan)); + reaction((_) => defaultNanoRep, (String nanoRep) => _sharedPreferences.setString(PreferencesKey.defaultNanoRep, nanoRep)); @@ -404,34 +419,32 @@ abstract class SettingsStoreBase with Store { (String bananoRep) => _sharedPreferences.setString(PreferencesKey.defaultBananoRep, bananoRep)); reaction( - (_) => lookupsTwitter, - (bool looksUpTwitter) => + (_) => lookupsTwitter, + (bool looksUpTwitter) => _sharedPreferences.setBool(PreferencesKey.lookupsTwitter, looksUpTwitter)); reaction( - (_) => lookupsMastodon, - (bool looksUpMastodon) => + (_) => lookupsMastodon, + (bool looksUpMastodon) => _sharedPreferences.setBool(PreferencesKey.lookupsMastodon, looksUpMastodon)); reaction( - (_) => lookupsYatService, - (bool looksUpYatService) => + (_) => lookupsYatService, + (bool looksUpYatService) => _sharedPreferences.setBool(PreferencesKey.lookupsYatService, looksUpYatService)); reaction( - (_) => lookupsUnstoppableDomains, - (bool looksUpUnstoppableDomains) => - _sharedPreferences.setBool(PreferencesKey.lookupsUnstoppableDomains, looksUpUnstoppableDomains)); + (_) => lookupsUnstoppableDomains, + (bool looksUpUnstoppableDomains) => _sharedPreferences.setBool( + PreferencesKey.lookupsUnstoppableDomains, looksUpUnstoppableDomains)); reaction( - (_) => lookupsOpenAlias, - (bool looksUpOpenAlias) => + (_) => lookupsOpenAlias, + (bool looksUpOpenAlias) => _sharedPreferences.setBool(PreferencesKey.lookupsOpenAlias, looksUpOpenAlias)); - reaction( - (_) => lookupsENS, - (bool looksUpENS) => - _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + reaction((_) => lookupsENS, + (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); this.nodes.observe((change) { if (change.newValue != null && change.key != null) { @@ -573,6 +586,9 @@ abstract class SettingsStoreBase with Store { @observable bool useEtherscan; + @observable + bool usePolygonScan; + @observable String defaultNanoRep; @@ -662,6 +678,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? havenTransactionPriority; TransactionPriority? litecoinTransactionPriority; TransactionPriority? ethereumTransactionPriority; + TransactionPriority? polygonTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { @@ -673,9 +690,13 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!); } if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { - ethereumTransactionPriority = bitcoin?.deserializeLitecoinTransactionPriority( + ethereumTransactionPriority = ethereum?.deserializeEthereumTransactionPriority( sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!); } + if (sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { + polygonTransactionPriority = polygon?.deserializePolygonTransactionPriority( + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!); + } if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { bitcoinCashTransactionPriority = bitcoinCash?.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); @@ -687,6 +708,7 @@ abstract class SettingsStoreBase with Store { litecoinTransactionPriority ??= bitcoin?.getLitecoinTransactionPriorityMedium(); ethereumTransactionPriority ??= ethereum?.getDefaultTransactionPriority(); bitcoinCashTransactionPriority ??= bitcoinCash?.getDefaultTransactionPriority(); + polygonTransactionPriority ??= polygon?.getDefaultTransactionPriority(); final currentBalanceDisplayMode = BalanceDisplayMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentBalanceDisplayModeKey)!); @@ -762,12 +784,14 @@ abstract class SettingsStoreBase with Store { final pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; + final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; final defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; final lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; final lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; final lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; - final lookupsUnstoppableDomains = sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; + final lookupsUnstoppableDomains = + sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; @@ -787,6 +811,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); + final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoPowNodeIdKey); final moneroNode = nodeSource.get(nodeId); @@ -794,6 +819,7 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); final ethereumNode = nodeSource.get(ethereumNodeId); + final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashElectrumServer = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); final nanoPowNode = powNodeSource.get(nanoPowNodeId); @@ -837,6 +863,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (polygonNode != null) { + nodes[WalletType.polygon] = polygonNode; + } + if (bitcoinCashElectrumServer != null) { nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; } @@ -854,19 +884,19 @@ abstract class SettingsStoreBase with Store { }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; - return SettingsStore( - sharedPreferences: sharedPreferences, - initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, - nodes: nodes, - powNodes: powNodes, - appVersion: packageInfo.version, - deviceName: deviceName, - isBitcoinBuyEnabled: isBitcoinBuyEnabled, - initialFiatCurrency: currentFiatCurrency, - initialBalanceDisplayMode: currentBalanceDisplayMode, - initialSaveRecipientAddress: shouldSaveRecipientAddress, - initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, - initialMoneroSeedType: moneroSeedType, + return SettingsStore( + sharedPreferences: sharedPreferences, + initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, + nodes: nodes, + powNodes: powNodes, + appVersion: packageInfo.version, + deviceName: deviceName, + isBitcoinBuyEnabled: isBitcoinBuyEnabled, + initialFiatCurrency: currentFiatCurrency, + initialBalanceDisplayMode: currentBalanceDisplayMode, + initialSaveRecipientAddress: shouldSaveRecipientAddress, + initialAutoGenerateSubaddressStatus: autoGenerateSubaddressStatus, + initialMoneroSeedType: moneroSeedType, initialAppSecure: isAppSecure, initialDisableBuy: disableBuy, initialDisableSell: disableSell, @@ -883,42 +913,45 @@ abstract class SettingsStoreBase with Store { actionlistDisplayMode: actionListDisplayMode, initialPinLength: pinLength, pinTimeOutDuration: pinCodeTimeOutDuration, - seedPhraseLength: seedPhraseWordCount,initialLanguageCode: savedLanguageCode, + seedPhraseLength: seedPhraseWordCount, + initialLanguageCode: savedLanguageCode, sortBalanceBy: sortBalanceBy, pinNativeTokenAtTop: pinNativeTokenAtTop, useEtherscan: useEtherscan, + usePolygonScan: usePolygonScan, defaultNanoRep: defaultNanoRep, - defaultBananoRep: defaultBananoRep, - lookupsTwitter: lookupsTwitter, - lookupsMastodon: lookupsMastodon, - lookupsYatService: lookupsYatService, - lookupsUnstoppableDomains: lookupsUnstoppableDomains, - lookupsOpenAlias: lookupsOpenAlias, - lookupsENS: lookupsENS, - initialMoneroTransactionPriority: moneroTransactionPriority, + defaultBananoRep: defaultBananoRep, + lookupsTwitter: lookupsTwitter, + lookupsMastodon: lookupsMastodon, + lookupsYatService: lookupsYatService, + lookupsUnstoppableDomains: lookupsUnstoppableDomains, + lookupsOpenAlias: lookupsOpenAlias, + lookupsENS: lookupsENS, + initialMoneroTransactionPriority: moneroTransactionPriority, initialBitcoinTransactionPriority: bitcoinTransactionPriority, initialHavenTransactionPriority: havenTransactionPriority, initialLitecoinTransactionPriority: litecoinTransactionPriority, initialBitcoinCashTransactionPriority: bitcoinCashTransactionPriority, - initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, - initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, - initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, - initialShouldRequireTOTP2FAForSendsToInternalWallets: - shouldRequireTOTP2FAForSendsToInternalWallets, - initialShouldRequireTOTP2FAForExchangesToInternalWallets: - shouldRequireTOTP2FAForExchangesToInternalWallets, + initialShouldRequireTOTP2FAForAccessingWallet: shouldRequireTOTP2FAForAccessingWallet, + initialShouldRequireTOTP2FAForSendsToContact: shouldRequireTOTP2FAForSendsToContact, + initialShouldRequireTOTP2FAForSendsToNonContact: shouldRequireTOTP2FAForSendsToNonContact, + initialShouldRequireTOTP2FAForSendsToInternalWallets: + shouldRequireTOTP2FAForSendsToInternalWallets, + initialShouldRequireTOTP2FAForExchangesToInternalWallets: + shouldRequireTOTP2FAForExchangesToInternalWallets, initialShouldRequireTOTP2FAForExchangesToExternalWallets: shouldRequireTOTP2FAForExchangesToExternalWallets, - initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, - initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, - initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: - shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - initialEthereumTransactionPriority: ethereumTransactionPriority, - backgroundTasks: backgroundTasks, - initialSyncMode: savedSyncMode, - initialSyncAll: savedSyncAll, - shouldShowYatPopup: shouldShowYatPopup); - } + initialShouldRequireTOTP2FAForAddingContacts: shouldRequireTOTP2FAForAddingContacts, + initialShouldRequireTOTP2FAForCreatingNewWallets: shouldRequireTOTP2FAForCreatingNewWallets, + initialShouldRequireTOTP2FAForAllSecurityAndBackupSettings: + shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + initialEthereumTransactionPriority: ethereumTransactionPriority, + initialPolygonTransactionPriority: polygonTransactionPriority, + backgroundTasks: backgroundTasks, + initialSyncMode: savedSyncMode, + initialSyncAll: savedSyncAll, + shouldShowYatPopup: shouldShowYatPopup); + } Future reload({required Box nodeSource}) async { final sharedPreferences = await getIt.getAsync(); @@ -948,6 +981,11 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ?? priority[WalletType.ethereum]!; } + if (sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { + priority[WalletType.polygon] = polygon?.deserializePolygonTransactionPriority( + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!) ?? + priority[WalletType.polygon]!; + } if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { priority[WalletType.bitcoinCash] = bitcoinCash?.deserializeBitcoinCashTransactionPriority( sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!) ?? @@ -1041,12 +1079,14 @@ abstract class SettingsStoreBase with Store { .values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? sortBalanceBy.index]; pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; + usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; defaultBananoRep = sharedPreferences.getString(PreferencesKey.defaultBananoRep) ?? ""; lookupsTwitter = sharedPreferences.getBool(PreferencesKey.lookupsTwitter) ?? true; lookupsMastodon = sharedPreferences.getBool(PreferencesKey.lookupsMastodon) ?? true; lookupsYatService = sharedPreferences.getBool(PreferencesKey.lookupsYatService) ?? true; - lookupsUnstoppableDomains = sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; + lookupsUnstoppableDomains = + sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true; lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true; lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true; @@ -1059,6 +1099,7 @@ abstract class SettingsStoreBase with Store { sharedPreferences.getInt(PreferencesKey.currentBitcoinCashNodeIdKey); final havenNodeId = sharedPreferences.getInt(PreferencesKey.currentHavenNodeIdKey); final ethereumNodeId = sharedPreferences.getInt(PreferencesKey.currentEthereumNodeIdKey); + final polygonNodeId = sharedPreferences.getInt(PreferencesKey.currentPolygonNodeIdKey); final nanoNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final nanoPowNodeId = sharedPreferences.getInt(PreferencesKey.currentNanoNodeIdKey); final moneroNode = nodeSource.get(nodeId); @@ -1066,6 +1107,7 @@ abstract class SettingsStoreBase with Store { final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); final havenNode = nodeSource.get(havenNodeId); final ethereumNode = nodeSource.get(ethereumNodeId); + final polygonNode = nodeSource.get(polygonNodeId); final bitcoinCashNode = nodeSource.get(bitcoinCashElectrumServerId); final nanoNode = nodeSource.get(nanoNodeId); @@ -1089,6 +1131,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (polygonNode != null) { + nodes[WalletType.polygon] = polygonNode; + } + if (bitcoinCashNode != null) { nodes[WalletType.bitcoinCash] = bitcoinCashNode; } @@ -1124,6 +1170,9 @@ abstract class SettingsStoreBase with Store { case WalletType.nano: await _sharedPreferences.setInt(PreferencesKey.currentNanoNodeIdKey, node.key as int); break; + case WalletType.polygon: + await _sharedPreferences.setInt(PreferencesKey.currentPolygonNodeIdKey, node.key as int); + break; default: break; } @@ -1155,7 +1204,6 @@ abstract class SettingsStoreBase with Store { trocadorProviderStates[providerName] = state; } - static Future _getDeviceName() async { String? deviceName = ''; final deviceInfoPlugin = DeviceInfoPlugin(); diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 9366985b5..5c95ab3ab 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cake_wallet/entities/sort_balance_types.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cw_core/transaction_history.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/balance.dart'; @@ -81,7 +82,7 @@ abstract class BalanceViewModelBase with Store { bool get isFiatDisabled => settingsStore.fiatApiMode == FiatApiMode.disabled; @computed - bool get isHomeScreenSettingsEnabled => wallet.type == WalletType.ethereum; + bool get isHomeScreenSettingsEnabled => isEVMCompatibleChain(wallet.type); @computed bool get hasAccounts => wallet.type == WalletType.monero; @@ -123,6 +124,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: + case WalletType.polygon: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -135,6 +137,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: + case WalletType.polygon: return S.current.xmr_full_balance; default: return S.current.unconfirmed; @@ -272,7 +275,8 @@ abstract class BalanceViewModelBase with Store { } @computed - bool get hasAdditionalBalance => wallet.type != WalletType.ethereum; + bool get hasAdditionalBalance => !isEVMCompatibleChain(wallet.type); + @computed List get formattedBalances { diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 66620f951..fc2c27a7c 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -2,10 +2,12 @@ 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/polygon/polygon.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:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; part 'home_settings_view_model.g.dart'; @@ -42,18 +44,41 @@ abstract class HomeSettingsViewModelBase with Store { void setPinNativeToken(bool value) => _settingsStore.pinNativeTokenAtTop = value; Future addErc20Token(Erc20Token token) async { - await ethereum!.addErc20Token(_balanceViewModel.wallet, token); + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + await ethereum!.addErc20Token(_balanceViewModel.wallet, token); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + await polygon!.addErc20Token(_balanceViewModel.wallet, token); + } + _updateTokensList(); _updateFiatPrices(token); } Future deleteErc20Token(Erc20Token token) async { - await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token); + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + await ethereum!.deleteErc20Token(_balanceViewModel.wallet, token); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + await polygon!.deleteErc20Token(_balanceViewModel.wallet, token); + } + _updateTokensList(); } - Future getErc20Token(String contractAddress) async => - await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress); + Future getErc20Token(String contractAddress) async { + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + return await ethereum!.getErc20Token(_balanceViewModel.wallet, contractAddress); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + return await polygon!.getErc20Token(_balanceViewModel.wallet, contractAddress); + } + + return null; + } CryptoCurrency get nativeToken => _balanceViewModel.wallet.currency; @@ -69,7 +94,12 @@ abstract class HomeSettingsViewModelBase with Store { void changeTokenAvailability(Erc20Token token, bool value) async { token.enabled = value; - ethereum!.addErc20Token(_balanceViewModel.wallet, token); + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + ethereum!.addErc20Token(_balanceViewModel.wallet, token); + } + if (_balanceViewModel.wallet.type == WalletType.polygon) { + polygon!.addErc20Token(_balanceViewModel.wallet, token); + } _refreshTokensList(); } @@ -83,7 +113,8 @@ abstract class HomeSettingsViewModelBase with Store { return -1; } else if (e2.enabled && !e1.enabled) { return 1; - } else if (!e1.enabled && !e2.enabled) { // if both are disabled then sort alphabetically + } else if (!e1.enabled && !e2.enabled) { + // if both are disabled then sort alphabetically return e1.name.compareTo(e2.name); } @@ -92,11 +123,21 @@ abstract class HomeSettingsViewModelBase with Store { tokens.clear(); - tokens.addAll(ethereum! - .getERC20Currencies(_balanceViewModel.wallet) - .where((element) => _matchesSearchText(element)) - .toList() - ..sort(_sortFunc)); + if (_balanceViewModel.wallet.type == WalletType.ethereum) { + tokens.addAll(ethereum! + .getERC20Currencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } + + if (_balanceViewModel.wallet.type == WalletType.polygon) { + tokens.addAll(polygon! + .getERC20Currencies(_balanceViewModel.wallet) + .where((element) => _matchesSearchText(element)) + .toList() + ..sort(_sortFunc)); + } } @action diff --git a/lib/view_model/dashboard/nft_view_model.dart b/lib/view_model/dashboard/nft_view_model.dart index c5acf5523..f00f929a3 100644 --- a/lib/view_model/dashboard/nft_view_model.dart +++ b/lib/view_model/dashboard/nft_view_model.dart @@ -1,10 +1,9 @@ -// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:convert'; import 'dart:developer'; import 'package:cake_wallet/core/wallet_connect/wc_bottom_sheet_service.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/wallet_connect/widgets/message_display_widget.dart'; -import 'package:cw_core/wallet_type.dart'; import 'package:http/http.dart' as http; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/.secrets.g.dart' as secrets; @@ -39,23 +38,26 @@ abstract class NFTViewModelBase with Store { @action Future getNFTAssetByWallet() async { - if (appStore.wallet!.type != WalletType.ethereum) return; + if (!isEVMCompatibleChain(appStore.wallet!.type)) return; final walletAddress = appStore.wallet!.walletInfo.address; log('Fetching wallet NFTs for $walletAddress'); + final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); // the [chain] refers to the chain network that the nft is on // the [format] refers to the number format type of the responses // the [normalizedMetadata] field is a boolean that determines if // the response would include a json string of the NFT Metadata that can be decoded // and used within the wallet + // the [excludeSpam] field is a boolean that determines if spam nfts be excluded from the response. final uri = Uri.https( 'deep-index.moralis.io', '/api/v2.2/$walletAddress/nft', { - "chain": "eth", + "chain": chainName, "format": "decimal", "media_items": "false", + "exclude_spam": "true", "normalizeMetadata": "true", }, ); @@ -94,7 +96,7 @@ abstract class NFTViewModelBase with Store { @action Future importNFT(String tokenAddress, String tokenId) async { - + final chainName = getChainNameBasedOnWalletType(appStore.wallet!.type); // the [chain] refers to the chain network that the nft is on // the [format] refers to the number format type of the responses // the [normalizedMetadata] field is a boolean that determines if @@ -104,7 +106,7 @@ abstract class NFTViewModelBase with Store { 'deep-index.moralis.io', '/api/v2.2/nft/$tokenAddress/$tokenId', { - "chain": "eth", + "chain": chainName, "format": "decimal", "media_items": "false", "normalizeMetadata": "true", diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index fd7971001..bc7f70517 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/entities/fiat_currency.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -91,6 +92,13 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: ethereum!.formatterEthereumAmountToDouble(transaction: transaction), price: price); break; + case WalletType.polygon: + final asset = polygon!.assetOfTransaction(balanceViewModel.wallet, transaction); + final price = balanceViewModel.fiatConvertationStore.prices[asset]; + amount = calculateFiatAmountRaw( + cryptoAmount: polygon!.formatterPolygonAmountToDouble(transaction: transaction), + price: price); + break; case WalletType.nano: amount = calculateFiatAmountRaw( cryptoAmount: double.parse(nanoUtil!.getRawAsDecimalString( diff --git a/lib/view_model/exchange/exchange_trade_view_model.dart b/lib/view_model/exchange/exchange_trade_view_model.dart index bc7f53af0..93877a525 100644 --- a/lib/view_model/exchange/exchange_trade_view_model.dart +++ b/lib/view_model/exchange/exchange_trade_view_model.dart @@ -28,10 +28,7 @@ abstract class ExchangeTradeViewModelBase with Store { required this.tradesStore, required this.sendViewModel}) : trade = tradesStore.trade!, - isSendable = tradesStore.trade!.from == wallet.currency || - tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || - (wallet.currency == CryptoCurrency.eth && - tradesStore.trade!.from.tag == CryptoCurrency.eth.title), + isSendable = _checkIfCanSend(tradesStore, wallet), items = ObservableList() { switch (trade.provider) { case ExchangeProviderDescription.changeNow: @@ -155,4 +152,19 @@ abstract class ExchangeTradeViewModelBase with Store { isCopied: true), ]); } + + static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) { + bool _isEthToken() => + wallet.currency == CryptoCurrency.eth && + tradesStore.trade!.from.tag == CryptoCurrency.eth.title; + + bool _isPolygonToken() => + wallet.currency == CryptoCurrency.maticpoly && + tradesStore.trade!.from.tag == CryptoCurrency.maticpoly.tag; + + return tradesStore.trade!.from == wallet.currency || + tradesStore.trade!.provider == ExchangeProviderDescription.xmrto || + _isEthToken() || + _isPolygonToken(); + } } diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 80c331ab2..fb7019885 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -23,6 +23,7 @@ import 'package:cake_wallet/exchange/trade.dart'; import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/store/dashboard/trades_store.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -287,6 +288,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with return transactionPriority == ethereum!.getEthereumTransactionPrioritySlow(); case WalletType.bitcoinCash: return transactionPriority == bitcoinCash!.getBitcoinCashTransactionPrioritySlow(); + case WalletType.polygon: + return transactionPriority == polygon!.getPolygonTransactionPrioritySlow(); default: return false; } @@ -626,6 +629,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.nano; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.polygon: + depositCurrency = CryptoCurrency.maticpoly; + receiveCurrency = CryptoCurrency.xmr; + break; default: break; } @@ -713,6 +720,9 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with case WalletType.bitcoinCash: _settingsStore.priority[wallet.type] = bitcoinCash!.getDefaultTransactionPriority(); break; + case WalletType.polygon: + _settingsStore.priority[wallet.type] = polygon!.getDefaultTransactionPriority(); + break; default: break; } 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 ae0edba30..0cd4d7491 100644 --- a/lib/view_model/node_list/node_list_view_model.dart +++ b/lib/view_model/node_list/node_list_view_model.dart @@ -72,6 +72,9 @@ abstract class NodeListViewModelBase with Store { case WalletType.nano: node = getNanoDefaultNode(nodes: _nodeSource)!; break; + case WalletType.polygon: + node = getPolygonDefaultNode(nodes: _nodeSource)!; + break; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.type}'); } diff --git a/lib/view_model/restore/restore_from_qr_vm.dart b/lib/view_model/restore/restore_from_qr_vm.dart index 4ffc81cef..c8637c4be 100644 --- a/lib/view_model/restore/restore_from_qr_vm.dart +++ b/lib/view_model/restore/restore_from_qr_vm.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/nano/nano.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/view_model/restore/restore_mode.dart'; import 'package:cake_wallet/view_model/restore/restore_wallet.dart'; import 'package:hive/hive.dart'; @@ -71,6 +72,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.ethereum: return ethereum!.createEthereumRestoreWalletFromPrivateKey( name: name, password: password, privateKey: restoreWallet.privateKey!); + case WalletType.polygon: + return polygon!.createPolygonRestoreWalletFromPrivateKey( + name: name, password: password, privateKey: restoreWallet.privateKey!); default: throw Exception('Unexpected type: ${restoreWallet.type.toString()}'); } @@ -95,6 +99,9 @@ abstract class WalletRestorationFromQRVMBase extends WalletCreationVM with Store case WalletType.nano: return nano!.createNanoRestoreWalletFromSeedCredentials( name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); + case WalletType.polygon: + return polygon!.createPolygonRestoreWalletFromSeedCredentials( + name: name, mnemonic: restoreWallet.mnemonicSeed ?? '', password: password); default: throw Exception('Unexpected type: ${type.toString()}'); } diff --git a/lib/view_model/restore/wallet_restore_from_qr_code.dart b/lib/view_model/restore/wallet_restore_from_qr_code.dart index 6085f4354..bfc9b7980 100644 --- a/lib/view_model/restore/wallet_restore_from_qr_code.dart +++ b/lib/view_model/restore/wallet_restore_from_qr_code.dart @@ -26,6 +26,7 @@ class WalletRestoreFromQRCode { 'litecoin-wallet': WalletType.litecoin, 'litecoin_wallet': WalletType.litecoin, 'ethereum-wallet': WalletType.ethereum, + 'polygon-wallet': WalletType.polygon, 'nano-wallet': WalletType.nano, 'nano_wallet': WalletType.nano, 'bitcoincash': WalletType.bitcoinCash, @@ -157,7 +158,16 @@ class WalletRestoreFromQRCode { return WalletRestoreMode.keys; } - if ((type == WalletType.nano || type == WalletType.banano) && credentials.containsKey('hexSeed')) { + if (type == WalletType.polygon && credentials.containsKey('private_key')) { + final privateKey = credentials['private_key'] as String; + if (privateKey.isEmpty) { + throw Exception('Unexpected restore mode: private_key'); + } + return WalletRestoreMode.keys; + } + + if ((type == WalletType.nano || type == WalletType.banano) && + credentials.containsKey('hexSeed')) { final hexSeed = credentials['hexSeed'] as String; if (hexSeed.isEmpty) { throw Exception('Unexpected restore mode: hexSeed'); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 2e696e16f..73fb535f2 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -4,6 +4,8 @@ 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/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; @@ -27,7 +29,8 @@ const String cryptoNumberPattern = '0.0'; class Output = OutputBase with _$Output; abstract class OutputBase with Store { - OutputBase(this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler) + OutputBase( + this._wallet, this._settingsStore, this._fiatConversationStore, this.cryptoCurrencyHandler) : _cryptoNumberFormat = NumberFormat(cryptoNumberPattern), key = UniqueKey(), sendAll = false, @@ -65,8 +68,7 @@ abstract class OutputBase with Store { @computed bool get isParsedAddress => - parsedAddress.parseFrom != ParseFrom.notParsed && - parsedAddress.name.isNotEmpty; + parsedAddress.parseFrom != ParseFrom.notParsed && parsedAddress.name.isNotEmpty; @computed int get formattedCryptoAmount { @@ -83,8 +85,7 @@ abstract class OutputBase with Store { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.bitcoinCash: - _amount = - bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); + _amount = bitcoin!.formatterStringDoubleToBitcoinAmount(_cryptoAmount); break; case WalletType.haven: _amount = haven!.formatterMoneroParseAmount(amount: _cryptoAmount); @@ -92,6 +93,9 @@ abstract class OutputBase with Store { case WalletType.ethereum: _amount = ethereum!.formatterEthereumParseAmount(_cryptoAmount); break; + case WalletType.polygon: + _amount = polygon!.formatterPolygonParseAmount(_cryptoAmount); + break; default: break; } @@ -130,6 +134,10 @@ abstract class OutputBase with Store { if (_wallet.type == WalletType.ethereum) { return ethereum!.formatterEthereumAmountToDouble(amount: BigInt.from(fee)); } + + if (_wallet.type == WalletType.polygon) { + return polygon!.formatterPolygonAmountToDouble(amount: BigInt.from(fee)); + } } catch (e) { print(e.toString()); } @@ -140,10 +148,11 @@ abstract class OutputBase with Store { @computed String get estimatedFeeFiatAmount { try { - final currency = _wallet.type == WalletType.ethereum ? _wallet.currency : cryptoCurrencyHandler(); + final currency = isEVMCompatibleChain(_wallet.type) + ? _wallet.currency + : cryptoCurrencyHandler(); final fiat = calculateFiatAmountRaw( - price: _fiatConversationStore.prices[currency]!, - cryptoAmount: estimatedFee); + price: _fiatConversationStore.prices[currency]!, cryptoAmount: estimatedFee); return fiat; } catch (_) { return '0.00'; @@ -240,6 +249,7 @@ abstract class OutputBase with Store { maximumFractionDigits = 12; break; case WalletType.ethereum: + case WalletType.polygon: maximumFractionDigits = 12; break; default: diff --git a/lib/view_model/send/send_template_view_model.dart b/lib/view_model/send/send_template_view_model.dart index b881ed71f..007c4b8c0 100644 --- a/lib/view_model/send/send_template_view_model.dart +++ b/lib/view_model/send/send_template_view_model.dart @@ -50,7 +50,9 @@ abstract class SendTemplateViewModelBase with Store { TemplateValidator get templateValidator => TemplateValidator(); bool get hasMultiRecipient => - _wallet.type != WalletType.haven && _wallet.type != WalletType.ethereum; + _wallet.type != WalletType.haven && + _wallet.type != WalletType.ethereum && + _wallet.type != WalletType.polygon; @computed CryptoCurrency get cryptoCurrency => _wallet.currency; diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index be822aff3..885e2efe0 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -5,6 +5,8 @@ import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/entities/contact_record.dart'; import 'package:cake_wallet/entities/wallet_contact.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/contact_list/contact_list_view_model.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; @@ -42,7 +44,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor void onWalletChange(wallet) { currencies = wallet.balance.keys.toList(); selectedCryptoCurrency = wallet.currency; - hasMultipleTokens = wallet.type == WalletType.ethereum; + hasMultipleTokens = isEVMCompatibleChain(wallet.type); } SendViewModelBase( @@ -55,7 +57,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor ) : state = InitialExecutionState(), currencies = appStore.wallet!.balance.keys.toList(), selectedCryptoCurrency = appStore.wallet!.currency, - hasMultipleTokens = appStore.wallet!.type == WalletType.ethereum, + hasMultipleTokens = isEVMCompatibleChain(appStore.wallet!.type), outputs = ObservableList(), _settingsStore = appStore.settingsStore, fiatFromSettings = appStore.settingsStore.fiatCurrency, @@ -119,7 +121,7 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor try { if (pendingTransaction != null) { final currency = - walletType == WalletType.ethereum ? wallet.currency : selectedCryptoCurrency; + isEVMCompatibleChain(walletType) ? wallet.currency : selectedCryptoCurrency; final fiat = calculateFiatAmount( price: _fiatConversationStore.prices[currency]!, cryptoAmount: pendingTransaction!.feeFormatted); @@ -372,6 +374,9 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor priority: priority!, currency: selectedCryptoCurrency); case WalletType.nano: return nano!.createNanoTransactionCredentials(outputs); + case WalletType.polygon: + return polygon!.createPolygonTransactionCredentials(outputs, + priority: priority!, currency: selectedCryptoCurrency); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } @@ -412,11 +417,15 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor WalletType walletType, CryptoCurrency currency, ) { - if (walletType == WalletType.ethereum || walletType == WalletType.haven) { + if (walletType == WalletType.ethereum || + walletType == WalletType.polygon || + walletType == WalletType.haven) { if (error.contains('gas required exceeds allowance') || error.contains('insufficient funds for')) { return S.current.do_not_have_enough_gas_asset(currency.toString()); } + + return error; } return error; diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index b3ffeb353..e4dd86b37 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -1,6 +1,7 @@ import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/transaction_history.dart'; @@ -12,8 +13,7 @@ import 'package:cake_wallet/entities/fiat_api_mode.dart'; part 'privacy_settings_view_model.g.dart'; -class -PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel; +class PrivacySettingsViewModel = PrivacySettingsViewModelBase with _$PrivacySettingsViewModel; abstract class PrivacySettingsViewModelBase with Store { PrivacySettingsViewModelBase(this._settingsStore, this._wallet); @@ -58,6 +58,9 @@ abstract class PrivacySettingsViewModelBase with Store { @computed bool get useEtherscan => _settingsStore.useEtherscan; + @computed + bool get usePolygonScan => _settingsStore.usePolygonScan; + @computed bool get lookupTwitter => _settingsStore.lookupsTwitter; @@ -78,6 +81,8 @@ abstract class PrivacySettingsViewModelBase with Store { bool get canUseEtherscan => _wallet.type == WalletType.ethereum; + bool get canUsePolygonScan => _wallet.type == WalletType.polygon; + @action void setShouldSaveRecipientAddress(bool value) => _settingsStore.shouldSaveRecipientAddress = value; @@ -120,4 +125,10 @@ abstract class PrivacySettingsViewModelBase with Store { _settingsStore.useEtherscan = value; ethereum!.updateEtherscanUsageState(_wallet, value); } + + @action + void setUsePolygonScan(bool value) { + _settingsStore.usePolygonScan = value; + polygon!.updatePolygonScanUsageState(_wallet, value); + } } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index a8c892284..4e17866cb 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -51,6 +51,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.nano: _addNanoListItems(tx, dateFormat); break; + case WalletType.polygon: + _addPolygonListItems(tx, dateFormat); + break; default: break; } @@ -125,7 +128,9 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.nano: return 'https://nanolooker.com/block/${txId}'; case WalletType.banano: - return 'https://bananolooker.com/block/${txId}'; + return 'https://bananolooker.com/block/${txId}'; + case WalletType.polygon: + return 'https://polygonscan.com/tx/${txId}'; default: return ''; } @@ -148,6 +153,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'nanolooker.com'; case WalletType.banano: return S.current.view_transaction_on + 'bananolooker.com'; + case WalletType.polygon: + return S.current.view_transaction_on + 'polygonscan.com'; default: return ''; } @@ -237,7 +244,6 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } - void _addNanoListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), @@ -250,4 +256,21 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + + void _addPolygonListItems(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()!), + if (showRecipientAddress && tx.to != null) + StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + ]; + + 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 4d5eefdb7..ade279124 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,6 +1,7 @@ import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; import 'package:cw_core/currency.dart'; @@ -139,6 +140,22 @@ class NanoURI extends PaymentURI { } } +class PolygonURI extends PaymentURI { + PolygonURI({required String amount, required String address}) + : super(amount: amount, address: address); + + @override + String toString() { + var base = 'polygon:' + address; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewModel with Store { WalletAddressListViewModelBase({ required AppStore appStore, @@ -216,6 +233,10 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return NanoURI(amount: amount, address: address.address); } + if (wallet.type == WalletType.polygon) { + return PolygonURI(amount: amount, address: address.address); + } + throw Exception('Unexpected type: ${type.toString()}'); } @@ -272,6 +293,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (wallet.type == WalletType.polygon) { + final primaryAddress = polygon!.getAddress(wallet); + + addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); + } + return addressList; } diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index d9de9473d..56ca190be 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -19,7 +20,8 @@ abstract class WalletKeysViewModelBase with Store { : title = _appStore.wallet!.type == WalletType.bitcoin || _appStore.wallet!.type == WalletType.litecoin || _appStore.wallet!.type == WalletType.bitcoinCash || - _appStore.wallet!.type == WalletType.ethereum + _appStore.wallet!.type == WalletType.ethereum || + _appStore.wallet!.type == WalletType.polygon ? S.current.wallet_seed : S.current.wallet_keys, _restoreHeight = _appStore.wallet!.walletInfo.restoreHeight, @@ -98,7 +100,7 @@ abstract class WalletKeysViewModelBase with Store { ]); } - if (_appStore.wallet!.type == WalletType.ethereum) { + if (isEVMCompatibleChain(_appStore.wallet!.type)) { items.addAll([ if (_appStore.wallet!.privateKey != null) StandartListItem(title: S.current.private_key, value: _appStore.wallet!.privateKey!), @@ -151,6 +153,8 @@ abstract class WalletKeysViewModelBase with Store { return 'nano-wallet'; case WalletType.banano: return 'banano-wallet'; + case WalletType.polygon: + return 'polygon-wallet'; default: throw Exception('Unexpected wallet type: ${_appStore.wallet!.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index acf829168..5e508d646 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -14,6 +14,8 @@ import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/haven/haven.dart'; +import '../polygon/polygon.dart'; + part 'wallet_new_vm.g.dart'; class WalletNewVM = WalletNewVMBase with _$WalletNewVM; @@ -30,6 +32,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { bool get hasLanguageSelector => type == WalletType.monero || type == WalletType.haven; + bool get hasSeedType => type == WalletType.monero; + @override WalletCredentials getCredentials(dynamic _options) { final options = _options as List?; @@ -50,6 +54,8 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { return bitcoinCash!.createBitcoinCashNewWalletCredentials(name: name); case WalletType.nano: return nano!.createNanoNewWalletCredentials(name: name); + case WalletType.polygon: + return polygon!.createPolygonNewWalletCredentials(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 058948c2f..8d1e3b223 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -3,6 +3,7 @@ import 'package:cake_wallet/di.dart'; import 'package:cake_wallet/nano/nano.dart'; import 'package:cake_wallet/ethereum/ethereum.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; +import 'package:cake_wallet/polygon/polygon.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/store/app_store.dart'; @@ -28,7 +29,10 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { : hasSeedLanguageSelector = type == WalletType.monero || type == WalletType.haven, hasBlockchainHeightLanguageSelector = type == WalletType.monero || type == WalletType.haven, hasRestoreFromPrivateKey = - type == WalletType.ethereum || type == WalletType.nano || type == WalletType.banano, + type == WalletType.ethereum || + type == WalletType.polygon || + type == WalletType.nano || + type == WalletType.banano, isButtonEnabled = false, mode = WalletRestoreMode.seed, super(appStore, walletInfoSource, walletCreationService, type: type, isRecovery: true) { @@ -36,6 +40,7 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { case WalletType.monero: case WalletType.haven: case WalletType.ethereum: + case WalletType.polygon: availableModes = WalletRestoreMode.values; break; case WalletType.nano: @@ -107,6 +112,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { mnemonic: seed, password: password, derivationType: derivationType); + case WalletType.polygon: + return polygon!.createPolygonRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); default: break; } @@ -153,6 +164,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { password: password, seedKey: options['private_key'] as String, derivationType: options["derivationType"] as DerivationType); + case WalletType.polygon: + return polygon!.createPolygonRestoreWalletFromPrivateKey( + name: name, + password: password, + privateKey: options['private_key'] as String, + ); default: break; } diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 329732075..c1659f4cc 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -41,6 +41,7 @@ PODS: - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS + - tor (0.0.1) - url_launcher_macos (0.0.1): - FlutterMacOS - wakelock_plus (0.0.1): @@ -59,6 +60,7 @@ DEPENDENCIES: - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - share_plus_macos (from `Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos`) - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) + - tor (from `Flutter/ephemeral/.symlinks/plugins/tor/macos`) - url_launcher_macos (from `Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos`) - wakelock_plus (from `Flutter/ephemeral/.symlinks/plugins/wakelock_plus/macos`) @@ -91,6 +93,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/share_plus_macos/macos shared_preferences_foundation: :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin + tor: + :path: Flutter/ephemeral/.symlinks/plugins/tor/macos url_launcher_macos: :path: Flutter/ephemeral/.symlinks/plugins/url_launcher_macos/macos wakelock_plus: @@ -98,7 +102,7 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 - cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_monero: f8b7f104508efba2591548e76b5c058d05cba3f0 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea @@ -110,6 +114,7 @@ SPEC CHECKSUMS: ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + tor: 2138c48428e696b83eacdda404de6d5574932e26 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 diff --git a/model_generator.sh b/model_generator.sh index 50cb3d353..32d863aeb 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -5,4 +5,5 @@ cd cw_haven && flutter pub get && flutter packages pub run build_runner build -- cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_nano && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin_cash && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_polygon && 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 f71d38578..a4738bf82 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -98,10 +98,10 @@ dependencies: url: https://github.com/cake-tech/bitcoin_flutter.git ref: cake-update-v3 fluttertoast: 8.1.4 - tor: - git: - url: https://github.com/cake-tech/tor.git - ref: main +# tor: +# git: +# url: https://github.com/cake-tech/tor.git +# ref: main socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 polyseed: @@ -147,6 +147,7 @@ flutter: - assets/bitcoin_cash_electrum_server_list.yml - assets/nano_node_list.yml - assets/nano_pow_node_list.yml + - assets/polygon_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index d7571fa0a..c2681459f 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -160,7 +160,7 @@ "seed_title": "سييد", "seed_share": "شارك السييد", "copy": "نسخ", - "seed_language_choose": "الرجاء اختيار لغة السييد:", + "seed_language": "لغة البذور", "seed_choose": "اختر لغة السييد", "seed_language_next": "التالي", "seed_language_english": "إنجليزي", @@ -569,7 +569,7 @@ "always": "دائماً", "minutes_to_pin_code": "${minutes} دقيقة", "disable_exchange": "تعطيل التبادل", - "advanced_privacy_settings": "إعدادات الخصوصية المتقدمة", + "advanced_settings": "إعدادات متقدمة", "settings_can_be_changed_later": "يمكن تغيير هذه الإعدادات لاحقًا في إعدادات التطبيق", "add_custom_node": "إضافة عقدة مخصصة جديدة", "disable_fiat": "تعطيل fiat", @@ -750,5 +750,6 @@ "seedtype_polyseed": "بوليسيد (16 كلمة)", "seed_language_czech": "التشيكية", "seed_language_korean": "الكورية", - "seed_language_chinese_traditional": "تقاليد صينية)" + "seed_language_chinese_traditional": "تقاليد صينية)", + "polygonscan_history": "ﻥﺎﻜﺴﻧﻮﺠﻴﻟﻮﺑ ﺦﻳﺭﺎﺗ" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index d19f7e58d..60affa839 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "Споделяне на seed", "copy": "Копиране", - "seed_language_choose": "Моля, изберете език на seed-а:", + "seed_language": "Език на семената", "seed_choose": "Изберете език на seed-а", "seed_language_next": "Следващ", "seed_language_english": "Английски", @@ -569,7 +569,7 @@ "always": "Винаги", "minutes_to_pin_code": "${minute} минути", "disable_exchange": "Деактивиране на борса", - "advanced_privacy_settings": "Допълнителни настройки за поверителност", + "advanced_settings": "Разширени настройки", "settings_can_be_changed_later": "Тези настройки могат да бъдат променени по-късно от приложението", "add_custom_node": "Добавяне на нов персонализиран Node", "disable_fiat": "Деактивиране на fiat", @@ -746,5 +746,6 @@ "seedtype_polyseed": "Поли семе (16 думи)", "seed_language_czech": "Чех", "seed_language_korean": "Корейски", - "seed_language_chinese_traditional": "Традиционен китайски)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Традиционен китайски)", + "polygonscan_history": "История на PolygonScan" +} diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 4e5091b45..0db2f9bc3 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "Sdílet seed", "copy": "Kopírovat", - "seed_language_choose": "Prosím zvolte si jazyk seedu:", + "seed_language": "Jazyk semen", "seed_choose": "Zvolte si jazyk seedu", "seed_language_next": "Další", "seed_language_english": "Angličtina", @@ -569,7 +569,7 @@ "always": "Vždy", "minutes_to_pin_code": "${minute} minutách", "disable_exchange": "Zakázat směnárny", - "advanced_privacy_settings": "Pokročilá nastavení soukromí", + "advanced_settings": "Pokročilé nastavení", "settings_can_be_changed_later": "Tato nastavení mohou být změněna později v nastavení v této aplikaci", "add_custom_node": "Přidat vlastní uzel", "disable_fiat": "Zakázat fiat", @@ -746,5 +746,6 @@ "seedtype_polyseed": "Polyseed (16 slov)", "seed_language_czech": "čeština", "seed_language_korean": "korejština", - "seed_language_chinese_traditional": "Číňan (tradiční)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Číňan (tradiční)", + "polygonscan_history": "Historie PolygonScan" +} diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index f92659b20..75788e212 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "Seed teilen", "copy": "Kopieren", - "seed_language_choose": "Bitte wählen Sie die Sprache des Seeds:", + "seed_language": "Seed-Sprache", "seed_choose": "Seed-Sprache auswählen", "seed_language_next": "Weiter", "seed_language_english": "Englisch", @@ -571,7 +571,7 @@ "always": "immer", "minutes_to_pin_code": "${minute} Minuten", "disable_exchange": "Exchange deaktivieren", - "advanced_privacy_settings": "Erweiterte Datenschutzeinstellungen", + "advanced_settings": "Erweiterte Einstellungen", "settings_can_be_changed_later": "Diese Einstellungen können später in den App-Einstellungen geändert werden", "add_custom_node": "Neuen benutzerdefinierten Knoten hinzufügen", "disable_fiat": "Fiat deaktivieren", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Polyseed (16 Wörter)", "seed_language_czech": "Tschechisch", "seed_language_korean": "Koreanisch", - "seed_language_chinese_traditional": "Chinesisch (Traditionell)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chinesisch (Traditionell)", + "polygonscan_history": "PolygonScan-Verlauf" +} diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index c299e77ba..9be843743 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "Share seed", "copy": "Copy", - "seed_language_choose": "Please choose seed language:", + "seed_language": "Seed language", "seed_choose": "Choose seed language", "seed_language_next": "Next", "seed_language_english": "English", @@ -572,7 +572,7 @@ "always": "Always", "minutes_to_pin_code": "${minute} minutes", "disable_exchange": "Disable exchange", - "advanced_privacy_settings": "Advanced Privacy Settings", + "advanced_settings": "Advanced Settings", "settings_can_be_changed_later": "These settings can be changed later in the app settings", "add_custom_node": "Add New Custom Node", "disable_fiat": "Disable fiat", @@ -755,5 +755,6 @@ "seedtype_polyseed": "Polyseed (16 words)", "seed_language_czech": "Czech", "seed_language_korean": "Korean", - "seed_language_chinese_traditional": "Chinese (Traditional)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chinese (Traditional)", + "polygonscan_history": "PolygonScan history" +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 07a3b68a9..0d886f69b 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -160,7 +160,7 @@ "seed_title": "Semilla", "seed_share": "Compartir semillas", "copy": "Dupdo", - "seed_language_choose": "Por favor elija el idioma semilla:", + "seed_language": "Lenguaje de semillas", "seed_choose": "Elige el idioma semilla", "seed_language_next": "Próximo", "seed_language_english": "Inglés", @@ -571,7 +571,7 @@ "always": "siempre", "minutes_to_pin_code": "${minute} minutos", "disable_exchange": "Deshabilitar intercambio", - "advanced_privacy_settings": "Configuración avanzada de privacidad", + "advanced_settings": "Ajustes avanzados", "settings_can_be_changed_later": "Estas configuraciones se pueden cambiar más tarde en la configuración de la aplicación", "add_custom_node": "Agregar nuevo nodo personalizado", "disable_fiat": "Deshabilitar fiat", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Polieta (16 palabras)", "seed_language_czech": "checo", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Chino tradicional)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chino (tradicional)", + "polygonscan_history": "Historial de PolygonScan" +} diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index ee3bac7b1..5a3a07ae4 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -160,7 +160,7 @@ "seed_title": "Phrase secrète (seed)", "seed_share": "Partager la phrase secrète (seed)", "copy": "Copier", - "seed_language_choose": "Merci de choisir la langue de la phrase secrète (seed) :", + "seed_language": "Langage de la phrase secrète", "seed_choose": "Choisissez la langue de la phrase secrète (seed)", "seed_language_next": "Suivant", "seed_language_english": "Anglais", @@ -571,7 +571,7 @@ "always": "toujours", "minutes_to_pin_code": "${minute} minutes", "disable_exchange": "Désactiver l'échange", - "advanced_privacy_settings": "Paramètres de confidentialité avancés", + "advanced_settings": "Réglages avancés", "settings_can_be_changed_later": "Ces paramètres peuvent être modifiés ultérieurement dans les paramètres de l'application", "add_custom_node": "Ajouter un nouveau nœud personnalisé", "disable_fiat": "Désactiver les montants en fiat", @@ -747,16 +747,14 @@ "seed_phrase_length": "Longueur de la phrase de départ", "unavailable_balance": "Solde indisponible", "unavailable_balance_description": "Solde indisponible : ce total comprend les fonds bloqués dans les transactions en attente et ceux que vous avez activement gelés dans vos paramètres de contrôle des pièces. Les soldes bloqués deviendront disponibles une fois leurs transactions respectives terminées, tandis que les soldes gelés resteront inaccessibles aux transactions jusqu'à ce que vous décidiez de les débloquer.", - "camera_permission_is_required": "L'autorisation d'accès à la caméra est requise.\nVeuillez l'activer depuis les paramètres de l'application.", - "switchToETHWallet": "Veuillez passer à un portefeuille (wallet) Ethereum et réessayer", "unspent_change": "Changement", "tor_connection": "Connexion Tor", "seed_hex_form": "Graine du portefeuille (forme hexagonale)", - "camera_permission_is_required": "L'autorisation de la caméra est requise.\nVeuillez l'activer à partir des paramètres de l'application.", "seedtype": "Type de type graine", "seedtype_legacy": "Héritage (25 mots)", "seedtype_polyseed": "Polyseed (16 mots)", "seed_language_czech": "tchèque", "seed_language_korean": "coréen", - "seed_language_chinese_traditional": "Chinois (Traditionnel)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chinois (Traditionnel)", + "polygonscan_history": "Historique de PolygonScan" +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 3c3df27bc..12116f42e 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -160,7 +160,7 @@ "seed_title": "iri", "seed_share": "Raba iri", "copy": "Kwafi", - "seed_language_choose": "Don Allah zaɓi harshen seed:", + "seed_language": "Harshen Magani", "seed_choose": "Zaɓi harshen seed", "seed_language_next": "Na gaba", "seed_language_english": "Ingilishi", @@ -570,7 +570,7 @@ "always": "Koyaushe", "minutes_to_pin_code": "${minute} minti", "disable_exchange": "Kashe musanya", - "advanced_privacy_settings": "Babban Saitunan Sirri", + "advanced_settings": "Saitunan ci gaba", "settings_can_be_changed_later": "Ana iya canza waɗannan saitunan daga baya a cikin saitunan app", "add_custom_node": "Ƙara Sabon Kulli na Custom", "disable_fiat": "Dakatar da fiat", @@ -732,5 +732,6 @@ "seedtype_polyseed": "Polyseed (16 kalmomi)", "seed_language_czech": "Czech", "seed_language_korean": "Yaren Koriya", - "seed_language_chinese_traditional": "Sinanci (na gargajiya)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Sinanci (na gargajiya)", + "polygonscan_history": "PolygonScan tarihin kowane zamani" +} diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index 7bed37502..6d5277773 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -160,7 +160,7 @@ "seed_title": "बीज", "seed_share": "बीज साझा करें", "copy": "प्रतिलिपि", - "seed_language_choose": "कृपया बीज भाषा चुनें:", + "seed_language": "बीज", "seed_choose": "बीज भाषा चुनें", "seed_language_next": "आगामी", "seed_language_english": "अंग्रेज़ी", @@ -571,7 +571,7 @@ "always": "हमेशा", "minutes_to_pin_code": "${minute} मिनट", "disable_exchange": "एक्सचेंज अक्षम करें", - "advanced_privacy_settings": "उन्नत गोपनीयता सेटिंग्स", + "advanced_settings": "एडवांस सेटिंग", "settings_can_be_changed_later": "इन सेटिंग्स को बाद में ऐप सेटिंग में बदला जा सकता है", "add_custom_node": "नया कस्टम नोड जोड़ें", "disable_fiat": "िएट को अक्षम करें", @@ -754,5 +754,6 @@ "seedtype_polyseed": "पॉलीसीड (16 शब्द)", "seed_language_czech": "चेक", "seed_language_korean": "कोरियाई", - "seed_language_chinese_traditional": "चीनी पारंपरिक)" -} \ No newline at end of file + "seed_language_chinese_traditional": "चीनी पारंपरिक)", + "polygonscan_history": "पॉलीगॉनस्कैन इतिहास" +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index ef3366d17..ca58df24b 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -160,7 +160,7 @@ "seed_title": "Prisupni izraz", "seed_share": "Podijeli pristupni izraz", "copy": "Kopiraj", - "seed_language_choose": "Molimo odaberite jezik pristupnog izraza:", + "seed_language": "Sjemeni jezik", "seed_choose": "Odaberi jezik pristupnog izraza", "seed_language_next": "Dalje", "seed_language_english": "Engleski", @@ -571,7 +571,7 @@ "always": "Uvijek", "minutes_to_pin_code": "${minute} minuta", "disable_exchange": "Onemogući exchange", - "advanced_privacy_settings": "Napredne postavke privatnosti", + "advanced_settings": "Napredne postavke", "settings_can_be_changed_later": "Te se postavke mogu promijeniti kasnije u postavkama aplikacije", "add_custom_node": "Dodaj novi prilagođeni čvor", "disable_fiat": "Isključi, fiat", @@ -752,5 +752,6 @@ "seedtype_polyseed": "Poliseed (16 riječi)", "seed_language_czech": "češki", "seed_language_korean": "korejski", - "seed_language_chinese_traditional": "Kinesko tradicionalno)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Kinesko (tradicionalno)", + "polygonscan_history": "Povijest PolygonScan" +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 5df6875ef..c32dbbe11 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -160,7 +160,7 @@ "seed_title": "Bibit", "seed_share": "Bagikan bibit", "copy": "Salin", - "seed_language_choose": "Silakan pilih bahasa bibit:", + "seed_language": "Bahasa benih", "seed_choose": "Pilih bahasa bibit", "seed_language_next": "Selanjutnya", "seed_language_english": "Inggris", @@ -570,7 +570,7 @@ "always": "Selalu", "minutes_to_pin_code": "${minute} menit", "disable_exchange": "Nonaktifkan pertukaran", - "advanced_privacy_settings": "Pengaturan Privasi Lanjutan", + "advanced_settings": "Pengaturan lanjutan", "settings_can_be_changed_later": "Pengaturan ini dapat diubah nanti di pengaturan aplikasi", "add_custom_node": "Tambahkan Node Kustom Baru", "disable_fiat": "Nonaktifkan fiat", @@ -742,5 +742,6 @@ "seedtype_polyseed": "Polyseed (16 kata)", "seed_language_czech": "Ceko", "seed_language_korean": "Korea", - "seed_language_chinese_traditional": "Cina tradisional)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Cina (tradisional)", + "polygonscan_history": "Sejarah PolygonScan" +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 8b24cff29..d5879e555 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -160,7 +160,7 @@ "seed_title": "Seme", "seed_share": "Condividi seme", "copy": "Copia", - "seed_language_choose": "Gentilmente scegli la lingua del seme:", + "seed_language": "Linguaggio di semi", "seed_choose": "Scegli la lingua del seme", "seed_language_next": "Prossimo", "seed_language_english": "Inglese", @@ -571,7 +571,7 @@ "always": "sempre", "minutes_to_pin_code": "${minute} minuti", "disable_exchange": "Disabilita scambio", - "advanced_privacy_settings": "Impostazioni avanzate sulla privacy", + "advanced_settings": "Impostazioni avanzate", "settings_can_be_changed_later": "Queste impostazioni possono essere modificate in seguito nelle impostazioni dell'app", "add_custom_node": "Aggiungi nuovo nodo personalizzato", "disable_fiat": "Disabilita fiat", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Polyseed (16 parole)", "seed_language_czech": "ceco", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Cinese tradizionale)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Cinese tradizionale)", + "polygonscan_history": "Cronologia PolygonScan" +} diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index b51bce963..e5138155e 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -160,7 +160,7 @@ "seed_title": "シード", "seed_share": "シードを共有する", "copy": "コピー", - "seed_language_choose": "シード言語を選択してください:", + "seed_language": "シード言語", "seed_choose": "シード言語を選択してください", "seed_language_next": "次", "seed_language_english": "英語", @@ -571,7 +571,7 @@ "always": "いつも", "minutes_to_pin_code": "${minute} 分", "disable_exchange": "交換を無効にする", - "advanced_privacy_settings": "高度なプライバシー設定", + "advanced_settings": "高度な設定", "settings_can_be_changed_later": "これらの設定は、後でアプリの設定で変更できます", "add_custom_node": "新しいカスタム ノードを追加", "disable_fiat": "フィアットを無効にする", @@ -754,5 +754,6 @@ "seedtype_polyseed": "ポリシード(16語)", "seed_language_czech": "チェコ", "seed_language_korean": "韓国語", - "seed_language_chinese_traditional": "中国の伝統的な)" -} \ No newline at end of file + "seed_language_chinese_traditional": "中国の伝統的な)", + "polygonscan_history": "ポリゴンスキャン履歴" +} diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 3468fadb3..846223705 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -160,7 +160,7 @@ "seed_title": "씨", "seed_share": "시드 공유", "copy": "부", - "seed_language_choose": "종자 언어를 선택하십시오:", + "seed_language": "종자 언어", "seed_choose": "시드 언어를 선택하십시오", "seed_language_next": "다음 것", "seed_language_english": "영어", @@ -571,7 +571,7 @@ "always": "언제나", "minutes_to_pin_code": "${minute}분", "disable_exchange": "교환 비활성화", - "advanced_privacy_settings": "고급 개인 정보 설정", + "advanced_settings": "고급 설정", "settings_can_be_changed_later": "이 설정은 나중에 앱 설정에서 변경할 수 있습니다.", "add_custom_node": "새 사용자 정의 노드 추가", "disable_fiat": "법정화폐 비활성화", @@ -752,5 +752,6 @@ "seedtype_polyseed": "다문 (16 단어)", "seed_language_czech": "체코 사람", "seed_language_korean": "한국인", - "seed_language_chinese_traditional": "중국 전통)" -} \ No newline at end of file + "seed_language_chinese_traditional": "중국 전통)", + "polygonscan_history": "다각형 스캔 기록" +} diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 4cc2be529..907032957 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -160,7 +160,7 @@ "seed_title": "မျိုးစေ့", "seed_share": "မျိုးစေ့မျှဝေပါ။", "copy": "ကော်ပီ", - "seed_language_choose": "ကျေးဇူးပြု၍ မျိုးစေ့ဘာသာစကားကို ရွေးပါ-", + "seed_language": "မျိုးစေ့ဘာသာ", "seed_choose": "မျိုးစေ့ဘာသာစကားကို ရွေးချယ်ပါ။", "seed_language_next": "နောက်တစ်ခု", "seed_language_english": "အင်္ဂလိပ်စာ", @@ -569,7 +569,7 @@ "always": "အမြဲတမ်း", "minutes_to_pin_code": "${minute} မိနစ်", "disable_exchange": "လဲလှယ်မှုကို ပိတ်ပါ။", - "advanced_privacy_settings": "အဆင့်မြင့် ကိုယ်ရေးကိုယ်တာ ဆက်တင်များ", + "advanced_settings": "အဆင့်မြင့်ချိန်ညှိချက်များ", "settings_can_be_changed_later": "အက်ပ်ဆက်တင်များတွင် ဤဆက်တင်များကို နောက်ပိုင်းတွင် ပြောင်းလဲနိုင်သည်။", "add_custom_node": "စိတ်ကြိုက် Node အသစ်ကို ထည့်ပါ။", "disable_fiat": "Fiat ကိုပိတ်ပါ။", @@ -752,5 +752,6 @@ "seedtype_polyseed": "polyseed (စကားလုံး 16 လုံး)", "seed_language_czech": "ချက်", "seed_language_korean": "ကိုးရီးယား", - "seed_language_chinese_traditional": "တရုတ်ရိုးရာ)" -} \ No newline at end of file + "seed_language_chinese_traditional": "တရုတ်ရိုးရာ)", + "polygonscan_history": "PolygonScan မှတ်တမ်း" +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 1996261c3..d80f30ec6 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -160,7 +160,7 @@ "seed_title": "Zaad", "seed_share": "Deel zaad", "copy": "Kopiëren", - "seed_language_choose": "Kies een starttaal:", + "seed_language": "Zaadtaal", "seed_choose": "Kies een starttaal", "seed_language_next": "Volgende", "seed_language_english": "Engels", @@ -571,7 +571,7 @@ "always": "altijd", "minutes_to_pin_code": "${minute} minuten", "disable_exchange": "Uitwisseling uitschakelen", - "advanced_privacy_settings": "Geavanceerde privacy-instellingen", + "advanced_settings": "Geavanceerde instellingen", "settings_can_be_changed_later": "Deze instellingen kunnen later worden gewijzigd in de app-instellingen", "add_custom_node": "Voeg een nieuw aangepast knooppunt toe", "disable_fiat": "Schakel Fiat uit", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Polyseed (16 woorden)", "seed_language_czech": "Tsjechisch", "seed_language_korean": "Koreaans", - "seed_language_chinese_traditional": "Chinese traditionele)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chinese (traditionele)", + "polygonscan_history": "PolygonScan-geschiedenis" +} diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 69d11a86d..09a47ab8c 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "Udostępnij seed", "copy": "Kopiuj", - "seed_language_choose": "Proszę wybrać język słów we frazie seed:", + "seed_language": "Język nasion", "seed_choose": "Wybierz język", "seed_language_next": "Następny", "seed_language_english": "Angielski", @@ -571,7 +571,7 @@ "always": "zawsze", "minutes_to_pin_code": "${minute} minut", "disable_exchange": "Wyłącz wymianę", - "advanced_privacy_settings": "Zaawansowane ustawienia prywatności", + "advanced_settings": "Zaawansowane ustawienia", "settings_can_be_changed_later": "Te ustawienia można później zmienić w ustawieniach aplikacji", "add_custom_node": "Dodaj nowy węzeł niestandardowy", "disable_fiat": "Wyłącz waluty FIAT", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Poliqueed (16 słów)", "seed_language_czech": "Czech", "seed_language_korean": "koreański", - "seed_language_chinese_traditional": "Chiński tradycyjny)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chiński tradycyjny)", + "polygonscan_history": "Historia PolygonScan" +} diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 710c871c9..4719158c8 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -160,7 +160,7 @@ "seed_title": "Semente", "seed_share": "Compartilhar semente", "copy": "Copiar", - "seed_language_choose": "Por favor, escolha o idioma da semente:", + "seed_language": "Linguagem de semente", "seed_choose": "Escolha o idioma da semente", "seed_language_next": "Próximo", "seed_language_english": "Inglesa", @@ -570,7 +570,7 @@ "always": "sempre", "minutes_to_pin_code": "${minute} minutos", "disable_exchange": "Desativar troca", - "advanced_privacy_settings": "Configurações de privacidade avançadas", + "advanced_settings": "Configurações avançadas", "settings_can_be_changed_later": "Essas configurações podem ser alteradas posteriormente nas configurações do aplicativo", "add_custom_node": "Adicionar novo nó personalizado", "disable_fiat": "Desativar fiat", @@ -753,5 +753,6 @@ "seedtype_polyseed": "Polyseed (16 palavras)", "seed_language_czech": "Tcheco", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Chinês tradicional)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Chinês tradicional)", + "polygonscan_history": "História do PolygonScan" +} diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index 0d927a279..1e80f01f2 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -160,7 +160,7 @@ "seed_title": "Мнемоническая фраза", "seed_share": "Поделиться мнемонической фразой", "copy": "Скопировать", - "seed_language_choose": "Пожалуйста, выберите язык мнемонической фразы:", + "seed_language": "Язык семян", "seed_choose": "Выберите язык мнемонической фразы", "seed_language_next": "Продолжить", "seed_language_english": "Английский", @@ -571,7 +571,7 @@ "always": "всегда", "minutes_to_pin_code": "${minute} минут", "disable_exchange": "Отключить обмен", - "advanced_privacy_settings": "Расширенные настройки конфиденциальности", + "advanced_settings": "Расширенные настройки", "settings_can_be_changed_later": "Эти настройки можно изменить позже в настройках приложения.", "add_custom_node": "Добавить новый пользовательский узел", "disable_fiat": "Отключить фиат", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Полиса (16 слов)", "seed_language_czech": "Чешский", "seed_language_korean": "Корейский", - "seed_language_chinese_traditional": "Китайский традиционный)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Китайский традиционный)", + "polygonscan_history": "История PolygonScan" +} diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 13536cfca..f43fdd951 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -160,7 +160,7 @@ "seed_title": "Seed", "seed_share": "แบ่งปัน seed", "copy": "คัดลอก", - "seed_language_choose": "โปรดเลือกภาษาของ seed:", + "seed_language": "ภาษาเมล็ด", "seed_choose": "เลือกภาษาของ seed", "seed_language_next": "ถัดไป", "seed_language_english": "อังกฤษ", @@ -569,7 +569,7 @@ "always": "เสมอ", "minutes_to_pin_code": "${minute} นาที", "disable_exchange": "ปิดใช้งานการแลกเปลี่ยน", - "advanced_privacy_settings": "การตั้งค่าความเป็นส่วนตัวขั้นสูง", + "advanced_settings": "ตั้งค่าขั้นสูง", "settings_can_be_changed_later": "การตั้งค่านี้สามารถเปลี่ยนแปลงได้ภายหลังในการตั้งค่าแอพฯ", "add_custom_node": "เพิ่มจุดโหนดแบบกำหนดเอง", "disable_fiat": "ปิดใช้งานสกุลเงินตรา", @@ -752,5 +752,6 @@ "seedtype_polyseed": "โพลีส (16 คำ)", "seed_language_czech": "ภาษาเช็ก", "seed_language_korean": "เกาหลี", - "seed_language_chinese_traditional": "จีน (ดั้งเดิม)" -} \ No newline at end of file + "seed_language_chinese_traditional": "จีน (ดั้งเดิม)", + "polygonscan_history": "ประวัติ PolygonScan" +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index f3ed4621c..23397e0f7 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -160,7 +160,7 @@ "seed_title": "Binhi", "seed_share": "Magbahagi ng binhi", "copy": "Kopya", - "seed_language_choose": "Mangyaring pumili ng wika ng binhi:", + "seed_language": "Wika ng binhi", "seed_choose": "Pumili ng wika ng binhi", "seed_language_next": "Susunod", "seed_language_english": "Ingles", @@ -572,7 +572,7 @@ "always": "Palagi", "minutes_to_pin_code": "${minute} minuto", "disable_exchange": "Huwag paganahin ang palitan", - "advanced_privacy_settings": "Mga setting ng advanced na privacy", + "advanced_settings": "Mga Advanced na Setting", "settings_can_be_changed_later": "Ang mga setting na ito ay maaaring mabago mamaya sa mga setting ng app", "add_custom_node": "Magdagdag ng bagong pasadyang node", "disable_fiat": "Huwag paganahin ang Fiat", @@ -748,5 +748,6 @@ "seedtype_polyseed": "Polyseed (16 na salita)", "seed_language_czech": "Czech", "seed_language_korean": "Korean", - "seed_language_chinese_traditional": "Intsik (tradisyonal)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Intsik (tradisyonal)", + "polygonscan_history": "Kasaysayan ng PolygonScan" +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 1903c745c..1b278374a 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -160,7 +160,7 @@ "seed_title": "Tohum", "seed_share": "Tohumu paylaş", "copy": "Kopyala", - "seed_language_choose": "Lütfen tohum dilini seç:", + "seed_language": "Tohum dili", "seed_choose": "Tohum dilini seçin", "seed_language_next": "İleri", "seed_language_english": "İngilizce", @@ -569,7 +569,7 @@ "always": "Her Zaman", "minutes_to_pin_code": "${minute} dakika", "disable_exchange": "Borsayı devre dışı bırak", - "advanced_privacy_settings": "Gelişmiş Gizlilik Ayarları", + "advanced_settings": "Gelişmiş Ayarlar", "settings_can_be_changed_later": "Bu ayarlar daha sonra uygulama ayarlarından da değiştirilebilir", "add_custom_node": "Yeni Özel Düğüm Ekleme", "disable_fiat": "İtibari paraları devre dışı bırak", @@ -752,5 +752,6 @@ "seedtype_polyseed": "Polyseed (16 kelime)", "seed_language_czech": "Çek", "seed_language_korean": "Koreli", - "seed_language_chinese_traditional": "Çin geleneği)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Çin geleneği)", + "polygonscan_history": "PolygonScan geçmişi" +} diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 9993220e2..6a102b957 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -160,7 +160,7 @@ "seed_title": "Мнемонічна фраза", "seed_share": "Поділитися мнемонічною фразою", "copy": "Скопіювати", - "seed_language_choose": "Будь ласка, виберіть мову мнемонічної фрази:", + "seed_language": "Насіннєва мова", "seed_choose": "Виберіть мову мнемонічної фрази", "seed_language_next": "Продовжити", "seed_language_english": "Англійська", @@ -571,7 +571,7 @@ "always": "Завжди", "minutes_to_pin_code": "${minute} хвилин", "disable_exchange": "Вимкнути exchange", - "advanced_privacy_settings": "Розширені налаштування конфіденційності", + "advanced_settings": "Розширені налаштування", "settings_can_be_changed_later": "Ці параметри можна змінити пізніше в налаштуваннях програми", "add_custom_node": "Додати новий спеціальний вузол", "disable_fiat": "Вимкнути фиат", @@ -754,5 +754,6 @@ "seedtype_polyseed": "Полісей (16 слів)", "seed_language_czech": "Чеський", "seed_language_korean": "Корейський", - "seed_language_chinese_traditional": "Китайський традиційний)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Китайський (традиційний)", + "polygonscan_history": "Історія PolygonScan" +} diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 2b91d623a..a799cde66 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -160,7 +160,7 @@ "seed_title": "بیج", "seed_share": "بیج بانٹیں۔", "copy": "کاپی", - "seed_language_choose": "براہ کرم بیج کی زبان کا انتخاب کریں:", + "seed_language": "بیج کی زبان", "seed_choose": "بیج کی زبان کا انتخاب کریں۔", "seed_language_next": "اگلے", "seed_language_english": "انگریزی", @@ -570,7 +570,7 @@ "always": "ہمیشہ", "minutes_to_pin_code": "${minute} منٹ", "disable_exchange": "تبادلے کو غیر فعال کریں۔", - "advanced_privacy_settings": "اعلی درجے کی رازداری کی ترتیبات", + "advanced_settings": "اعلی درجے کی ترتیبات", "settings_can_be_changed_later": "ان ترتیبات کو بعد میں ایپ کی ترتیبات میں تبدیل کیا جا سکتا ہے۔", "add_custom_node": "نیا کسٹم نوڈ شامل کریں۔", "disable_fiat": "فیاٹ کو غیر فعال کریں۔", @@ -746,5 +746,6 @@ "seedtype_polyseed": "پالیسیڈ (16 الفاظ)", "seed_language_czech": "چیک", "seed_language_korean": "کورین", - "seed_language_chinese_traditional": "چینی (روایتی)" -} \ No newline at end of file + "seed_language_chinese_traditional": "چینی (روایتی)", + "polygonscan_history": "ﺦﯾﺭﺎﺗ ﯽﮐ ﻦﯿﮑﺳﺍ ﻥﻮﮔ ﯽﻟﻮﭘ" +} diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index df8d8a263..dbed11d0f 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -160,7 +160,7 @@ "seed_title": "Hóró", "seed_share": "Pín hóró", "copy": "Ṣẹ̀dà", - "seed_language_choose": "Ẹ jọ̀wọ́ yan èdè hóró:", + "seed_language": "Ewu ọmọ", "seed_choose": "Yan èdè hóró", "seed_language_next": "Tẹ̀síwájú", "seed_language_english": "Èdè Gẹ̀ẹ́sì", @@ -567,7 +567,7 @@ "always": "Ní gbogbo àwọn ìgbà", "minutes_to_pin_code": "${minute} ìṣẹ́jú", "disable_exchange": "Pa ilé pàṣípààrọ̀", - "advanced_privacy_settings": "Àwọn ààtò àdáni títóbi", + "advanced_settings": "Awọn eto ilọsiwaju", "settings_can_be_changed_later": "Ẹ lè pààrọ̀ àwọn ààtò yìí nínú ààtò áàpù t’ó bá yá", "add_custom_node": "Fikún apẹka títun t'ẹ́ pààrọ̀", "disable_fiat": "Pa owó tí ìjọba pàṣẹ wa lò", @@ -748,5 +748,6 @@ "seedtype_polyseed": "Polyseed (awọn ọrọ 16)", "seed_language_czech": "Czech", "seed_language_korean": "Ara ẹni", - "seed_language_chinese_traditional": "Kannada (ibile)" -} \ No newline at end of file + "seed_language_chinese_traditional": "Kannada (ibile)", + "polygonscan_history": "PolygonScan itan" +} diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 83ef08541..dfde7a69d 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -160,7 +160,7 @@ "seed_title": "种子", "seed_share": "分享种子", "copy": "复制", - "seed_language_choose": "请选择种子语言:", + "seed_language": "种子语言", "seed_choose": "选择种子语言", "seed_language_next": "下一个", "seed_language_english": "英文", @@ -570,7 +570,7 @@ "always": "总是", "minutes_to_pin_code": "${minute} 分钟", "disable_exchange": "禁用交换", - "advanced_privacy_settings": "高级隐私设置", + "advanced_settings": "高级设置", "settings_can_be_changed_later": "稍后可以在应用设置中更改这些设置", "add_custom_node": "添加新的自定义节点", "disable_fiat": "禁用法令", @@ -753,5 +753,6 @@ "seedtype_polyseed": "多种物品(16个单词)", "seed_language_czech": "捷克", "seed_language_korean": "韩国人", - "seed_language_chinese_traditional": "中国传统的)" -} \ No newline at end of file + "seed_language_chinese_traditional": "中国传统的)", + "polygonscan_history": "多边形扫描历史" +} diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index d241c4421..63a29221b 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -16,14 +16,14 @@ APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.8.0" -MONERO_COM_BUILD_NUMBER=68 +MONERO_COM_BUILD_NUMBER=69 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.11.0" -CAKEWALLET_BUILD_NUMBER=182 +CAKEWALLET_BUILD_NUMBER=184 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index 874501cb5..a831396f1 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -14,12 +14,12 @@ APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" MONERO_COM_VERSION="1.8.0" -MONERO_COM_BUILD_NUMBER=66 +MONERO_COM_BUILD_NUMBER=67 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="4.11.0" -CAKEWALLET_BUILD_NUMBER=200 +CAKEWALLET_BUILD_NUMBER=202 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index f69670c9a..f6fa8b5e8 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,7 +16,7 @@ fi CAKEWALLET_NAME="Cake Wallet" CAKEWALLET_VERSION="1.4.0" -CAKEWALLET_BUILD_NUMBER=43 +CAKEWALLET_BUILD_NUMBER=45 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/tool/configure.dart b/tool/configure.dart index 7d50ddf53..67732faa9 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -6,6 +6,7 @@ const havenOutputPath = 'lib/haven/haven.dart'; const ethereumOutputPath = 'lib/ethereum/ethereum.dart'; const bitcoinCashOutputPath = 'lib/bitcoin_cash/bitcoin_cash.dart'; const nanoOutputPath = 'lib/nano/nano.dart'; +const polygonOutputPath = 'lib/polygon/polygon.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; const pubspecOutputPath = 'pubspec.yaml'; @@ -19,6 +20,7 @@ Future main(List args) async { final hasBitcoinCash = args.contains('${prefix}bitcoinCash'); final hasNano = args.contains('${prefix}nano'); final hasBanano = args.contains('${prefix}banano'); + final hasPolygon = args.contains('${prefix}polygon'); await generateBitcoin(hasBitcoin); await generateMonero(hasMonero); @@ -26,6 +28,7 @@ Future main(List args) async { await generateEthereum(hasEthereum); await generateBitcoinCash(hasBitcoinCash); await generateNano(hasNano); + await generatePolygon(hasPolygon); // await generateBanano(hasEthereum); await generatePubspec( @@ -36,6 +39,7 @@ Future main(List args) async { hasNano: hasNano, hasBanano: hasBanano, hasBitcoinCash: hasBitcoinCash, + hasPolygon: hasPolygon, ); await generateWalletTypes( hasMonero: hasMonero, @@ -45,6 +49,7 @@ Future main(List args) async { hasNano: hasNano, hasBanano: hasBanano, hasBitcoinCash: hasBitcoinCash, + hasPolygon: hasPolygon, ); } @@ -572,6 +577,93 @@ abstract class Ethereum { await outputFile.writeAsString(output); } +Future generatePolygon(bool hasImplementation) async { + final outputFile = File(polygonOutputPath); + const polygonCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.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:eth_sig_util/util/utils.dart'; +import 'package:hive/hive.dart'; +import 'package:web3dart/web3dart.dart'; +"""; + const polygonCWHeaders = """ +import 'package:cw_polygon/polygon_formatter.dart'; +import 'package:cw_polygon/polygon_transaction_credentials.dart'; +import 'package:cw_polygon/polygon_transaction_info.dart'; +import 'package:cw_polygon/polygon_wallet.dart'; +import 'package:cw_polygon/polygon_wallet_creation_credentials.dart'; +import 'package:cw_polygon/polygon_wallet_service.dart'; +import 'package:cw_polygon/polygon_transaction_priority.dart'; +import 'package:cw_ethereum/ethereum_mnemonics.dart'; +"""; + const polygonCwPart = "part 'cw_polygon.dart';"; + const polygonContent = """ +abstract class Polygon { + List getPolygonWordList(String language); + WalletService createPolygonWalletService(Box walletInfoSource); + WalletCredentials createPolygonNewWalletCredentials({required String name, WalletInfo? walletInfo}); + WalletCredentials createPolygonRestoreWalletFromSeedCredentials({required String name, required String mnemonic, required String password}); + WalletCredentials createPolygonRestoreWalletFromPrivateKey({required String name, required String privateKey, required String password}); + String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority getPolygonTransactionPrioritySlow(); + List getTransactionPriorities(); + TransactionPriority deserializePolygonTransactionPriority(int raw); + + Object createPolygonTransactionCredentials( + List outputs, { + required TransactionPriority priority, + required CryptoCurrency currency, + int? feeRate, + }); + + Object createPolygonTransactionCredentialsRaw( + List outputs, { + TransactionPriority? priority, + required CryptoCurrency currency, + required int feeRate, + }); + + int formatterPolygonParseAmount(String amount); + double formatterPolygonAmountToDouble({TransactionInfo? transaction, BigInt? amount, int exponent = 18}); + List getERC20Currencies(WalletBase wallet); + Future addErc20Token(WalletBase wallet, Erc20Token token); + Future deleteErc20Token(WalletBase wallet, Erc20Token token); + Future getErc20Token(WalletBase wallet, String contractAddress); + + CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); + void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled); + Web3Client? getWeb3Client(WalletBase wallet); +} + """; + + const polygonEmptyDefinition = 'Polygon? polygon;\n'; + const polygonCWDefinition = 'Polygon? polygon = CWPolygon();\n'; + + final output = '$polygonCommonHeaders\n' + + (hasImplementation ? '$polygonCWHeaders\n' : '\n') + + (hasImplementation ? '$polygonCwPart\n\n' : '\n') + + (hasImplementation ? polygonCWDefinition : polygonEmptyDefinition) + + '\n' + + polygonContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generateBitcoinCash(bool hasImplementation) async { final outputFile = File(bitcoinCashOutputPath); const bitcoinCashCommonHeaders = """ @@ -783,7 +875,8 @@ Future generatePubspec( required bool hasEthereum, required bool hasNano, required bool hasBanano, - required bool hasBitcoinCash}) async { + required bool hasBitcoinCash, + required bool hasPolygon}) async { const cwCore = """ cw_core: path: ./cw_core @@ -820,6 +913,10 @@ Future generatePubspec( cw_banano: path: ./cw_banano """; + const cwPolygon = """ + cw_polygon: + path: ./cw_polygon + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -850,6 +947,10 @@ Future generatePubspec( output += '\n$cwBitcoinCash'; } + if (hasPolygon) { + output += '\n$cwPolygon'; + } + if (hasHaven && !hasMonero) { output += '\n$cwSharedExternal\n$cwHaven'; } else if (hasHaven) { @@ -875,7 +976,8 @@ Future generateWalletTypes( required bool hasEthereum, required bool hasNano, required bool hasBanano, - required bool hasBitcoinCash}) async { + required bool hasBitcoinCash, + required bool hasPolygon}) async { final walletTypesFile = File(walletTypesPath); if (walletTypesFile.existsSync()) { @@ -906,6 +1008,10 @@ Future generateWalletTypes( outputContent += '\tWalletType.bitcoinCash,\n'; } + if (hasPolygon) { + outputContent += '\tWalletType.polygon,\n'; + } + if (hasNano) { outputContent += '\tWalletType.nano,\n'; } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index 163b80135..e6f625426 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -41,6 +41,7 @@ class SecretKey { static final ethereumSecrets = [ SecretKey('etherScanApiKey', () => ''), + SecretKey('polygonScanApiKey', () => ''), ]; final String name;