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/node.dart b/cw_core/lib/node.dart index 484325f91..cc749f23a 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.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; } 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_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_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 0a7097b7b..e0faa6db1 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -18,6 +18,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'; @@ -750,7 +752,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, ); }); @@ -847,6 +849,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 ec31b0539..45803899e 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -27,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'; @@ -65,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) { @@ -175,6 +178,11 @@ 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; @@ -332,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); @@ -575,6 +588,7 @@ Future checkCurrentNodes( 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 = @@ -589,6 +603,8 @@ Future checkCurrentNodes( 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); final currentNanoPowNodeServer = @@ -648,6 +664,12 @@ Future checkCurrentNodes( await nodeSource.add(node); 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); + } } Future resetBitcoinElectrumServer( @@ -742,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 46865cbcc..1ef388d31 100644 --- a/lib/entities/main_actions.dart +++ b/lib/entities/main_actions.dart @@ -19,7 +19,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, @@ -52,6 +53,7 @@ class MainActions { case WalletType.bitcoin: case WalletType.litecoin: case WalletType.ethereum: + case WalletType.polygon: case WalletType.bitcoinCash: switch (defaultBuyProvider) { case BuyProviderType.AskEachTime: @@ -124,6 +126,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 bdd6c24a7..67f1aca97 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'; @@ -37,6 +38,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'; @@ -50,6 +52,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/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/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/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/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/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 4aaa44e13..3e38fed1f 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -12,6 +12,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'; @@ -88,6 +89,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, @@ -101,6 +103,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialHavenTransactionPriority, TransactionPriority? initialLitecoinTransactionPriority, TransactionPriority? initialEthereumTransactionPriority, + TransactionPriority? initialPolygonTransactionPriority, TransactionPriority? initialBitcoinCashTransactionPriority}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), @@ -165,6 +168,10 @@ abstract class SettingsStoreBase with Store { priority[WalletType.ethereum] = initialEthereumTransactionPriority; } + if (initialPolygonTransactionPriority != null) { + priority[WalletType.polygon] = initialPolygonTransactionPriority; + } + if (initialBitcoinCashTransactionPriority != null) { priority[WalletType.bitcoinCash] = initialBitcoinCashTransactionPriority; } @@ -202,6 +209,9 @@ abstract class SettingsStoreBase with Store { case WalletType.bitcoinCash: key = PreferencesKey.bitcoinCashTransactionPriority; break; + case WalletType.polygon: + key = PreferencesKey.polygonTransactionPriority; + break; default: key = null; } @@ -245,8 +255,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, @@ -342,9 +352,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, @@ -388,6 +398,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)); @@ -396,34 +411,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) { @@ -562,6 +575,9 @@ abstract class SettingsStoreBase with Store { @observable bool useEtherscan; + @observable + bool usePolygonScan; + @observable String defaultNanoRep; @@ -651,6 +667,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? havenTransactionPriority; TransactionPriority? litecoinTransactionPriority; TransactionPriority? ethereumTransactionPriority; + TransactionPriority? polygonTransactionPriority; TransactionPriority? bitcoinCashTransactionPriority; if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { @@ -662,9 +679,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)!); @@ -676,6 +697,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)!); @@ -749,12 +771,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; @@ -774,6 +798,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); @@ -781,6 +806,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); @@ -824,6 +850,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (polygonNode != null) { + nodes[WalletType.polygon] = polygonNode; + } + if (bitcoinCashElectrumServer != null) { nodes[WalletType.bitcoinCash] = bitcoinCashElectrumServer; } @@ -841,19 +871,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, @@ -869,42 +899,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(); @@ -934,6 +967,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)!) ?? @@ -1027,12 +1065,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; @@ -1045,6 +1085,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); @@ -1052,6 +1093,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); @@ -1075,6 +1117,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.ethereum] = ethereumNode; } + if (polygonNode != null) { + nodes[WalletType.polygon] = polygonNode; + } + if (bitcoinCashNode != null) { nodes[WalletType.bitcoinCash] = bitcoinCashNode; } @@ -1110,6 +1156,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; } @@ -1141,7 +1190,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 a36d68482..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; @@ -52,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..d0a295047 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -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 6158342f3..45ad96764 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -745,5 +745,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 8c3a528bb..d52843798 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -741,5 +741,6 @@ "seedtype_polyseed": "Поли семе (16 думи)", "seed_language_czech": "Чех", "seed_language_korean": "Корейски", - "seed_language_chinese_traditional": "Традиционен китайски)" + "seed_language_chinese_traditional": "Традиционен китайски)", + "polygonscan_history": "История на PolygonScan" } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 64f2b45e5..3e1d68543 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -741,5 +741,6 @@ "seedtype_polyseed": "Polyseed (16 slov)", "seed_language_czech": "čeština", "seed_language_korean": "korejština", - "seed_language_chinese_traditional": "Číňan (tradiční)" + "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 2bb9e1404..62a73361c 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Polyseed (16 Wörter)", "seed_language_czech": "Tschechisch", "seed_language_korean": "Koreanisch", - "seed_language_chinese_traditional": "Chinesisch (Traditionell)" + "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 88949525c..19c435ce8 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -750,5 +750,6 @@ "seedtype_polyseed": "Polyseed (16 words)", "seed_language_czech": "Czech", "seed_language_korean": "Korean", - "seed_language_chinese_traditional": "Chinese (Traditional)" + "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 7a3907fb2..2b94e51b9 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Polieta (16 palabras)", "seed_language_czech": "checo", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Chino (tradicional)" + "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 0b2f54c6c..0c71ba06e 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Polyseed (16 mots)", "seed_language_czech": "tchèque", "seed_language_korean": "coréen", - "seed_language_chinese_traditional": "Chinois (Traditionnel)" + "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 107599684..0eb9bc7cd 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -727,5 +727,6 @@ "seedtype_polyseed": "Polyseed (16 kalmomi)", "seed_language_czech": "Czech", "seed_language_korean": "Yaren Koriya", - "seed_language_chinese_traditional": "Sinanci (na gargajiya)" + "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 20cf5dd40..c4c11fa58 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -749,5 +749,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_hr.arb b/res/values/strings_hr.arb index 156d885ea..82df37e69 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -747,5 +747,6 @@ "seedtype_polyseed": "Poliseed (16 riječi)", "seed_language_czech": "češki", "seed_language_korean": "korejski", - "seed_language_chinese_traditional": "Kinesko (tradicionalno)" + "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 c6836b65b..de04ecb12 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -737,5 +737,6 @@ "seedtype_polyseed": "Polyseed (16 kata)", "seed_language_czech": "Ceko", "seed_language_korean": "Korea", - "seed_language_chinese_traditional": "Cina (tradisional)" + "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 1d6d355b6..acb7a71c3 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Polyseed (16 parole)", "seed_language_czech": "ceco", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Cinese tradizionale)" + "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 595a348d3..7907d3835 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -749,5 +749,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_ko.arb b/res/values/strings_ko.arb index 0b0fff282..e9a54974e 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -747,5 +747,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_my.arb b/res/values/strings_my.arb index fea305f2d..02e3d38c2 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -747,5 +747,6 @@ "seedtype_polyseed": "polyseed (စကားလုံး 16 လုံး)", "seed_language_czech": "ချက်", "seed_language_korean": "ကိုးရီးယား", - "seed_language_chinese_traditional": "တရုတ်ရိုးရာ)" + "seed_language_chinese_traditional": "တရုတ်ရိုးရာ)", + "polygonscan_history": "PolygonScan မှတ်တမ်း" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index e400de4c6..4fd0fbab7 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Polyseed (16 woorden)", "seed_language_czech": "Tsjechisch", "seed_language_korean": "Koreaans", - "seed_language_chinese_traditional": "Chinese (traditionele)" + "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 698115f8b..bf3cc5329 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Poliqueed (16 słów)", "seed_language_czech": "Czech", "seed_language_korean": "koreański", - "seed_language_chinese_traditional": "Chiński tradycyjny)" + "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 9a926834a..ba4beb6c5 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -748,5 +748,6 @@ "seedtype_polyseed": "Polyseed (16 palavras)", "seed_language_czech": "Tcheco", "seed_language_korean": "coreano", - "seed_language_chinese_traditional": "Chinês tradicional)" + "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 e2a09009d..83a1aaf2b 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Полиса (16 слов)", "seed_language_czech": "Чешский", "seed_language_korean": "Корейский", - "seed_language_chinese_traditional": "Китайский традиционный)" + "seed_language_chinese_traditional": "Китайский традиционный)", + "polygonscan_history": "История PolygonScan" } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index a4d7f405e..e046eaba8 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -747,5 +747,6 @@ "seedtype_polyseed": "โพลีส (16 คำ)", "seed_language_czech": "ภาษาเช็ก", "seed_language_korean": "เกาหลี", - "seed_language_chinese_traditional": "จีน (ดั้งเดิม)" + "seed_language_chinese_traditional": "จีน (ดั้งเดิม)", + "polygonscan_history": "ประวัติ PolygonScan" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 04ba2af6d..06ee4bc89 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -743,5 +743,6 @@ "seedtype_polyseed": "Polyseed (16 na salita)", "seed_language_czech": "Czech", "seed_language_korean": "Korean", - "seed_language_chinese_traditional": "Intsik (tradisyonal)" + "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 b7b5d33f8..f300951ba 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -747,5 +747,6 @@ "seedtype_polyseed": "Polyseed (16 kelime)", "seed_language_czech": "Çek", "seed_language_korean": "Koreli", - "seed_language_chinese_traditional": "Çin geleneği)" + "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 900e95acc..6f8d87e86 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -749,5 +749,6 @@ "seedtype_polyseed": "Полісей (16 слів)", "seed_language_czech": "Чеський", "seed_language_korean": "Корейський", - "seed_language_chinese_traditional": "Китайський (традиційний)" + "seed_language_chinese_traditional": "Китайський (традиційний)", + "polygonscan_history": "Історія PolygonScan" } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index 5535bdf73..f75d287ef 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -741,5 +741,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_yo.arb b/res/values/strings_yo.arb index a867778f4..b5937a49c 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -743,5 +743,6 @@ "seedtype_polyseed": "Polyseed (awọn ọrọ 16)", "seed_language_czech": "Czech", "seed_language_korean": "Ara ẹni", - "seed_language_chinese_traditional": "Kannada (ibile)" + "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 7762af3a0..b487f83f5 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -748,5 +748,6 @@ "seedtype_polyseed": "多种物品(16个单词)", "seed_language_czech": "捷克", "seed_language_korean": "韩国人", - "seed_language_chinese_traditional": "中国传统的)" + "seed_language_chinese_traditional": "中国传统的)", + "polygonscan_history": "多边形扫描历史" } diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index dd9852072..0277922d3 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano --bitcoinCash" + CONFIG_ARGS="--monero --bitcoin --haven --ethereum --nano --bitcoinCash --polygon" ;; $HAVEN) CONFIG_ARGS="--haven" diff --git a/tool/configure.dart b/tool/configure.dart index 7d50ddf53..e3efb3275 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:cw_ethereum/ethereum_mnemonics.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'; +"""; + 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;