diff --git a/.github/ISSUE_TEMPLATE/bug-report-🪲-.md b/.github/ISSUE_TEMPLATE/bug-report-🪲-.md new file mode 100644 index 000000000..457cb84a4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report-🪲-.md @@ -0,0 +1,33 @@ +--- +name: "Bug Report \U0001FAB2 " +about: 'Report a bug ' +title: '' +labels: Bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Platform:** + - OS: [e.g. iOS 15.1, Android 14] + - Device: [e.g. iPhone 14, Galaxy S21] + - Cake Wallet Version: [e.g. 4.12.1] + + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 000000000..d7a1a3ed9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,8 @@ +blank_issues_enabled: false +contact_links: + - name: Not sure where to start? + url: https://guides.cakewallet.com + about: Start by reading checking out the guides! + - name: Need help? + url: https://cakewallet.com/#contact + about: Use our live chat or send a support email! \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md new file mode 100644 index 000000000..20bf2d53f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-or-enhancement-request-✨.md @@ -0,0 +1,20 @@ +--- +name: Feature or Enhancement Request ✨ +about: Suggest an idea for Cake Wallet +title: '' +labels: Enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 88cdc6f6c..6e5f9e171 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -105,21 +105,19 @@ jobs: run: | cd /opt/android/cake_wallet cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. + cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. - cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. 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 run: | cd /opt/android/cake_wallet touch lib/.secrets.g.dart - touch cw_ethereum/lib/.secrets.g.dart - touch cw_polygon/lib/.secrets.g.dart + touch cw_evm/lib/.secrets.g.dart echo "const salt = '${{ secrets.SALT }}';" > lib/.secrets.g.dart echo "const keychainSalt = '${{ secrets.KEY_CHAIN_SALT }}';" >> lib/.secrets.g.dart echo "const key = '${{ secrets.KEY }}';" >> lib/.secrets.g.dart @@ -146,14 +144,14 @@ jobs: echo "const anonPayReferralCode = '${{ secrets.ANON_PAY_REFERRAL_CODE }}';" >> lib/.secrets.g.dart echo "const fiatApiKey = '${{ secrets.FIAT_API_KEY }}';" >> lib/.secrets.g.dart echo "const payfuraApiKey = '${{ secrets.PAYFURA_API_KEY }}';" >> lib/.secrets.g.dart - echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart + echo "const etherScanApiKey = '${{ secrets.ETHER_SCAN_API_KEY }}';" >> cw_evm/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 echo "const robinhoodCIdApiSecret = '${{ secrets.ROBINHOOD_CID_CLIENT_SECRET }}';" >> lib/.secrets.g.dart echo "const walletConnectProjectId = '${{ secrets.WALLET_CONNECT_PROJECT_ID }}';" >> lib/.secrets.g.dart echo "const moralisApiKey = '${{ secrets.MORALIS_API_KEY }}';" >> lib/.secrets.g.dart - echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_ethereum/lib/.secrets.g.dart + echo "const polygonScanApiKey = '${{ secrets.POLYGON_SCAN_API_KEY }}';" >> cw_evm/lib/.secrets.g.dart - name: Rename app run: echo -e "id=com.cakewallet.test\nname=${{ env.BRANCH_NAME }}" > /opt/android/cake_wallet/android/app.properties diff --git a/.gitignore b/.gitignore index 0a883dd18..f084f8d0d 100644 --- a/.gitignore +++ b/.gitignore @@ -90,9 +90,10 @@ android/key.properties **/tool/.secrets-prod.json **/tool/.secrets-test.json **/tool/.secrets-config.json +**/tool/.evm-secrets-config.json **/tool/.ethereum-secrets-config.json **/lib/.secrets.g.dart -**/cw_ethereum/lib/.secrets.g.dart +**/cw_evm/lib/.secrets.g.dart vendor/ @@ -147,3 +148,12 @@ assets/images/app_logo.png macos/Runner/Info.plist macos/Runner/DebugProfile.entitlements macos/Runner/Release.entitlements + +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png +macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png +macos/Runner/Configs/AppInfo.xcconfig diff --git a/PRIVACY.md b/PRIVACY.md index 88f180c5e..76cfcc4d3 100644 --- a/PRIVACY.md +++ b/PRIVACY.md @@ -1,6 +1,6 @@ Privacy Policy -Last modified: August 9, 2023 +Last modified: January 24, 2024 Introduction ============ @@ -112,12 +112,12 @@ Data Security In any situation, Cake Labs takes no responsibility for interception of personal data by any outside individual, group, corporation, or institution. You should understand this and take any and all appropriate actions to secure your own data. -Links to Other Websites ------------------------ +Other Websites and Third-Party Services +--------------------------------------- The App may contain links to other websites that are not operated by us. If you click on a Third-Party Service link, you will be directed to that third party's site. We strongly advise you to review the Privacy Policy of every site you visit. We have no control over and assume no responsibility for the content, privacy policies or practices of any third-party sites or services. - The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. + The App includes several optional Third-Party Services, which may not be available to all users. If you use Third-Party Services, you must agree to their respective Privacy Policies. When using certain optional features in the app such as buying and selling, you may be asked to provide information to a Third-Party Service. You will need to read and accept the privacy policy for that third party. This Third-Party Service may ask for your name, your photo ID, your social security number or other similar number, mailing address, cryptocurrency address, or other information. They may ask you to take a selfie image. Information shared with a Third-Party Service is subject to their respective Privacy Policies. Changes to Our Privacy Policy ----------------------------- diff --git a/android/app/build.gradle b/android/app/build.gradle index e6b9eb2f8..5e27aeb9e 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -37,7 +37,7 @@ if (appPropertiesFile.exists()) { } android { - compileSdkVersion 33 + compileSdkVersion 34 lintOptions { disable 'InvalidPackage' diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index f32482e22..2dceca577 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -71,8 +71,8 @@ android:name="flutterEmbedding" android:value="2" /> - o is BitcoinAddressRecord && address == o.address; - final String address; final bool isHidden; final int index; + int _txCount; + int _balance; + String _name; + bool _isUsed; + + int get txCount => _txCount; + + String get name => _name; + + int get balance => _balance; + + set txCount(int value) => _txCount = value; + + set balance(int value) => _balance = value; + bool get isUsed => _isUsed; + void setAsUsed() => _isUsed = true; + void setNewName(String label) => _name = label; + + @override + bool operator ==(Object o) => o is BitcoinAddressRecord && address == o.address; + @override int get hashCode => address.hashCode; - bool _isUsed; + String get cashAddr => bitbox.Address.toCashAddress(address); - void setAsUsed() => _isUsed = true; - - String toJSON() => - json.encode({ + String toJSON() => json.encode({ 'address': address, 'index': index, 'isHidden': isHidden, - 'isUsed': isUsed}); + 'txCount': txCount, + 'name': name, + 'isUsed': isUsed, + 'balance': balance, + }); } diff --git a/cw_bitcoin/lib/bitcoin_wallet.dart b/cw_bitcoin/lib/bitcoin_wallet.dart index 2c66d02fe..9cdb78f2d 100644 --- a/cw_bitcoin/lib/bitcoin_wallet.dart +++ b/cw_bitcoin/lib/bitcoin_wallet.dart @@ -47,6 +47,9 @@ abstract class BitcoinWalletBase extends ElectrumWallet with Store { sideHd: bitcoin.HDWallet.fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } static Future create({ diff --git a/cw_bitcoin/lib/electrum_balance.dart b/cw_bitcoin/lib/electrum_balance.dart index a4824db0f..165ea447e 100644 --- a/cw_bitcoin/lib/electrum_balance.dart +++ b/cw_bitcoin/lib/electrum_balance.dart @@ -1,13 +1,9 @@ import 'dart:convert'; -import 'package:flutter/foundation.dart'; import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_core/balance.dart'; class ElectrumBalance extends Balance { - const ElectrumBalance( - {required this.confirmed, - required this.unconfirmed, - required this.frozen}) + const ElectrumBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) : super(confirmed, unconfirmed); static ElectrumBalance? fromJSON(String? jsonSource) { @@ -28,12 +24,10 @@ class ElectrumBalance extends Balance { final int frozen; @override - String get formattedAvailableBalance => - bitcoinAmountToString(amount: confirmed - unconfirmed.abs() - frozen); + String get formattedAvailableBalance => bitcoinAmountToString(amount: confirmed - frozen); @override - String get formattedAdditionalBalance => - bitcoinAmountToString(amount: unconfirmed); + String get formattedAdditionalBalance => bitcoinAmountToString(amount: unconfirmed); @override String get formattedUnAvailableBalance { @@ -41,6 +35,6 @@ class ElectrumBalance extends Balance { return frozenFormatted == '0.0' ? '' : frozenFormatted; } - String toJSON() => json.encode( - {'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); + String toJSON() => + json.encode({'confirmed': confirmed, 'unconfirmed': unconfirmed, 'frozen': frozen}); } diff --git a/cw_bitcoin/lib/electrum_wallet.dart b/cw_bitcoin/lib/electrum_wallet.dart index 4c5ecc7f2..6be525a1f 100644 --- a/cw_bitcoin/lib/electrum_wallet.dart +++ b/cw_bitcoin/lib/electrum_wallet.dart @@ -63,6 +63,7 @@ abstract class ElectrumWalletBase _password = password, _feeRates = [], _isTransactionUpdating = false, + isEnabledAutoGenerateSubaddress = true, unspentCoins = [], _scripthashesUpdateSubject = {}, balance = ObservableMap.of(currency != null @@ -87,6 +88,10 @@ abstract class ElectrumWalletBase final bitcoin.HDWallet hd; final String mnemonic; + @override + @observable + bool isEnabledAutoGenerateSubaddress; + late ElectrumClient electrumClient; Box unspentCoinsInfo; @@ -583,38 +588,66 @@ abstract class ElectrumWalletBase Future> fetchTransactions() async { final addressHashes = {}; final normalizedHistories = >[]; + final newTxCounts = {}; + walletAddresses.addresses.forEach((addressRecord) { final sh = scriptHash(addressRecord.address, networkType: networkType); addressHashes[sh] = addressRecord; + newTxCounts[sh] = 0; }); - final histories = addressHashes.keys.map((scriptHash) => - electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); - final historyResults = await Future.wait(histories); - historyResults.forEach((history) { - history.entries.forEach((historyItem) { - if (historyItem.value.isNotEmpty) { - final address = addressHashes[historyItem.key]; - address?.setAsUsed(); - normalizedHistories.addAll(historyItem.value); - } + + try { + final histories = addressHashes.keys.map((scriptHash) => + electrumClient.getHistory(scriptHash).then((history) => {scriptHash: history})); + final historyResults = await Future.wait(histories); + + + + historyResults.forEach((history) { + history.entries.forEach((historyItem) { + if (historyItem.value.isNotEmpty) { + final address = addressHashes[historyItem.key]; + address?.setAsUsed(); + newTxCounts[historyItem.key] = historyItem.value.length; + normalizedHistories.addAll(historyItem.value); + } + }); }); - }); - final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { - try { - return fetchTransactionInfo( - hash: transaction['tx_hash'] as String, height: transaction['height'] as int); - } catch (_) { - return Future.value(null); + + for (var sh in addressHashes.keys) { + var balanceData = await electrumClient.getBalance(sh); + var addressRecord = addressHashes[sh]; + if (addressRecord != null) { + addressRecord.balance = balanceData['confirmed'] as int? ?? 0; + } } - })); - return historiesWithDetails - .fold>({}, (acc, tx) { - if (tx == null) { + + + addressHashes.forEach((sh, addressRecord) { + addressRecord.txCount = newTxCounts[sh] ?? 0; + }); + + final historiesWithDetails = await Future.wait(normalizedHistories.map((transaction) { + try { + return fetchTransactionInfo( + hash: transaction['tx_hash'] as String, height: transaction['height'] as int); + } catch (_) { + return Future.value(null); + } + })); + + return historiesWithDetails.fold>( + {}, (acc, tx) { + if (tx == null) { + return acc; + } + acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; return acc; - } - acc[tx.id] = acc[tx.id]?.updated(tx) ?? tx; - return acc; - }); + }); + } catch (e) { + print(e.toString()); + return {}; + } } Future updateTransactions() async { diff --git a/cw_bitcoin/lib/electrum_wallet_addresses.dart b/cw_bitcoin/lib/electrum_wallet_addresses.dart index ab99a875c..a60a7e8ec 100644 --- a/cw_bitcoin/lib/electrum_wallet_addresses.dart +++ b/cw_bitcoin/lib/electrum_wallet_addresses.dart @@ -1,5 +1,5 @@ -import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:bitbox/bitbox.dart' as bitbox; +import 'package:bitcoin_flutter/bitcoin_flutter.dart' as bitcoin; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/electrum.dart'; import 'package:cw_bitcoin/script_hash.dart'; @@ -10,8 +10,7 @@ import 'package:mobx/mobx.dart'; part 'electrum_wallet_addresses.g.dart'; -class ElectrumWalletAddresses = ElectrumWalletAddressesBase - with _$ElectrumWalletAddresses; +class ElectrumWalletAddresses = ElectrumWalletAddressesBase with _$ElectrumWalletAddresses; abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { ElectrumWalletAddressesBase(WalletInfo walletInfo, @@ -22,19 +21,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List? initialAddresses, int initialRegularAddressIndex = 0, int initialChangeAddressIndex = 0}) - : addresses = ObservableList.of( - (initialAddresses ?? []).toSet()), - receiveAddresses = ObservableList.of( - (initialAddresses ?? []) + : addresses = ObservableList.of((initialAddresses ?? []).toSet()), + receiveAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), - changeAddresses = ObservableList.of( - (initialAddresses ?? []) + .toSet()), + changeAddresses = ObservableList.of((initialAddresses ?? []) .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed) - .toSet()), + .toSet()), currentReceiveAddressIndex = initialRegularAddressIndex, currentChangeAddressIndex = initialChangeAddressIndex, - super(walletInfo); + super(walletInfo); static const defaultReceiveAddressesCount = 22; static const defaultChangeAddressesCount = 17; @@ -42,6 +38,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { static String toCashAddr(String address) => bitbox.Address.toCashAddress(address); + static String toLegacy(String address) => bitbox.Address.toLegacyAddress(address); + final ObservableList addresses; final ObservableList receiveAddresses; final ObservableList changeAddresses; @@ -53,38 +51,67 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { @override @computed String get address { - if (receiveAddresses.isEmpty) { - final address = generateNewAddress().address; - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(address) : address; - } - final receiveAddress = receiveAddresses.first.address; + if (isEnabledAutoGenerateSubaddress) { + if (receiveAddresses.isEmpty) { + final newAddress = generateNewAddress().address; + return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(newAddress) : newAddress; + } + final receiveAddress = receiveAddresses.first.address; - return walletInfo.type == WalletType.bitcoinCash ? toCashAddr(receiveAddress) : receiveAddress; + return walletInfo.type == WalletType.bitcoinCash + ? toCashAddr(receiveAddress) + : receiveAddress; + } else { + final receiveAddress = (receiveAddresses.first.address != addresses.first.address && + previousAddressRecord != null) + ? previousAddressRecord!.address + : addresses.first.address; + + return walletInfo.type == WalletType.bitcoinCash + ? toCashAddr(receiveAddress) + : receiveAddress; + } + } + + @observable + bool isEnabledAutoGenerateSubaddress = true; + + @override + set address(String addr) { + if (addr.startsWith('bitcoincash:')) { + addr = toLegacy(addr); + } + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == addr); + + previousAddressRecord = addressRecord; + receiveAddresses.remove(addressRecord); + receiveAddresses.insert(0, addressRecord); } @override - set address(String addr) => null; + String get primaryAddress => getAddress(index: 0, hd: mainHd); int currentReceiveAddressIndex; int currentChangeAddressIndex; - @computed - int get totalCountOfReceiveAddresses => - addresses.fold(0, (acc, addressRecord) { - if (!addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + @observable + BitcoinAddressRecord? previousAddressRecord; @computed - int get totalCountOfChangeAddresses => - addresses.fold(0, (acc, addressRecord) { - if (addressRecord.isHidden) { - return acc + 1; - } - return acc; - }); + int get totalCountOfReceiveAddresses => addresses.fold(0, (acc, addressRecord) { + if (!addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); + + @computed + int get totalCountOfChangeAddresses => addresses.fold(0, (acc, addressRecord) { + if (addressRecord.isHidden) { + return acc + 1; + } + return acc; + }); Future discoverAddresses() async { await _discoverAddresses(mainHd, false); @@ -114,11 +141,9 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (changeAddresses.isEmpty) { final newAddresses = await _createNewAddresses(gap, - hd: sideHd, - startIndex: totalCountOfChangeAddresses > 0 - ? totalCountOfChangeAddresses - 1 - : 0, - isHidden: true); + hd: sideHd, + startIndex: totalCountOfChangeAddresses > 0 ? totalCountOfChangeAddresses - 1 : 0, + isHidden: true); _addAddresses(newAddresses); } @@ -132,14 +157,14 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { return address; } - BitcoinAddressRecord generateNewAddress( - {bitcoin.HDWallet? hd, bool isHidden = false}) { - currentReceiveAddressIndex += 1; - // FIX-ME: Check logic for whichi HD should be used here ??? - final address = BitcoinAddressRecord( - getAddress(index: currentReceiveAddressIndex, hd: hd ?? sideHd), - index: currentReceiveAddressIndex, - isHidden: isHidden); + BitcoinAddressRecord generateNewAddress({bitcoin.HDWallet? hd, String? label}) { + final isHidden = hd == sideHd; + + final newAddressIndex = addresses.fold( + 0, (int acc, addressRecord) => isHidden == addressRecord.isHidden ? acc + 1 : acc); + + final address = BitcoinAddressRecord(getAddress(index: newAddressIndex, hd: hd ?? sideHd), + index: newAddressIndex, isHidden: isHidden, name: label ?? ''); addresses.add(address); return address; } @@ -157,20 +182,32 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { } } + @action + void updateAddress(String address, String label) { + if (address.startsWith('bitcoincash:')) { + address = toLegacy(address); + } + final addressRecord = addresses.firstWhere((addressRecord) => addressRecord.address == address); + addressRecord.setNewName(label); + final index = addresses.indexOf(addressRecord); + addresses.remove(addressRecord); + addresses.insert(index, addressRecord); + } + @action void updateReceiveAddresses() { receiveAddresses.removeRange(0, receiveAddresses.length); - final newAdresses = addresses - .where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); - receiveAddresses.addAll(newAdresses); + final newAddresses = + addresses.where((addressRecord) => !addressRecord.isHidden && !addressRecord.isUsed); + receiveAddresses.addAll(newAddresses); } @action void updateChangeAddresses() { changeAddresses.removeRange(0, changeAddresses.length); - final newAdresses = addresses - .where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); - changeAddresses.addAll(newAdresses); + final newAddresses = + addresses.where((addressRecord) => addressRecord.isHidden && !addressRecord.isUsed); + changeAddresses.addAll(newAddresses); } Future _discoverAddresses(bitcoin.HDWallet hd, bool isHidden) async { @@ -178,20 +215,16 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { List addrs; if (addresses.isNotEmpty) { - addrs = addresses - .where((addr) => addr.isHidden == isHidden) - .toList(); + addrs = addresses.where((addr) => addr.isHidden == isHidden).toList(); } else { addrs = await _createNewAddresses( - isHidden - ? defaultChangeAddressesCount - : defaultReceiveAddressesCount, + isHidden ? defaultChangeAddressesCount : defaultReceiveAddressesCount, startIndex: 0, hd: hd, isHidden: isHidden); } - while(hasAddrUse) { + while (hasAddrUse) { final addr = addrs.last.address; hasAddrUse = await _hasAddressUsed(addr); @@ -201,11 +234,7 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final start = addrs.length; final count = start + gap; - final batch = await _createNewAddresses( - count, - startIndex: start, - hd: hd, - isHidden: isHidden); + final batch = await _createNewAddresses(count, startIndex: start, hd: hd, isHidden: isHidden); addrs.addAll(batch); } @@ -229,21 +258,15 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { if (countOfReceiveAddresses < defaultReceiveAddressesCount) { final addressesCount = defaultReceiveAddressesCount - countOfReceiveAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfReceiveAddresses, - hd: mainHd, - isHidden: false); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfReceiveAddresses, hd: mainHd, isHidden: false); addresses.addAll(newAddresses); } if (countOfHiddenAddresses < defaultChangeAddressesCount) { final addressesCount = defaultChangeAddressesCount - countOfHiddenAddresses; - final newAddresses = await _createNewAddresses( - addressesCount, - startIndex: countOfHiddenAddresses, - hd: sideHd, - isHidden: true); + final newAddresses = await _createNewAddresses(addressesCount, + startIndex: countOfHiddenAddresses, hd: sideHd, isHidden: true); addresses.addAll(newAddresses); } } @@ -253,10 +276,8 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final list = []; for (var i = startIndex; i < count + startIndex; i++) { - final address = BitcoinAddressRecord( - getAddress(index: i, hd: hd), - index: i, - isHidden: isHidden); + final address = + BitcoinAddressRecord(getAddress(index: i, hd: hd), index: i, isHidden: isHidden); list.add(address); } @@ -275,4 +296,4 @@ abstract class ElectrumWalletAddressesBase extends WalletAddresses with Store { final transactionHistory = await electrumClient.getHistory(sh); return transactionHistory.isNotEmpty; } -} \ No newline at end of file +} diff --git a/cw_bitcoin/lib/litecoin_wallet.dart b/cw_bitcoin/lib/litecoin_wallet.dart index 6bf1c5735..222e95acc 100644 --- a/cw_bitcoin/lib/litecoin_wallet.dart +++ b/cw_bitcoin/lib/litecoin_wallet.dart @@ -51,6 +51,9 @@ abstract class LitecoinWalletBase extends ElectrumWallet with Store { .fromSeed(seedBytes, network: networkType) .derivePath("m/0'/1"), networkType: networkType,); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } static Future create({ diff --git a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart index c23220423..1b87e2231 100644 --- a/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart +++ b/cw_bitcoin_cash/lib/src/bitcoin_cash_wallet.dart @@ -57,6 +57,9 @@ abstract class BitcoinCashWalletBase extends ElectrumWallet with Store { sideHd: bitcoin.HDWallet.fromSeed(seedBytes) .derivePath("m/44'/145'/0'/1"), networkType: networkType); + autorun((_) { + this.walletAddresses.isEnabledAutoGenerateSubaddress = this.isEnabledAutoGenerateSubaddress; + }); } diff --git a/cw_core/lib/amount_converter.dart b/cw_core/lib/amount_converter.dart index 6fd43dd82..249b87bd3 100644 --- a/cw_core/lib/amount_converter.dart +++ b/cw_core/lib/amount_converter.dart @@ -81,6 +81,7 @@ class AmountConverter { return _moneroAmountToString(amount); case CryptoCurrency.btc: case CryptoCurrency.bch: + case CryptoCurrency.ltc: return _bitcoinAmountToString(amount); case CryptoCurrency.xhv: case CryptoCurrency.xag: diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index 0f7f25d9b..162ffb1d2 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -95,6 +95,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.banano, CryptoCurrency.usdtPoly, CryptoCurrency.usdcEPoly, + CryptoCurrency.kaspa, ]; static const havenCurrencies = [ @@ -206,6 +207,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const banano = CryptoCurrency(title: 'BAN', fullName: 'Banano', raw: 86, name: 'banano', iconPath: 'assets/images/nano_icon.png', decimals: 29); static const usdtPoly = CryptoCurrency(title: 'USDT', tag: 'POLY', fullName: 'Tether USD (PoS)', raw: 87, name: 'usdtpoly', iconPath: 'assets/images/usdt_icon.png', decimals: 6); static const usdcEPoly = CryptoCurrency(title: 'USDC.E', tag: 'POLY', fullName: 'USD Coin (PoS)', raw: 88, name: 'usdcepoly', iconPath: 'assets/images/usdc_icon.png', decimals: 6); + static const kaspa = CryptoCurrency(title: 'KAS', fullName: 'Kaspa', raw: 89, name: 'kaspa', iconPath: 'assets/images/kaspa_icon.png', decimals: 8); static final Map _rawCurrencyMap = diff --git a/cw_core/lib/transaction_info.dart b/cw_core/lib/transaction_info.dart index 38b4b799d..7624b147f 100644 --- a/cw_core/lib/transaction_info.dart +++ b/cw_core/lib/transaction_info.dart @@ -15,6 +15,7 @@ abstract class TransactionInfo extends Object with Keyable { String? feeFormatted(); void changeFiatAmount(String amount); String? to; + String? from; @override dynamic get keyIndex => id; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index 632eb1332..d8c84c80c 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -10,6 +10,8 @@ abstract class WalletAddresses { String get address; + String? get primaryAddress => null; + set address(String address); Map addressesMap; diff --git a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart index a8f82d181..c26ee1efc 100644 --- a/cw_ethereum/lib/default_ethereum_erc20_tokens.dart +++ b/cw_ethereum/lib/default_ethereum_erc20_tokens.dart @@ -1,7 +1,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; -class DefaultErc20Tokens { +class DefaultEthereumErc20Tokens { final List _defaultTokens = [ Erc20Token( name: "USD Coin", diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index fccbf778d..929dadf2f 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -1,225 +1,22 @@ -import 'dart:async'; import 'dart:convert'; +import 'dart:developer'; +import 'dart:typed_data'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:cw_ethereum/erc20_balance.dart'; -import 'package:cw_core/erc20_token.dart'; -import 'package:cw_ethereum/ethereum_transaction_model.dart'; -import 'package:cw_ethereum/pending_ethereum_transaction.dart'; -import 'package:flutter/services.dart'; -import 'package:http/http.dart'; +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/.secrets.g.dart' as secrets; +import 'package:cw_evm/evm_chain_transaction_model.dart'; import 'package:web3dart/web3dart.dart'; -import 'package:erc20/erc20.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_ethereum/ethereum_transaction_priority.dart'; -import 'package:cw_ethereum/.secrets.g.dart' as secrets; - -class EthereumClient { - final httpClient = Client(); - Web3Client? _client; - - bool connect(Node node) { - try { - _client = Web3Client(node.uri.toString(), httpClient); - - return true; - } catch (e) { - return false; - } - } - - void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { - // _client?.pendingTransactions().listen((transactionHash) async { - // final transaction = await _client!.getTransactionByHash(transactionHash); - // - // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { - // onNewTransaction(); - // } - // }); - } - - Future getBalance(EthereumAddress address) async { - try { - return await _client!.getBalance(address); - } catch (_) { - return EtherAmount.zero(); - } - } - - Future getGasUnitPrice() async { - try { - final gasPrice = await _client!.getGasPrice(); - return gasPrice.getInWei.toInt(); - } catch (_) { - return 0; - } - } - - Future getEstimatedGas() async { - try { - final estimatedGas = await _client!.estimateGas(); - return estimatedGas.toInt(); - } catch (_) { - return 0; - } - } - - Future signTransaction({ - required EthPrivateKey privateKey, - required String toAddress, - required String amount, - required int gas, - required EthereumTransactionPriority priority, - required CryptoCurrency currency, - required int exponent, - String? contractAddress, - }) async { - assert(currency == CryptoCurrency.eth || - currency == CryptoCurrency.maticpoly || - contractAddress != null); - - bool _isEVMCompatibleChain = - currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; - - final price = _client!.getGasPrice(); - - final Transaction transaction = createTransaction( - from: privateKey.address, - to: EthereumAddress.fromHex(toAddress), - maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), - amount: _isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), - ); - - final signedTransaction = - await _client!.signTransaction(privateKey, transaction, chainId: chainId); - - final Function _sendTransaction; - - if (_isEVMCompatibleChain) { - _sendTransaction = () async => await sendTransaction(signedTransaction); - } else { - final erc20 = ERC20( - client: _client!, - address: EthereumAddress.fromHex(contractAddress!), - chainId: chainId, - ); - - _sendTransaction = () async { - await erc20.transfer( - EthereumAddress.fromHex(toAddress), - BigInt.parse(amount), - credentials: privateKey, - transaction: transaction, - ); - }; - } - - return PendingEthereumTransaction( - signedTransaction: signedTransaction, - amount: amount, - fee: BigInt.from(gas) * (await price).getInWei, - sendTransaction: _sendTransaction, - exponent: exponent, - ); - } +class EthereumClient extends EVMChainClient { + @override int get chainId => 1; - Transaction createTransaction({ - required EthereumAddress from, - required EthereumAddress to, - required EtherAmount amount, - EtherAmount? maxPriorityFeePerGas, - }) { - return Transaction( - from: from, - to: to, - maxPriorityFeePerGas: maxPriorityFeePerGas, - value: amount, - ); - } - - Future sendTransaction(Uint8List signedTransaction) async => - await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); - + @override Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction) => prependTransactionType(0x02, signedTransaction); - Future getTransactionDetails(String transactionHash) async { - // Wait for the transaction receipt to become available - TransactionReceipt? receipt; - while (receipt == null) { - receipt = await _client!.getTransactionReceipt(transactionHash); - await Future.delayed(Duration(seconds: 1)); - } - - // Print the receipt information - print('Transaction Hash: ${receipt.transactionHash}'); - print('Block Hash: ${receipt.blockHash}'); - print('Block Number: ${receipt.blockNumber}'); - print('Gas Used: ${receipt.gasUsed}'); - - /* - Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] -I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] -I/flutter ( 4474): Block Number: 17120242 -I/flutter ( 4474): Gas Used: 21000 - */ - - // Wait for the transaction receipt to become available - TransactionInformation? transactionInformation; - while (transactionInformation == null) { - print("********************************"); - transactionInformation = await _client!.getTransactionByHash(transactionHash); - await Future.delayed(Duration(seconds: 1)); - } - // Print the receipt information - print('Transaction Hash: ${transactionInformation.hash}'); - print('Block Hash: ${transactionInformation.blockHash}'); - print('Block Number: ${transactionInformation.blockNumber}'); - print('Gas Used: ${transactionInformation.gas}'); - - /* - Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 -I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 -I/flutter ( 4474): Block Number: 17120242 -I/flutter ( 4474): Gas Used: 53000 - */ - } - - Future fetchERC20Balances( - EthereumAddress userAddress, String contractAddress) async { - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); - final balance = await erc20.balanceOf(userAddress); - - int exponent = (await erc20.decimals()).toInt(); - - return ERC20Balance(balance, exponent: exponent); - } - - Future getErc20Token(String contractAddress) async { - try { - final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); - final name = await erc20.name(); - final symbol = await erc20.symbol(); - final decimal = await erc20.decimals(); - - return Erc20Token( - name: name, - symbol: symbol, - contractAddress: contractAddress, - decimal: decimal.toInt(), - ); - } catch (e) { - return null; - } - } - - void stop() { - _client?.dispose(); - } - - Future> fetchTransactions(String address, + @override + Future> fetchTransactions(String address, {String? contractAddress}) async { try { final response = await httpClient.get(Uri.https("api.etherscan.io", "/api", { @@ -230,41 +27,18 @@ I/flutter ( 4474): Gas Used: 53000 "apikey": secrets.etherScanApiKey, })); - final _jsonResponse = json.decode(response.body) as Map; + 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) => EthereumTransactionModel.fromJson(e as Map)) + if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { + return (jsonResponse['result'] as List) + .map((e) => EVMChainTransactionModel.fromJson(e as Map, 'ETH')) .toList(); } return []; } catch (e) { - print(e); + log(e.toString()); return []; } } - - Web3Client? getWeb3Client() { - return _client; - } - -// Future _getDecimalPlacesForContract(DeployedContract contract) async { -// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); -// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); -// -// final contract = DeployedContract( -// contractAbi, -// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), -// ); -// final decimalsFunction = contract.function('decimals'); -// final decimals = await _client!.call( -// contract: contract, -// function: decimalsFunction, -// params: [], -// ); -// -// int exponent = int.parse(decimals.first.toString()); -// return exponent; -// } } diff --git a/cw_ethereum/lib/ethereum_exceptions.dart b/cw_ethereum/lib/ethereum_exceptions.dart deleted file mode 100644 index 518f46275..000000000 --- a/cw_ethereum/lib/ethereum_exceptions.dart +++ /dev/null @@ -1,11 +0,0 @@ -import 'package:cw_core/crypto_currency.dart'; - -class EthereumTransactionCreationException implements Exception { - final String exceptionMessage; - - EthereumTransactionCreationException(CryptoCurrency currency) : - this.exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; - - @override - String toString() => exceptionMessage; -} diff --git a/cw_ethereum/lib/ethereum_formatter.dart b/cw_ethereum/lib/ethereum_formatter.dart deleted file mode 100644 index 468c536f8..000000000 --- a/cw_ethereum/lib/ethereum_formatter.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:intl/intl.dart'; - -const ethereumAmountLength = 12; -const ethereumAmountDivider = 1000000000000; -final ethereumAmountFormat = NumberFormat() - ..maximumFractionDigits = ethereumAmountLength - ..minimumFractionDigits = 1; - -class EthereumFormatter { - static int parseEthereumAmount(String amount) { - try { - return (double.parse(amount) * ethereumAmountDivider).round(); - } catch (_) { - return 0; - } - } - - static double parseEthereumAmountToDouble(int amount) { - try { - return amount / ethereumAmountDivider; - } catch (_) { - return 0; - } - } -} diff --git a/cw_ethereum/lib/ethereum_mnemonics_exception.dart b/cw_ethereum/lib/ethereum_mnemonics_exception.dart new file mode 100644 index 000000000..b91a15c94 --- /dev/null +++ b/cw_ethereum/lib/ethereum_mnemonics_exception.dart @@ -0,0 +1,5 @@ +class EthereumMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_ethereum/lib/ethereum_transaction_history.dart b/cw_ethereum/lib/ethereum_transaction_history.dart index 4511f4436..f774ae905 100644 --- a/cw_ethereum/lib/ethereum_transaction_history.dart +++ b/cw_ethereum/lib/ethereum_transaction_history.dart @@ -1,77 +1,18 @@ -import 'dart:convert'; import 'dart:core'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_ethereum/file.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_core/transaction_history.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; -part 'ethereum_transaction_history.g.dart'; - -const transactionsHistoryFileName = 'transactions.json'; - -class EthereumTransactionHistory = EthereumTransactionHistoryBase with _$EthereumTransactionHistory; - -abstract class EthereumTransactionHistoryBase - extends TransactionHistoryBase with Store { - EthereumTransactionHistoryBase({required this.walletInfo, required String password}) - : _password = password { - transactions = ObservableMap(); - } - - final WalletInfo walletInfo; - String _password; - - Future init() async => await _load(); +class EthereumTransactionHistory extends EVMChainTransactionHistory { + EthereumTransactionHistory({ + required super.walletInfo, + required super.password, + }); @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 save ethereum transaction history: ${e.toString()}'); - print(s); - } - } + String getTransactionHistoryFileName() => 'transactions.json'; @override - void addOne(EthereumTransactionInfo 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 = EthereumTransactionInfo.fromJson(val); - _update(tx); - } - }); - } catch (e) { - print(e); - } - } - - void _update(EthereumTransactionInfo transaction) => transactions[transaction.id] = transaction; + EVMChainTransactionInfo getTransactionInfo(Map val) => + EthereumTransactionInfo.fromJson(val); } diff --git a/cw_ethereum/lib/ethereum_transaction_info.dart b/cw_ethereum/lib/ethereum_transaction_info.dart index f0deae931..d5d3fea8d 100644 --- a/cw_ethereum/lib/ethereum_transaction_info.dart +++ b/cw_ethereum/lib/ethereum_transaction_info.dart @@ -1,57 +1,21 @@ -import 'dart:math'; - -import 'package:cw_core/format_amount.dart'; import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; -class EthereumTransactionInfo extends TransactionInfo { +class EthereumTransactionInfo extends EVMChainTransactionInfo { EthereumTransactionInfo({ - required this.id, - required this.height, - required this.ethAmount, - required this.ethFee, - this.tokenSymbol = "ETH", - this.exponent = 18, - required this.direction, - required this.isPending, - required this.date, - required this.confirmations, - required this.to, - }) : this.amount = ethAmount.toInt(), - this.fee = ethFee.toInt(); - - final String id; - final int height; - final int amount; - final BigInt ethAmount; - final int exponent; - final TransactionDirection direction; - final DateTime date; - final bool isPending; - final int fee; - final BigInt ethFee; - final int confirmations; - final String tokenSymbol; - String? _fiatAmount; - final String? to; - - @override - 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 ?? ''; - - @override - void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); - - @override - String feeFormatted() { - final amount = (ethFee / BigInt.from(10).pow(18)).toString(); - return '${amount.substring(0, min(10, amount.length))} ETH'; - } + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.exponent, + }); factory EthereumTransactionInfo.fromJson(Map data) { return EthereumTransactionInfo( @@ -66,20 +30,10 @@ class EthereumTransactionInfo extends TransactionInfo { confirmations: data['confirmations'] as int, tokenSymbol: data['tokenSymbol'] as String, to: data['to'], + from: data['from'], ); } - Map toJson() => { - 'id': id, - 'height': height, - 'amount': ethAmount.toString(), - 'exponent': exponent, - 'fee': ethFee.toString(), - 'direction': direction.index, - 'date': date.millisecondsSinceEpoch, - 'isPending': isPending, - 'confirmations': confirmations, - 'tokenSymbol': tokenSymbol, - 'to': to, - }; + @override + String get feeCurrency => 'ETH'; } diff --git a/cw_ethereum/lib/ethereum_transaction_priority.dart b/cw_ethereum/lib/ethereum_transaction_priority.dart deleted file mode 100644 index ff5668397..000000000 --- a/cw_ethereum/lib/ethereum_transaction_priority.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:cw_core/transaction_priority.dart'; - -class EthereumTransactionPriority extends TransactionPriority { - final int tip; - - const EthereumTransactionPriority({required String title, required int raw, required this.tip}) - : super(title: title, raw: raw); - - static const List all = [fast, medium, slow]; - static const EthereumTransactionPriority slow = - EthereumTransactionPriority(title: 'slow', raw: 0, tip: 1); - static const EthereumTransactionPriority medium = - EthereumTransactionPriority(title: 'Medium', raw: 1, tip: 2); - static const EthereumTransactionPriority fast = - EthereumTransactionPriority(title: 'Fast', raw: 2, tip: 4); - - static EthereumTransactionPriority deserialize({required int raw}) { - switch (raw) { - case 0: - return slow; - case 1: - return medium; - case 2: - return fast; - default: - throw Exception('Unexpected token: $raw for EthereumTransactionPriority deserialize'); - } - } - - String get units => 'gas'; - - @override - String toString() { - var label = ''; - - switch (this) { - case EthereumTransactionPriority.slow: - label = 'Slow'; - break; - case EthereumTransactionPriority.medium: - label = 'Medium'; - break; - case EthereumTransactionPriority.fast: - label = 'Fast'; - break; - default: - break; - } - - return label; - } -} diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index cd4bd84cc..4604db662 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -1,126 +1,58 @@ -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/crypto_currency.dart'; +import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/sync_status.dart'; import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/wallet_addresses.dart'; -import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_ethereum/default_ethereum_erc20_tokens.dart'; -import 'package:cw_ethereum/erc20_balance.dart'; import 'package:cw_ethereum/ethereum_client.dart'; -import 'package:cw_ethereum/ethereum_exceptions.dart'; -import 'package:cw_ethereum/ethereum_formatter.dart'; -import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; import 'package:cw_ethereum/ethereum_transaction_history.dart'; import 'package:cw_ethereum/ethereum_transaction_info.dart'; -import 'package:cw_ethereum/ethereum_transaction_model.dart'; -import 'package:cw_ethereum/ethereum_transaction_priority.dart'; -import 'package:cw_ethereum/ethereum_wallet_addresses.dart'; -import 'package:cw_ethereum/file.dart'; -import 'package:cw_core/erc20_token.dart'; -import 'package:hive/hive.dart'; -import 'package:hex/hex.dart'; -import 'package:mobx/mobx.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:web3dart/crypto.dart'; -import 'package:web3dart/web3dart.dart'; -import 'package:bip39/bip39.dart' as bip39; -import 'package:bip32/bip32.dart' as bip32; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/file.dart'; -part 'ethereum_wallet.g.dart'; +class EthereumWallet extends EVMChainWallet { + EthereumWallet({ + required super.client, + required super.password, + required super.walletInfo, + super.mnemonic, + super.initialBalance, + super.privateKey, + }) : super(nativeCurrency: CryptoCurrency.eth); -class EthereumWallet = EthereumWalletBase with _$EthereumWallet; + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultEthereumErc20Tokens().initialErc20Tokens; -abstract class EthereumWalletBase - extends WalletBase - with Store { - EthereumWalletBase({ - required WalletInfo walletInfo, - String? mnemonic, - String? privateKey, - required String password, - ERC20Balance? initialBalance, - }) : syncStatus = NotConnectedSyncStatus(), - _password = password, - _mnemonic = mnemonic, - _hexPrivateKey = privateKey, - _isTransactionUpdating = false, - _client = EthereumClient(), - walletAddresses = EthereumWalletAddresses(walletInfo), - balance = ObservableMap.of( - {CryptoCurrency.eth: initialBalance ?? ERC20Balance(BigInt.zero)}), - super(walletInfo) { - this.walletInfo = walletInfo; - transactionHistory = EthereumTransactionHistory(walletInfo: walletInfo, password: password); - - if (!CakeHive.isAdapterRegistered(Erc20Token.typeId)) { - CakeHive.registerAdapter(Erc20TokenAdapter()); + for (var token in initialErc20Tokens) { + evmChainErc20TokensBox.put(token.contractAddress, token); } - - _sharedPrefs.complete(SharedPreferences.getInstance()); } - final String? _mnemonic; - final String? _hexPrivateKey; - final String _password; - - late final Box erc20TokensBox; - - late final Box ethereumErc20TokensBox; - - late final EthPrivateKey _ethPrivateKey; - - EthPrivateKey get ethPrivateKey => _ethPrivateKey; - - late EthereumClient _client; - - int? _gasPrice; - int? _estimatedGas; - bool _isTransactionUpdating; - - // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter - Timer? _transactionsUpdateTimer; + @override + Future checkIfScanProviderIsEnabled() async { + bool isEtherscanEnabled = (await sharedPrefs.future).getBool("use_etherscan") ?? true; + return isEtherscanEnabled; + } @override - WalletAddresses walletAddresses; - - @override - @observable - SyncStatus syncStatus; - - @override - @observable - late ObservableMap balance; - - Completer _sharedPrefs = Completer(); - - Future init() async { + Future initErc20TokensBox() async { + // This is for ethereum wallets, + // Other wallets would override and initialize their respective boxes with their boxNames. await movePreviousErc20BoxConfigsToNewBox(); - - await walletAddresses.init(); - await transactionHistory.init(); - _ethPrivateKey = await getPrivateKey( - mnemonic: _mnemonic, - privateKey: _hexPrivateKey, - password: _password, - ); - walletAddresses.address = _ethPrivateKey.address.toString(); - await save(); } /// Majorly for backward compatibility for previous configs that have been set. Future movePreviousErc20BoxConfigsToNewBox() async { // Opens a box specific to this wallet - ethereumErc20TokensBox = await CakeHive.openBox( + evmChainErc20TokensBox = await CakeHive.openBox( "${walletInfo.name.replaceAll(" ", "_")}_${Erc20Token.ethereumBoxName}"); //Open the previous token configs box @@ -130,7 +62,7 @@ abstract class EthereumWalletBase if (erc20TokensBox.isEmpty) { // If it's empty, but the new wallet specific box is also empty, // we load the initial tokens to the new box. - if (ethereumErc20TokensBox.isEmpty) addInitialTokens(); + if (evmChainErc20TokensBox.isEmpty) addInitialTokens(); return; } @@ -141,319 +73,37 @@ abstract class EthereumWalletBase await erc20TokensBox.deleteFromDisk(); // Add all the previous tokens with configs to the new box - ethereumErc20TokensBox.addAll(allValues); + evmChainErc20TokensBox.addAll(allValues); } @override - int calculateEstimatedFee(TransactionPriority priority, int? amount) { - try { - if (priority is EthereumTransactionPriority) { - 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("Ethereum Node connection failed"); - } - - _client.setListeners(_ethPrivateKey.address, _onNewTransaction); - - _setTransactionUpdateTimer(); - - syncStatus = ConnectedSyncStatus(); - } catch (e) { - syncStatus = FailedSyncStatus(); - } - } - - @override - Future createTransaction(Object credentials) async { - final _credentials = credentials as EthereumTransactionCredentials; - 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 amountToEthereumMultiplier = pow(10, exponent); - - // so far this can not be made with Ethereum as Ethereum does not support multiple recipients - if (hasMultiDestination) { - if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw EthereumTransactionCreationException(transactionCurrency); - } - - final totalOriginalAmount = EthereumFormatter.parseEthereumAmountToDouble( - outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); - totalAmount = BigInt.from(totalOriginalAmount * amountToEthereumMultiplier); - - if (_erc20Balance.balance < totalAmount) { - throw EthereumTransactionCreationException(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 * amountToEthereumMultiplier); - - if (_erc20Balance.balance < totalAmount) { - throw EthereumTransactionCreationException(transactionCurrency); - } - } - - final pendingEthereumTransaction = await _client.signTransaction( - privateKey: _ethPrivateKey, - 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, + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address) { + final model = EthereumTransactionInfo( + id: transactionModel.hash, + height: transactionModel.blockNumber, + ethAmount: transactionModel.amount, + direction: transactionModel.from == address + ? TransactionDirection.outgoing + : TransactionDirection.incoming, + isPending: false, + date: transactionModel.date, + confirmations: transactionModel.confirmations, + ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, + exponent: transactionModel.tokenDecimal ?? 18, + tokenSymbol: transactionModel.tokenSymbol ?? "ETH", + to: transactionModel.to, + from: transactionModel.from, ); - - return pendingEthereumTransaction; - } - - Future _updateTransactions() async { - try { - if (_isTransactionUpdating) { - return; - } - bool isEtherscanEnabled = (await _sharedPrefs.future).getBool("use_etherscan") ?? true; - if (!isEtherscanEnabled) { - return; - } - - _isTransactionUpdating = true; - final transactions = await fetchTransactions(); - transactionHistory.addMany(transactions); - await transactionHistory.save(); - _isTransactionUpdating = false; - } catch (_) { - _isTransactionUpdating = false; - } + return model; } @override - Future> fetchTransactions() async { - final address = _ethPrivateKey.address.hex; - final transactions = await _client.fetchTransactions(address); - - final List>> erc20TokensTransactions = []; - - for (var token in balance.keys) { - if (token is Erc20Token) { - erc20TokensTransactions.add(_client.fetchTransactions( - address, - contractAddress: token.contractAddress, - )); - } - } - - final tokensTransaction = await Future.wait(erc20TokensTransactions); - transactions.addAll(tokensTransaction.expand((element) => element)); - - final Map result = {}; - - for (var transactionModel in transactions) { - if (transactionModel.isError) { - continue; - } - - result[transactionModel.hash] = EthereumTransactionInfo( - id: transactionModel.hash, - height: transactionModel.blockNumber, - ethAmount: transactionModel.amount, - direction: transactionModel.from == address - ? TransactionDirection.outgoing - : TransactionDirection.incoming, - isPending: false, - date: transactionModel.date, - confirmations: transactionModel.confirmations, - ethFee: BigInt.from(transactionModel.gasUsed) * transactionModel.gasPrice, - exponent: transactionModel.tokenDecimal ?? 18, - tokenSymbol: transactionModel.tokenSymbol ?? "ETH", - to: transactionModel.to, - ); - } - - return result; - } + String getTransactionHistoryFileName() => 'transactions.json'; @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(_ethPrivateKey.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 EthereumWallet( - walletInfo: walletInfo, - password: password, - mnemonic: mnemonic, - privateKey: privateKey, - initialBalance: balance, - ); - } - - Future _updateBalance() async { - balance[currency] = await _fetchEthBalance(); - - await _fetchErc20Balances(); - await save(); - } - - Future _fetchEthBalance() async { - final balance = await _client.getBalance(_ethPrivateKey.address); - return ERC20Balance(balance.getInWei); - } - - Future _fetchErc20Balances() async { - for (var token in ethereumErc20TokensBox.values) { - try { - if (token.enabled) { - balance[token] = await _client.fetchERC20Balances( - _ethPrivateKey.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 _hdPathEthereum = "m/44'/60'/0'/0"; - const index = 0; - final addressAtIndex = root.derivePath("$_hdPathEthereum/$index"); - - return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); - } - - Future? updateBalance() async => await _updateBalance(); - - List get erc20Currencies => ethereumErc20TokensBox.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( + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( name: token.name, symbol: token.symbol, contractAddress: token.contractAddress, @@ -462,85 +112,30 @@ abstract class EthereumWalletBase tag: token.tag ?? "ETH", iconPath: iconPath, ); - - await ethereumErc20TokensBox.put(_token.contractAddress, _token); - - if (_token.enabled) { - balance[_token] = await _client.fetchERC20Balances( - _ethPrivateKey.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 = DefaultErc20Tokens().initialErc20Tokens; - - initialErc20Tokens.forEach((token) => ethereumErc20TokensBox.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); + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { + return EthereumTransactionHistory(walletInfo: walletInfo, password: password); } - void _setTransactionUpdateTimer() { - if (_transactionsUpdateTimer?.isActive ?? false) { - _transactionsUpdateTimer!.cancel(); - } + 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 = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + EVMChainERC20Balance(BigInt.zero); - _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) { - _updateTransactions(); - _updateBalance(); - }); + return EthereumWallet( + walletInfo: walletInfo, + password: password, + mnemonic: mnemonic, + privateKey: privateKey, + initialBalance: balance, + client: EthereumClient(), + ); } - - void updateEtherscanUsageState(bool isEnabled) { - if (isEnabled) { - _updateTransactions(); - _setTransactionUpdateTimer(); - } else { - _transactionsUpdateTimer?.cancel(); - } - } - - @override - String signMessage(String message, {String? address}) => - bytesToHex(_ethPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); - - Web3Client? getWeb3Client() => _client.getWeb3Client(); } diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart deleted file mode 100644 index 6546f2fae..000000000 --- a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_info.dart'; - -class EthereumNewWalletCredentials extends WalletCredentials { - EthereumNewWalletCredentials({required String name, WalletInfo? walletInfo}) - : super(name: name, walletInfo: walletInfo); -} - -class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { - EthereumRestoreWalletFromSeedCredentials( - {required String name, - required String password, - required this.mnemonic, - WalletInfo? walletInfo}) - : super(name: name, password: password, walletInfo: walletInfo); - - final String mnemonic; -} - -class EthereumRestoreWalletFromPrivateKey extends WalletCredentials { - EthereumRestoreWalletFromPrivateKey( - {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_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 0acc90bac..1cd776867 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -1,32 +1,31 @@ -import 'dart:io'; - -import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cw_ethereum/ethereum_mnemonics.dart'; +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_mnemonics_exception.dart'; import 'package:cw_ethereum/ethereum_wallet.dart'; -import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; -import 'package:hive/hive.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; import 'package:bip39/bip39.dart' as bip39; -import 'package:collection/collection.dart'; -class EthereumWalletService extends WalletService { - EthereumWalletService(this.walletInfoSource); +class EthereumWalletService extends EVMChainWalletService { + EthereumWalletService(super.walletInfoSource, {required this.client}); - final Box walletInfoSource; + late EthereumClient client; @override - Future create(EthereumNewWalletCredentials credentials) async { + WalletType getType() => WalletType.ethereum; + + @override + Future create(EVMChainNewWalletCredentials credentials) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = bip39.generateMnemonic(strength: strength); + final wallet = EthereumWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + client: client, ); await wallet.init(); @@ -36,18 +35,11 @@ class EthereumWalletService extends WalletService WalletType.ethereum; - - @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 EthereumWalletBase.open( + final wallet = await EthereumWallet.open( name: name, password: password, walletInfo: walletInfo, @@ -60,19 +52,28 @@ class EthereumWalletService extends WalletService 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); + Future rename(String currentName, String password, String newName) async { + final currentWalletInfo = walletInfoSource.values + .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); + final currentWallet = await EthereumWallet.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); } @override - Future restoreFromKeys(EthereumRestoreWalletFromPrivateKey credentials) async { + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async { final wallet = EthereumWallet( password: credentials.password!, privateKey: credentials.privateKey, walletInfo: credentials.walletInfo!, + client: client, ); await wallet.init(); @@ -84,7 +85,7 @@ class EthereumWalletService extends WalletService restoreFromSeed( - EthereumRestoreWalletFromSeedCredentials credentials) async { + EVMChainRestoreWalletFromSeedCredentials credentials) async { if (!bip39.validateMnemonic(credentials.mnemonic)) { throw EthereumMnemonicIsIncorrectException(); } @@ -93,6 +94,7 @@ class EthereumWalletService extends WalletService rename(String currentName, String password, String newName) async { - final currentWalletInfo = walletInfoSource.values - .firstWhere((info) => info.id == WalletBase.idFor(currentName, getType())); - final currentWallet = await EthereumWalletBase.open( - password: password, name: currentName, walletInfo: currentWalletInfo); - - await currentWallet.renameWalletFiles(newName); - - final newWalletInfo = currentWalletInfo; - newWalletInfo.id = WalletBase.idFor(newName, getType()); - newWalletInfo.name = newName; - - await walletInfoSource.put(currentWalletInfo.key, newWalletInfo); - } } diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index 6946a43a1..649ec574b 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -13,56 +13,23 @@ dependencies: flutter: sdk: flutter web3dart: ^2.7.1 - erc20: ^1.0.1 - mobx: ^2.0.7+4 - bip39: ^1.0.6 - bip32: ^2.0.0 - hex: ^0.2.0 - http: ^1.1.0 - shared_preferences: ^2.0.15 cw_core: path: ../cw_core + cw_evm: + path: ../cw_evm + hive: ^2.2.3 dev_dependencies: flutter_test: sdk: flutter build_runner: ^2.1.11 - mobx_codegen: ^2.0.7 - hive_generator: ^1.1.3 - -# For information on the generic Dart part of this file, see the -# following page: https://dart.dev/tools/pub/pubspec - -# The following section is specific to Flutter packages. flutter: - - # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg # - images/a_dot_ham.jpeg - # - # For details regarding assets in packages, see - # https://flutter.dev/assets-and-images/#from-packages - # - # An image asset can refer to one or more resolution-specific "variants", see - # https://flutter.dev/assets-and-images/#resolution-aware - - # To add custom fonts to your package, add a fonts section here, - # in this "flutter" section. Each entry in this list should have a - # "family" key with the font family name, and a "fonts" key with a - # list giving the asset and other descriptors for the font. For - # example: # fonts: # - family: Schyler # fonts: # - asset: fonts/Schyler-Regular.ttf # - asset: fonts/Schyler-Italic.ttf # style: italic - # - family: Trajan Pro - # fonts: - # - asset: fonts/TrajanPro.ttf - # - asset: fonts/TrajanPro_Bold.ttf - # weight: 700 - # - # For details regarding fonts in packages, see - # https://flutter.dev/custom-fonts/#from-packages diff --git a/cw_evm/.gitignore b/cw_evm/.gitignore new file mode 100644 index 000000000..96486fd93 --- /dev/null +++ b/cw_evm/.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_evm/.metadata b/cw_evm/.metadata new file mode 100644 index 000000000..fa347fc6a --- /dev/null +++ b/cw_evm/.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_evm/CHANGELOG.md b/cw_evm/CHANGELOG.md new file mode 100644 index 000000000..41cc7d819 --- /dev/null +++ b/cw_evm/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/cw_evm/LICENSE b/cw_evm/LICENSE new file mode 100644 index 000000000..ba75c69f7 --- /dev/null +++ b/cw_evm/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/cw_evm/README.md b/cw_evm/README.md new file mode 100644 index 000000000..02fe8ecab --- /dev/null +++ b/cw_evm/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_evm/analysis_options.yaml b/cw_evm/analysis_options.yaml new file mode 100644 index 000000000..a5744c1cf --- /dev/null +++ b/cw_evm/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_evm/lib/cw_evm.dart b/cw_evm/lib/cw_evm.dart new file mode 100644 index 000000000..40f2bcaba --- /dev/null +++ b/cw_evm/lib/cw_evm.dart @@ -0,0 +1,7 @@ +library cw_evm; + +/// A Calculator. +class Calculator { + /// Returns [value] plus 1. + int addOne(int value) => value + 1; +} diff --git a/cw_evm/lib/evm_chain_client.dart b/cw_evm/lib/evm_chain_client.dart new file mode 100644 index 000000000..de5b3874a --- /dev/null +++ b/cw_evm/lib/evm_chain_client.dart @@ -0,0 +1,251 @@ +import 'dart:async'; +import 'dart:developer'; + +import 'package:cw_core/node.dart'; +import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/crypto_currency.dart'; + +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/pending_evm_chain_transaction.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:flutter/services.dart'; + +import 'package:http/http.dart'; +import 'package:erc20/erc20.dart'; +import 'package:web3dart/web3dart.dart'; + +abstract class EVMChainClient { + final httpClient = Client(); + Web3Client? _client; + + //! To be overridden by all child classes + + int get chainId; + + Future> fetchTransactions(String address, + {String? contractAddress}); + + Uint8List prepareSignedTransactionForSending(Uint8List signedTransaction); + + //! Common methods across all child classes + + bool connect(Node node) { + try { + _client = Web3Client(node.uri.toString(), httpClient); + + return true; + } catch (e) { + return false; + } + } + + void setListeners(EthereumAddress userAddress, Function() onNewTransaction) async { + // _client?.pendingTransactions().listen((transactionHash) async { + // final transaction = await _client!.getTransactionByHash(transactionHash); + // + // if (transaction.from.hex == userAddress || transaction.to?.hex == userAddress) { + // onNewTransaction(); + // } + // }); + } + + Future getBalance(EthereumAddress address) async { + try { + return await _client!.getBalance(address); + } catch (_) { + return EtherAmount.zero(); + } + } + + Future getGasUnitPrice() async { + try { + final gasPrice = await _client!.getGasPrice(); + return gasPrice.getInWei.toInt(); + } catch (_) { + return 0; + } + } + + Future getEstimatedGas() async { + try { + final estimatedGas = await _client!.estimateGas(); + return estimatedGas.toInt(); + } catch (_) { + return 0; + } + } + + Future signTransaction({ + required EthPrivateKey privateKey, + required String toAddress, + required String amount, + required int gas, + required EVMChainTransactionPriority priority, + required CryptoCurrency currency, + required int exponent, + String? contractAddress, + }) async { + assert(currency == CryptoCurrency.eth || + currency == CryptoCurrency.maticpoly || + contractAddress != null); + + bool isEVMCompatibleChain = + currency == CryptoCurrency.eth || currency == CryptoCurrency.maticpoly; + + final price = _client!.getGasPrice(); + + final Transaction transaction = createTransaction( + from: privateKey.address, + to: EthereumAddress.fromHex(toAddress), + maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), + amount: isEVMCompatibleChain ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), + ); + + final signedTransaction = + await _client!.signTransaction(privateKey, transaction, chainId: chainId); + + final Function _sendTransaction; + + if (isEVMCompatibleChain) { + _sendTransaction = () async => await sendTransaction(signedTransaction); + } else { + final erc20 = ERC20( + client: _client!, + address: EthereumAddress.fromHex(contractAddress!), + chainId: chainId, + ); + + _sendTransaction = () async { + await erc20.transfer( + EthereumAddress.fromHex(toAddress), + BigInt.parse(amount), + credentials: privateKey, + transaction: transaction, + ); + }; + } + + return PendingEVMChainTransaction( + signedTransaction: signedTransaction, + amount: amount, + fee: BigInt.from(gas) * (await price).getInWei, + sendTransaction: _sendTransaction, + exponent: exponent, + ); + } + + Transaction createTransaction({ + required EthereumAddress from, + required EthereumAddress to, + required EtherAmount amount, + EtherAmount? maxPriorityFeePerGas, + }) { + return Transaction( + from: from, + to: to, + maxPriorityFeePerGas: maxPriorityFeePerGas, + value: amount, + ); + } + + Future sendTransaction(Uint8List signedTransaction) async => + await _client!.sendRawTransaction(prepareSignedTransactionForSending(signedTransaction)); + + Future getTransactionDetails(String transactionHash) async { + // Wait for the transaction receipt to become available + TransactionReceipt? receipt; + while (receipt == null) { + receipt = await _client!.getTransactionReceipt(transactionHash); + await Future.delayed(const Duration(seconds: 1)); + } + + // Print the receipt information + log('Transaction Hash: ${receipt.transactionHash}'); + log('Block Hash: ${receipt.blockHash}'); + log('Block Number: ${receipt.blockNumber}'); + log('Gas Used: ${receipt.gasUsed}'); + + /* + Transaction Hash: [112, 244, 4, 238, 89, 199, 171, 191, 210, 236, 110, 42, 185, 202, 220, 21, 27, 132, 123, 221, 137, 90, 77, 13, 23, 43, 12, 230, 93, 63, 221, 116] + I/flutter ( 4474): Block Hash: [149, 44, 250, 119, 111, 104, 82, 98, 17, 89, 30, 190, 25, 44, 218, 118, 127, 189, 241, 35, 213, 106, 25, 95, 195, 37, 55, 131, 185, 180, 246, 200] + I/flutter ( 4474): Block Number: 17120242 + I/flutter ( 4474): Gas Used: 21000 + */ + + // Wait for the transaction receipt to become available + TransactionInformation? transactionInformation; + while (transactionInformation == null) { + log("********************************"); + transactionInformation = await _client!.getTransactionByHash(transactionHash); + await Future.delayed(const Duration(seconds: 1)); + } + // Print the receipt information + log('Transaction Hash: ${transactionInformation.hash}'); + log('Block Hash: ${transactionInformation.blockHash}'); + log('Block Number: ${transactionInformation.blockNumber}'); + log('Gas Used: ${transactionInformation.gas}'); + + /* + Transaction Hash: 0x70f404ee59c7abbfd2ec6e2ab9cadc151b847bdd895a4d0d172b0ce65d3fdd74 + I/flutter ( 4474): Block Hash: 0x952cfa776f68526211591ebe192cda767fbdf123d56a195fc3253783b9b4f6c8 + I/flutter ( 4474): Block Number: 17120242 + I/flutter ( 4474): Gas Used: 53000 + */ + } + + Future fetchERC20Balances( + EthereumAddress userAddress, String contractAddress) async { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final balance = await erc20.balanceOf(userAddress); + + int exponent = (await erc20.decimals()).toInt(); + + return EVMChainERC20Balance(balance, exponent: exponent); + } + + Future getErc20Token(String contractAddress) async { + try { + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final name = await erc20.name(); + final symbol = await erc20.symbol(); + final decimal = await erc20.decimals(); + + return Erc20Token( + name: name, + symbol: symbol, + contractAddress: contractAddress, + decimal: decimal.toInt(), + ); + } catch (e) { + return null; + } + } + + void stop() { + _client?.dispose(); + } + + Web3Client? getWeb3Client() { + return _client; + } + +// Future _getDecimalPlacesForContract(DeployedContract contract) async { +// final String abi = await rootBundle.loadString("assets/abi_json/erc20_abi.json"); +// final contractAbi = ContractAbi.fromJson(abi, "ERC20"); +// +// final contract = DeployedContract( +// contractAbi, +// EthereumAddress.fromHex(_erc20Currencies[erc20Currency]!), +// ); +// final decimalsFunction = contract.function('decimals'); +// final decimals = await _client!.call( +// contract: contract, +// function: decimalsFunction, +// params: [], +// ); +// +// int exponent = int.parse(decimals.first.toString()); +// return exponent; +// } +} diff --git a/cw_evm/lib/evm_chain_exceptions.dart b/cw_evm/lib/evm_chain_exceptions.dart new file mode 100644 index 000000000..1c09ecf6d --- /dev/null +++ b/cw_evm/lib/evm_chain_exceptions.dart @@ -0,0 +1,11 @@ +import 'package:cw_core/crypto_currency.dart'; + +class EVMChainTransactionCreationException implements Exception { + final String exceptionMessage; + + EVMChainTransactionCreationException(CryptoCurrency currency) + : exceptionMessage = 'Wrong balance. Not enough ${currency.title} on your balance.'; + + @override + String toString() => exceptionMessage; +} diff --git a/cw_evm/lib/evm_chain_formatter.dart b/cw_evm/lib/evm_chain_formatter.dart new file mode 100644 index 000000000..cb9b7346c --- /dev/null +++ b/cw_evm/lib/evm_chain_formatter.dart @@ -0,0 +1,25 @@ +import 'package:intl/intl.dart'; + +const evmChainAmountLength = 12; +const evmChainAmountDivider = 1000000000000; +final evmChainAmountFormat = NumberFormat() + ..maximumFractionDigits = evmChainAmountLength + ..minimumFractionDigits = 1; + +class EVMChainFormatter { + static int parseEVMChainAmount(String amount) { + try { + return (double.parse(amount) * evmChainAmountDivider).round(); + } catch (_) { + return 0; + } + } + + static double parseEVMChainAmountToDouble(int amount) { + try { + return amount / evmChainAmountDivider; + } catch (_) { + return 0; + } + } +} diff --git a/cw_ethereum/lib/ethereum_mnemonics.dart b/cw_evm/lib/evm_chain_mnemonics.dart similarity index 99% rename from cw_ethereum/lib/ethereum_mnemonics.dart rename to cw_evm/lib/evm_chain_mnemonics.dart index 8af7b10f3..55fa4c3a8 100644 --- a/cw_ethereum/lib/ethereum_mnemonics.dart +++ b/cw_evm/lib/evm_chain_mnemonics.dart @@ -1,10 +1,4 @@ -class EthereumMnemonicIsIncorrectException implements Exception { - @override - String toString() => - 'Ethereum mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; -} - -class EthereumMnemonics { +class EVMChainMnemonics { static const englishWordlist = [ 'abandon', 'ability', diff --git a/cw_ethereum/lib/ethereum_transaction_credentials.dart b/cw_evm/lib/evm_chain_transaction_credentials.dart similarity index 60% rename from cw_ethereum/lib/ethereum_transaction_credentials.dart rename to cw_evm/lib/evm_chain_transaction_credentials.dart index b015b7141..5b5bdf170 100644 --- a/cw_ethereum/lib/ethereum_transaction_credentials.dart +++ b/cw_evm/lib/evm_chain_transaction_credentials.dart @@ -1,9 +1,9 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/output_info.dart'; -import 'package:cw_ethereum/ethereum_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; -class EthereumTransactionCredentials { - EthereumTransactionCredentials( +class EVMChainTransactionCredentials { + EVMChainTransactionCredentials( this.outputs, { required this.priority, required this.currency, @@ -11,7 +11,7 @@ class EthereumTransactionCredentials { }); final List outputs; - final EthereumTransactionPriority? priority; + final EVMChainTransactionPriority? priority; final int? feeRate; final CryptoCurrency currency; } diff --git a/cw_evm/lib/evm_chain_transaction_history.dart b/cw_evm/lib/evm_chain_transaction_history.dart new file mode 100644 index 000000000..2f5c31e82 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_history.dart @@ -0,0 +1,88 @@ +import 'dart:convert'; +import 'dart:core'; +import 'dart:developer'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/file.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_history.dart'; + +part 'evm_chain_transaction_history.g.dart'; + +abstract class EVMChainTransactionHistory = EVMChainTransactionHistoryBase + with _$EVMChainTransactionHistory; + +abstract class EVMChainTransactionHistoryBase + extends TransactionHistoryBase with Store { + EVMChainTransactionHistoryBase({required this.walletInfo, required String password}) + : _password = password { + transactions = ObservableMap(); + } + + String _password; + + final WalletInfo walletInfo; + + //! Method to be overridden by all child classes + + String getTransactionHistoryFileName(); + + EVMChainTransactionInfo getTransactionInfo(Map val); + + //! Common methods across all child classes + + Future init() async => await _load(); + + @override + Future save() async { + final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); + try { + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + final data = json.encode({'transactions': transactions}); + await writeData(path: path, password: _password, data: data); + } catch (e, s) { + log('Error while saving ${walletInfo.type.name} transaction history: ${e.toString()}'); + log(s.toString()); + } + } + + @override + void addOne(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction; + + @override + void addMany(Map transactions) => + this.transactions.addAll(transactions); + + Future> _read() async { + final transactionsHistoryFileNameForWallet = getTransactionHistoryFileName(); + final dirPath = await pathForWalletDir(name: walletInfo.name, type: walletInfo.type); + String path = '$dirPath/$transactionsHistoryFileNameForWallet'; + 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? ?? {}; + + for (var entry in txs.entries) { + final val = entry.value; + + if (val is Map) { + final tx = getTransactionInfo(val); + _update(tx); + } + } + } catch (e) { + log(e.toString()); + } + } + + void _update(EVMChainTransactionInfo transaction) => transactions[transaction.id] = transaction; +} diff --git a/cw_evm/lib/evm_chain_transaction_info.dart b/cw_evm/lib/evm_chain_transaction_info.dart new file mode 100644 index 000000000..329061db2 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_info.dart @@ -0,0 +1,77 @@ +// ignore_for_file: overridden_fields, annotate_overrides + +import 'dart:math'; + +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; + +abstract class EVMChainTransactionInfo extends TransactionInfo { + EVMChainTransactionInfo({ + required this.id, + required this.height, + required this.ethAmount, + required this.ethFee, + required this.tokenSymbol, + this.exponent = 18, + required this.direction, + required this.isPending, + required this.date, + required this.confirmations, + required this.to, + required this.from, + }) : amount = ethAmount.toInt(), + fee = ethFee.toInt(); + + final String id; + final int height; + final int amount; + final BigInt ethAmount; + final int exponent; + final TransactionDirection direction; + final DateTime date; + final bool isPending; + final int fee; + final BigInt ethFee; + final int confirmations; + final String tokenSymbol; + String? _fiatAmount; + final String? to; + final String? from; + + //! Getter to be overridden in child classes + String get feeCurrency; + + @override + 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 ?? ''; + + @override + void changeFiatAmount(String amount) => _fiatAmount = formatAmount(amount); + + @override + String feeFormatted() { + final amount = (ethFee / BigInt.from(10).pow(18)).toString(); + return '${amount.substring(0, min(10, amount.length))} $feeCurrency'; + } + + Map toJson() => { + 'id': id, + 'height': height, + 'amount': ethAmount.toString(), + 'exponent': exponent, + 'fee': ethFee.toString(), + 'direction': direction.index, + 'date': date.millisecondsSinceEpoch, + 'isPending': isPending, + 'confirmations': confirmations, + 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, + }; +} diff --git a/cw_ethereum/lib/ethereum_transaction_model.dart b/cw_evm/lib/evm_chain_transaction_model.dart similarity index 81% rename from cw_ethereum/lib/ethereum_transaction_model.dart rename to cw_evm/lib/evm_chain_transaction_model.dart index 3b5f724fc..a328a2d6d 100644 --- a/cw_ethereum/lib/ethereum_transaction_model.dart +++ b/cw_evm/lib/evm_chain_transaction_model.dart @@ -1,5 +1,4 @@ -//! Model used for in parsing transactions fetched using etherscan -class EthereumTransactionModel { +class EVMChainTransactionModel { final DateTime date; final String hash; final String from; @@ -14,7 +13,7 @@ class EthereumTransactionModel { final int? tokenDecimal; final bool isError; - EthereumTransactionModel({ + EVMChainTransactionModel({ required this.date, required this.hash, required this.from, @@ -30,7 +29,8 @@ class EthereumTransactionModel { required this.isError, }); - factory EthereumTransactionModel.fromJson(Map json) => EthereumTransactionModel( + factory EVMChainTransactionModel.fromJson(Map json, String defaultSymbol) => + EVMChainTransactionModel( date: DateTime.fromMillisecondsSinceEpoch(int.parse(json["timeStamp"]) * 1000), hash: json["hash"], from: json["from"], @@ -41,7 +41,7 @@ class EthereumTransactionModel { contractAddress: json["contractAddress"], confirmations: int.parse(json["confirmations"]), blockNumber: int.parse(json["blockNumber"]), - tokenSymbol: json["tokenSymbol"] ?? "ETH", + tokenSymbol: json["tokenSymbol"] ?? defaultSymbol, tokenDecimal: int.tryParse(json["tokenDecimal"] ?? ""), isError: json["isError"] == "1", ); diff --git a/cw_evm/lib/evm_chain_transaction_priority.dart b/cw_evm/lib/evm_chain_transaction_priority.dart new file mode 100644 index 000000000..b4ce55490 --- /dev/null +++ b/cw_evm/lib/evm_chain_transaction_priority.dart @@ -0,0 +1,52 @@ +import 'package:cw_core/transaction_priority.dart'; + +class EVMChainTransactionPriority extends TransactionPriority { + final int tip; + + const EVMChainTransactionPriority({required String title, required int raw, required this.tip}) + : super(title: title, raw: raw); + + static const List all = [fast, medium, slow]; + static const EVMChainTransactionPriority slow = + EVMChainTransactionPriority(title: 'slow', raw: 0, tip: 1); + static const EVMChainTransactionPriority medium = + EVMChainTransactionPriority(title: 'Medium', raw: 1, tip: 2); + static const EVMChainTransactionPriority fast = + EVMChainTransactionPriority(title: 'Fast', raw: 2, tip: 4); + + static EVMChainTransactionPriority 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 EVMChainTransactionPriority deserialize'); + } + } + + String get units => 'gas'; + + @override + String toString() { + var label = ''; + + switch (this) { + case EVMChainTransactionPriority.slow: + label = 'Slow'; + break; + case EVMChainTransactionPriority.medium: + label = 'Medium'; + break; + case EVMChainTransactionPriority.fast: + label = 'Fast'; + break; + default: + break; + } + + return label; + } +} diff --git a/cw_evm/lib/evm_chain_wallet.dart b/cw_evm/lib/evm_chain_wallet.dart new file mode 100644 index 000000000..ea19a8557 --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet.dart @@ -0,0 +1,512 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:math'; + +import 'package:bip32/bip32.dart' as bip32; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/cake_hive.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/erc20_token.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_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_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_client.dart'; +import 'package:cw_evm/evm_chain_exceptions.dart'; +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_wallet_addresses.dart'; +import 'package:cw_evm/file.dart'; +import 'package:hex/hex.dart'; +import 'package:hive/hive.dart'; +import 'package:mobx/mobx.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:web3dart/crypto.dart'; +import 'package:web3dart/web3dart.dart'; + +import 'evm_chain_transaction_info.dart'; +import 'evm_erc20_balance.dart'; + +part 'evm_chain_wallet.g.dart'; + +abstract class EVMChainWallet = EVMChainWalletBase with _$EVMChainWallet; + +abstract class EVMChainWalletBase + extends WalletBase + with Store { + EVMChainWalletBase({ + required WalletInfo walletInfo, + required EVMChainClient client, + required CryptoCurrency nativeCurrency, + String? mnemonic, + String? privateKey, + required String password, + EVMChainERC20Balance? initialBalance, + }) : syncStatus = const NotConnectedSyncStatus(), + _password = password, + _mnemonic = mnemonic, + _hexPrivateKey = privateKey, + _isTransactionUpdating = false, + _client = client, + walletAddresses = EVMChainWalletAddresses(walletInfo), + balance = ObservableMap.of( + { + // Not sure of this yet, will it work? will it not? + nativeCurrency: initialBalance ?? EVMChainERC20Balance(BigInt.zero), + }, + ), + super(walletInfo) { + this.walletInfo = walletInfo; + transactionHistory = setUpTransactionHistory(walletInfo, 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 erc20TokensBox; + + late final Box evmChainErc20TokensBox; + + late final EthPrivateKey _evmChainPrivateKey; + + EthPrivateKey get evmChainPrivateKey => _evmChainPrivateKey; + + late EVMChainClient _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(); + + //! Methods to be overridden by every child + + void addInitialTokens(); + + // Future open({ + // required String name, + // required String password, + // required WalletInfo walletInfo, + // }); + + Future initErc20TokensBox(); + + String getTransactionHistoryFileName(); + + Future checkIfScanProviderIsEnabled(); + + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address); + + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath); + + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password); + + //! Common Methods across child classes + + String idFor(String name, WalletType type) => '${walletTypeToString(type).toLowerCase()}_$name'; + + Future init() async { + await initErc20TokensBox(); + + await walletAddresses.init(); + await transactionHistory.init(); + _evmChainPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + walletAddresses.address = _evmChainPrivateKey.address.toString(); + await save(); + } + + @override + int calculateEstimatedFee(TransactionPriority priority, int? amount) { + try { + if (priority is EVMChainTransactionPriority) { + 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("${walletInfo.type.name.toUpperCase()} Node connection failed"); + } + + _client.setListeners(_evmChainPrivateKey.address, _onNewTransaction); + + _setTransactionUpdateTimer(); + + syncStatus = ConnectedSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(); + } + } + + @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(); + } + } + + @override + Future createTransaction(Object credentials) async { + final _credentials = credentials as EVMChainTransactionCredentials; + 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 amountToEVMChainMultiplier = pow(10, exponent); + + // so far this can not be made with Ethereum as Ethereum does not support multiple recipients + if (hasMultiDestination) { + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw EVMChainTransactionCreationException(transactionCurrency); + } + + final totalOriginalAmount = EVMChainFormatter.parseEVMChainAmountToDouble( + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0))); + totalAmount = BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EVMChainTransactionCreationException(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 = + EVMChainFormatter.parseEVMChainAmountToDouble(output.formattedCryptoAmount ?? 0); + totalAmount = output.sendAll + ? allAmount + : BigInt.from(totalOriginalAmount * amountToEVMChainMultiplier); + + if (_erc20Balance.balance < totalAmount) { + throw EVMChainTransactionCreationException(transactionCurrency); + } + } + + final pendingEVMChainTransaction = await _client.signTransaction( + privateKey: _evmChainPrivateKey, + 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 pendingEVMChainTransaction; + } + + Future _updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + final isProviderEnabled = await checkIfScanProviderIsEnabled(); + + if (!isProviderEnabled) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + transactionHistory.addMany(transactions); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (_) { + _isTransactionUpdating = false; + } + } + + @override + Future> fetchTransactions() async { + final address = _evmChainPrivateKey.address.hex; + final transactions = await _client.fetchTransactions(address); + + final List>> erc20TokensTransactions = []; + + for (var token in balance.keys) { + if (token is Erc20Token) { + erc20TokensTransactions.add(_client.fetchTransactions( + address, + contractAddress: token.contractAddress, + )); + } + } + + final tokensTransaction = await Future.wait(erc20TokensTransactions); + transactions.addAll(tokensTransaction.expand((element) => element)); + + final Map result = {}; + + for (var transactionModel in transactions) { + if (transactionModel.isError) { + continue; + } + + result[transactionModel.hash] = getTransactionInfo(transactionModel, address); + } + + 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(_evmChainPrivateKey.privateKey); + + Future makePath() async => pathForWallet(name: walletInfo.name, type: walletInfo.type); + + String toJSON() => json.encode({ + 'mnemonic': _mnemonic, + 'private_key': privateKey, + 'balance': balance[currency]!.toJSON(), + }); + + Future _updateBalance() async { + balance[currency] = await _fetchEVMChainBalance(); + + await _fetchErc20Balances(); + await save(); + } + + Future _fetchEVMChainBalance() async { + final balance = await _client.getBalance(_evmChainPrivateKey.address); + return EVMChainERC20Balance(balance.getInWei); + } + + Future _fetchErc20Balances() async { + for (var token in evmChainErc20TokensBox.values) { + try { + if (token.enabled) { + balance[token] = await _client.fetchERC20Balances( + _evmChainPrivateKey.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 hdPathEVMChain = "m/44'/60'/0'/0"; + const index = 0; + final addressAtIndex = root.derivePath("$hdPathEVMChain/$index"); + + return EthPrivateKey.fromHex(HEX.encode(addressAtIndex.privateKey as List)); + } + + Future? updateBalance() async => await _updateBalance(); + + List get erc20Currencies => evmChainErc20TokensBox.values.toList(); + + Future addErc20Token(Erc20Token token) async { + String? iconPath; + try { + iconPath = CryptoCurrency.all + .firstWhere((element) => element.title.toUpperCase() == token.symbol.toUpperCase()) + .iconPath; + } catch (_) {} + + final newToken = createNewErc20TokenObject(token, iconPath); + + await evmChainErc20TokensBox.put(newToken.contractAddress, newToken); + + if (newToken.enabled) { + balance[newToken] = await _client.fetchERC20Balances( + _evmChainPrivateKey.address, + newToken.contractAddress, + ); + } else { + balance.remove(newToken); + } + } + + 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(); + } + + @override + Future renameWalletFiles(String newWalletName) async { + final transactionHistoryFileNameForWallet = getTransactionHistoryFileName(); + + 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/$transactionHistoryFileNameForWallet'); + + // 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/$transactionHistoryFileNameForWallet'); + } + + // Delete old name's dir and files + await Directory(currentDirPath).delete(recursive: true); + } + + void _setTransactionUpdateTimer() { + if (_transactionsUpdateTimer?.isActive ?? false) { + _transactionsUpdateTimer!.cancel(); + } + + _transactionsUpdateTimer = Timer.periodic(const Duration(seconds: 10), (_) { + _updateTransactions(); + _updateBalance(); + }); + } + + /// Scan Providers: + /// + /// EtherScan for Ethereum. + /// + /// PolygonScan for Polygon. + void updateScanProviderUsageState(bool isEnabled) { + if (isEnabled) { + _updateTransactions(); + _setTransactionUpdateTimer(); + } else { + _transactionsUpdateTimer?.cancel(); + } + } + + @override + String signMessage(String message, {String? address}) => + bytesToHex(_evmChainPrivateKey.signPersonalMessageToUint8List(ascii.encode(message))); + + Web3Client? getWeb3Client() => _client.getWeb3Client(); +} diff --git a/cw_ethereum/lib/ethereum_wallet_addresses.dart b/cw_evm/lib/evm_chain_wallet_addresses.dart similarity index 63% rename from cw_ethereum/lib/ethereum_wallet_addresses.dart rename to cw_evm/lib/evm_chain_wallet_addresses.dart index 4a3492e6f..d5d39f21d 100644 --- a/cw_ethereum/lib/ethereum_wallet_addresses.dart +++ b/cw_evm/lib/evm_chain_wallet_addresses.dart @@ -1,13 +1,15 @@ +import 'dart:developer'; + import 'package:cw_core/wallet_addresses.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:mobx/mobx.dart'; -part 'ethereum_wallet_addresses.g.dart'; +part 'evm_chain_wallet_addresses.g.dart'; -class EthereumWalletAddresses = EthereumWalletAddressesBase with _$EthereumWalletAddresses; +class EVMChainWalletAddresses = EVMChainWalletAddressesBase with _$EVMChainWalletAddresses; -abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { - EthereumWalletAddressesBase(WalletInfo walletInfo) +abstract class EVMChainWalletAddressesBase extends WalletAddresses with Store { + EVMChainWalletAddressesBase(WalletInfo walletInfo) : address = '', super(walletInfo); @@ -27,7 +29,7 @@ abstract class EthereumWalletAddressesBase extends WalletAddresses with Store { addressesMap[address] = ''; await saveAddressesInBox(); } catch (e) { - print(e.toString()); + log(e.toString()); } } } diff --git a/cw_evm/lib/evm_chain_wallet_creation_credentials.dart b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart new file mode 100644 index 000000000..7c3271daf --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet_creation_credentials.dart @@ -0,0 +1,29 @@ +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; + +class EVMChainNewWalletCredentials extends WalletCredentials { + EVMChainNewWalletCredentials({required String name, WalletInfo? walletInfo}) + : super(name: name, walletInfo: walletInfo); +} + +class EVMChainRestoreWalletFromSeedCredentials extends WalletCredentials { + EVMChainRestoreWalletFromSeedCredentials({ + required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo, + }) : super(name: name, password: password, walletInfo: walletInfo); + + final String mnemonic; +} + +class EVMChainRestoreWalletFromPrivateKey extends WalletCredentials { + EVMChainRestoreWalletFromPrivateKey({ + 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_evm/lib/evm_chain_wallet_service.dart b/cw_evm/lib/evm_chain_wallet_service.dart new file mode 100644 index 000000000..988a38684 --- /dev/null +++ b/cw_evm/lib/evm_chain_wallet_service.dart @@ -0,0 +1,50 @@ +import 'dart:io'; + +import 'package:collection/collection.dart'; +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_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:hive/hive.dart'; + +abstract class EVMChainWalletService extends WalletService< + EVMChainNewWalletCredentials, + EVMChainRestoreWalletFromSeedCredentials, + EVMChainRestoreWalletFromPrivateKey> { + EVMChainWalletService(this.walletInfoSource); + + final Box walletInfoSource; + + @override + WalletType getType(); + + @override + Future create(EVMChainNewWalletCredentials credentials); + + @override + Future openWallet(String name, String password); + + @override + Future rename(String currentName, String password, String newName); + + @override + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials); + + @override + Future restoreFromSeed(EVMChainRestoreWalletFromSeedCredentials credentials); + + @override + Future isWalletExit(String name) async => + File(await pathForWallet(name: name, type: getType())).existsSync(); + + @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); + } +} diff --git a/cw_ethereum/lib/erc20_balance.dart b/cw_evm/lib/evm_erc20_balance.dart similarity index 76% rename from cw_ethereum/lib/erc20_balance.dart rename to cw_evm/lib/evm_erc20_balance.dart index 7d11f8e45..1727d7962 100644 --- a/cw_ethereum/lib/erc20_balance.dart +++ b/cw_evm/lib/evm_erc20_balance.dart @@ -3,10 +3,9 @@ import 'dart:math'; import 'package:cw_core/balance.dart'; -class ERC20Balance extends Balance { - ERC20Balance(this.balance, {this.exponent = 18}) - : super(balance.toInt(), - balance.toInt()); +class EVMChainERC20Balance extends Balance { + EVMChainERC20Balance(this.balance, {this.exponent = 18}) + : super(balance.toInt(), balance.toInt()); final BigInt balance; final int exponent; @@ -28,7 +27,7 @@ class ERC20Balance extends Balance { 'exponent': exponent, }); - static ERC20Balance? fromJSON(String? jsonSource) { + static EVMChainERC20Balance? fromJSON(String? jsonSource) { if (jsonSource == null) { return null; } @@ -36,12 +35,12 @@ class ERC20Balance extends Balance { final decoded = json.decode(jsonSource) as Map; try { - return ERC20Balance( + return EVMChainERC20Balance( BigInt.parse(decoded['balanceInWei']), exponent: decoded['exponent'], ); } catch (e) { - return ERC20Balance(BigInt.zero); + return EVMChainERC20Balance(BigInt.zero); } } } diff --git a/cw_ethereum/lib/file.dart b/cw_evm/lib/file.dart similarity index 100% rename from cw_ethereum/lib/file.dart rename to cw_evm/lib/file.dart diff --git a/cw_ethereum/lib/pending_ethereum_transaction.dart b/cw_evm/lib/pending_evm_chain_transaction.dart similarity index 91% rename from cw_ethereum/lib/pending_ethereum_transaction.dart rename to cw_evm/lib/pending_evm_chain_transaction.dart index d47630fd6..8129de728 100644 --- a/cw_ethereum/lib/pending_ethereum_transaction.dart +++ b/cw_evm/lib/pending_evm_chain_transaction.dart @@ -4,14 +4,14 @@ import 'dart:typed_data'; import 'package:cw_core/pending_transaction.dart'; import 'package:web3dart/crypto.dart'; -class PendingEthereumTransaction with PendingTransaction { +class PendingEVMChainTransaction with PendingTransaction { final Function sendTransaction; final Uint8List signedTransaction; final BigInt fee; final String amount; final int exponent; - PendingEthereumTransaction({ + PendingEVMChainTransaction({ required this.sendTransaction, required this.signedTransaction, required this.fee, diff --git a/cw_evm/pubspec.yaml b/cw_evm/pubspec.yaml new file mode 100644 index 000000000..c202cc72a --- /dev/null +++ b/cw_evm/pubspec.yaml @@ -0,0 +1,45 @@ +name: cw_evm +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 + web3dart: ^2.7.1 + erc20: ^1.0.1 + bip39: ^1.0.6 + bip32: ^2.0.0 + hex: ^0.2.0 + http: ^1.1.0 + hive: ^2.2.3 + collection: ^1.17.1 + shared_preferences: ^2.0.15 + cw_core: + path: ../cw_core + +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.1.11 + mobx_codegen: ^2.0.7 + hive_generator: ^1.1.3 + flutter_lints: ^2.0.0 + +flutter: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic diff --git a/cw_evm/test/cw_evm_test.dart b/cw_evm/test/cw_evm_test.dart new file mode 100644 index 000000000..6a4dea276 --- /dev/null +++ b/cw_evm/test/cw_evm_test.dart @@ -0,0 +1,12 @@ +import 'package:flutter_test/flutter_test.dart'; + +import 'package:cw_evm/cw_evm.dart'; + +void main() { + test('adds one to input values', () { + final calculator = Calculator(); + expect(calculator.addOne(2), 3); + expect(calculator.addOne(-7), -6); + expect(calculator.addOne(0), 1); + }); +} diff --git a/cw_nano/lib/banano_balance.dart b/cw_nano/lib/banano_balance.dart index b85609b60..b904a35cb 100644 --- a/cw_nano/lib/banano_balance.dart +++ b/cw_nano/lib/banano_balance.dart @@ -1,20 +1,19 @@ import 'package:cw_core/balance.dart'; -import 'package:cw_nano/nano_util.dart'; +import 'package:nanoutil/nanoutil.dart'; class BananoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; - BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) { - } + BananoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); @override String get formattedAvailableBalance { - return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerBanano); + return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerBanano); } @override String get formattedAdditionalBalance { - return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerBanano); + return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerBanano); } } diff --git a/cw_nano/lib/nano_balance.dart b/cw_nano/lib/nano_balance.dart index dbb39d2a3..8b8c93b33 100644 --- a/cw_nano/lib/nano_balance.dart +++ b/cw_nano/lib/nano_balance.dart @@ -1,34 +1,35 @@ import 'package:cw_core/balance.dart'; -import 'package:cw_nano/nano_util.dart'; +import 'package:nanoutil/nanoutil.dart'; BigInt stringAmountToBigInt(String amount) { - return BigInt.parse(NanoUtil.getAmountAsRaw(amount, NanoUtil.rawPerNano)); + return BigInt.parse(NanoAmounts.getAmountAsRaw(amount, NanoAmounts.rawPerNano)); } class NanoBalance extends Balance { final BigInt currentBalance; final BigInt receivableBalance; - late String formattedCurrentBalance; - late String formattedReceivableBalance; - NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0) { - this.formattedCurrentBalance = ""; - this.formattedReceivableBalance = ""; - } + NanoBalance({required this.currentBalance, required this.receivableBalance}) : super(0, 0); - NanoBalance.fromString( - {required this.formattedCurrentBalance, required this.formattedReceivableBalance}) + NanoBalance.fromFormattedString( + {required String formattedCurrentBalance, required String formattedReceivableBalance}) : currentBalance = stringAmountToBigInt(formattedCurrentBalance), receivableBalance = stringAmountToBigInt(formattedReceivableBalance), super(0, 0); + NanoBalance.fromRawString( + {required String currentBalance, required String receivableBalance}) + : currentBalance = BigInt.parse(currentBalance), + receivableBalance = BigInt.parse(receivableBalance), + super(0, 0); + @override String get formattedAvailableBalance { - return NanoUtil.getRawAsUsableString(currentBalance.toString(), NanoUtil.rawPerNano); + return NanoAmounts.getRawAsUsableString(currentBalance.toString(), NanoAmounts.rawPerNano); } @override String get formattedAdditionalBalance { - return NanoUtil.getRawAsUsableString(receivableBalance.toString(), NanoUtil.rawPerNano); + return NanoAmounts.getRawAsUsableString(receivableBalance.toString(), NanoAmounts.rawPerNano); } } diff --git a/cw_nano/lib/nano_client.dart b/cw_nano/lib/nano_client.dart index f1d08204a..661fbcab8 100644 --- a/cw_nano/lib/nano_client.dart +++ b/cw_nano/lib/nano_client.dart @@ -4,10 +4,10 @@ import 'dart:convert'; import 'package:cw_core/nano_account_info_response.dart'; import 'package:cw_nano/nano_balance.dart'; import 'package:cw_nano/nano_transaction_model.dart'; -import 'package:cw_nano/nano_util.dart'; import 'package:http/http.dart' as http; import 'package:nanodart/nanodart.dart'; import 'package:cw_core/node.dart'; +import 'package:nanoutil/nanoutil.dart'; import 'package:shared_preferences/shared_preferences.dart'; class NanoClient { @@ -61,6 +61,13 @@ class NanoClient { ), ); final data = await jsonDecode(response.body); + if (response.statusCode != 200 || + data["error"] != null || + data["balance"] == null || + data["receivable"] == null) { + throw Exception( + "Error while trying to get balance! ${data["error"] != null ? data["error"] : ""}"); + } final String currentBalance = data["balance"] as String; final String receivableBalance = data["receivable"] as String; final BigInt cur = BigInt.parse(currentBalance); @@ -203,7 +210,7 @@ class NanoClient { String? previousHash, }) async { // our address: - final String publicAddress = NanoUtil.privateKeyToAddress(privateKey); + final String publicAddress = NanoDerivations.privateKeyToAddress(privateKey); // first get the current account balance: if (balanceAfterTx == null) { diff --git a/cw_nano/lib/nano_transaction_info.dart b/cw_nano/lib/nano_transaction_info.dart index 8958086dd..9195a06ef 100644 --- a/cw_nano/lib/nano_transaction_info.dart +++ b/cw_nano/lib/nano_transaction_info.dart @@ -1,7 +1,7 @@ import 'package:cw_core/format_amount.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; -import 'package:cw_nano/nano_util.dart'; +import 'package:nanoutil/nanoutil.dart'; class NanoTransactionInfo extends TransactionInfo { NanoTransactionInfo({ @@ -13,6 +13,8 @@ class NanoTransactionInfo extends TransactionInfo { required this.confirmed, required this.date, required this.confirmations, + required this.to, + required this.from, }) : this.amount = amountRaw.toInt(); final String id; @@ -24,14 +26,17 @@ class NanoTransactionInfo extends TransactionInfo { final bool confirmed; final int confirmations; final String tokenSymbol; + final String? to; + final String? from; String? _fiatAmount; bool get isPending => !this.confirmed; @override String amountFormatted() { - final String amt = NanoUtil.getRawAsUsableString(amountRaw.toString(), NanoUtil.rawPerNano); - final String acc = NanoUtil.getRawAccuracy(amountRaw.toString(), NanoUtil.rawPerNano); + final String amt = + NanoAmounts.getRawAsUsableString(amountRaw.toString(), NanoAmounts.rawPerNano); + final String acc = NanoAmounts.getRawAccuracy(amountRaw.toString(), NanoAmounts.rawPerNano); return "$acc$amt $tokenSymbol"; } @@ -54,6 +59,8 @@ class NanoTransactionInfo extends TransactionInfo { confirmed: data['confirmed'] as bool, confirmations: data['confirmations'] as int, tokenSymbol: data['tokenSymbol'] as String, + to: data['to'] as String, + from: data['from'] as String, ); } @@ -66,5 +73,7 @@ class NanoTransactionInfo extends TransactionInfo { 'confirmed': confirmed, 'confirmations': confirmations, 'tokenSymbol': tokenSymbol, + 'to': to, + 'from': from, }; } diff --git a/cw_nano/lib/nano_util.dart b/cw_nano/lib/nano_util.dart deleted file mode 100644 index 13d6f5649..000000000 --- a/cw_nano/lib/nano_util.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'dart:convert'; -import 'dart:typed_data'; - -import 'package:convert/convert.dart'; -import "package:ed25519_hd_key/ed25519_hd_key.dart"; -import 'package:libcrypto/libcrypto.dart'; -import 'package:nanodart/nanodart.dart'; -import 'package:decimal/decimal.dart'; - -class NanoUtil { - // standard: - static String seedToPrivate(String seed, int index) { - return NanoKeys.seedToPrivate(seed, index); - } - - static String seedToAddress(String seed, int index) { - return NanoAccounts.createAccount( - NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); - } - - static String seedToMnemonic(String seed) { - return NanoMnemomics.seedToMnemonic(seed).join(" "); - } - - static Future mnemonicToSeed(String mnemonic) async { - return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); - } - - static String privateKeyToPublic(String privateKey) { - // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); - return NanoKeys.createPublicKey(privateKey); - } - - static String addressToPublicKey(String publicAddress) { - return NanoAccounts.extractPublicKey(publicAddress); - } - - // universal: - static String privateKeyToAddress(String privateKey) { - return NanoAccounts.createAccount(NanoAccountType.NANO, privateKeyToPublic(privateKey)); - } - - static String publicKeyToAddress(String publicKey) { - return NanoAccounts.createAccount(NanoAccountType.NANO, publicKey); - } - - // standard + hd: - static bool isValidSeed(String seed) { - // Ensure seed is 64 or 128 characters long - if (seed == null || (seed.length != 64 && seed.length != 128)) { - return false; - } - // Ensure seed only contains hex characters, 0-9;A-F - return NanoHelpers.isHexString(seed); - } - - // // hd: - static Future hdMnemonicListToSeed(List words) async { - // if (words.length != 24) { - // throw Exception('Expected a 24-word list, got a ${words.length} list'); - // } - final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic')); - final Pbkdf2 hasher = Pbkdf2(iterations: 2048); - final String seed = await hasher.sha512(words.join(' '), salt); - return seed; - } - - static Future hdSeedToPrivate(String seed, int index) async { - List seedBytes = hex.decode(seed); - KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes); - return hex.encode(data.key); - } - - static Future hdSeedToAddress(String seed, int index) async { - return NanoAccounts.createAccount( - NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index))); - } - - static Future uniSeedToAddress(String seed, int index, String type) { - if (type == "standard") { - return Future.value(seedToAddress(seed, index)); - } else if (type == "hd") { - return hdSeedToAddress(seed, index); - } else { - throw Exception('Unknown seed type'); - } - } - - static Future uniSeedToPrivate(String seed, int index, String type) { - if (type == "standard") { - return Future.value(seedToPrivate(seed, index)); - } else if (type == "hd") { - return hdSeedToPrivate(seed, index); - } else { - throw Exception('Unknown seed type'); - } - } - - static bool isValidBip39Seed(String seed) { - // Ensure seed is 128 characters long - if (seed.length != 128) { - return false; - } - // Ensure seed only contains hex characters, 0-9;A-F - return NanoHelpers.isHexString(seed); - } - - // number util: - - static const int maxDecimalDigits = 6; // Max digits after decimal - static BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); - static BigInt rawPerNyano = BigInt.parse("1000000000000000000000000"); - static BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); - static BigInt rawPerXMR = BigInt.parse("1000000000000"); - static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); - // static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000"); - - /// Convert raw to ban and return as BigDecimal - /// - /// @param raw 100000000000000000000000000000 - /// @return Decimal value 1.000000000000000000000000000000 - /// - static Decimal getRawAsDecimal(String? raw, BigInt? rawPerCur) { - rawPerCur ??= rawPerNano; - final Decimal amount = Decimal.parse(raw.toString()); - final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal(); - return result; - } - - static String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) { - Decimal bigger = input.shift(digits); - bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05 - bigger = bigger.shift(-digits); - return bigger.toString(); - } - - /// Return raw as a NANO amount. - /// - /// @param raw 100000000000000000000000000000 - /// @returns 1 - /// - static String getRawAsUsableString(String? raw, BigInt rawPerCur) { - final String res = - truncateDecimal(getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9); - - if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") { - return "0"; - } - - if (!res.contains(".")) { - return res; - } - - final String numAmount = res.split(".")[0]; - String decAmount = res.split(".")[1]; - - // truncate: - if (decAmount.length > maxDecimalDigits) { - decAmount = decAmount.substring(0, maxDecimalDigits); - // remove trailing zeros: - decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => ''); - if (decAmount.isEmpty) { - return numAmount; - } - } - - return "$numAmount.$decAmount"; - } - - static String getRawAccuracy(String? raw, BigInt rawPerCur) { - final String rawString = getRawAsUsableString(raw, rawPerCur); - final String rawDecimalString = getRawAsDecimal(raw, rawPerCur).toString(); - - if (raw == null || raw.isEmpty || raw == "0") { - return ""; - } - - if (rawString != rawDecimalString) { - return "~"; - } - return ""; - } - - /// Return readable string amount as raw string - /// @param amount 1.01 - /// @returns 101000000000000000000000000000 - /// - static String getAmountAsRaw(String amount, BigInt rawPerCur) { - final Decimal asDecimal = Decimal.parse(amount); - final Decimal rawDecimal = Decimal.parse(rawPerCur.toString()); - return (asDecimal * rawDecimal).toString(); - } -} diff --git a/cw_nano/lib/nano_wallet.dart b/cw_nano/lib/nano_wallet.dart index b0d98efec..1f6ec36ae 100644 --- a/cw_nano/lib/nano_wallet.dart +++ b/cw_nano/lib/nano_wallet.dart @@ -18,7 +18,6 @@ import 'package:cw_nano/nano_client.dart'; import 'package:cw_nano/nano_transaction_credentials.dart'; import 'package:cw_nano/nano_transaction_history.dart'; import 'package:cw_nano/nano_transaction_info.dart'; -import 'package:cw_nano/nano_util.dart'; import 'package:cw_nano/nano_wallet_keys.dart'; import 'package:cw_nano/pending_nano_transaction.dart'; import 'package:mobx/mobx.dart'; @@ -27,6 +26,7 @@ import 'package:cw_nano/nano_wallet_addresses.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:nanodart/nanodart.dart'; import 'package:bip39/bip39.dart' as bip39; +import 'package:nanoutil/nanoutil.dart'; part 'nano_wallet.g.dart'; @@ -83,6 +83,8 @@ abstract class NanoWalletBase @observable late ObservableMap balance; + static const int POLL_INTERVAL_SECONDS = 10; + // initialize the different forms of private / public key we'll need: Future init() async { if (_derivationType == DerivationType.unknown) { @@ -100,11 +102,21 @@ abstract class NanoWalletBase if (_derivationType == DerivationType.nano) { _hexSeed = bip39.mnemonicToEntropy(_mnemonic).toUpperCase(); } else { - _hexSeed = await NanoUtil.hdMnemonicListToSeed(_mnemonic.split(' ')); + _hexSeed = await NanoDerivations.hdMnemonicListToSeed(_mnemonic.split(' ')); } } - _privateKey = await NanoUtil.uniSeedToPrivate(_hexSeed!, 0, type); - _publicAddress = await NanoUtil.uniSeedToAddress(_hexSeed!, 0, type); + NanoDerivationType derivationType = + type == "standard" ? NanoDerivationType.STANDARD : NanoDerivationType.HD; + _privateKey = await NanoDerivations.universalSeedToPrivate( + _hexSeed!, + index: 0, + type: derivationType, + ); + _publicAddress = await NanoDerivations.universalSeedToAddress( + _hexSeed!, + index: 0, + type: derivationType, + ); this.walletInfo.address = _publicAddress!; await walletAddresses.init(); @@ -125,6 +137,7 @@ abstract class NanoWalletBase @override void close() { _client.stop(); + _receiveTimer?.cancel(); } @action @@ -139,6 +152,7 @@ abstract class NanoWalletBase try { await _updateBalance(); + await updateTransactions(); await _updateRep(); await _receiveAll(); } catch (e) { @@ -173,8 +187,8 @@ abstract class NanoWalletBase if (txOut.sendAll) { amt = balance[currency]?.currentBalance ?? BigInt.zero; } else { - amt = BigInt.tryParse(NanoUtil.getAmountAsRaw( - txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoUtil.rawPerNano)) ?? + amt = BigInt.tryParse(NanoAmounts.getAmountAsRaw( + txOut.cryptoAmount?.replaceAll(',', '.') ?? "0", NanoAmounts.rawPerNano)) ?? BigInt.zero; } @@ -186,9 +200,7 @@ abstract class NanoWalletBase final block = await _client.constructSendBlock( amountRaw: amt.toString(), - destinationAddress: txOut.isParsedAddress - ? txOut.extractedAddress! - : txOut.address, + destinationAddress: txOut.isParsedAddress ? txOut.extractedAddress! : txOut.address, privateKey: _privateKey!, balanceAfterTx: runningBalance, previousHash: previousHash, @@ -236,10 +248,10 @@ abstract class NanoWalletBase } } - Future updateTransactions() async { + Future updateTransactions() async { try { if (_isTransactionUpdating) { - return; + return false; } _isTransactionUpdating = true; @@ -247,8 +259,10 @@ abstract class NanoWalletBase transactionHistory.addMany(transactions); await transactionHistory.save(); _isTransactionUpdating = false; + return true; } catch (_) { _isTransactionUpdating = false; + return false; } } @@ -261,16 +275,17 @@ abstract class NanoWalletBase final Map result = {}; for (var transactionModel in transactions) { + final bool isSend = transactionModel.type == "send"; result[transactionModel.hash] = NanoTransactionInfo( id: transactionModel.hash, amountRaw: transactionModel.amount, height: transactionModel.height, - direction: transactionModel.type == "send" - ? TransactionDirection.outgoing - : TransactionDirection.incoming, + direction: isSend ? TransactionDirection.outgoing : TransactionDirection.incoming, confirmed: transactionModel.confirmed, date: transactionModel.date ?? DateTime.now(), confirmations: transactionModel.confirmed ? 1 : 0, + to: isSend ? transactionModel.account : address, + from: isSend ? address : transactionModel.account, ); } @@ -312,11 +327,10 @@ abstract class NanoWalletBase Future startSync() async { try { syncStatus = AttemptingSyncStatus(); - await _updateBalance(); - await updateTransactions(); + // setup a timer to receive transactions periodically: _receiveTimer?.cancel(); - _receiveTimer = Timer.periodic(const Duration(seconds: 15), (timer) async { + _receiveTimer = Timer.periodic(const Duration(seconds: POLL_INTERVAL_SECONDS), (timer) async { // get our balance: await _updateBalance(); // if we have anything to receive, process it: @@ -325,6 +339,14 @@ abstract class NanoWalletBase } }); + // also run once, immediately: + await _updateBalance(); + bool updateSuccess = await updateTransactions(); + if (!updateSuccess) { + syncStatus = FailedSyncStatus(); + return; + } + syncStatus = SyncedSyncStatus(); } catch (e) { print(e); @@ -353,9 +375,11 @@ abstract class NanoWalletBase final data = json.decode(jsonSource) as Map; final mnemonic = data['mnemonic'] as String; - final balance = NanoBalance.fromString( - formattedCurrentBalance: data['currentBalance'] as String? ?? "0", - formattedReceivableBalance: data['receivableBalance'] as String? ?? "0"); + + final balance = NanoBalance.fromRawString( + currentBalance: data['currentBalance'] as String? ?? "0", + receivableBalance: data['receivableBalance'] as String? ?? "0", + ); DerivationType derivationType = DerivationType.nano; if (data['derivationType'] == "DerivationType.bip39") { @@ -374,12 +398,26 @@ abstract class NanoWalletBase } Future _updateBalance() async { + var oldBalance = balance[currency]; try { balance[currency] = await _client.getBalance(_publicAddress!); } catch (e) { print("Failed to get balance $e"); + // if we don't have a balance, we should at least create one, since it's a late binding + // otherwise, it's better to just leave it as whatever it was before: + if (balance[currency] == null) { + balance[currency] = + NanoBalance(currentBalance: BigInt.zero, receivableBalance: BigInt.zero); + } + } + // don't save unnecessarily: + // trying to save too frequently can cause problems with the file system + // since nano is updated frequently this can be a problem, so we only save if there is a change: + if (oldBalance == null || + balance[currency]!.currentBalance != oldBalance.currentBalance || + balance[currency]!.receivableBalance != oldBalance.receivableBalance) { + await save(); } - await save(); } Future _updateRep() async { @@ -394,11 +432,19 @@ abstract class NanoWalletBase } Future regenerateAddress() async { - final String type = (_derivationType == DerivationType.nano) ? "standard" : "hd"; - _privateKey = - await NanoUtil.uniSeedToPrivate(_hexSeed!, this.walletAddresses.account!.id, type); - _publicAddress = - await NanoUtil.uniSeedToAddress(_hexSeed!, this.walletAddresses.account!.id, type); + final NanoDerivationType type = (_derivationType == DerivationType.nano) + ? NanoDerivationType.STANDARD + : NanoDerivationType.HD; + _privateKey = await NanoDerivations.universalSeedToPrivate( + _hexSeed!, + index: this.walletAddresses.account!.id, + type: type, + ); + _publicAddress = await NanoDerivations.universalSeedToAddress( + _hexSeed!, + index: this.walletAddresses.account!.id, + type: type, + ); this.walletInfo.address = _publicAddress!; this.walletAddresses.address = _publicAddress!; diff --git a/cw_nano/lib/nano_wallet_service.dart b/cw_nano/lib/nano_wallet_service.dart index 2f183d1cc..7a66f8c9f 100644 --- a/cw_nano/lib/nano_wallet_service.dart +++ b/cw_nano/lib/nano_wallet_service.dart @@ -6,12 +6,12 @@ import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_nano/nano_mnemonic.dart' as nm; -import 'package:cw_nano/nano_util.dart'; import 'package:cw_nano/nano_wallet.dart'; import 'package:cw_nano/nano_wallet_creation_credentials.dart'; import 'package:hive/hive.dart'; import 'package:bip39/bip39.dart' as bip39; import 'package:nanodart/nanodart.dart'; +import 'package:nanoutil/nanoutil.dart'; class NanoWalletService extends WalletService { @@ -30,7 +30,7 @@ class NanoWalletService extends WalletService 137; @override - Future> fetchTransactions(String address, + Future> fetchTransactions(String address, {String? contractAddress}) async { try { final response = await httpClient.get(Uri.https("api.polygonscan.com", "/api", { @@ -43,7 +43,9 @@ class PolygonClient extends EthereumClient { if (response.statusCode >= 200 && response.statusCode < 300 && jsonResponse['status'] != 0) { return (jsonResponse['result'] as List) - .map((e) => PolygonTransactionModel.fromJson(e as Map)) + .map( + (e) => EVMChainTransactionModel.fromJson(e as Map, 'MATIC'), + ) .toList(); } diff --git a/cw_polygon/lib/polygon_exceptions.dart b/cw_polygon/lib/polygon_exceptions.dart deleted file mode 100644 index 2d08106b6..000000000 --- a/cw_polygon/lib/polygon_exceptions.dart +++ /dev/null @@ -1,6 +0,0 @@ -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 deleted file mode 100644 index f016db7ab..000000000 --- a/cw_polygon/lib/polygon_formatter.dart +++ /dev/null @@ -1,25 +0,0 @@ -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_transaction_credentials.dart b/cw_polygon/lib/polygon_transaction_credentials.dart deleted file mode 100644 index 6611e15da..000000000 --- a/cw_polygon/lib/polygon_transaction_credentials.dart +++ /dev/null @@ -1,18 +0,0 @@ -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 index a06b8be4a..8674882cd 100644 --- a/cw_polygon/lib/polygon_transaction_history.dart +++ b/cw_polygon/lib/polygon_transaction_history.dart @@ -1,77 +1,19 @@ -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_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.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(); +class PolygonTransactionHistory extends EVMChainTransactionHistory { + PolygonTransactionHistory({ + required super.walletInfo, + required super.password, + }); @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); - } - } + String getTransactionHistoryFileName() => 'polygon_transactions.json'; @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; + EVMChainTransactionInfo getTransactionInfo(Map val) => + PolygonTransactionInfo.fromJson(val); } diff --git a/cw_polygon/lib/polygon_transaction_info.dart b/cw_polygon/lib/polygon_transaction_info.dart index f1976a601..1fbe1c5d4 100644 --- a/cw_polygon/lib/polygon_transaction_info.dart +++ b/cw_polygon/lib/polygon_transaction_info.dart @@ -1,32 +1,21 @@ import 'package:cw_core/transaction_direction.dart'; -import 'package:cw_ethereum/ethereum_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; -class PolygonTransactionInfo extends EthereumTransactionInfo { +class PolygonTransactionInfo extends EVMChainTransactionInfo { 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, - ); + required super.id, + required super.height, + required super.ethAmount, + required super.ethFee, + required super.tokenSymbol, + required super.direction, + required super.isPending, + required super.date, + required super.confirmations, + required super.to, + required super.from, + super.exponent, + }); factory PolygonTransactionInfo.fromJson(Map data) { return PolygonTransactionInfo( @@ -41,9 +30,10 @@ class PolygonTransactionInfo extends EthereumTransactionInfo { confirmations: data['confirmations'] as int, tokenSymbol: data['tokenSymbol'] as String, to: data['to'], + from: data['from'], ); } @override - String feeFormatted() => '${(ethFee / BigInt.from(10).pow(18)).toString()} MATIC'; + String get feeCurrency => 'MATIC'; } diff --git a/cw_polygon/lib/polygon_transaction_model.dart b/cw_polygon/lib/polygon_transaction_model.dart deleted file mode 100644 index 704d674e5..000000000 --- a/cw_polygon/lib/polygon_transaction_model.dart +++ /dev/null @@ -1,49 +0,0 @@ -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 deleted file mode 100644 index dba1dab55..000000000 --- a/cw_polygon/lib/polygon_transaction_priority.dart +++ /dev/null @@ -1,51 +0,0 @@ -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 index 5749a95ac..60c7ad2ff 100644 --- a/cw_polygon/lib/polygon_wallet.dart +++ b/cw_polygon/lib/polygon_wallet.dart @@ -1,362 +1,109 @@ -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/file.dart'; import 'package:cw_core/erc20_token.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_evm/evm_chain_transaction_history.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_model.dart'; +import 'package:cw_evm/evm_chain_wallet.dart'; +import 'package:cw_evm/evm_erc20_balance.dart'; +import 'package:cw_evm/file.dart'; import 'package:cw_polygon/default_polygon_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; +import 'package:cw_polygon/polygon_client.dart'; +import 'package:cw_polygon/polygon_transaction_history.dart'; -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 = const 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; - - late final PolygonClient _client; - - EthPrivateKey get polygonPrivateKey => _polygonPrivateKey; - - int? _gasPrice; - int? _estimatedGas; - bool _isTransactionUpdating; - - // TODO: remove after integrating our own node and having eth_newPendingTransactionFilter - Timer? _transactionsUpdateTimer; +class PolygonWallet extends EVMChainWallet { + PolygonWallet({ + required super.walletInfo, + required super.password, + super.mnemonic, + super.initialBalance, + super.privateKey, + required super.client, + }) : super(nativeCurrency: CryptoCurrency.maticpoly); @override - WalletAddresses walletAddresses; - - @override - @observable - SyncStatus syncStatus; - - @override - @observable - late ObservableMap balance; - - final Completer _sharedPrefs = Completer(); - - Future init() async { - polygonErc20TokensBox = await CakeHive.openBox( - "${walletInfo.name.replaceAll(" ", "_")}_${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 credentials0 = credentials as PolygonTransactionCredentials; - final outputs = credentials0.outputs; - final hasMultiDestination = outputs.length > 1; - - final CryptoCurrency transactionCurrency = - balance.keys.firstWhere((element) => element.title == credentials0.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); - } + Future initErc20TokensBox() async { + final boxName = "${walletInfo.name.replaceAll(" ", "_")}_ ${Erc20Token.polygonBoxName}"; + if (await CakeHive.boxExists(boxName)) { + evmChainErc20TokensBox = await CakeHive.openBox(boxName); } 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(credentials0.priority!, null)); - } - final totalOriginalAmount = - EthereumFormatter.parseEthereumAmountToDouble(output.formattedCryptoAmount ?? 0); - totalAmount = - output.sendAll ? allAmount : BigInt.from(totalOriginalAmount * amountToPolygonMultiplier); - - if (erc20Balance.balance < totalAmount) { - throw PolygonTransactionCreationException(transactionCurrency); - } + evmChainErc20TokensBox = await CakeHive.openBox(boxName.replaceAll(" ", "")); } + } - final pendingPolygonTransaction = await _client.signTransaction( - privateKey: _polygonPrivateKey, - toAddress: credentials0.outputs.first.isParsedAddress - ? credentials0.outputs.first.extractedAddress! - : credentials0.outputs.first.address, - amount: totalAmount.toString(), - gas: _estimatedGas!, - priority: credentials0.priority!, - currency: transactionCurrency, - exponent: exponent, - contractAddress: - transactionCurrency is Erc20Token ? transactionCurrency.contractAddress : null, + @override + void addInitialTokens() { + final initialErc20Tokens = DefaultPolygonErc20Tokens().initialPolygonErc20Tokens; + + for (var token in initialErc20Tokens) { + evmChainErc20TokensBox.put(token.contractAddress, token); + } + } + + @override + Future checkIfScanProviderIsEnabled() async { + bool isPolygonScanEnabled = (await sharedPrefs.future).getBool("use_polygonscan") ?? true; + return isPolygonScanEnabled; + } + + @override + String getTransactionHistoryFileName() => 'polygon_transactions.json'; + + @override + Erc20Token createNewErc20TokenObject(Erc20Token token, String? iconPath) { + return Erc20Token( + name: token.name, + symbol: token.symbol, + contractAddress: token.contractAddress, + decimal: token.decimal, + enabled: token.enabled, + tag: token.tag ?? "MATIC", + iconPath: iconPath, ); - - 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, - ), - ); - } - } - - 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; + EVMChainTransactionInfo getTransactionInfo( + EVMChainTransactionModel transactionModel, String address) { + final model = 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, + from: transactionModel.from, + ); + return model; } @override - Object get keys => throw UnimplementedError("keys"); - - @override - Future rescan({required int height}) { - throw UnimplementedError("rescan"); + EVMChainTransactionHistory setUpTransactionHistory(WalletInfo walletInfo, String password) { + return PolygonTransactionHistory(walletInfo: walletInfo, password: password); } - @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 { + 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); + final balance = EVMChainERC20Balance.fromJSON(data['balance'] as String) ?? + EVMChainERC20Balance(BigInt.zero); return PolygonWallet( walletInfo: walletInfo, @@ -364,158 +111,7 @@ abstract class PolygonWalletBase mnemonic: mnemonic, privateKey: privateKey, initialBalance: balance, + client: PolygonClient(), ); } - - 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)); - } - - @override - 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 token0 = Erc20Token( - name: token.name, - symbol: token.symbol, - contractAddress: token.contractAddress, - decimal: token.decimal, - enabled: token.enabled, - tag: token.tag ?? "POLY", - iconPath: iconPath, - ); - - await polygonErc20TokensBox.put(token0.contractAddress, token0); - - if (token0.enabled) { - balance[token0] = await _client.fetchERC20Balances( - _polygonPrivateKey.address, - token0.contractAddress, - ); - } else { - balance.remove(token0); - } - } - - 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(const Duration(seconds: 10), (_) { - _updateTransactions(); - _updateBalance(); - }); - } - - void updatePolygonScanUsageState(bool isEnabled) { - if (isEnabled) { - _updateTransactions(); - _setTransactionUpdateTimer(); - } else { - _transactionsUpdateTimer?.cancel(); - } - } - - @override - String signMessage(String message, {String? address}) => - 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 deleted file mode 100644 index 0a6a407c7..000000000 --- a/cw_polygon/lib/polygon_wallet_addresses.dart +++ /dev/null @@ -1,5 +0,0 @@ -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 deleted file mode 100644 index 74c7c5ed7..000000000 --- a/cw_polygon/lib/polygon_wallet_creation_credentials.dart +++ /dev/null @@ -1,28 +0,0 @@ -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 index 43c6269f6..451dc7288 100644 --- a/cw_polygon/lib/polygon_wallet_service.dart +++ b/cw_polygon/lib/polygon_wallet_service.dart @@ -1,32 +1,34 @@ -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'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_service.dart'; +import 'package:cw_polygon/polygon_client.dart'; +import 'package:cw_polygon/polygon_mnemonics_exception.dart'; +import 'package:cw_polygon/polygon_wallet.dart'; -class PolygonWalletService extends WalletService { - PolygonWalletService(this.walletInfoSource); +class PolygonWalletService extends EVMChainWalletService { + PolygonWalletService( + super.walletInfoSource, { + required this.client, + }); - final Box walletInfoSource; + late PolygonClient client; @override - Future create(PolygonNewWalletCredentials credentials) async { + WalletType getType() => WalletType.polygon; + + @override + Future create(EVMChainNewWalletCredentials credentials) async { final strength = credentials.seedPhraseLength == 24 ? 256 : 128; final mnemonic = bip39.generateMnemonic(strength: strength); + final wallet = PolygonWallet( walletInfo: credentials.walletInfo!, mnemonic: mnemonic, password: credentials.password!, + client: client, ); await wallet.init(); @@ -36,18 +38,11 @@ class PolygonWalletService extends WalletService 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( + final wallet = await PolygonWallet.open( name: name, password: password, walletInfo: walletInfo, @@ -60,19 +55,13 @@ class PolygonWalletService extends WalletService 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); - } + Future restoreFromKeys(EVMChainRestoreWalletFromPrivateKey credentials) async { - @override - Future restoreFromKeys(PolygonRestoreWalletFromPrivateKey credentials) async { final wallet = PolygonWallet( password: credentials.password!, privateKey: credentials.privateKey, walletInfo: credentials.walletInfo!, + client: client, ); await wallet.init(); @@ -83,15 +72,17 @@ class PolygonWalletService extends WalletService restoreFromSeed(PolygonRestoreWalletFromSeedCredentials credentials) async { + Future restoreFromSeed( + EVMChainRestoreWalletFromSeedCredentials credentials) async { if (!bip39.validateMnemonic(credentials.mnemonic)) { - throw EthereumMnemonicIsIncorrectException(); + throw PolygonMnemonicIsIncorrectException(); } final wallet = PolygonWallet( password: credentials.password!, mnemonic: credentials.mnemonic, walletInfo: credentials.walletInfo!, + client: client, ); await wallet.init(); @@ -105,7 +96,7 @@ class PolygonWalletService extends WalletService 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( + final currentWallet = await PolygonWallet.open( password: password, name: currentName, walletInfo: currentWalletInfo); await currentWallet.renameWalletFiles(newName); diff --git a/cw_polygon/pubspec.yaml b/cw_polygon/pubspec.yaml index e99e6dbbb..505838d7c 100644 --- a/cw_polygon/pubspec.yaml +++ b/cw_polygon/pubspec.yaml @@ -16,15 +16,12 @@ dependencies: 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 + cw_evm: + path: ../cw_evm web3dart: ^2.7.1 - bip32: ^2.0.0 - hex: ^0.2.0 - shared_preferences: ^2.0.15 + hive: ^2.2.3 + bip39: ^1.0.6 + collection: ^1.17.1 dev_dependencies: @@ -32,8 +29,6 @@ dev_dependencies: 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 diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 3118e6ad7..2545e90ce 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -102,11 +102,11 @@ PODS: - DKImagePickerController/PhotoGallery - Flutter - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): + - flutter_inappwebview_ios (0.0.1): - Flutter - - flutter_inappwebview/Core (= 0.0.1) + - flutter_inappwebview_ios/Core (= 0.0.1) - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): + - flutter_inappwebview_ios/Core (0.0.1): - Flutter - OrderedSet (~> 5.0) - flutter_mailer (0.0.1): @@ -169,7 +169,7 @@ DEPENDENCIES: - devicelocale (from `.symlinks/plugins/devicelocale/ios`) - file_picker (from `.symlinks/plugins/file_picker/ios`) - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) + - flutter_inappwebview_ios (from `.symlinks/plugins/flutter_inappwebview_ios/ios`) - flutter_mailer (from `.symlinks/plugins/flutter_mailer/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - fluttertoast (from `.symlinks/plugins/fluttertoast/ios`) @@ -224,8 +224,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/file_picker/ios" Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" + flutter_inappwebview_ios: + :path: ".symlinks/plugins/flutter_inappwebview_ios/ios" flutter_mailer: :path: ".symlinks/plugins/flutter_mailer/ios" flutter_secure_storage: @@ -274,29 +274,29 @@ SPEC CHECKSUMS: DKPhotoGallery: fdfad5125a9fdda9cc57df834d49df790dbb4179 file_picker: 15fd9539e4eb735dc54bae8c0534a7a9511a03de Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 - flutter_inappwebview: 3d32228f1304635e7c028b0d4252937730bbc6cf + flutter_inappwebview_ios: 97215cf7d4677db55df76782dbd2930c5e1c1ea0 flutter_mailer: 2ef5a67087bc8c6c4cefd04a178bf1ae2c94cd83 flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be fluttertoast: eb263d302cc92e04176c053d2385237e9f43fad0 in_app_review: 318597b3a06c22bb46dc454d56828c85f444f99d - local_auth_ios: c6cf091ded637a88f24f86a8875d8b0f526e2605 + local_auth_ios: 1ba1475238daa33a6ffa2a29242558437be435ac MTBBarcodeScanner: f453b33c4b7dfe545d8c6484ed744d55671788cb OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 873975fc26034f0b863a300ad47e7f1ac6c7ec62 package_info_plus: 115f4ad11e0698c8c1c5d8a689390df880f47e85 - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c permission_handler_apple: e76247795d700c14ea09e3a2d8855d41ee80a2e6 ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 SDWebImage: 2aea163b50bfcb569a2726b6a754c54a4506fcf6 sensitive_clipboard: d4866e5d176581536c27bb1618642ee83adca986 share_plus: 056a1e8ac890df3e33cb503afffaf1e9b4fbae68 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 SwiftProtobuf: 40bd808372cb8706108f22d28f8ab4a6b9bc6989 SwiftyGif: 93a1cc87bf3a51916001cf8f3d63835fb64c819f Toast: 91b396c56ee72a5790816f40d3a94dd357abc196 uni_links: d97da20c7701486ba192624d99bffaaffcfc298a UnstoppableDomainsResolution: c3c67f4d0a5e2437cb00d4bd50c2e00d6e743841 - url_launcher_ios: bf5ce03e0e2088bad9cc378ea97fa0ed5b49673b + url_launcher_ios: bbd758c6e7f9fd7b5b1d4cde34d2b95fcce5e812 wakelock_plus: 8b09852c8876491e4b6d179e17dfe2a0b5f60d47 workmanager: 0afdcf5628bbde6924c21af7836fed07b42e30e6 diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index dd713fd15..6836cc4dc 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -63,9 +63,17 @@ class CWBitcoin extends Bitcoin { } @override - Future generateNewAddress(Object wallet) async { + Future generateNewAddress(Object wallet, String label) async { final bitcoinWallet = wallet as ElectrumWallet; - await bitcoinWallet.walletAddresses.generateNewAddress(); + await bitcoinWallet.walletAddresses.generateNewAddress(label: label); + await wallet.save(); + } + + @override + Future updateAddress(Object wallet,String address, String label) async { + final bitcoinWallet = wallet as ElectrumWallet; + bitcoinWallet.walletAddresses.updateAddress(address, label); + await wallet.save(); } @override @@ -99,6 +107,21 @@ class CWBitcoin extends Bitcoin { .toList(); } + @override + @computed + List getSubAddresses(Object wallet) { + final electrumWallet = wallet as ElectrumWallet; + return electrumWallet.walletAddresses.addresses + .map((BitcoinAddressRecord addr) => ElectrumSubAddress( + id: addr.index, + name: addr.name, + address: electrumWallet.type == WalletType.bitcoinCash ? addr.cashAddr : addr.address, + txCount: addr.txCount, + balance: addr.balance, + isChange: addr.isHidden)) + .toList(); + } + @override String getAddress(Object wallet) { final bitcoinWallet = wallet as ElectrumWallet; @@ -127,7 +150,7 @@ class CWBitcoin extends Bitcoin { return bitcoinWallet.unspentCoins; } - void updateUnspents(Object wallet) async { + Future updateUnspents(Object wallet) async { final bitcoinWallet = wallet as ElectrumWallet; await bitcoinWallet.updateUnspent(); } diff --git a/lib/buy/dfx/dfx_buy_provider.dart b/lib/buy/dfx/dfx_buy_provider.dart index f74039caa..8e2d58d11 100644 --- a/lib/buy/dfx/dfx_buy_provider.dart +++ b/lib/buy/dfx/dfx_buy_provider.dart @@ -46,6 +46,8 @@ class DFXBuyProvider extends BuyProvider { return 'XMR'; case WalletType.ethereum: return 'ETH'; + case WalletType.polygon: + return 'MATIC'; default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } @@ -61,13 +63,17 @@ class DFXBuyProvider extends BuyProvider { return 'Monero'; case WalletType.ethereum: return 'Ethereum'; + case WalletType.polygon: + return 'Polygon'; default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } } + String get walletAddress => + wallet.walletAddresses.primaryAddress ?? wallet.walletAddresses.address; + Future getSignMessage() async { - final walletAddress = wallet.walletAddresses.address; final uri = Uri.https(_baseUrl, _authPath, {'address': walletAddress}); var response = await http.get(uri, headers: {'accept': 'application/json'}); @@ -83,7 +89,6 @@ class DFXBuyProvider extends BuyProvider { Future signUp() async { final signMessage = getSignature(await getSignMessage()); - final walletAddress = wallet.walletAddresses.address; final requestBody = jsonEncode({ 'wallet': walletName, @@ -92,8 +97,11 @@ class DFXBuyProvider extends BuyProvider { }); final uri = Uri.https(_baseUrl, _signUpPath); - var response = await http.post(uri, - headers: {'Content-Type': 'application/json'}, body: requestBody); + var response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: requestBody, + ); if (response.statusCode == 201) { final responseBody = jsonDecode(response.body); @@ -103,14 +111,12 @@ class DFXBuyProvider extends BuyProvider { final message = responseBody['message'] ?? 'Service unavailable in your country'; throw Exception(message); } else { - throw Exception( - 'Failed to sign up. Status: ${response.statusCode} ${response.body}'); + throw Exception('Failed to sign up. Status: ${response.statusCode} ${response.body}'); } } Future signIn() async { final signMessage = getSignature(await getSignMessage()); - final walletAddress = wallet.walletAddresses.address; final requestBody = jsonEncode({ 'address': walletAddress, @@ -118,8 +124,11 @@ class DFXBuyProvider extends BuyProvider { }); final uri = Uri.https(_baseUrl, _signInPath); - var response = await http.post(uri, - headers: {'Content-Type': 'application/json'}, body: requestBody); + var response = await http.post( + uri, + headers: {'Content-Type': 'application/json'}, + body: requestBody, + ); if (response.statusCode == 201) { final responseBody = jsonDecode(response.body); @@ -129,21 +138,20 @@ class DFXBuyProvider extends BuyProvider { final message = responseBody['message'] ?? 'Service unavailable in your country'; throw Exception(message); } else { - throw Exception( - 'Failed to sign in. Status: ${response.statusCode} ${response.body}'); + throw Exception('Failed to sign in. Status: ${response.statusCode} ${response.body}'); } } String getSignature(String message) { switch (wallet.type) { case WalletType.ethereum: + case WalletType.polygon: return wallet.signMessage(message); case WalletType.monero: case WalletType.litecoin: case WalletType.bitcoin: case WalletType.bitcoinCash: - return wallet.signMessage(message, - address: wallet.walletAddresses.address); + return wallet.signMessage(message, address: walletAddress); default: throw Exception("WalletType is not available for DFX ${wallet.type}"); } @@ -178,8 +186,7 @@ class DFXBuyProvider extends BuyProvider { if (await canLaunchUrl(uri)) { if (DeviceInfo.instance.isMobile) { - Navigator.of(context) - .pushNamed(Routes.webViewPage, arguments: ["DFX Connect", uri]); + Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [title, uri]); } else { await launchUrl(uri, mode: LaunchMode.externalApplication); } diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 014edb813..3819f074d 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -52,7 +52,7 @@ class OnRamperBuyProvider extends BuyProvider { return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); } - Uri requestOnramperUrl(BuildContext context) { + Uri requestOnramperUrl(BuildContext context, bool? isBuyAction) { String primaryColor, secondaryColor, primaryTextColor, @@ -79,23 +79,24 @@ class OnRamperBuyProvider extends BuyProvider { return Uri.https(_baseUrl, '', { 'apiKey': _apiKey, 'defaultCrypto': _normalizeCryptoCurrency, + 'sell_defaultCrypto': _normalizeCryptoCurrency, 'networkWallets': '${networkName}:${wallet.walletAddresses.address}', - 'supportSell': "false", 'supportSwap': "false", 'primaryColor': primaryColor, 'secondaryColor': secondaryColor, 'primaryTextColor': primaryTextColor, 'secondaryTextColor': secondaryTextColor, 'containerColor': containerColor, - 'cardColor': cardColor + 'cardColor': cardColor, + 'mode': isBuyAction == true ? 'buy' : 'sell', }); } Future launchProvider(BuildContext context, bool? isBuyAction) async { - final uri = requestOnramperUrl(context); + final uri = requestOnramperUrl(context, isBuyAction); if (DeviceInfo.instance.isMobile) { Navigator.of(context) - .pushNamed(Routes.webViewPage, arguments: [S.of(context).buy, uri]); + .pushNamed(Routes.webViewPage, arguments: [title, uri]); } else { await launchUrl(uri); } diff --git a/lib/core/auth_service.dart b/lib/core/auth_service.dart index c072bf65e..a99aef31d 100644 --- a/lib/core/auth_service.dart +++ b/lib/core/auth_service.dart @@ -72,11 +72,11 @@ class AuthService with Store { void saveLastAuthTime() { int timestamp = DateTime.now().millisecondsSinceEpoch; - sharedPreferences.setInt(PreferencesKey.lastAuthTimeMilliseconds, timestamp); + secureStorage.write(key: SecureKey.lastAuthTimeMilliseconds, value: timestamp.toString()); } - bool requireAuth() { - final timestamp = sharedPreferences.getInt(PreferencesKey.lastAuthTimeMilliseconds); + Future requireAuth() async { + final timestamp = int.tryParse(await secureStorage.read(key: SecureKey.lastAuthTimeMilliseconds) ?? '0'); final duration = _durationToRequireAuth(timestamp ?? 0); final requiredPinInterval = settingsStore.pinTimeOutDuration; @@ -100,7 +100,7 @@ class AuthService with Store { 'Either route or onAuthSuccess param must be passed.'); if (!conditionToDetermineIfToUse2FA) { - if (!requireAuth() && !_alwaysAuthenticateRoutes.contains(route)) { + if (!(await requireAuth()) && !_alwaysAuthenticateRoutes.contains(route)) { if (onAuthSuccess != null) { onAuthSuccess(true); } else { diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 7dd0b50f3..9b5c4c8db 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -213,8 +213,6 @@ class BackupService { final defaultBuyProvider = data[PreferencesKey.defaultBuyProvider] as int?; final currentTransactionPriorityKeyLegacy = data[PreferencesKey.currentTransactionPriorityKeyLegacy] as int?; - final allowBiometricalAuthentication = - data[PreferencesKey.allowBiometricalAuthenticationKey] as bool?; final currentBitcoinElectrumSererId = data[PreferencesKey.currentBitcoinElectrumSererIdKey] as int?; final currentLanguageCode = data[PreferencesKey.currentLanguageCode] as String?; @@ -227,23 +225,6 @@ class BackupService { data[PreferencesKey.currentDefaultSettingsMigrationVersion] as int?; final moneroTransactionPriority = data[PreferencesKey.moneroTransactionPriority] as int?; final bitcoinTransactionPriority = data[PreferencesKey.bitcoinTransactionPriority] as int?; - final selectedCake2FAPreset = data[PreferencesKey.selectedCake2FAPreset] as int?; - final shouldRequireTOTP2FAForAccessingWallet = - data[PreferencesKey.shouldRequireTOTP2FAForAccessingWallet] as bool?; - final shouldRequireTOTP2FAForSendsToContact = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToContact] as bool?; - final shouldRequireTOTP2FAForSendsToNonContact = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact] as bool?; - final shouldRequireTOTP2FAForSendsToInternalWallets = - data[PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets] as bool?; - final shouldRequireTOTP2FAForExchangesToInternalWallets = - data[PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets] as bool?; - final shouldRequireTOTP2FAForAddingContacts = - data[PreferencesKey.shouldRequireTOTP2FAForAddingContacts] as bool?; - final shouldRequireTOTP2FAForCreatingNewWallets = - data[PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets] as bool?; - final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = - data[PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings] as bool?; final sortBalanceTokensBy = data[PreferencesKey.sortBalanceBy] as int?; final pinNativeTokenAtTop = data[PreferencesKey.pinNativeTokenAtTop] as bool?; final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; @@ -294,14 +275,7 @@ class BackupService { if (currentTransactionPriorityKeyLegacy != null) await _sharedPreferences.setInt( PreferencesKey.currentTransactionPriorityKeyLegacy, currentTransactionPriorityKeyLegacy); - - if (DeviceInfo.instance.isDesktop) { - await _sharedPreferences.setBool(PreferencesKey.allowBiometricalAuthenticationKey, false); - } else if (allowBiometricalAuthentication != null) { - await _sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, allowBiometricalAuthentication); - } - + if (currentBitcoinElectrumSererId != null) await _sharedPreferences.setInt( PreferencesKey.currentBitcoinElectrumSererIdKey, currentBitcoinElectrumSererId); @@ -344,43 +318,6 @@ class BackupService { await _sharedPreferences.setInt( PreferencesKey.bitcoinTransactionPriority, bitcoinTransactionPriority); - if (selectedCake2FAPreset != null) - await _sharedPreferences.setInt(PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset); - - if (shouldRequireTOTP2FAForAccessingWallet != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, - shouldRequireTOTP2FAForAccessingWallet); - - if (shouldRequireTOTP2FAForSendsToContact != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact, - shouldRequireTOTP2FAForSendsToContact); - - if (shouldRequireTOTP2FAForSendsToNonContact != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, - shouldRequireTOTP2FAForSendsToNonContact); - - if (shouldRequireTOTP2FAForSendsToInternalWallets != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, - shouldRequireTOTP2FAForSendsToInternalWallets); - - if (shouldRequireTOTP2FAForExchangesToInternalWallets != null) - await _sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, - shouldRequireTOTP2FAForExchangesToInternalWallets); - - if (shouldRequireTOTP2FAForAddingContacts != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts, - shouldRequireTOTP2FAForAddingContacts); - - if (shouldRequireTOTP2FAForCreatingNewWallets != null) - await _sharedPreferences.setBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, - shouldRequireTOTP2FAForCreatingNewWallets); - - if (shouldRequireTOTP2FAForAllSecurityAndBackupSettings != null) - await _sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - shouldRequireTOTP2FAForAllSecurityAndBackupSettings); - if (sortBalanceTokensBy != null) await _sharedPreferences.setInt(PreferencesKey.sortBalanceBy, sortBalanceTokensBy); @@ -532,8 +469,6 @@ class BackupService { PreferencesKey.currentPinLength: _sharedPreferences.getInt(PreferencesKey.currentPinLength), PreferencesKey.currentTransactionPriorityKeyLegacy: _sharedPreferences.getInt(PreferencesKey.currentTransactionPriorityKeyLegacy), - PreferencesKey.allowBiometricalAuthenticationKey: - _sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey), PreferencesKey.currentBitcoinElectrumSererIdKey: _sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey), PreferencesKey.currentLanguageCode: @@ -550,24 +485,6 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority), PreferencesKey.currentFiatApiModeKey: _sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey), - PreferencesKey.selectedCake2FAPreset: - _sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset), - PreferencesKey.shouldRequireTOTP2FAForAccessingWallet: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet), - PreferencesKey.shouldRequireTOTP2FAForSendsToContact: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact), - PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact), - PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets), - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets: _sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets), - PreferencesKey.shouldRequireTOTP2FAForAddingContacts: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts), - PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets: - _sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets), - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings: _sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings), PreferencesKey.sortBalanceBy: _sharedPreferences.getInt(PreferencesKey.sortBalanceBy), PreferencesKey.pinNativeTokenAtTop: _sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop), diff --git a/lib/core/wallet_connect/evm_chain_service.dart b/lib/core/wallet_connect/evm_chain_service.dart index 81b934e08..74bff7479 100644 --- a/lib/core/wallet_connect/evm_chain_service.dart +++ b/lib/core/wallet_connect/evm_chain_service.dart @@ -253,7 +253,7 @@ class EvmChainServiceImpl implements ChainService { return result; } catch (e) { - log('An error has occured while signing transaction: ${e.toString()}'); + log('An error has occurred while signing transaction: ${e.toString()}'); bottomSheetService.queueBottomSheet( isModalDismissible: true, widget: BottomSheetMessageDisplayWidget( diff --git a/lib/di.dart b/lib/di.dart index 61a04bf1c..05019a562 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -276,6 +276,7 @@ Future setup({ if (!_isSetupFinished) { getIt.registerSingletonAsync(() => SharedPreferences.getInstance()); + getIt.registerSingleton(secureStorage); } if (!_isSetupFinished) { getIt.registerFactory(() => BackgroundTasks()); @@ -302,7 +303,6 @@ Future setup({ getIt.registerFactory>(() => _nodeSource); getIt.registerFactory>(() => _powNodeSource, instanceName: Node.boxName + "pow"); - getIt.registerSingleton(secureStorage); getIt.registerSingleton(AuthenticationStore()); getIt.registerSingleton(WalletListStore()); getIt.registerSingleton(NodeListStoreBase.instance); @@ -344,17 +344,19 @@ Future setup({ walletInfoSource: _walletInfoSource)); getIt.registerFactoryParam( - (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); + (type, _) => AdvancedPrivacySettingsViewModel(type, getIt.get())); getIt.registerFactory(() => WalletLoadingService( getIt.get(), getIt.get(), (WalletType type) => getIt.get(param1: type))); - - getIt.registerFactoryParam((type, _) => - WalletNewVM(getIt.get(), - getIt.get(param1: type), _walletInfoSource, - getIt.get(param1: type),type: type)); + + getIt.registerFactoryParam((type, _) => WalletNewVM( + getIt.get(), + getIt.get(param1: type), + _walletInfoSource, + getIt.get(param1: type), + type: type)); getIt.registerFactoryParam((WalletType type, _) { return WalletRestorationFromQRVM(getIt.get(), @@ -610,7 +612,6 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), - getIt.get(), ), ); } else { @@ -621,7 +622,6 @@ Future setup({ _walletInfoSource, getIt.get(), getIt.get(), - getIt.get(), ), ); } @@ -723,7 +723,7 @@ Future setup({ }); getIt.registerFactory(() { - return SecuritySettingsViewModel(getIt.get(), getIt.get()); + return SecuritySettingsViewModel(getIt.get()); }); getIt.registerFactory(() => WalletSeedViewModel(getIt.get().wallet!)); @@ -805,8 +805,7 @@ Future setup({ .registerFactory(() => DFXBuyProvider(wallet: getIt.get().wallet!)); getIt.registerFactory(() => MoonPaySellProvider( - settingsStore: getIt.get().settingsStore, - wallet: getIt.get().wallet!)); + settingsStore: getIt.get().settingsStore, wallet: getIt.get().wallet!)); getIt.registerFactory(() => OnRamperBuyProvider( getIt.get().settingsStore, @@ -919,8 +918,7 @@ Future setup({ (param1, isCreate) => NewWalletTypePage(onTypeSelected: param1, isCreate: isCreate ?? true)); getIt.registerFactoryParam( - (seedPhraseLength, _) - => PreSeedPage(seedPhraseLength)); + (seedPhraseLength, _) => PreSeedPage(seedPhraseLength)); getIt.registerFactoryParam((trade, _) => TradeDetailsViewModel( @@ -954,7 +952,7 @@ Future setup({ getIt.registerFactory(() => BuyAmountViewModel()); getIt.registerFactoryParam( - (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); + (isBuyOption, _) => BuySellOptionsPage(getIt.get(), isBuyOption)); getIt.registerFactory(() { final wallet = getIt.get().wallet; diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 22868ae28..68e76d423 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -185,6 +185,12 @@ Future defaultSettingsMigration( case 25: await rewriteSecureStoragePin(secureStorage: secureStorage); break; + case 26: + /// commented out as it was a probable cause for some users to have white screen issues + /// maybe due to multiple access on Secure Storage at once + /// or long await time on start of the app + // await insecureStorageMigration(secureStorage: secureStorage, sharedPreferences: sharedPreferences); + break; default: break; } @@ -378,6 +384,82 @@ Node getMoneroDefaultNode({required Box nodes}) { } } +Future insecureStorageMigration({ + required SharedPreferences sharedPreferences, + required FlutterSecureStorage secureStorage, +}) async { + bool? allowBiometricalAuthentication = + sharedPreferences.getBool(SecureKey.allowBiometricalAuthenticationKey); + bool? useTOTP2FA = sharedPreferences.getBool(SecureKey.useTOTP2FA); + bool? shouldRequireTOTP2FAForAccessingWallet = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAccessingWallet); + bool? shouldRequireTOTP2FAForSendsToContact = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToContact); + bool? shouldRequireTOTP2FAForSendsToNonContact = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToNonContact); + bool? shouldRequireTOTP2FAForSendsToInternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets); + bool? shouldRequireTOTP2FAForExchangesToInternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets); + bool? shouldRequireTOTP2FAForExchangesToExternalWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets); + bool? shouldRequireTOTP2FAForAddingContacts = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAddingContacts); + bool? shouldRequireTOTP2FAForCreatingNewWallets = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForCreatingNewWallets); + bool? shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + sharedPreferences.getBool(SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings); + int? selectedCake2FAPreset = sharedPreferences.getInt(SecureKey.selectedCake2FAPreset); + String? totpSecretKey = sharedPreferences.getString(SecureKey.totpSecretKey); + int? pinTimeOutDuration = sharedPreferences.getInt(SecureKey.pinTimeOutDuration); + int? lastAuthTimeMilliseconds = sharedPreferences.getInt(SecureKey.lastAuthTimeMilliseconds); + + try { + await secureStorage.write( + key: SecureKey.allowBiometricalAuthenticationKey, + value: allowBiometricalAuthentication.toString()); + await secureStorage.write(key: SecureKey.useTOTP2FA, value: useTOTP2FA.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + value: shouldRequireTOTP2FAForAccessingWallet.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + value: shouldRequireTOTP2FAForSendsToContact.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + value: shouldRequireTOTP2FAForSendsToNonContact.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + value: shouldRequireTOTP2FAForSendsToInternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + value: shouldRequireTOTP2FAForExchangesToInternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + value: shouldRequireTOTP2FAForExchangesToExternalWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + value: shouldRequireTOTP2FAForAddingContacts.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + value: shouldRequireTOTP2FAForCreatingNewWallets.toString()); + await secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + value: shouldRequireTOTP2FAForAllSecurityAndBackupSettings.toString()); + await secureStorage.write( + key: SecureKey.selectedCake2FAPreset, value: selectedCake2FAPreset.toString()); + await secureStorage.write(key: SecureKey.totpSecretKey, value: totpSecretKey.toString()); + await secureStorage.write( + key: SecureKey.pinTimeOutDuration, value: pinTimeOutDuration.toString()); + await secureStorage.write( + key: SecureKey.lastAuthTimeMilliseconds, value: lastAuthTimeMilliseconds.toString()); + } catch (e) { + print("Error migrating shared preferences to secure storage!: $e"); + // this actually shouldn't be that big of a problem since we don't delete the old keys in this update + // and we read and write to the new locations when loading storage, the migration is just for extra safety + } +} + Future rewriteSecureStoragePin({required FlutterSecureStorage secureStorage}) async { // the bug only affects ios/mac: if (!Platform.isIOS && !Platform.isMacOS) { @@ -428,7 +510,7 @@ Future changeLitecoinCurrentElectrumServerToDefault( Future changeBitcoinCashCurrentNodeToDefault( {required SharedPreferences sharedPreferences, required Box nodes}) async { final server = getBitcoinCashDefaultElectrumServer(nodes: nodes); - final serverId = server?.key as int ?? 0; + final serverId = server?.key as int? ?? 0; await sharedPreferences.setInt(PreferencesKey.currentBitcoinCashNodeIdKey, serverId); } diff --git a/lib/entities/fiat_currency.dart b/lib/entities/fiat_currency.dart index 8310eb18f..a8e2829d8 100644 --- a/lib/entities/fiat_currency.dart +++ b/lib/entities/fiat_currency.dart @@ -10,7 +10,7 @@ class FiatCurrency extends EnumerableItem with Serializable impl static List get all => _all.values.toList(); static List get currenciesAvailableToBuyWith => - [aud, bgn, brl, cad, chf, clp, cop, czk, dkk, egp, eur, gbp, gtq, hkd, hrk, huf, idr, ils, inr, isk, jpy, krw, mad, mxn, myr, ngn, nok, nzd, php, pkr, pln, ron, sek, sgd, thb, twd, usd, vnd, zar]; + [aud, bgn, brl, cad, chf, clp, cop, czk, dkk, egp, eur, gbp, gtq, hkd, hrk, huf, idr, ils, inr, isk, jpy, krw, mad, mxn, myr, ngn, nok, nzd, php, pkr, pln, ron, sek, sgd, thb, twd, usd, vnd, zar, tur]; static const ars = FiatCurrency(symbol: 'ARS', countryCode: "arg", fullName: "Argentine Peso"); static const aud = FiatCurrency(symbol: 'AUD', countryCode: "aus", fullName: "Australian Dollar"); @@ -60,6 +60,7 @@ class FiatCurrency extends EnumerableItem with Serializable impl static const vef = FiatCurrency(symbol: 'VEF', countryCode: "ven", fullName: "Venezuelan Bolivar Bolívar"); static const vnd = FiatCurrency(symbol: 'VND', countryCode: "vnm", fullName: "Vietnamese Dong đồng"); static const zar = FiatCurrency(symbol: 'ZAR', countryCode: "saf", fullName: "South African Rand"); + static const tur = FiatCurrency(symbol: 'TRY', countryCode: "tur", fullName: "Turkish Lira"); static final _all = { FiatCurrency.ars.raw: FiatCurrency.ars, @@ -109,7 +110,8 @@ class FiatCurrency extends EnumerableItem with Serializable impl FiatCurrency.usd.raw: FiatCurrency.usd, FiatCurrency.vef.raw: FiatCurrency.vef, FiatCurrency.vnd.raw: FiatCurrency.vnd, - FiatCurrency.zar.raw: FiatCurrency.zar + FiatCurrency.zar.raw: FiatCurrency.zar, + FiatCurrency.tur.raw: FiatCurrency.tur, }; static FiatCurrency deserialize({required String raw}) => _all[raw]!; diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index e184f5649..52bcc495b 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -6,12 +6,14 @@ import 'package:cake_wallet/entities/parsed_address.dart'; import 'package:cake_wallet/entities/unstoppable_domain_address.dart'; import 'package:cake_wallet/entities/emoji_string_extension.dart'; import 'package:cake_wallet/mastodon/mastodon_api.dart'; +import 'package:cake_wallet/nostr/nostr_api.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/twitter/twitter_api.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cake_wallet/entities/fio_address_provider.dart'; +import 'package:flutter/cupertino.dart'; class AddressResolver { AddressResolver({required this.yatService, required this.wallet, required this.settingsStore}) @@ -58,7 +60,16 @@ class AddressResolver { }); } - Future resolve(String text, String ticker) async { + bool isEmailFormat(String address) { + final RegExp emailRegex = RegExp( + r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', + caseSensitive: false, + ); + return emailRegex.hasMatch(address); + } + + + Future resolve(BuildContext context, String text, String ticker) async { try { if (text.startsWith('@') && !text.substring(1).contains('@')) { if(settingsStore.lookupsTwitter) { @@ -165,6 +176,21 @@ class AddressResolver { } } } + if (isEmailFormat(text)) { + final nostrProfile = await NostrProfileHandler.queryProfile(context, text); + if (nostrProfile?.relays != null) { + final nostrUserData = + await NostrProfileHandler.processRelays(context, nostrProfile!, text); + + if (nostrUserData != null) { + String? addressFromBio = extractAddressByType( + raw: nostrUserData.about, type: CryptoCurrency.fromString(ticker)); + if (addressFromBio != null) { + return ParsedAddress.nostrAddress(address: addressFromBio, name: text); + } + } + } + } } catch (e) { print(e.toString()); } diff --git a/lib/entities/parsed_address.dart b/lib/entities/parsed_address.dart index df20dd9ee..d414a827d 100644 --- a/lib/entities/parsed_address.dart +++ b/lib/entities/parsed_address.dart @@ -2,7 +2,18 @@ import 'package:cake_wallet/entities/openalias_record.dart'; import 'package:cake_wallet/entities/yat_record.dart'; -enum ParseFrom { unstoppableDomains, openAlias, yatRecord, fio, notParsed, twitter, ens, contact, mastodon } +enum ParseFrom { + unstoppableDomains, + openAlias, + yatRecord, + fio, + notParsed, + twitter, + ens, + contact, + mastodon, + nostr +} class ParsedAddress { ParsedAddress({ @@ -11,9 +22,9 @@ class ParsedAddress { this.description = '', this.parseFrom = ParseFrom.notParsed, }); - + factory ParsedAddress.fetchEmojiAddress({ - List? addresses, + List? addresses, required String name, }){ if (addresses?.isEmpty ?? true) { @@ -28,7 +39,7 @@ class ParsedAddress { } factory ParsedAddress.fetchUnstoppableDomainAddress({ - String? address, + String? address, required String name, }){ if (address?.isEmpty ?? true) { @@ -94,6 +105,14 @@ class ParsedAddress { ); } + factory ParsedAddress.nostrAddress({required String address, required String name}) { + return ParsedAddress( + addresses: [address], + name: name, + parseFrom: ParseFrom.nostr, + ); + } + final List addresses; final String name; final String description; diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 2d5e64817..75e61b5e8 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -23,8 +23,6 @@ class PreferencesKey { static const walletListOrder = 'wallet_list_order'; static const walletListAscending = 'wallet_list_ascending'; static const currentFiatApiModeKey = 'current_fiat_api_mode'; - static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; - static const useTOTP2FA = 'use_totp_2fa'; static const failedTotpTokenTrials = 'failed_token_trials'; static const disableExchangeKey = 'disable_exchange'; static const exchangeStatusKey = 'exchange_status'; @@ -33,6 +31,7 @@ class PreferencesKey { static const displayActionListModeKey = 'display_list_mode'; static const currentPinLength = 'current_pin_length'; static const currentLanguageCode = 'language_code'; + static const currentSeedPhraseLength = 'current_seed_phrase_length'; static const currentDefaultSettingsMigrationVersion = 'current_default_settings_migration_version'; static const moneroTransactionPriority = 'current_fee_priority_monero'; @@ -47,8 +46,6 @@ class PreferencesKey { static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1'; static const syncModeKey = 'sync_mode'; static const syncAllKey = 'sync_all'; - static const pinTimeOutDuration = 'pin_timeout_duration'; - static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; static const lastPopupDate = 'last_popup_date'; static const lastAppReviewDate = 'last_app_review_date'; static const sortBalanceBy = 'sort_balance_by'; @@ -75,25 +72,4 @@ class PreferencesKey { static const lastSeenAppVersion = 'last_seen_app_version'; static const shouldShowMarketPlaceInDashboard = 'should_show_marketplace_in_dashboard'; static const isNewInstall = 'is_new_install'; - static const shouldRequireTOTP2FAForAccessingWallet = - 'should_require_totp_2fa_for_accessing_wallets'; - static const shouldRequireTOTP2FAForSendsToContact = - 'should_require_totp_2fa_for_sends_to_contact'; - static const shouldRequireTOTP2FAForSendsToNonContact = - 'should_require_totp_2fa_for_sends_to_non_contact'; - static const shouldRequireTOTP2FAForSendsToInternalWallets = - 'should_require_totp_2fa_for_sends_to_internal_wallets'; - static const shouldRequireTOTP2FAForExchangesToInternalWallets = - 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; - static const shouldRequireTOTP2FAForExchangesToExternalWallets = - 'should_require_totp_2fa_for_exchanges_to_external_wallets'; - static const shouldRequireTOTP2FAForAddingContacts = - 'should_require_totp_2fa_for_adding_contacts'; - static const shouldRequireTOTP2FAForCreatingNewWallets = - 'should_require_totp_2fa_for_creating_new_wallets'; - static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = - 'should_require_totp_2fa_for_all_security_and_backup_settings'; - static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; - static const totpSecretKey = 'totp_secret_key'; - static const currentSeedPhraseLength = 'current_seed_phrase_length'; } diff --git a/lib/entities/provider_types.dart b/lib/entities/provider_types.dart index 4cb9a934f..ed688590c 100644 --- a/lib/entities/provider_types.dart +++ b/lib/entities/provider_types.dart @@ -65,28 +65,34 @@ class ProvidersHelper { case WalletType.litecoin: case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.onramper, ProviderType.robinhood]; + case WalletType.polygon: + return [ProviderType.askEachTime, ProviderType.dfx]; case WalletType.none: case WalletType.haven: - case WalletType.polygon: return []; } } static List getAvailableSellProviderTypes(WalletType walletType) { switch (walletType) { - case WalletType.monero: - return [ProviderType.askEachTime, ProviderType.dfx]; case WalletType.bitcoin: case WalletType.ethereum: - return [ProviderType.askEachTime, ProviderType.moonpaySell, ProviderType.dfx]; + return [ + ProviderType.askEachTime, + ProviderType.onramper, + ProviderType.moonpaySell, + ProviderType.dfx, + ]; case WalletType.litecoin: case WalletType.bitcoinCash: return [ProviderType.askEachTime, ProviderType.moonpaySell]; + case WalletType.polygon: + return [ProviderType.askEachTime, ProviderType.dfx]; + case WalletType.monero: case WalletType.nano: case WalletType.banano: case WalletType.none: case WalletType.haven: - case WalletType.polygon: return []; } } diff --git a/lib/entities/secret_store_key.dart b/lib/entities/secret_store_key.dart index 00ec06767..2ee490c74 100644 --- a/lib/entities/secret_store_key.dart +++ b/lib/entities/secret_store_key.dart @@ -1,3 +1,6 @@ +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + enum SecretStoreKey { moneroWalletPassword, pinCodePassword, backupPassword } const moneroWalletPassword = "MONERO_WALLET_PASSWORD"; @@ -35,3 +38,65 @@ String generateStoreKeyFor({ return _key; } + +class SecureKey { + static const allowBiometricalAuthenticationKey = 'allow_biometrical_authentication'; + static const useTOTP2FA = 'use_totp_2fa'; + static const shouldRequireTOTP2FAForAccessingWallet = + 'should_require_totp_2fa_for_accessing_wallets'; + static const shouldRequireTOTP2FAForSendsToContact = + 'should_require_totp_2fa_for_sends_to_contact'; + static const shouldRequireTOTP2FAForSendsToNonContact = + 'should_require_totp_2fa_for_sends_to_non_contact'; + static const shouldRequireTOTP2FAForSendsToInternalWallets = + 'should_require_totp_2fa_for_sends_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToInternalWallets = + 'should_require_totp_2fa_for_exchanges_to_internal_wallets'; + static const shouldRequireTOTP2FAForExchangesToExternalWallets = + 'should_require_totp_2fa_for_exchanges_to_external_wallets'; + static const shouldRequireTOTP2FAForAddingContacts = + 'should_require_totp_2fa_for_adding_contacts'; + static const shouldRequireTOTP2FAForCreatingNewWallets = + 'should_require_totp_2fa_for_creating_new_wallets'; + static const shouldRequireTOTP2FAForAllSecurityAndBackupSettings = + 'should_require_totp_2fa_for_all_security_and_backup_settings'; + static const selectedCake2FAPreset = 'selected_cake_2fa_preset'; + static const totpSecretKey = 'totp_secret_key'; + static const pinTimeOutDuration = 'pin_timeout_duration'; + static const lastAuthTimeMilliseconds = 'last_auth_time_milliseconds'; + + static Future getInt({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + int? value = int.tryParse((await secureStorage.read(key: key) ?? '')); + value ??= sharedPreferences.getInt(key); + return value; + } + + static Future getBool({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + String? value = (await secureStorage.read(key: key) ?? ''); + if (value.toLowerCase() == "true") { + return true; + } else if (value.toLowerCase() == "false") { + return false; + } else { + return sharedPreferences.getBool(key); + } + } + + static Future getString({ + required FlutterSecureStorage secureStorage, + required SharedPreferences sharedPreferences, + required String key, + }) async { + String? value = await secureStorage.read(key: key); + value ??= sharedPreferences.getString(key); + return value; + } +} diff --git a/lib/ethereum/cw_ethereum.dart b/lib/ethereum/cw_ethereum.dart index abafc2f26..d7c174e1a 100644 --- a/lib/ethereum/cw_ethereum.dart +++ b/lib/ethereum/cw_ethereum.dart @@ -2,17 +2,17 @@ part of 'ethereum.dart'; class CWEthereum extends Ethereum { @override - List getEthereumWordList(String language) => EthereumMnemonics.englishWordlist; + List getEthereumWordList(String language) => EVMChainMnemonics.englishWordlist; WalletService createEthereumWalletService(Box walletInfoSource) => - EthereumWalletService(walletInfoSource); + EthereumWalletService(walletInfoSource, client: EthereumClient()); @override WalletCredentials createEthereumNewWalletCredentials({ required String name, WalletInfo? walletInfo, }) => - EthereumNewWalletCredentials(name: name, walletInfo: walletInfo); + EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo); @override WalletCredentials createEthereumRestoreWalletFromSeedCredentials({ @@ -20,7 +20,7 @@ class CWEthereum extends Ethereum { required String mnemonic, required String password, }) => - EthereumRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); @override WalletCredentials createEthereumRestoreWalletFromPrivateKey({ @@ -28,37 +28,37 @@ class CWEthereum extends Ethereum { required String privateKey, required String password, }) => - EthereumRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + EVMChainRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); @override String getAddress(WalletBase wallet) => (wallet as EthereumWallet).walletAddresses.address; @override String getPrivateKey(WalletBase wallet) { - final privateKeyHolder = (wallet as EthereumWallet).ethPrivateKey; + final privateKeyHolder = (wallet as EthereumWallet).evmChainPrivateKey; String stringKey = bytesToHex(privateKeyHolder.privateKey); return stringKey; } @override String getPublicKey(WalletBase wallet) { - final privateKeyInUnitInt = (wallet as EthereumWallet).ethPrivateKey; + final privateKeyInUnitInt = (wallet as EthereumWallet).evmChainPrivateKey; final publicKey = privateKeyInUnitInt.address.hex; return publicKey; } @override - TransactionPriority getDefaultTransactionPriority() => EthereumTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; @override - TransactionPriority getEthereumTransactionPrioritySlow() => EthereumTransactionPriority.slow; + TransactionPriority getEthereumTransactionPrioritySlow() => EVMChainTransactionPriority.slow; @override - List getTransactionPriorities() => EthereumTransactionPriority.all; + List getTransactionPriorities() => EVMChainTransactionPriority.all; @override TransactionPriority deserializeEthereumTransactionPriority(int raw) => - EthereumTransactionPriority.deserialize(raw: raw); + EVMChainTransactionPriority.deserialize(raw: raw); Object createEthereumTransactionCredentials( List outputs, { @@ -66,7 +66,7 @@ class CWEthereum extends Ethereum { required CryptoCurrency currency, int? feeRate, }) => - EthereumTransactionCredentials( + EVMChainTransactionCredentials( outputs .map((out) => OutputInfo( fiatAmount: out.fiatAmount, @@ -78,7 +78,7 @@ class CWEthereum extends Ethereum { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority as EthereumTransactionPriority, + priority: priority as EVMChainTransactionPriority, currency: currency, feeRate: feeRate, ); @@ -89,15 +89,15 @@ class CWEthereum extends Ethereum { required CryptoCurrency currency, required int feeRate, }) => - EthereumTransactionCredentials( + EVMChainTransactionCredentials( outputs, - priority: priority as EthereumTransactionPriority?, + priority: priority as EVMChainTransactionPriority?, currency: currency, feeRate: feeRate, ); @override - int formatterEthereumParseAmount(String amount) => EthereumFormatter.parseEthereumAmount(amount); + int formatterEthereumParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); @override double formatterEthereumAmountToDouble( @@ -105,7 +105,7 @@ class CWEthereum extends Ethereum { assert(transaction != null || amount != null); if (transaction != null) { - transaction as EthereumTransactionInfo; + transaction as EVMChainTransactionInfo; return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); } else { return (amount!) / BigInt.from(10).pow(exponent); @@ -134,7 +134,7 @@ class CWEthereum extends Ethereum { @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as EthereumTransactionInfo; + transaction as EVMChainTransactionInfo; if (transaction.tokenSymbol == CryptoCurrency.eth.title) { return CryptoCurrency.eth; } @@ -146,7 +146,7 @@ class CWEthereum extends Ethereum { @override void updateEtherscanUsageState(WalletBase wallet, bool isEnabled) { - (wallet as EthereumWallet).updateEtherscanUsageState(isEnabled); + (wallet as EthereumWallet).updateScanProviderUsageState(isEnabled); } @override diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 300741a08..c4a96bc5b 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -265,6 +265,9 @@ class ChangeNowExchangeProvider extends ExchangeProvider { } String _normalizeCurrency(CryptoCurrency currency) { + if (currency.title == "USDC" && currency.tag == "POLY") { + throw "Only Bridged USDC (USDC.e) is allowed in ChangeNow"; + } switch (currency) { case CryptoCurrency.zec: return 'zec'; diff --git a/lib/main.dart b/lib/main.dart index 165db1ddd..306b109a0 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -163,7 +163,7 @@ Future initializeAppConfigs() async { transactionDescriptions: transactionDescriptions, secureStorage: secureStorage, anonpayInvoiceInfo: anonpayInvoiceInfo, - initialMigrationVersion: 25); + initialMigrationVersion: 26); } Future initialSetup( diff --git a/lib/monero/cw_monero.dart b/lib/monero/cw_monero.dart index 947d274a8..959ae92ce 100644 --- a/lib/monero/cw_monero.dart +++ b/lib/monero/cw_monero.dart @@ -331,7 +331,7 @@ class CWMonero extends Monero { } @override - void updateUnspents(Object wallet) async { + Future updateUnspents(Object wallet) async { final moneroWallet = wallet as MoneroWallet; await moneroWallet.updateUnspent(); } diff --git a/lib/nano/cw_nano.dart b/lib/nano/cw_nano.dart index 0c072c95c..06ebf60c2 100644 --- a/lib/nano/cw_nano.dart +++ b/lib/nano/cw_nano.dart @@ -173,7 +173,7 @@ class CWNano extends Nano { } @override - Future updateTransactions(Object wallet) async { + Future updateTransactions(Object wallet) async { return (wallet as NanoWallet).updateTransactions(); } @@ -189,116 +189,10 @@ class CWNano extends Nano { } class CWNanoUtil extends NanoUtil { - // standard: - @override - String seedToPrivate(String seed, int index) { - return ND.NanoKeys.seedToPrivate(seed, index); - } - - @override - String seedToAddress(String seed, int index) { - return ND.NanoAccounts.createAccount( - ND.NanoAccountType.NANO, privateKeyToPublic(seedToPrivate(seed, index))); - } - - @override - String seedToMnemonic(String seed) { - return NanoMnemomics.seedToMnemonic(seed).join(" "); - } - - @override - Future mnemonicToSeed(String mnemonic) async { - return NanoMnemomics.mnemonicListToSeed(mnemonic.split(' ')); - } - - @override - String privateKeyToPublic(String privateKey) { - // return NanoHelpers.byteToHex(Ed25519Blake2b.getPubkey(NanoHelpers.hexToBytes(privateKey))!); - return ND.NanoKeys.createPublicKey(privateKey); - } - - @override - String addressToPublicKey(String publicAddress) { - return ND.NanoAccounts.extractPublicKey(publicAddress); - } - - // universal: - @override - String privateKeyToAddress(String privateKey) { - return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, privateKeyToPublic(privateKey)); - } - - @override - String publicKeyToAddress(String publicKey) { - return ND.NanoAccounts.createAccount(ND.NanoAccountType.NANO, publicKey); - } - - // standard + hd: - @override - bool isValidSeed(String seed) { - // Ensure seed is 64 or 128 characters long - if (seed.length != 64 && seed.length != 128) { - return false; - } - // Ensure seed only contains hex characters, 0-9;A-F - return ND.NanoHelpers.isHexString(seed); - } - - // hd: - @override - Future hdMnemonicListToSeed(List words) async { - // if (words.length != 24) { - // throw Exception('Expected a 24-word list, got a ${words.length} list'); - // } - final Uint8List salt = Uint8List.fromList(utf8.encode('mnemonic')); - final Pbkdf2 hasher = Pbkdf2(iterations: 2048); - final String seed = await hasher.sha512(words.join(' '), salt); - return seed; - } - - @override - Future hdSeedToPrivate(String seed, int index) async { - List seedBytes = hex.decode(seed); - KeyData data = await ED25519_HD_KEY.derivePath("m/44'/165'/$index'", seedBytes); - return hex.encode(data.key); - } - - @override - Future hdSeedToAddress(String seed, int index) async { - return ND.NanoAccounts.createAccount( - ND.NanoAccountType.NANO, privateKeyToPublic(await hdSeedToPrivate(seed, index))); - } - - @override - Future uniSeedToAddress(String seed, int index, String type) { - if (type == "standard") { - return Future.value(seedToAddress(seed, index)); - } else if (type == "hd") { - return hdSeedToAddress(seed, index); - } else { - throw Exception('Unknown seed type'); - } - } - - @override - Future uniSeedToPrivate(String seed, int index, String type) { - if (type == "standard") { - return Future.value(seedToPrivate(seed, index)); - } else if (type == "hd") { - return hdSeedToPrivate(seed, index); - } else { - throw Exception('Unknown seed type'); - } - } @override bool isValidBip39Seed(String seed) { - // Ensure seed is 128 characters long - if (seed.length != 128) { - return false; - } - // Ensure seed only contains hex characters, 0-9;A-F - return ND.NanoHelpers.isHexString(seed); + return NanoDerivations.isValidBip39Seed(seed); } // number util: @@ -309,92 +203,20 @@ class CWNanoUtil extends NanoUtil { BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); BigInt rawPerXMR = BigInt.parse("1000000000000"); BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); - // static BigInt convertXMRtoNano = BigInt.parse("1000000000000000000000000000"); - /// Convert raw to ban and return as BigDecimal - /// - /// @param raw 100000000000000000000000000000 - /// @return Decimal value 1.000000000000000000000000000000 - /// - Decimal _getRawAsDecimal(String? raw, BigInt? rawPerCur) { - rawPerCur ??= rawPerNano; - final Decimal amount = Decimal.parse(raw.toString()); - final Decimal result = (amount / Decimal.parse(rawPerCur.toString())).toDecimal(); - return result; - } - - @override - String getRawAsDecimalString(String? raw, BigInt? rawPerCur) { - final Decimal result = _getRawAsDecimal(raw, rawPerCur); - return result.toString(); - } - - @override - String truncateDecimal(Decimal input, {int digits = maxDecimalDigits}) { - Decimal bigger = input.shift(digits); - bigger = bigger.floor(); // chop off the decimal: 1.059 -> 1.05 - bigger = bigger.shift(-digits); - return bigger.toString(); - } - - /// Return raw as a NANO amount. - /// - /// @param raw 100000000000000000000000000000 - /// @returns 1 - /// @override String getRawAsUsableString(String? raw, BigInt rawPerCur) { - final String res = - truncateDecimal(_getRawAsDecimal(raw, rawPerCur), digits: maxDecimalDigits + 9); - - if (raw == null || raw == "0" || raw == "00000000000000000000000000000000") { - return "0"; - } - - if (!res.contains(".")) { - return res; - } - - final String numAmount = res.split(".")[0]; - String decAmount = res.split(".")[1]; - - // truncate: - if (decAmount.length > maxDecimalDigits) { - decAmount = decAmount.substring(0, maxDecimalDigits); - // remove trailing zeros: - decAmount = decAmount.replaceAllMapped(RegExp(r'0+$'), (Match match) => ''); - if (decAmount.isEmpty) { - return numAmount; - } - } - - return "$numAmount.$decAmount"; + return NanoAmounts.getRawAsUsableString(raw, rawPerCur); } @override String getRawAccuracy(String? raw, BigInt rawPerCur) { - final String rawString = getRawAsUsableString(raw, rawPerCur); - final String rawDecimalString = _getRawAsDecimal(raw, rawPerCur).toString(); - - if (raw == null || raw.isEmpty || raw == "0") { - return ""; - } - - if (rawString != rawDecimalString) { - return "~"; - } - return ""; + return NanoAmounts.getRawAccuracy(raw, rawPerCur); } - /// Return readable string amount as raw string - /// @param amount 1.01 - /// @returns 101000000000000000000000000000 - /// @override String getAmountAsRaw(String amount, BigInt rawPerCur) { - final Decimal asDecimal = Decimal.parse(amount); - final Decimal rawDecimal = Decimal.parse(rawPerCur.toString()); - return (asDecimal * rawDecimal).toString(); + return NanoAmounts.getAmountAsRaw(amount, rawPerCur); } @override @@ -411,29 +233,29 @@ class CWNanoUtil extends NanoUtil { if (seedKey != null) { if (seedKey.length == 64) { try { - mnemonic = nanoUtil!.seedToMnemonic(seedKey); + mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey); } catch (e) { print("not a valid 'nano' seed key"); } } if (derivationType == DerivationType.bip39) { - publicAddress = await hdSeedToAddress(seedKey, 0); + publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); } else if (derivationType == DerivationType.nano) { - publicAddress = await seedToAddress(seedKey, 0); + publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); } } if (derivationType == DerivationType.bip39) { if (mnemonic != null) { - seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); - publicAddress = await hdSeedToAddress(seedKey, 0); + seedKey = await NanoDerivations.hdMnemonicListToSeed(mnemonic.split(' ')); + publicAddress = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); } } if (derivationType == DerivationType.nano) { if (mnemonic != null) { - seedKey = await mnemonicToSeed(mnemonic); - publicAddress = await seedToAddress(seedKey, 0); + seedKey = await NanoDerivations.standardMnemonicToSeed(mnemonic); + publicAddress = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); } } @@ -461,7 +283,7 @@ class CWNanoUtil extends NanoUtil { return [DerivationType.bip39]; } else if (seedKey?.length == 64) { try { - mnemonic = nanoUtil!.seedToMnemonic(seedKey!); + mnemonic = NanoDerivations.standardSeedToMnemonic(seedKey!); } catch (e) { print("not a valid 'nano' seed key"); } @@ -475,19 +297,19 @@ class CWNanoUtil extends NanoUtil { nanoClient.connect(node); if (mnemonic != null) { - seedKey = await hdMnemonicListToSeed(mnemonic.split(' ')); - publicAddressBip39 = await hdSeedToAddress(seedKey, 0); + seedKey = await NanoDerivations.hdMnemonicListToSeed(mnemonic.split(' ')); + publicAddressBip39 = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); - seedKey = await mnemonicToSeed(mnemonic); - publicAddressStandard = await seedToAddress(seedKey, 0); + seedKey = await NanoDerivations.standardMnemonicToSeed(mnemonic); + publicAddressStandard = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); } else if (seedKey != null) { try { - publicAddressBip39 = await hdSeedToAddress(seedKey, 0); + publicAddressBip39 = await NanoDerivations.hdSeedToAddress(seedKey, index: 0); } catch (e) { return [DerivationType.nano]; } try { - publicAddressStandard = await seedToAddress(seedKey, 0); + publicAddressStandard = await NanoDerivations.standardSeedToAddress(seedKey, index: 0); } catch (e) { return [DerivationType.bip39]; } diff --git a/lib/nostr/nostr_api.dart b/lib/nostr/nostr_api.dart new file mode 100644 index 000000000..7c0eea5ef --- /dev/null +++ b/lib/nostr/nostr_api.dart @@ -0,0 +1,142 @@ +import 'dart:convert'; + +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/nostr/nostr_user.dart'; +import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; +import 'package:cake_wallet/src/widgets/picker.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:flutter/material.dart'; +import 'package:nostr_tools/nostr_tools.dart'; + +class NostrProfileHandler { + static final relayToDomainMap = { + 'relay.snort.social': 'snort.social', + }; + + static Nip05 _nip05 = Nip05(); + + static Future queryProfile(BuildContext context, String nip05Address) async { + var profile = await _nip05.queryProfile(nip05Address); + if (profile?.pubkey != null) { + if (profile?.relays?.isNotEmpty == true) { + return profile; + } else { + await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relays_message); + } + } + return null; + } + + static Future processRelays( + BuildContext context, ProfilePointer profile, String nip05Address) async { + String userDomain = _extractDomain(nip05Address); + const int metaData = 0; + + for (String relayUrl in profile.relays ?? []) { + final relayDomain = _getDomainFromRelayUrl(relayUrl); + final formattedRelayDomain = relayToDomainMap[relayDomain] ?? relayDomain; + if (formattedRelayDomain == userDomain) { + final userDomainData = await _fetchInfoFromRelay(relayUrl, profile.pubkey, [metaData]); + if (userDomainData != null) { + return userDomainData; + } + } + } + await _showErrorDialog(context, S.of(context).no_relays, S.of(context).no_relay_on_domain); + + String? chosenRelayUrl = await _showRelayChoiceDialog(context, profile.relays ?? []); + if (chosenRelayUrl != null) { + final userData = await _fetchInfoFromRelay(chosenRelayUrl, profile.pubkey, [metaData]); + if (userData != null) { + return userData; + } + } + + return null; + } + + static Future _fetchInfoFromRelay( + String relayUrl, String userPubKey, List kinds) async { + try { + final relay = RelayApi(relayUrl: relayUrl); + final stream = await relay.connect(); + + relay.sub([ + Filter( + kinds: kinds, + authors: [userPubKey], + ) + ]); + + await for (var message in stream) { + if (message.type == 'EVENT') { + final event = message.message as Event; + + final eventContent = json.decode(event.content) as Map; + + final userMetadata = UserMetadata.fromJson(eventContent); + relay.close(); + return userMetadata; + } + } + + relay.close(); + return null; + } catch (e) { + print('[!] Error with relay $relayUrl: $e'); + return null; + } + } + + static Future _showErrorDialog( + BuildContext context, String title, String errorMessage) async { + if (context.mounted) { + await showPopUp( + context: context, + builder: (BuildContext dialogContext) { + return AlertWithOneAction( + alertTitle: title, + alertContent: errorMessage, + buttonText: S.of(dialogContext).ok, + buttonAction: () => Navigator.of(dialogContext).pop(), + ); + }, + ); + } + } + + static String _extractDomain(String nip05Address) { + var parts = nip05Address.split('@'); + return parts.length == 2 ? parts[1] : ''; + } + + static String _getDomainFromRelayUrl(String relayUrl) { + try { + var uri = Uri.parse(relayUrl); + return uri.host; + } catch (e) { + print('Error parsing URL: $e'); + return ''; + } + } + + static Future _showRelayChoiceDialog(BuildContext context, List relays) async { + String? selectedRelay; + + if (context.mounted) { + await showPopUp( + context: context, + builder: (BuildContext dialogContext) { + return Picker( + selectedAtIndex: 0, + title: S.of(dialogContext).choose_relay, + items: relays, + onItemSelected: (String relay) => selectedRelay = relay, + ); + }, + ); + } + + return selectedRelay; + } +} diff --git a/lib/nostr/nostr_user.dart b/lib/nostr/nostr_user.dart new file mode 100644 index 000000000..4b1694a11 --- /dev/null +++ b/lib/nostr/nostr_user.dart @@ -0,0 +1,35 @@ + +class UserMetadata { + final String name; + final String lnurl; + final String email; + final String picture; + final String about; + final String nip05; + final String banner; + final String website; + + UserMetadata({ + required this.name, + required this.lnurl, + required this.email, + required this.picture, + required this.about, + required this.nip05, + required this.banner, + required this.website, + }); + + factory UserMetadata.fromJson(Map json) { + return UserMetadata( + name: json['name'] as String? ?? '', + lnurl: json['lud06'] as String? ?? '', + email: json['lud16'] as String? ?? '', + picture: json['picture'] as String? ?? '', + about: json['about'] as String? ?? '', + nip05: json['nip05'] as String? ?? '', + banner: json['banner'] as String? ?? '', + website: json['website'] as String? ?? '', + ); + } +} \ No newline at end of file diff --git a/lib/polygon/cw_polygon.dart b/lib/polygon/cw_polygon.dart index 066b29d43..6e5fbe2c6 100644 --- a/lib/polygon/cw_polygon.dart +++ b/lib/polygon/cw_polygon.dart @@ -2,17 +2,17 @@ part of 'polygon.dart'; class CWPolygon extends Polygon { @override - List getPolygonWordList(String language) => EthereumMnemonics.englishWordlist; + List getPolygonWordList(String language) => EVMChainMnemonics.englishWordlist; WalletService createPolygonWalletService(Box walletInfoSource) => - PolygonWalletService(walletInfoSource); + PolygonWalletService(walletInfoSource, client: PolygonClient()); @override WalletCredentials createPolygonNewWalletCredentials({ required String name, WalletInfo? walletInfo, }) => - PolygonNewWalletCredentials(name: name, walletInfo: walletInfo); + EVMChainNewWalletCredentials(name: name, walletInfo: walletInfo); @override WalletCredentials createPolygonRestoreWalletFromSeedCredentials({ @@ -20,7 +20,7 @@ class CWPolygon extends Polygon { required String mnemonic, required String password, }) => - PolygonRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); + EVMChainRestoreWalletFromSeedCredentials(name: name, password: password, mnemonic: mnemonic); @override WalletCredentials createPolygonRestoreWalletFromPrivateKey({ @@ -28,37 +28,37 @@ class CWPolygon extends Polygon { required String privateKey, required String password, }) => - PolygonRestoreWalletFromPrivateKey(name: name, password: password, privateKey: privateKey); + EVMChainRestoreWalletFromPrivateKey(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; + final privateKeyHolder = (wallet as PolygonWallet).evmChainPrivateKey; String stringKey = bytesToHex(privateKeyHolder.privateKey); return stringKey; } @override String getPublicKey(WalletBase wallet) { - final privateKeyInUnitInt = (wallet as PolygonWallet).polygonPrivateKey; + final privateKeyInUnitInt = (wallet as PolygonWallet).evmChainPrivateKey; final publicKey = privateKeyInUnitInt.address.hex; return publicKey; } @override - TransactionPriority getDefaultTransactionPriority() => PolygonTransactionPriority.medium; + TransactionPriority getDefaultTransactionPriority() => EVMChainTransactionPriority.medium; @override - TransactionPriority getPolygonTransactionPrioritySlow() => PolygonTransactionPriority.slow; + TransactionPriority getPolygonTransactionPrioritySlow() => EVMChainTransactionPriority.slow; @override - List getTransactionPriorities() => PolygonTransactionPriority.all; + List getTransactionPriorities() => EVMChainTransactionPriority.all; @override TransactionPriority deserializePolygonTransactionPriority(int raw) => - PolygonTransactionPriority.deserialize(raw: raw); + EVMChainTransactionPriority.deserialize(raw: raw); Object createPolygonTransactionCredentials( List outputs, { @@ -66,7 +66,7 @@ class CWPolygon extends Polygon { required CryptoCurrency currency, int? feeRate, }) => - PolygonTransactionCredentials( + EVMChainTransactionCredentials( outputs .map((out) => OutputInfo( fiatAmount: out.fiatAmount, @@ -78,7 +78,7 @@ class CWPolygon extends Polygon { isParsedAddress: out.isParsedAddress, formattedCryptoAmount: out.formattedCryptoAmount)) .toList(), - priority: priority as PolygonTransactionPriority, + priority: priority as EVMChainTransactionPriority, currency: currency, feeRate: feeRate, ); @@ -89,15 +89,15 @@ class CWPolygon extends Polygon { required CryptoCurrency currency, required int feeRate, }) => - PolygonTransactionCredentials( + EVMChainTransactionCredentials( outputs, - priority: priority as PolygonTransactionPriority?, + priority: priority as EVMChainTransactionPriority?, currency: currency, feeRate: feeRate, ); @override - int formatterPolygonParseAmount(String amount) => PolygonFormatter.parsePolygonAmount(amount); + int formatterPolygonParseAmount(String amount) => EVMChainFormatter.parseEVMChainAmount(amount); @override double formatterPolygonAmountToDouble( @@ -105,7 +105,7 @@ class CWPolygon extends Polygon { assert(transaction != null || amount != null); if (transaction != null) { - transaction as PolygonTransactionInfo; + transaction as EVMChainTransactionInfo; return transaction.ethAmount / BigInt.from(10).pow(transaction.exponent); } else { return (amount!) / BigInt.from(10).pow(exponent); @@ -134,7 +134,7 @@ class CWPolygon extends Polygon { @override CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction) { - transaction as PolygonTransactionInfo; + transaction as EVMChainTransactionInfo; if (transaction.tokenSymbol == CryptoCurrency.maticpoly.title) { return CryptoCurrency.maticpoly; } @@ -146,7 +146,7 @@ class CWPolygon extends Polygon { @override void updatePolygonScanUsageState(WalletBase wallet, bool isEnabled) { - (wallet as PolygonWallet).updatePolygonScanUsageState(isEnabled); + (wallet as PolygonWallet).updateScanProviderUsageState(isEnabled); } @override diff --git a/lib/reactions/on_current_wallet_change.dart b/lib/reactions/on_current_wallet_change.dart index 42fbd182e..ade9927ff 100644 --- a/lib/reactions/on_current_wallet_change.dart +++ b/lib/reactions/on_current_wallet_change.dart @@ -68,7 +68,8 @@ void startCurrentWalletChangeReaction( .get() .setInt(PreferencesKey.currentWalletType, serializeToInt(wallet.type)); - if (wallet.type == WalletType.monero) { + if (wallet.type == WalletType.monero || wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin || wallet.type == WalletType.bitcoinCash ) { _setAutoGenerateSubaddressStatus(wallet, settingsStore); } diff --git a/lib/src/screens/buy/buy_webview_page.dart b/lib/src/screens/buy/buy_webview_page.dart index 6f7a39322..829bff3d9 100644 --- a/lib/src/screens/buy/buy_webview_page.dart +++ b/lib/src/screens/buy/buy_webview_page.dart @@ -1,10 +1,7 @@ import 'dart:async'; -import 'dart:io'; -import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/buy/moonpay/moonpay_provider.dart'; import 'package:cake_wallet/buy/wyre/wyre_buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/palette.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/store/dashboard/orders_store.dart'; import 'package:cake_wallet/view_model/buy/buy_view_model.dart'; @@ -72,10 +69,10 @@ class BuyWebViewPageBodyState extends State { Widget build(BuildContext context) { return InAppWebView( key: _webViewkey, - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(transparentBackground: true), + initialSettings: InAppWebViewSettings( + transparentBackground: true, ), - initialUrlRequest: URLRequest(url: Uri.tryParse(widget.url ?? '')), + initialUrlRequest: URLRequest(url: WebUri(widget.url ?? '')), onWebViewCreated: (InAppWebViewController controller) => setState(() => _webViewController = controller)); } diff --git a/lib/src/screens/buy/webview_page.dart b/lib/src/screens/buy/webview_page.dart index 205b87c47..0af5952c4 100644 --- a/lib/src/screens/buy/webview_page.dart +++ b/lib/src/screens/buy/webview_page.dart @@ -1,6 +1,7 @@ -import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; +import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:flutter/material.dart'; import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -16,13 +17,14 @@ class WebViewPage extends BasePage { @override Widget body(BuildContext context) { - return WebViewPageBody(_url); + return WebViewPageBody(_title, _url); } } class WebViewPageBody extends StatefulWidget { - WebViewPageBody(this.uri); + WebViewPageBody(this.title, this.uri); + final String title; final Uri uri; @override @@ -35,21 +37,42 @@ class WebViewPageBodyState extends State { @override Widget build(BuildContext context) { return InAppWebView( - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(transparentBackground: true), + initialSettings: InAppWebViewSettings( + transparentBackground: true, ), - initialUrlRequest: URLRequest(url: widget.uri), - androidOnPermissionRequest: (_, __, resources) async { + initialUrlRequest: URLRequest(url: WebUri.uri(widget.uri)), + onPermissionRequest: (controller, request) async { bool permissionGranted = await Permission.camera.status == PermissionStatus.granted; if (!permissionGranted) { + final bool userConsent = await showPopUp( + context: context, + builder: (BuildContext context) { + return AlertWithTwoActions( + alertTitle: S.of(context).privacy, + alertContent: S.of(context).camera_consent(widget.title), + rightButtonText: S.of(context).agree, + leftButtonText: S.of(context).cancel, + actionRightButton: () => Navigator.of(context).pop(true), + actionLeftButton: () => Navigator.of(context).pop(false)); + }) ?? + false; + + /// if user did NOT give the consent then return permission denied + if (!userConsent) { + return PermissionResponse( + resources: request.resources, + action: PermissionResponseAction.DENY, + ); + } + permissionGranted = await Permission.camera.request().isGranted; } - return PermissionRequestResponse( - resources: resources, + return PermissionResponse( + resources: request.resources, action: permissionGranted - ? PermissionRequestResponseAction.GRANT - : PermissionRequestResponseAction.DENY, + ? PermissionResponseAction.GRANT + : PermissionResponseAction.DENY, ); }, ); 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 59c31aa62..a81a3f6e4 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 @@ -123,8 +123,8 @@ class _DesktopWalletSelectionDropDownState extends State Navigator.of(context).pop(false), - actionRightButton: () => Navigator.of(context).pop(true)); + actionLeftButton: () => Navigator.of(dialogContext).pop(false), + actionRightButton: () => Navigator.of(dialogContext).pop(true)); }) ?? false; @@ -166,12 +166,16 @@ class _DesktopWalletSelectionDropDownState extends State dashboardViewModel.isAutoGenerateSubaddressesEnabled + return SelectButton( + text: addressListViewModel.buttonTitle, + onTap: () async => dashboardViewModel.isAutoGenerateSubaddressesEnabled && + (WalletType.monero == addressListViewModel.wallet.type || + WalletType.haven == addressListViewModel.wallet.type) ? await showPopUp( - context: context, builder: (_) => getIt.get()) + context: context, + builder: (_) => getIt.get()) : Navigator.of(context).pushNamed(Routes.receive), - child: Container( - height: 50, - padding: EdgeInsets.only(left: 24, right: 12), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(25)), - border: Border.all( - color: - Theme.of(context).extension()!.cardBorderColor, - width: 1), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Observer( - builder: (_) { - String label = addressListViewModel.hasAccounts - ? S.of(context).accounts_subaddresses - : S.of(context).addresses; - - if (dashboardViewModel.isAutoGenerateSubaddressesEnabled) { - label = addressListViewModel.hasAccounts - ? S.of(context).accounts - : S.of(context).account; - } - return Text( - label, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: Theme.of(context) - .extension()! - .textColor), - ); - }, - ), - Icon( - Icons.arrow_forward_ios, - size: 14, - color: Theme.of(context).extension()!.textColor, - ) - ], - ), - ), + textColor: Theme.of(context).extension()!.textColor, + color: Theme.of(context).extension()!.syncedBackgroundColor, + borderColor: Theme.of(context).extension()!.cardBorderColor, + arrowColor: Theme.of(context).extension()!.textColor, + textSize: 14, + height: 50, ); } else if (dashboardViewModel.isAutoGenerateSubaddressesEnabled || - addressListViewModel.showElectrumAddressDisclaimer) { + addressListViewModel.isElectrumWallet) { return Text(S.of(context).electrum_address_disclaimer, textAlign: TextAlign.center, style: TextStyle( diff --git a/lib/src/screens/dashboard/pages/balance_page.dart b/lib/src/screens/dashboard/pages/balance_page.dart index 283d2171d..453adccf5 100644 --- a/lib/src/screens/dashboard/pages/balance_page.dart +++ b/lib/src/screens/dashboard/pages/balance_page.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/reactions/wallet_connect.dart'; @@ -14,6 +16,7 @@ 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/dashboard/nft_view_model.dart'; +import 'package:cw_core/crypto_currency.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -200,7 +203,7 @@ class CryptoBalanceWidget extends StatelessWidget { additionalFiatBalance: balance.fiatAdditionalBalance, frozenBalance: balance.frozenBalance, frozenFiatBalance: balance.fiatFrozenBalance, - currency: balance.formattedAssetTitle, + currency: balance.asset, hasAdditionalBalance: dashboardViewModel.balanceViewModel.hasAdditionalBalance, ); @@ -216,7 +219,7 @@ class CryptoBalanceWidget extends StatelessWidget { } class BalanceRowWidget extends StatelessWidget { - const BalanceRowWidget({ + BalanceRowWidget({ required this.availableBalanceLabel, required this.availableBalance, required this.availableFiatBalance, @@ -238,7 +241,7 @@ class BalanceRowWidget extends StatelessWidget { final String additionalFiatBalance; final String frozenBalance; final String frozenFiatBalance; - final String currency; + final CryptoCurrency currency; final bool hasAdditionalBalance; // void _showBalanceDescription(BuildContext context) { @@ -262,13 +265,13 @@ class BalanceRowWidget extends StatelessWidget { color: Theme.of(context).extension()!.syncedBackgroundColor, ), child: Container( - margin: const EdgeInsets.only(top: 16, left: 24, right: 24, bottom: 24), + margin: const EdgeInsets.only(top: 16, left: 24, right: 8, bottom: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, children: [ GestureDetector( behavior: HitTestBehavior.opaque, @@ -325,13 +328,48 @@ class BalanceRowWidget extends StatelessWidget { ], ), ), - Text(currency, - style: TextStyle( - fontSize: 28, - fontFamily: 'Lato', - fontWeight: FontWeight.w800, - color: Theme.of(context).extension()!.assetTitleColor, - height: 1)), + SizedBox( + width: min(MediaQuery.of(context).size.width * 0.2, 100), + child: Center( + child: Column( + children: [ + currency.iconPath != null + ? Container( + child: Image.asset( + currency.iconPath!, + height: 40.0, + width: 40.0, + ), + ) + : Container( + height: 30.0, + width: 30.0, + child: Center( + child: Text( + currency.title.substring(0, min(currency.title.length, 2)), + style: TextStyle(fontSize: 11), + ), + ), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.grey.shade400, + ), + ), + const SizedBox(height: 10), + Text( + currency.title, + style: TextStyle( + fontSize: 15, + fontFamily: 'Lato', + fontWeight: FontWeight.w800, + color: Theme.of(context).extension()!.assetTitleColor, + height: 1, + ), + ), + ], + ), + ), + ), ], ), if (frozenBalance.isNotEmpty) @@ -374,7 +412,9 @@ class BalanceRowWidget extends StatelessWidget { fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, + color: Theme.of(context) + .extension()! + .balanceAmountColor, height: 1, ), maxLines: 1, @@ -388,7 +428,7 @@ class BalanceRowWidget extends StatelessWidget { fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context).extension()!.textColor, + color: Theme.of(context).extension()!.textColor, height: 1, ), ), diff --git a/lib/src/screens/dashboard/pages/nft_details_page.dart b/lib/src/screens/dashboard/pages/nft_details_page.dart index 4bddb550b..bb642fd4b 100644 --- a/lib/src/screens/dashboard/pages/nft_details_page.dart +++ b/lib/src/screens/dashboard/pages/nft_details_page.dart @@ -101,26 +101,26 @@ class NFTDetailsPage extends BasePage { SizedBox(height: 16), _NFTSingleInfoTile( infoType: S.current.name, - infoValue: nftAsset.normalizedMetadata?.name ?? '', + infoValue: nftAsset.normalizedMetadata?.name ?? '---', ), if (nftAsset.normalizedMetadata?.description != null) ...[ SizedBox(height: 16), _NFTSingleInfoTile( - infoType: 'Description', - infoValue: nftAsset.normalizedMetadata?.description ?? '', + infoType: S.current.description, + infoValue: nftAsset.normalizedMetadata?.description ?? '---', ), ], SizedBox(height: 16), _NFTSingleInfoTile( - infoType: 'Contract Name', - infoValue: nftAsset.name ?? '', + infoType: S.current.contractName, + infoValue: nftAsset.name ?? '---', ), SizedBox(height: 8), _NFTSingleInfoTile( - infoType: 'Contract Symbol', - infoValue: nftAsset.symbol ?? '', + infoType: S.current.contractSymbol, + infoValue: nftAsset.symbol ?? '---', ), ], ), diff --git a/lib/src/screens/dashboard/pages/transactions_page.dart b/lib/src/screens/dashboard/pages/transactions_page.dart index 1be8a2a65..c983b1c37 100644 --- a/lib/src/screens/dashboard/pages/transactions_page.dart +++ b/lib/src/screens/dashboard/pages/transactions_page.dart @@ -49,7 +49,7 @@ class TransactionsPage extends StatelessWidget { onTap: () => Navigator.of(context).pushNamed(Routes.webViewPage, arguments: [ '', Uri.parse( - 'https://guides.cakewallet.com/docs/bugs-service-status/why_are_my_funds_not_appearing/') + 'https://guides.cakewallet.com/docs/FAQ/why_are_my_funds_not_appearing/') ]), title: S.of(context).syncing_wallet_alert_title, subTitle: S.of(context).syncing_wallet_alert_content, diff --git a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart index 0be027f44..e7391b970 100644 --- a/lib/src/screens/dashboard/widgets/nft_tile_widget.dart +++ b/lib/src/screens/dashboard/widgets/nft_tile_widget.dart @@ -13,22 +13,15 @@ class NFTTileWidget extends StatelessWidget { @override Widget build(BuildContext context) { return InkWell( - onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, - arguments: nftAsset), + onTap: () => Navigator.pushNamed(context, Routes.nftDetailsPage, arguments: nftAsset), child: Container( width: double.infinity, margin: const EdgeInsets.only(left: 16, right: 16), decoration: BoxDecoration( borderRadius: BorderRadius.circular(30.0), border: Border.all( - color: Theme.of(context) - .extension()! - .cardBorderColor, - width: 1, - ), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, + color: Theme.of(context).extension()!.cardBorderColor, width: 1), + color: Theme.of(context).extension()!.syncedBackgroundColor, ), child: Row( children: [ @@ -40,14 +33,10 @@ class NFTTileWidget extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(16.0), border: Border.all( - color: Theme.of(context) - .extension()! - .cardBorderColor, + color: Theme.of(context).extension()!.cardBorderColor, width: 1, ), - color: Theme.of(context) - .extension()! - .syncedBackgroundColor, + color: Theme.of(context).extension()!.syncedBackgroundColor, ), child: NFTImageWidget( imageUrl: nftAsset.normalizedMetadata?.imageUrl, @@ -59,27 +48,23 @@ class NFTTileWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - '${nftAsset.name ?? ''} - ${nftAsset.symbol ?? ''}', + '${nftAsset.name ?? '---'} - ${nftAsset.symbol ?? '---'}', style: TextStyle( fontSize: 12, fontFamily: 'Lato', fontWeight: FontWeight.w400, - color: Theme.of(context) - .extension()! - .labelTextColor, + color: Theme.of(context).extension()!.labelTextColor, height: 1, ), ), SizedBox(height: 8), Text( - nftAsset.normalizedMetadata?.name ?? nftAsset.name ?? "", + nftAsset.normalizedMetadata?.name ?? nftAsset.name ?? "---", style: TextStyle( fontSize: 20, fontFamily: 'Lato', fontWeight: FontWeight.w900, - color: Theme.of(context) - .extension()! - .assetTitleColor, + color: Theme.of(context).extension()!.assetTitleColor, height: 1, ), ), @@ -92,4 +77,3 @@ class NFTTileWidget extends StatelessWidget { ); } } - diff --git a/lib/src/screens/exchange/exchange_page.dart b/lib/src/screens/exchange/exchange_page.dart index b3fbd19a7..94b51301c 100644 --- a/lib/src/screens/exchange/exchange_page.dart +++ b/lib/src/screens/exchange/exchange_page.dart @@ -557,7 +557,7 @@ class ExchangePage extends BasePage { } Future fetchParsedAddress(BuildContext context, String domain, String ticker) async { - final parsedAddress = await getIt.get().resolve(domain, ticker); + final parsedAddress = await getIt.get().resolve(context, domain, ticker); final address = await extractAddressFromParsed(context, parsedAddress); return address; } diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index e72b0f0a8..c4dcae32c 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,3 +1,4 @@ +import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'dart:ui'; import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart'; @@ -261,7 +262,7 @@ class ExchangeTradeState extends State { fee: S.of(popupContext).send_fee, feeValue: widget.exchangeTradeViewModel.sendViewModel .pendingTransaction!.feeFormatted, - rightButtonText: S.of(popupContext).ok, + rightButtonText: S.of(popupContext).send, leftButtonText: S.of(popupContext).cancel, actionRightButton: () async { Navigator.of(popupContext).pop(); @@ -282,15 +283,17 @@ class ExchangeTradeState extends State { if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithOneAction( - alertTitle: S.of(popupContext).sending, - alertContent: S.of(popupContext).transaction_sent, - buttonText: S.of(popupContext).ok, - buttonAction: () => Navigator.of(popupContext).pop()); - }); + if (context.mounted) { + showPopUp( + context: context, + builder: (BuildContext popupContext) { + return AlertWithOneAction( + alertTitle: S.of(popupContext).sending, + alertContent: S.of(popupContext).transaction_sent, + buttonText: S.of(popupContext).ok, + buttonAction: () => Navigator.of(popupContext).pop()); + }); + } }); } }); @@ -343,7 +346,11 @@ class ExchangeTradeState extends State { bottom: 24, child: PrimaryButton( onPressed: () { - Navigator.of(popupContext).pop(); + Navigator.pushNamedAndRemoveUntil( + popupContext, + Routes.dashboard, + (route) => false, + ); RequestReviewHandler.requestReview(); }, text: S.of(popupContext).got_it, diff --git a/lib/src/screens/new_wallet/new_wallet_type_page.dart b/lib/src/screens/new_wallet/new_wallet_type_page.dart index 225e5b82d..8c8a94a7e 100644 --- a/lib/src/screens/new_wallet/new_wallet_type_page.dart +++ b/lib/src/screens/new_wallet/new_wallet_type_page.dart @@ -1,12 +1,14 @@ import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart'; +import 'package:cake_wallet/src/screens/setup_2fa/widgets/popup_cancellable_alert.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/search_bar_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/wallet_types.g.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; @@ -26,15 +28,18 @@ class NewWalletTypePage extends BasePage { @override Widget body(BuildContext context) => WalletTypeForm( - onTypeSelected: onTypeSelected, - walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage); + onTypeSelected: onTypeSelected, + walletImage: currentTheme.type == ThemeType.dark ? walletTypeImage : walletTypeLightImage, + isCreate: isCreate, + ); } class WalletTypeForm extends StatefulWidget { - WalletTypeForm({required this.onTypeSelected, required this.walletImage}); + WalletTypeForm({required this.onTypeSelected, required this.walletImage, required this.isCreate}); final void Function(BuildContext, WalletType) onTypeSelected; final Image walletImage; + final bool isCreate; @override WalletTypeFormState createState() => WalletTypeFormState(); @@ -70,7 +75,8 @@ class WalletTypeFormState extends State { Widget build(BuildContext context) { return Center( child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), + constraints: + BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint), child: Column( children: [ Padding( @@ -128,6 +134,19 @@ class WalletTypeFormState extends State { throw Exception('Wallet Type is not selected yet.'); } + if (selected == WalletType.haven && widget.isCreate) { + return await showPopUp( + context: context, + builder: (BuildContext context) { + return PopUpCancellableAlertDialog( + contentText: S.of(context).pause_wallet_creation, + actionButtonText: S.of(context).ok, + buttonAction: () => Navigator.of(context).pop(), + ); + }, + ); + } + widget.onTypeSelected(context, selected!); } } diff --git a/lib/src/screens/new_wallet/widgets/select_button.dart b/lib/src/screens/new_wallet/widgets/select_button.dart index e220b281e..d94c9767d 100644 --- a/lib/src/screens/new_wallet/widgets/select_button.dart +++ b/lib/src/screens/new_wallet/widgets/select_button.dart @@ -11,29 +11,37 @@ class SelectButton extends StatelessWidget { this.isSelected = false, this.showTrailingIcon = true, this.height = 60, + this.textSize = 18, + this.color, + this.textColor, + this.arrowColor, + this.borderColor, }); final Image? image; final String text; + final double textSize; final bool isSelected; final VoidCallback onTap; final bool showTrailingIcon; final double height; + final Color? color; + final Color? textColor; + final Color? arrowColor; + final Color? borderColor; @override Widget build(BuildContext context) { - final color = isSelected - ? Colors.green - : Theme.of(context).cardColor; - final textColor = isSelected + final backgroundColor = color ?? (isSelected ? Colors.green : Theme.of(context).cardColor); + final effectiveTextColor = textColor ?? (isSelected ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.buttonTextColor; - final arrowColor = isSelected + : Theme.of(context).extension()!.buttonTextColor); + final effectiveArrowColor = arrowColor ?? (isSelected ? Theme.of(context).extension()!.restoreWalletButtonTextColor - : Theme.of(context).extension()!.titlesColor; + : Theme.of(context).extension()!.titlesColor); final selectArrowImage = Image.asset('assets/images/select_arrow.png', - color: arrowColor); + color: effectiveArrowColor); return GestureDetector( onTap: onTap, @@ -44,7 +52,9 @@ class SelectButton extends StatelessWidget { alignment: Alignment.center, decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(30)), - color: color + color: backgroundColor, + border: borderColor != null ? Border.all(color: borderColor!) : null, + ), child: Row( mainAxisSize: MainAxisSize.max, @@ -63,9 +73,9 @@ class SelectButton extends StatelessWidget { child: Text( text, style: TextStyle( - fontSize: 18, + fontSize: textSize, fontWeight: FontWeight.w500, - color: textColor + color: effectiveTextColor, ), ), ) diff --git a/lib/src/screens/receive/receive_page.dart b/lib/src/screens/receive/receive_page.dart index 87b668e34..75719d123 100644 --- a/lib/src/screens/receive/receive_page.dart +++ b/lib/src/screens/receive/receive_page.dart @@ -49,7 +49,7 @@ class ReceivePage extends BasePage { bool get gradientBackground => true; @override - bool get resizeToAvoidBottomInset => false; + bool get resizeToAvoidBottomInset => true; final FocusNode _cryptoAmountFocus; @@ -99,10 +99,11 @@ class ReceivePage extends BasePage { @override Widget body(BuildContext context) { + final isElectrumWallet = addressListViewModel.isElectrumWallet; return (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven || addressListViewModel.type == WalletType.nano || - addressListViewModel.type == WalletType.banano) + isElectrumWallet) ? KeyboardActions( config: KeyboardActionsConfig( keyboardActionsPlatform: KeyboardActionsPlatform.IOS, @@ -140,7 +141,9 @@ class ReceivePage extends BasePage { if (item is WalletAccountListHeader) { cell = HeaderTile( - onTap: () async { + showTrailingButton: true, + walletAddressListViewModel: addressListViewModel, + trailingButtonTap: () async { if (addressListViewModel.type == WalletType.monero || addressListViewModel.type == WalletType.haven) { await showPopUp( @@ -153,7 +156,7 @@ class ReceivePage extends BasePage { } }, title: S.of(context).accounts, - icon: Icon( + trailingIcon: Icon( Icons.arrow_forward_ios, size: 14, color: Theme.of(context).extension()!.iconsColor, @@ -161,16 +164,21 @@ class ReceivePage extends BasePage { } if (item is WalletAddressListHeader) { - cell = HeaderTile( - onTap: () => - Navigator.of(context).pushNamed(Routes.newSubaddress), - title: S.of(context).addresses, - icon: Icon( - Icons.add, - size: 20, - color: Theme.of(context).extension()!.iconsColor, - )); - } + cell = HeaderTile( + title: S.of(context).addresses, + walletAddressListViewModel: addressListViewModel, + showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled, + showSearchButton: true, + trailingButtonTap: () => + Navigator.of(context).pushNamed(Routes.newSubaddress), + trailingIcon: Icon( + Icons.add, + size: 20, + color: Theme.of(context) + .extension()! + .iconsColor, + )); + } if (item is WalletAddressListItem) { cell = Observer(builder: (_) { @@ -185,6 +193,7 @@ class ReceivePage extends BasePage { return AddressCell.fromItem(item, isCurrent: isCurrent, + hasBalance: addressListViewModel.isElectrumWallet, backgroundColor: backgroundColor, textColor: textColor, onTap: (_) => addressListViewModel.setAddress(item), diff --git a/lib/src/screens/receive/widgets/address_cell.dart b/lib/src/screens/receive/widgets/address_cell.dart index 733612e0b..a07456284 100644 --- a/lib/src/screens/receive/widgets/address_cell.dart +++ b/lib/src/screens/receive/widgets/address_cell.dart @@ -1,7 +1,8 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_slidable/flutter_slidable.dart'; +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_slidable/flutter_slidable.dart'; class AddressCell extends StatelessWidget { AddressCell( @@ -12,13 +13,18 @@ class AddressCell extends StatelessWidget { required this.backgroundColor, required this.textColor, this.onTap, - this.onEdit}); + this.onEdit, + this.txCount, + this.balance, + this.isChange = false, + this.hasBalance = false}); factory AddressCell.fromItem(WalletAddressListItem item, {required bool isCurrent, required Color backgroundColor, required Color textColor, Function(String)? onTap, + bool hasBalance = false, Function()? onEdit}) => AddressCell( address: item.address, @@ -28,7 +34,11 @@ class AddressCell extends StatelessWidget { backgroundColor: backgroundColor, textColor: textColor, onTap: onTap, - onEdit: onEdit); + onEdit: onEdit, + txCount: item.txCount, + balance: item.balance, + isChange: item.isChange, + hasBalance: hasBalance); final String address; final String name; @@ -38,17 +48,22 @@ class AddressCell extends StatelessWidget { final Color textColor; final Function(String)? onTap; final Function()? onEdit; + final int? txCount; + final String? balance; + final bool isChange; + final bool hasBalance; - String get label { - if (name.isEmpty){ - if(address.length<=16){ - return address; - }else{ - return address.substring(0,8)+'...'+ - address.substring(address.length-8,address.length); - } - }else{ - return name; + static const int addressPreviewLength = 8; + + String get formattedAddress { + final formatIfCashAddr = address.replaceAll('bitcoincash:', ''); + + if (formatIfCashAddr.length <= (name.isNotEmpty ? 16 : 43)) { + return formatIfCashAddr; + } else { + return formatIfCashAddr.substring(0, addressPreviewLength) + + '...' + + formatIfCashAddr.substring(formatIfCashAddr.length - addressPreviewLength, formatIfCashAddr.length); } } @@ -59,41 +74,116 @@ class AddressCell extends StatelessWidget { child: Container( width: double.infinity, color: backgroundColor, - padding: EdgeInsets.only(left: 24, right: 24, top: 28, bottom: 28), - child: Text( - label, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: TextStyle( - fontSize: 14, - color: textColor, - ), + padding: EdgeInsets.only(left: 24, right: 24, top: 20, bottom: 20), + child: Row( + children: [ + Expanded( + child: Column( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.max, + children: [ + if (isChange) + Padding( + padding: const EdgeInsets.only(right: 8.0), + child: Container( + height: 20, + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(8.5)), + color: textColor), + alignment: Alignment.center, + child: Text( + S.of(context).unspent_change, + style: TextStyle( + color: backgroundColor, + fontSize: 10, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + if (name.isNotEmpty) + Text( + '$name - ', + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + Flexible( + child: AutoSizeText( + formattedAddress, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + fontSize: isChange ? 10 : 14, + color: textColor, + ), + ), + ), + ], + ), + if (hasBalance) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + mainAxisSize: MainAxisSize.max, + children: [ + Text( + 'Balance: $balance', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + Text( + '${S.of(context).transactions.toLowerCase()}: $txCount', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: textColor, + ), + ), + ], + ), + ), + ], + ), + ), + ], ), )); - return Semantics( - label: S.of(context).slidable, - selected: isCurrent, - enabled: !isCurrent, - child: Slidable( - key: Key(address), - startActionPane: _actionPane(context), - endActionPane: _actionPane(context), - child: cell, - ), - ); + return onEdit == null + ? cell + : Semantics( + label: S.of(context).slidable, + selected: isCurrent, + enabled: !isCurrent, + child: Slidable( + key: Key(address), + startActionPane: _actionPane(context), + endActionPane: _actionPane(context), + child: cell, + ), + ); } ActionPane _actionPane(BuildContext context) => ActionPane( - motion: const ScrollMotion(), - extentRatio: 0.3, - children: [ - SlidableAction( - onPressed: (_) => onEdit?.call(), - backgroundColor: Colors.blue, - foregroundColor: Colors.white, - icon: Icons.edit, - label: S.of(context).edit, - ), - ], - ); + motion: const ScrollMotion(), + extentRatio: 0.3, + children: [ + SlidableAction( + onPressed: (_) => onEdit?.call(), + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + icon: Icons.edit, + label: S.of(context).edit, + ), + ], + ); } diff --git a/lib/src/screens/receive/widgets/header_tile.dart b/lib/src/screens/receive/widgets/header_tile.dart index e9c134ea5..faaa9ed07 100644 --- a/lib/src/screens/receive/widgets/header_tile.dart +++ b/lib/src/screens/receive/widgets/header_tile.dart @@ -1,50 +1,114 @@ -import 'package:flutter/material.dart'; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/themes/extensions/receive_page_theme.dart'; +import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart'; +import 'package:flutter/material.dart'; -class HeaderTile extends StatelessWidget { +class HeaderTile extends StatefulWidget { HeaderTile({ - required this.onTap, required this.title, - required this.icon + required this.walletAddressListViewModel, + this.showSearchButton = false, + this.showTrailingButton = false, + this.trailingButtonTap, + this.trailingIcon, }); - final VoidCallback onTap; final String title; - final Icon icon; + final WalletAddressListViewModel walletAddressListViewModel; + final bool showSearchButton; + final bool showTrailingButton; + final VoidCallback? trailingButtonTap; + final Icon? trailingIcon; + + @override + _HeaderTileState createState() => _HeaderTileState(); +} + +class _HeaderTileState extends State { + bool _isSearchActive = false; @override Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( - padding: EdgeInsets.only( - left: 24, - right: 24, - top: 24, - bottom: 24 - ), - color: Theme.of(context).extension()!.tilesBackgroundColor, - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - title, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).extension()!.tilesTextColor), - ), - Container( - height: 32, - width: 32, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Theme.of(context).extension()!.iconsBackgroundColor), - child: icon, - ) - ], - ), + final searchIcon = Image.asset("assets/images/search_icon.png", + color: Theme.of(context).extension()!.iconsColor); + + return Container( + padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12), + color: Theme.of(context).extension()!.tilesBackgroundColor, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _isSearchActive + ? Expanded( + child: TextField( + onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value), + cursorColor: Theme.of(context).extension()!.tilesTextColor, + cursorWidth: 0.5, + decoration: InputDecoration( + hintText: '${S.of(context).search}...', + isDense: true, + contentPadding: EdgeInsets.zero, + hintStyle: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.tilesTextColor), + border: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Theme.of(context).dividerColor), + ), + ), + autofocus: true, + ), + ) + : Text( + widget.title, + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.tilesTextColor), + ), + Row( + children: [ + if (widget.showSearchButton) + GestureDetector( + onTap: () { + setState(() { + _isSearchActive = !_isSearchActive; + widget.walletAddressListViewModel.updateSearchText(''); + }); + }, + child: Container( + height: 32, + width: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Theme.of(context) + .extension()! + .iconsBackgroundColor), + child: searchIcon, + )), + const SizedBox(width: 8), + if (widget.showTrailingButton) + GestureDetector( + onTap: widget.trailingButtonTap, + child: Container( + height: 32, + width: 32, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + Theme.of(context).extension()!.iconsBackgroundColor), + child: widget.trailingIcon, + ), + ), + ], + ), + ], ), ); } diff --git a/lib/src/screens/receive/widgets/qr_widget.dart b/lib/src/screens/receive/widgets/qr_widget.dart index bbfd4d5c1..bedb3b526 100644 --- a/lib/src/screens/receive/widgets/qr_widget.dart +++ b/lib/src/screens/receive/widgets/qr_widget.dart @@ -1,3 +1,4 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:cake_wallet/entities/qr_view_data.dart'; import 'package:cake_wallet/themes/extensions/qr_code_theme.dart'; import 'package:cake_wallet/routes.dart'; @@ -6,6 +7,7 @@ import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dar import 'package:cake_wallet/utils/brightness_util.dart'; import 'package:cake_wallet/utils/show_bar.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; @@ -144,9 +146,10 @@ class QRWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: Text( + child: AutoSizeText( addressListViewModel.address.address, textAlign: TextAlign.center, + maxLines: addressListViewModel.wallet.type == WalletType.monero ? 2 : 1, style: TextStyle( fontSize: 15, fontWeight: FontWeight.w500, diff --git a/lib/src/screens/restore/widgets/backup_file_button.dart b/lib/src/screens/restore/widgets/backup_file_button.dart deleted file mode 100644 index e69de29bb..000000000 diff --git a/lib/src/screens/root/root.dart b/lib/src/screens/root/root.dart index 5f7fcd205..5704c99ad 100644 --- a/lib/src/screens/root/root.dart +++ b/lib/src/screens/root/root.dart @@ -53,13 +53,17 @@ class RootState extends State with WidgetsBindingObserver { @override void initState() { - _requestAuth = widget.authService.requireAuth(); + WidgetsBinding.instance.addObserver(this); + + widget.authService.requireAuth().then((value) { + WidgetsBinding.instance.addPostFrameCallback((_) { + setState(() => _requestAuth = value); + }); + }); _isInactiveController = StreamController.broadcast(); _isInactive = false; _postFrameCallback = false; - WidgetsBinding.instance.addObserver(this); super.initState(); - if (DeviceInfo.instance.isMobile) { initUniLinks(); } @@ -105,8 +109,10 @@ class RootState extends State with WidgetsBindingObserver { break; case AppLifecycleState.resumed: - setState(() { - _requestAuth = widget.authService.requireAuth(); + widget.authService.requireAuth().then((value) { + setState(() { + _requestAuth = value; + }); }); break; default: diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index b20b94cba..3746118d8 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -423,7 +423,7 @@ class SendPage extends BasePage { feeValue: sendViewModel.pendingTransaction!.feeFormatted, feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, outputs: sendViewModel.outputs, - rightButtonText: S.of(_dialogContext).ok, + rightButtonText: S.of(_dialogContext).send, leftButtonText: S.of(_dialogContext).cancel, actionRightButton: () { Navigator.of(_dialogContext).pop(); diff --git a/lib/src/screens/send/widgets/extract_address_from_parsed.dart b/lib/src/screens/send/widgets/extract_address_from_parsed.dart index 73bff23c1..bb09d4ca3 100644 --- a/lib/src/screens/send/widgets/extract_address_from_parsed.dart +++ b/lib/src/screens/send/widgets/extract_address_from_parsed.dart @@ -43,6 +43,11 @@ Future extractAddressFromParsed( content = S.of(context).extracted_address_content('${parsedAddress.name} (Mastodon)'); address = parsedAddress.addresses.first; break; + case ParseFrom.nostr: + title = S.of(context).address_detected; + content = S.of(context).extracted_address_content('${parsedAddress.name} (Nostr NIP-05)'); + address = parsedAddress.addresses.first; + break; case ParseFrom.yatRecord: if (parsedAddress.name.isEmpty) { title = S.of(context).yat_error; diff --git a/lib/src/screens/send/widgets/send_card.dart b/lib/src/screens/send/widgets/send_card.dart index 65069e903..07fb34cb2 100644 --- a/lib/src/screens/send/widgets/send_card.dart +++ b/lib/src/screens/send/widgets/send_card.dart @@ -478,7 +478,7 @@ class SendCardState extends State with AutomaticKeepAliveClientMixin { @override Widget build(BuildContext context) => InAppWebView( key: _webViewkey, - initialOptions: InAppWebViewGroupOptions( - crossPlatform: InAppWebViewOptions(transparentBackground: true), - ), - initialUrlRequest: URLRequest(url: Uri.tryParse(widget.supportUrl)), + initialSettings: InAppWebViewSettings( + transparentBackground: true, + ), + initialUrlRequest: URLRequest(url: WebUri(widget.supportUrl)), onWebViewCreated: (InAppWebViewController controller) { controller.addWebMessageListener( WebMessageListener( jsObjectName: 'ReactNativeWebView', - onPostMessage: (String? message, Uri? sourceOrigin, bool isMainFrame, - JavaScriptReplyProxy replyProxy) { - final shortenedMessage = message?.substring(16); + onPostMessage: (WebMessage? message, WebUri? sourceOrigin, bool isMainFrame, + PlatformJavaScriptReplyProxy replyProxy) { + final shortenedMessage = message?.data.toString().substring(16); if (shortenedMessage != null && isJsonString(shortenedMessage)) { final parsedMessage = jsonDecode(shortenedMessage); final eventType = parsedMessage["event"]; diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index 7bccc4fc5..253adf3ea 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; -import 'package:cake_wallet/buy/buy_provider.dart'; import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/provider_types.dart'; import 'package:cake_wallet/entities/cake_2fa_preset_options.dart'; @@ -10,6 +9,7 @@ import 'package:cake_wallet/entities/background_tasks.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/entities/preferences_key.dart'; +import 'package:cake_wallet/entities/secret_store_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'; @@ -24,6 +24,7 @@ import 'package:cake_wallet/themes/theme_base.dart'; import 'package:cake_wallet/themes/theme_list.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:package_info/package_info.dart'; @@ -38,13 +39,15 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/entities/action_list_display_mode.dart'; import 'package:cake_wallet/entities/fiat_api_mode.dart'; import 'package:cw_core/set_app_secure_native.dart'; + part 'settings_store.g.dart'; class SettingsStore = SettingsStoreBase with _$SettingsStore; abstract class SettingsStoreBase with Store { SettingsStoreBase( - {required BackgroundTasks backgroundTasks, + {required FlutterSecureStorage secureStorage, + required BackgroundTasks backgroundTasks, required SharedPreferences sharedPreferences, required bool initialShouldShowMarketPlaceInDashboard, required FiatCurrency initialFiatCurrency, @@ -109,6 +112,7 @@ abstract class SettingsStoreBase with Store { TransactionPriority? initialBitcoinCashTransactionPriority}) : nodes = ObservableMap.of(nodes), powNodes = ObservableMap.of(powNodes), + _secureStorage = secureStorage, _sharedPreferences = sharedPreferences, _backgroundTasks = backgroundTasks, fiatCurrency = initialFiatCurrency, @@ -187,8 +191,9 @@ abstract class SettingsStoreBase with Store { final key = 'buyProvider_${walletType.toString()}'; final providerId = sharedPreferences.getString(key); if (providerId != null) { - defaultBuyProviders[walletType] = ProviderType.values - .firstWhere((provider) => provider.id == providerId, orElse: () => ProviderType.askEachTime); + defaultBuyProviders[walletType] = ProviderType.values.firstWhere( + (provider) => provider.id == providerId, + orElse: () => ProviderType.askEachTime); } else { defaultBuyProviders[walletType] = ProviderType.askEachTime; } @@ -198,8 +203,9 @@ abstract class SettingsStoreBase with Store { final key = 'sellProvider_${walletType.toString()}'; final providerId = sharedPreferences.getString(key); if (providerId != null) { - defaultSellProviders[walletType] = ProviderType.values - .firstWhere((provider) => provider.id == providerId, orElse: () => ProviderType.askEachTime); + defaultSellProviders[walletType] = ProviderType.values.firstWhere( + (provider) => provider.id == providerId, + orElse: () => ProviderType.askEachTime); } else { defaultSellProviders[walletType] = ProviderType.askEachTime; } @@ -312,74 +318,6 @@ abstract class SettingsStoreBase with Store { reaction((_) => currentTheme, (ThemeBase theme) => sharedPreferences.setInt(PreferencesKey.currentTheme, theme.raw)); - reaction( - (_) => allowBiometricalAuthentication, - (bool biometricalAuthentication) => sharedPreferences.setBool( - PreferencesKey.allowBiometricalAuthenticationKey, biometricalAuthentication)); - - reaction( - (_) => selectedCake2FAPreset, - (Cake2FAPresetsOptions selectedCake2FAPreset) => sharedPreferences.setInt( - PreferencesKey.selectedCake2FAPreset, selectedCake2FAPreset.serialize())); - - reaction( - (_) => shouldRequireTOTP2FAForAccessingWallet, - (bool requireTOTP2FAForAccessingWallet) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAccessingWallet, - requireTOTP2FAForAccessingWallet)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToContact, - (bool requireTOTP2FAForSendsToContact) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToContact, requireTOTP2FAForSendsToContact)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToNonContact, - (bool requireTOTP2FAForSendsToNonContact) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact, - requireTOTP2FAForSendsToNonContact)); - - reaction( - (_) => shouldRequireTOTP2FAForSendsToInternalWallets, - (bool requireTOTP2FAForSendsToInternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets, - requireTOTP2FAForSendsToInternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, - (bool requireTOTP2FAForExchangesToInternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets, - requireTOTP2FAForExchangesToInternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForExchangesToExternalWallets, - (bool requireTOTP2FAForExchangesToExternalWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets, - requireTOTP2FAForExchangesToExternalWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForAddingContacts, - (bool requireTOTP2FAForAddingContacts) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAddingContacts, requireTOTP2FAForAddingContacts)); - - reaction( - (_) => shouldRequireTOTP2FAForCreatingNewWallets, - (bool requireTOTP2FAForCreatingNewWallets) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets, - requireTOTP2FAForCreatingNewWallets)); - - reaction( - (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - (bool requireTOTP2FAForAllSecurityAndBackupSettings) => sharedPreferences.setBool( - PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, - requireTOTP2FAForAllSecurityAndBackupSettings)); - - reaction( - (_) => useTOTP2FA, (bool use) => sharedPreferences.setBool(PreferencesKey.useTOTP2FA, use)); - - reaction((_) => totpSecretKey, - (String totpKey) => sharedPreferences.setString(PreferencesKey.totpSecretKey, totpKey)); - reaction( (_) => numberOfFailedTokenTrials, (int failedTokenTrail) => @@ -403,11 +341,6 @@ abstract class SettingsStoreBase with Store { (SeedPhraseLength seedPhraseWordCount) => sharedPreferences.setInt( PreferencesKey.currentSeedPhraseLength, seedPhraseWordCount.value)); - reaction( - (_) => pinTimeOutDuration, - (PinCodeRequiredDuration pinCodeInterval) => - sharedPreferences.setInt(PreferencesKey.pinTimeOutDuration, pinCodeInterval.value)); - reaction( (_) => balanceDisplayMode, (BalanceDisplayMode mode) => sharedPreferences.setInt( @@ -485,6 +418,84 @@ abstract class SettingsStoreBase with Store { reaction((_) => lookupsENS, (bool looksUpENS) => _sharedPreferences.setBool(PreferencesKey.lookupsENS, looksUpENS)); + // secure storage keys: + reaction( + (_) => allowBiometricalAuthentication, + (bool biometricalAuthentication) => secureStorage.write( + key: SecureKey.allowBiometricalAuthenticationKey, + value: biometricalAuthentication.toString())); + + reaction( + (_) => selectedCake2FAPreset, + (Cake2FAPresetsOptions selectedCake2FAPreset) => secureStorage.write( + key: SecureKey.selectedCake2FAPreset, + value: selectedCake2FAPreset.serialize().toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAccessingWallet, + (bool requireTOTP2FAForAccessingWallet) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + value: requireTOTP2FAForAccessingWallet.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToContact, + (bool requireTOTP2FAForSendsToContact) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + value: requireTOTP2FAForSendsToContact.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToNonContact, + (bool requireTOTP2FAForSendsToNonContact) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + value: requireTOTP2FAForSendsToNonContact.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForSendsToInternalWallets, + (bool requireTOTP2FAForSendsToInternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + value: requireTOTP2FAForSendsToInternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToInternalWallets, + (bool requireTOTP2FAForExchangesToInternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + value: requireTOTP2FAForExchangesToInternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForExchangesToExternalWallets, + (bool requireTOTP2FAForExchangesToExternalWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + value: requireTOTP2FAForExchangesToExternalWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAddingContacts, + (bool requireTOTP2FAForAddingContacts) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + value: requireTOTP2FAForAddingContacts.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForCreatingNewWallets, + (bool requireTOTP2FAForCreatingNewWallets) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + value: requireTOTP2FAForCreatingNewWallets.toString())); + + reaction( + (_) => shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + (bool requireTOTP2FAForAllSecurityAndBackupSettings) => secureStorage.write( + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + value: requireTOTP2FAForAllSecurityAndBackupSettings.toString())); + + reaction((_) => useTOTP2FA, + (bool use) => secureStorage.write(key: SecureKey.useTOTP2FA, value: use.toString())); + + reaction((_) => totpSecretKey, + (String totpKey) => secureStorage.write(key: SecureKey.totpSecretKey, value: totpKey)); + + reaction( + (_) => pinTimeOutDuration, + (PinCodeRequiredDuration pinCodeInterval) => secureStorage.write( + key: SecureKey.pinTimeOutDuration, value: pinCodeInterval.value.toString())); + this.nodes.observe((change) { if (change.newValue != null && change.key != null) { _saveCurrentNode(change.newValue!, change.key!); @@ -668,6 +679,7 @@ abstract class SettingsStoreBase with Store { String deviceName; + final FlutterSecureStorage _secureStorage; final SharedPreferences _sharedPreferences; final BackgroundTasks _backgroundTasks; @@ -710,6 +722,7 @@ abstract class SettingsStoreBase with Store { BalanceDisplayMode initialBalanceDisplayMode = BalanceDisplayMode.availableBalance, ThemeBase? initialTheme}) async { final sharedPreferences = await getIt.getAsync(); + final secureStorage = await getIt.get(); final backgroundTasks = getIt.get(); final currentFiatCurrency = FiatCurrency.deserialize( raw: sharedPreferences.getString(PreferencesKey.currentFiatCurrencyKey)!); @@ -770,36 +783,6 @@ abstract class SettingsStoreBase with Store { final currentFiatApiMode = FiatApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.currentFiatApiModeKey) ?? FiatApiMode.enabled.raw); - final allowBiometricalAuthentication = - sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? false; - final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.normal.raw); - final shouldRequireTOTP2FAForAccessingWallet = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; - final shouldRequireTOTP2FAForSendsToContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; - final shouldRequireTOTP2FAForSendsToNonContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; - final shouldRequireTOTP2FAForSendsToInternalWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? - false; - final shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? - false; - final shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? - false; - final shouldRequireTOTP2FAForAddingContacts = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; - final shouldRequireTOTP2FAForCreatingNewWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? - false; - final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? - false; - final useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? false; - final totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? ''; final tokenTrialNumber = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? 0; final shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? true; @@ -816,18 +799,14 @@ abstract class SettingsStoreBase with Store { actionListDisplayMode.addAll(deserializeActionlistDisplayModes( sharedPreferences.getInt(PreferencesKey.displayActionListModeKey) ?? defaultActionsMode)); var pinLength = sharedPreferences.getInt(PreferencesKey.currentPinLength); - final timeOutDuration = sharedPreferences.getInt(PreferencesKey.pinTimeOutDuration); - final seedPhraseCount = sharedPreferences.getInt(PreferencesKey.currentSeedPhraseLength); - final pinCodeTimeOutDuration = timeOutDuration != null - ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) - : defaultPinCodeTimeOutDuration; - final seedPhraseWordCount = seedPhraseCount != null - ? SeedPhraseLength.deserialize(raw: seedPhraseCount) - : defaultSeedPhraseLength; final sortBalanceBy = SortBalanceBy.values[sharedPreferences.getInt(PreferencesKey.sortBalanceBy) ?? 0]; final pinNativeTokenAtTop = sharedPreferences.getBool(PreferencesKey.pinNativeTokenAtTop) ?? true; + final seedPhraseCount = sharedPreferences.getInt(PreferencesKey.currentSeedPhraseLength); + final seedPhraseWordCount = seedPhraseCount != null + ? SeedPhraseLength.deserialize(raw: seedPhraseCount) + : defaultSeedPhraseLength; final useEtherscan = sharedPreferences.getBool(PreferencesKey.useEtherscan) ?? true; final usePolygonScan = sharedPreferences.getBool(PreferencesKey.usePolygonScan) ?? true; final defaultNanoRep = sharedPreferences.getString(PreferencesKey.defaultNanoRep) ?? ""; @@ -929,7 +908,101 @@ abstract class SettingsStoreBase with Store { }); final savedSyncAll = sharedPreferences.getBool(PreferencesKey.syncAllKey) ?? true; + // migrated to secure: + final timeOutDuration = await SecureKey.getInt( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.pinTimeOutDuration, + ); + + final pinCodeTimeOutDuration = timeOutDuration != null + ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) + : defaultPinCodeTimeOutDuration; + + final allowBiometricalAuthentication = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.allowBiometricalAuthenticationKey, + ) ?? + false; + + final selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: await SecureKey.getInt( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.selectedCake2FAPreset, + ) ?? + Cake2FAPresetsOptions.normal.raw); + + final shouldRequireTOTP2FAForAccessingWallet = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToContact = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToNonContact = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + ) ?? + false; + final shouldRequireTOTP2FAForSendsToInternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForExchangesToInternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForExchangesToExternalWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + ) ?? + false; + final shouldRequireTOTP2FAForAddingContacts = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + ) ?? + false; + final shouldRequireTOTP2FAForCreatingNewWallets = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + ) ?? + false; + final shouldRequireTOTP2FAForAllSecurityAndBackupSettings = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ) ?? + false; + final useTOTP2FA = await SecureKey.getBool( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.useTOTP2FA, + ) ?? + false; + final totpSecretKey = await SecureKey.getString( + secureStorage: secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.totpSecretKey, + ) ?? + ''; + return SettingsStore( + secureStorage: secureStorage, sharedPreferences: sharedPreferences, initialShouldShowMarketPlaceInDashboard: shouldShowMarketPlaceInDashboard, nodes: nodes, @@ -1007,34 +1080,37 @@ abstract class SettingsStoreBase with Store { priority[WalletType.monero] = monero?.deserializeMoneroTransactionPriority( raw: sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? priority[WalletType.monero]!; - priority[WalletType.bitcoin] = bitcoin?.deserializeBitcoinTransactionPriority( - sharedPreferences.getInt(PreferencesKey.moneroTransactionPriority)!) ?? - priority[WalletType.bitcoin]!; - if (sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { - priority[WalletType.haven] = monero?.deserializeMoneroTransactionPriority( - raw: sharedPreferences.getInt(PreferencesKey.havenTransactionPriority)!) ?? - priority[WalletType.haven]!; + if (bitcoin != null && + sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority) != null) { + priority[WalletType.bitcoin] = bitcoin!.deserializeBitcoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority) != null) { - priority[WalletType.litecoin] = bitcoin?.deserializeLitecoinTransactionPriority( - sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!) ?? - priority[WalletType.litecoin]!; + + if (monero != null && + sharedPreferences.getInt(PreferencesKey.havenTransactionPriority) != null) { + priority[WalletType.haven] = monero!.deserializeMoneroTransactionPriority( + raw: sharedPreferences.getInt(PreferencesKey.havenTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { - priority[WalletType.ethereum] = ethereum?.deserializeEthereumTransactionPriority( - sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!) ?? - priority[WalletType.ethereum]!; + if (bitcoin != null && + sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority) != null) { + priority[WalletType.litecoin] = bitcoin!.deserializeLitecoinTransactionPriority( + sharedPreferences.getInt(PreferencesKey.litecoinTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { - priority[WalletType.polygon] = polygon?.deserializePolygonTransactionPriority( - sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!) ?? - priority[WalletType.polygon]!; + if (ethereum != null && + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority) != null) { + priority[WalletType.ethereum] = ethereum!.deserializeEthereumTransactionPriority( + sharedPreferences.getInt(PreferencesKey.ethereumTransactionPriority)!); } - if (sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { - priority[WalletType.bitcoinCash] = bitcoinCash?.deserializeBitcoinCashTransactionPriority( - sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!) ?? - priority[WalletType.bitcoinCash]!; + if (polygon != null && + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority) != null) { + priority[WalletType.polygon] = polygon!.deserializePolygonTransactionPriority( + sharedPreferences.getInt(PreferencesKey.polygonTransactionPriority)!); + } + if (bitcoinCash != null && + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority) != null) { + priority[WalletType.bitcoinCash] = bitcoinCash!.deserializeBitcoinCashTransactionPriority( + sharedPreferences.getInt(PreferencesKey.bitcoinCashTransactionPriority)!); } final generateSubaddresses = @@ -1055,8 +1131,6 @@ abstract class SettingsStoreBase with Store { shouldSaveRecipientAddress = sharedPreferences.getBool(PreferencesKey.shouldSaveRecipientAddressKey) ?? shouldSaveRecipientAddress; - useTOTP2FA = sharedPreferences.getBool(PreferencesKey.useTOTP2FA) ?? useTOTP2FA; - totpSecretKey = sharedPreferences.getString(PreferencesKey.totpSecretKey) ?? totpSecretKey; numberOfFailedTokenTrials = sharedPreferences.getInt(PreferencesKey.failedTotpTokenTrials) ?? numberOfFailedTokenTrials; isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure; @@ -1065,41 +1139,10 @@ abstract class SettingsStoreBase with Store { walletListOrder = WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0]; walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true; - allowBiometricalAuthentication = - sharedPreferences.getBool(PreferencesKey.allowBiometricalAuthenticationKey) ?? - allowBiometricalAuthentication; - selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.normal.raw); - shouldRequireTOTP2FAForAccessingWallet = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAccessingWallet) ?? false; - shouldRequireTOTP2FAForSendsToContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToContact) ?? false; - shouldRequireTOTP2FAForSendsToNonContact = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToNonContact) ?? false; - shouldRequireTOTP2FAForSendsToInternalWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForSendsToInternalWallets) ?? - false; - shouldRequireTOTP2FAForExchangesToInternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToInternalWallets) ?? - false; - shouldRequireTOTP2FAForExchangesToExternalWallets = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForExchangesToExternalWallets) ?? - false; - shouldRequireTOTP2FAForAddingContacts = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForAddingContacts) ?? false; - shouldRequireTOTP2FAForCreatingNewWallets = - sharedPreferences.getBool(PreferencesKey.shouldRequireTOTP2FAForCreatingNewWallets) ?? - false; - shouldRequireTOTP2FAForAllSecurityAndBackupSettings = sharedPreferences - .getBool(PreferencesKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings) ?? - false; + shouldShowMarketPlaceInDashboard = sharedPreferences.getBool(PreferencesKey.shouldShowMarketPlaceInDashboard) ?? shouldShowMarketPlaceInDashboard; - selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( - raw: sharedPreferences.getInt(PreferencesKey.selectedCake2FAPreset) ?? - Cake2FAPresetsOptions.narrow.raw); exchangeStatus = ExchangeApiMode.deserialize( raw: sharedPreferences.getInt(PreferencesKey.exchangeStatusKey) ?? ExchangeApiMode.enabled.raw); @@ -1147,7 +1190,6 @@ abstract class SettingsStoreBase with Store { 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); final bitcoinElectrumServer = nodeSource.get(bitcoinElectrumServerId); final litecoinElectrumServer = nodeSource.get(litecoinElectrumServerId); @@ -1188,6 +1230,103 @@ abstract class SettingsStoreBase with Store { if (nanoNode != null) { nodes[WalletType.nano] = nanoNode; } + + // MIGRATED: + + useTOTP2FA = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.useTOTP2FA, + ) ?? + useTOTP2FA; + + totpSecretKey = await SecureKey.getString( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.totpSecretKey, + ) ?? + totpSecretKey; + + final timeOutDuration = await SecureKey.getInt( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.pinTimeOutDuration, + ); + + pinTimeOutDuration = timeOutDuration != null + ? PinCodeRequiredDuration.deserialize(raw: timeOutDuration) + : defaultPinCodeTimeOutDuration; + + allowBiometricalAuthentication = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.allowBiometricalAuthenticationKey, + ) ?? + allowBiometricalAuthentication; + + selectedCake2FAPreset = Cake2FAPresetsOptions.deserialize( + raw: await SecureKey.getInt( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.selectedCake2FAPreset, + ) ?? + Cake2FAPresetsOptions.normal.raw); + + shouldRequireTOTP2FAForAccessingWallet = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAccessingWallet, + ) ?? + false; + shouldRequireTOTP2FAForSendsToContact = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToContact, + ) ?? + false; + + shouldRequireTOTP2FAForSendsToNonContact = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToNonContact, + ) ?? + false; + shouldRequireTOTP2FAForSendsToInternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForSendsToInternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForExchangesToInternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToInternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForExchangesToExternalWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForExchangesToExternalWallets, + ) ?? + false; + shouldRequireTOTP2FAForAddingContacts = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAddingContacts, + ) ?? + false; + shouldRequireTOTP2FAForCreatingNewWallets = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForCreatingNewWallets, + ) ?? + false; + shouldRequireTOTP2FAForAllSecurityAndBackupSettings = await SecureKey.getBool( + secureStorage: _secureStorage, + sharedPreferences: sharedPreferences, + key: SecureKey.shouldRequireTOTP2FAForAllSecurityAndBackupSettings, + ) ?? + false; } Future _saveCurrentNode(Node node, WalletType walletType) async { diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 5c95ab3ab..e2c0382b0 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -125,6 +125,8 @@ abstract class BalanceViewModelBase with Store { case WalletType.haven: case WalletType.ethereum: case WalletType.polygon: + case WalletType.nano: + case WalletType.banano: return S.current.xmr_available_balance; default: return S.current.confirmed; @@ -139,6 +141,9 @@ abstract class BalanceViewModelBase with Store { case WalletType.ethereum: case WalletType.polygon: return S.current.xmr_full_balance; + case WalletType.nano: + case WalletType.banano: + return S.current.receivable_balance; default: return S.current.unconfirmed; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index bc7f70517..d8c4776b7 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -101,7 +101,7 @@ class TransactionListItem extends ActionListItem with Keyable { break; case WalletType.nano: amount = calculateFiatAmountRaw( - cryptoAmount: double.parse(nanoUtil!.getRawAsDecimalString( + cryptoAmount: double.parse(nanoUtil!.getRawAsUsableString( nano!.getTransactionAmountRaw(transaction).toString(), nanoUtil!.rawPerNano)), price: price); break; diff --git a/lib/view_model/restore_from_backup_view_model.dart b/lib/view_model/restore_from_backup_view_model.dart index 403ae3ff1..365afb1fa 100644 --- a/lib/view_model/restore_from_backup_view_model.dart +++ b/lib/view_model/restore_from_backup_view_model.dart @@ -1,5 +1,7 @@ import 'dart:io'; import 'package:cake_wallet/core/execution_state.dart'; +import 'package:cake_wallet/utils/exception_handler.dart'; +import 'package:flutter/material.dart'; import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/main.dart'; @@ -60,11 +62,17 @@ abstract class RestoreFromBackupViewModelBase with Store { }); state = ExecutedSuccessfullyState(); - } catch (e) { + } catch (e, s) { var msg = e.toString(); if (msg.toLowerCase().contains("message authentication code (mac)")) { msg = 'Incorrect backup password'; + } else { + ExceptionHandler.onError(FlutterErrorDetails( + exception: e, + stack: s, + library: this.toString(), + )); } state = FailureState(msg); diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 73fb535f2..e40000139 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -262,7 +262,7 @@ abstract class OutputBase with Store { Future fetchParsedAddress(BuildContext context) async { final domain = address; final ticker = cryptoCurrencyHandler().title.toLowerCase(); - parsedAddress = await getIt.get().resolve(domain, ticker); + parsedAddress = await getIt.get().resolve(context, domain, ticker); extractedAddress = await extractAddressFromParsed(context, parsedAddress); note = parsedAddress.description; } diff --git a/lib/view_model/set_up_2fa_viewmodel.dart b/lib/view_model/set_up_2fa_viewmodel.dart index 9587e3075..7347b32a8 100644 --- a/lib/view_model/set_up_2fa_viewmodel.dart +++ b/lib/view_model/set_up_2fa_viewmodel.dart @@ -27,7 +27,9 @@ abstract class Setup2FAViewModelBase with Store { unhighlightTabs = false, selected2FASettings = ObservableList(), state = InitialExecutionState() { - selectCakePreset(selectedCake2FAPreset); + if (selectedCake2FAPreset != Cake2FAPresetsOptions.none) { + selectCakePreset(selectedCake2FAPreset); + } reaction((_) => state, _saveLastAuthTime); } diff --git a/lib/view_model/settings/privacy_settings_view_model.dart b/lib/view_model/settings/privacy_settings_view_model.dart index e4dd86b37..65375b3e7 100644 --- a/lib/view_model/settings/privacy_settings_view_model.dart +++ b/lib/view_model/settings/privacy_settings_view_model.dart @@ -38,7 +38,11 @@ abstract class PrivacySettingsViewModelBase with Store { } } - bool get isAutoGenerateSubaddressesVisible => _wallet.type == WalletType.monero; + bool get isAutoGenerateSubaddressesVisible => + _wallet.type == WalletType.monero || + _wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.litecoin || + _wallet.type == WalletType.bitcoinCash; @computed bool get shouldSaveRecipientAddress => _settingsStore.shouldSaveRecipientAddress; diff --git a/lib/view_model/settings/security_settings_view_model.dart b/lib/view_model/settings/security_settings_view_model.dart index 5ea4dd4ea..b8812e224 100644 --- a/lib/view_model/settings/security_settings_view_model.dart +++ b/lib/view_model/settings/security_settings_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/entities/biometric_auth.dart'; import 'package:cake_wallet/entities/pin_code_required_duration.dart'; import 'package:cake_wallet/store/settings_store.dart'; @@ -9,14 +8,10 @@ part 'security_settings_view_model.g.dart'; class SecuritySettingsViewModel = SecuritySettingsViewModelBase with _$SecuritySettingsViewModel; abstract class SecuritySettingsViewModelBase with Store { - SecuritySettingsViewModelBase( - this._settingsStore, - this._authService, - ) : _biometricAuth = BiometricAuth(); + SecuritySettingsViewModelBase(this._settingsStore) : _biometricAuth = BiometricAuth(); final BiometricAuth _biometricAuth; final SettingsStore _settingsStore; - final AuthService _authService; @computed bool get allowBiometricalAuthentication => _settingsStore.allowBiometricalAuthentication; @@ -41,8 +36,6 @@ abstract class SecuritySettingsViewModelBase with Store { _settingsStore.allowBiometricalAuthentication = value; @action - setPinCodeRequiredDuration(PinCodeRequiredDuration duration) => + void setPinCodeRequiredDuration(PinCodeRequiredDuration duration) => _settingsStore.pinTimeOutDuration = duration; - - bool checkPinCodeRiquired() => _authService.requireAuth(); } diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 4e17866cb..a3bf281ca 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -239,6 +239,8 @@ abstract class TransactionDetailsViewModelBase with Store { 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!), + if (tx.direction == TransactionDirection.incoming && tx.from != null) + StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), ]; items.addAll(_items); @@ -247,11 +249,15 @@ abstract class TransactionDetailsViewModelBase with Store { void _addNanoListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ StandartListItem(title: S.current.transaction_details_transaction_id, value: tx.id), + if (showRecipientAddress && tx.to != null) + StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + if (showRecipientAddress && tx.from != null) + StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), + StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), StandartListItem( title: S.current.transaction_details_date, value: dateFormat.format(tx.date)), - StandartListItem(title: S.current.confirmations, value: (tx.confirmations > 0).toString()), + StandartListItem(title: S.current.confirmed_tx, value: (tx.confirmations > 0).toString()), StandartListItem(title: S.current.transaction_details_height, value: '${tx.height}'), - StandartListItem(title: S.current.transaction_details_amount, value: tx.amountFormatted()), ]; items.addAll(_items); @@ -267,8 +273,10 @@ abstract class TransactionDetailsViewModelBase with Store { 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) + if (showRecipientAddress && tx.to != null && tx.direction == TransactionDirection.outgoing) StandartListItem(title: S.current.transaction_details_recipient_address, value: tx.to!), + if (tx.direction == TransactionDirection.incoming && tx.from != null) + StandartListItem(title: S.current.transaction_details_source_address, value: tx.from!), ]; items.addAll(_items); diff --git a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart index 1815b1689..6380bb07e 100644 --- a/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart +++ b/lib/view_model/unspent_coins/unspent_coins_list_view_model.dart @@ -16,30 +16,17 @@ abstract class UnspentCoinsListViewModelBase with Store { UnspentCoinsListViewModelBase( {required this.wallet, required Box unspentCoinsInfo}) : _unspentCoinsInfo = unspentCoinsInfo { + _updateUnspentCoinsInfo(); _updateUnspents(); } WalletBase wallet; final Box _unspentCoinsInfo; - @computed - ObservableList get items => ObservableList.of(_getUnspents().map((elem) { - final info = - getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + final ObservableList _items = ObservableList(); - return UnspentCoinsItem( - address: elem.address, - amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', - hash: elem.hash, - isFrozen: info.isFrozen, - note: info.note, - isSending: info.isSending, - amountRaw: elem.value, - vout: elem.vout, - keyImage: elem.keyImage, - isChange: elem.isChange, - ); - })); + @computed + ObservableList get items => _items; Future saveUnspentCoinInfo(UnspentCoinsItem item) async { try { @@ -77,9 +64,14 @@ abstract class UnspentCoinsListViewModelBase with Store { } Future _updateUnspents() async { - if (wallet.type == WalletType.monero) return monero!.updateUnspents(wallet); - if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) - return bitcoin!.updateUnspents(wallet); + if (wallet.type == WalletType.monero) { + await monero!.updateUnspents(wallet); + } + if ([WalletType.bitcoin, WalletType.litecoin, WalletType.bitcoinCash].contains(wallet.type)) { + await bitcoin!.updateUnspents(wallet); + } + + _updateUnspentCoinsInfo(); } List _getUnspents() { @@ -88,4 +80,26 @@ abstract class UnspentCoinsListViewModelBase with Store { return bitcoin!.getUnspents(wallet); return List.empty(); } + + @action + void _updateUnspentCoinsInfo() { + _items.clear(); + _items.addAll(_getUnspents().map((elem) { + final info = + getUnspentCoinInfo(elem.hash, elem.address, elem.value, elem.vout, elem.keyImage); + + return UnspentCoinsItem( + address: elem.address, + amount: '${formatAmountToString(elem.value)} ${wallet.currency.title}', + hash: elem.hash, + isFrozen: info.isFrozen, + note: info.note, + isSending: info.isSending, + amountRaw: elem.value, + vout: elem.vout, + keyImage: elem.keyImage, + isChange: elem.isChange, + ); + })); + } } diff --git a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart index 9e2aa7187..0b4f969cb 100644 --- a/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_edit_or_create_view_model.dart @@ -1,6 +1,5 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:mobx/mobx.dart'; -import 'package:flutter/foundation.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/monero/monero.dart'; @@ -33,7 +32,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { state = AddressEditOrCreateStateInitial(), label = item?.name ?? '', _item = item, - _wallet = wallet; + _wallet = wallet; @observable AddressEditOrCreateState state; @@ -46,6 +45,10 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { final WalletAddressListItem? _item; final WalletBase _wallet; + bool get isElectrum => _wallet.type == WalletType.bitcoin || + _wallet.type == WalletType.bitcoinCash || + _wallet.type == WalletType.litecoin; + Future save() async { try { state = AddressIsSaving(); @@ -65,12 +68,7 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _createNew() async { final wallet = _wallet; - if (wallet.type == WalletType.bitcoin - || wallet.type == WalletType.litecoin - || wallet.type == WalletType.bitcoinCash) { - await bitcoin!.generateNewAddress(wallet); - await wallet.save(); - } + if (isElectrum) await bitcoin!.generateNewAddress(wallet, label); if (wallet.type == WalletType.monero) { await monero @@ -96,10 +94,8 @@ abstract class WalletAddressEditOrCreateViewModelBase with Store { Future _update() async { final wallet = _wallet; - /*if (wallet is BitcoinWallet) { - await wallet.walletAddresses.updateAddress(_item.address as String); - await wallet.save(); - }*/ + if (isElectrum) await bitcoin!.updateAddress(wallet, _item!.address, label); + final index = _item?.id; if (index != null) { if (wallet.type == WalletType.monero) { diff --git a/lib/view_model/wallet_address_list/wallet_address_list_item.dart b/lib/view_model/wallet_address_list/wallet_address_list_item.dart index ba7b93cf9..1152f1404 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_item.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_item.dart @@ -1,4 +1,3 @@ -import 'package:flutter/foundation.dart'; import 'package:cake_wallet/utils/list_item.dart'; class WalletAddressListItem extends ListItem { @@ -6,13 +5,19 @@ class WalletAddressListItem extends ListItem { required this.address, required this.isPrimary, this.id, - this.name}) + this.name, + this.txCount, + this.balance, + this.isChange = false}) : super(); final int? id; final bool isPrimary; final String address; final String? name; + final int? txCount; + final String? balance; + final bool isChange; @override String toString() => name ?? address; 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 ade279124..9270d1d44 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,21 +1,25 @@ +import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; -import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/fiat_currency.dart'; +import 'package:cake_wallet/ethereum/ethereum.dart'; +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/haven/haven.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/fiat_conversion_store.dart'; +import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/store/yat/yat_store.dart'; -import 'package:cw_core/currency.dart'; -import 'package:intl/intl.dart'; -import 'package:mobx/mobx.dart'; import 'package:cake_wallet/utils/list_item.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; +import 'package:cw_core/amount_converter.dart'; +import 'package:cw_core/currency.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:cake_wallet/bitcoin/bitcoin.dart'; -import 'package:cake_wallet/store/app_store.dart'; -import 'package:cake_wallet/monero/monero.dart'; -import 'package:cake_wallet/haven/haven.dart'; +import 'package:intl/intl.dart'; +import 'package:mobx/mobx.dart'; part 'wallet_address_list_view_model.g.dart'; @@ -110,7 +114,8 @@ class EthereumURI extends PaymentURI { class BitcoinCashURI extends PaymentURI { BitcoinCashURI({required String amount, required String address}) - : super(amount: amount, address: address); + : super(amount: amount, address: address); + @override String toString() { var base = address; @@ -121,9 +126,7 @@ class BitcoinCashURI extends PaymentURI { return base; } - } - - +} class NanoURI extends PaymentURI { NanoURI({required String amount, required String address}) @@ -167,6 +170,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo hasAccounts = appStore.wallet!.type == WalletType.monero || appStore.wallet!.type == WalletType.haven, amount = '', + _settingsStore = appStore.settingsStore, super(appStore: appStore) { _init(); } @@ -184,12 +188,28 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo final NumberFormat _cryptoNumberFormat; final FiatConversionStore fiatConversionStore; + final SettingsStore _settingsStore; List get currencies => [walletTypeToCryptoCurrency(wallet.type), ...FiatCurrency.all]; + String get buttonTitle { + if (isElectrumWallet) { + return S.current.addresses; + } + + if (isAutoGenerateSubaddressEnabled) { + return hasAccounts ? S.current.accounts : S.current.account; + } + + return hasAccounts ? S.current.accounts_subaddresses : S.current.addresses; + } + @observable Currency selectedCurrency; + @observable + String searchText = ''; + @computed int get selectedCurrencyIndex => currencies.indexOf(selectedCurrency); @@ -277,14 +297,21 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.addAll(addressItems); } - if (wallet.type == WalletType.bitcoin) { - final primaryAddress = bitcoin!.getAddress(wallet); - final bitcoinAddresses = bitcoin!.getAddresses(wallet).map((addr) { - final isPrimary = addr == primaryAddress; + if (isElectrumWallet) { + final addressItems = bitcoin!.getSubAddresses(wallet).map((subaddress) { + final isPrimary = subaddress.id == 0; - return WalletAddressListItem(isPrimary: isPrimary, name: null, address: addr); + return WalletAddressListItem( + id: subaddress.id, + isPrimary: isPrimary, + name: subaddress.name, + address: subaddress.address, + txCount: subaddress.txCount, + balance: AmountConverter.amountIntToString( + walletTypeToCryptoCurrency(type), subaddress.balance), + isChange: subaddress.isChange); }); - addressList.addAll(bitcoinAddresses); + addressList.addAll(addressItems); } if (wallet.type == WalletType.ethereum) { @@ -299,6 +326,15 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo addressList.add(WalletAddressListItem(isPrimary: true, name: null, address: primaryAddress)); } + if (searchText.isNotEmpty) { + return ObservableList.of(addressList.where((item) { + if (item is WalletAddressListItem) { + return item.address.toLowerCase().contains(searchText.toLowerCase()); + } + return false; + })); + } + return addressList; } @@ -321,15 +357,23 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo @computed bool get hasAddressList => wallet.type == WalletType.monero || - wallet.type == WalletType.haven;/* || - wallet.type == WalletType.nano || - wallet.type == WalletType.banano;*/// TODO: nano accounts are disabled for now + wallet.type == WalletType.haven || + wallet.type == WalletType.bitcoinCash || + wallet.type == WalletType.bitcoin || + wallet.type == WalletType.litecoin; + + // wallet.type == WalletType.nano || + // wallet.type == WalletType.banano; TODO: nano accounts are disabled for now @computed - bool get showElectrumAddressDisclaimer => + bool get isElectrumWallet => wallet.type == WalletType.bitcoin || - wallet.type == WalletType.litecoin || - wallet.type == WalletType.bitcoinCash; + wallet.type == WalletType.litecoin || + wallet.type == WalletType.bitcoinCash; + + @computed + bool get isAutoGenerateSubaddressEnabled => + _settingsStore.autoGenerateSubaddressStatus != AutoGenerateSubaddressStatus.disabled; List _baseItems; @@ -343,9 +387,12 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo _baseItems = []; if (wallet.type == WalletType.monero || - wallet.type == WalletType.haven /*|| + wallet.type == + WalletType + .haven /*|| wallet.type == WalletType.nano || - wallet.type == WalletType.banano*/) { + wallet.type == WalletType.banano*/ + ) { _baseItems.add(WalletAccountListHeader()); } @@ -367,6 +414,11 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo } } + @action + void updateSearchText(String text) { + searchText = text; + } + void _convertAmountToCrypto() { final cryptoCurrency = walletTypeToCryptoCurrency(wallet.type); try { diff --git a/lib/view_model/wallet_list/wallet_list_view_model.dart b/lib/view_model/wallet_list/wallet_list_view_model.dart index 407b6d3bc..b31133c7d 100644 --- a/lib/view_model/wallet_list/wallet_list_view_model.dart +++ b/lib/view_model/wallet_list/wallet_list_view_model.dart @@ -1,4 +1,3 @@ -import 'package:cake_wallet/core/auth_service.dart'; import 'package:cake_wallet/core/wallet_loading_service.dart'; import 'package:cake_wallet/entities/wallet_list_order_types.dart'; import 'package:hive/hive.dart'; @@ -18,7 +17,6 @@ abstract class WalletListViewModelBase with Store { this._walletInfoSource, this._appStore, this._walletLoadingService, - this._authService, ) : wallets = ObservableList() { setOrderType(_appStore.settingsStore.walletListOrder); reaction((_) => _appStore.wallet, (_) => updateList()); @@ -39,7 +37,6 @@ abstract class WalletListViewModelBase with Store { final AppStore _appStore; final Box _walletInfoSource; final WalletLoadingService _walletLoadingService; - final AuthService _authService; WalletType get currentWalletType => _appStore.wallet!.type; @@ -160,8 +157,4 @@ abstract class WalletListViewModelBase with Store { break; } } - - bool checkIfAuthRequired() { - return _authService.requireAuth(); - } } diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 68d03b5f8..05996a674 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -9,6 +9,7 @@ import connectivity_plus_macos import cw_monero import device_info_plus import devicelocale +import flutter_inappwebview_macos import flutter_secure_storage_macos import in_app_review import package_info @@ -24,6 +25,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { CwMoneroPlugin.register(with: registry.registrar(forPlugin: "CwMoneroPlugin")) DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) DevicelocalePlugin.register(with: registry.registrar(forPlugin: "DevicelocalePlugin")) + InAppWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "InAppWebViewFlutterPlugin")) FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin")) FLTPackageInfoPlugin.register(with: registry.registrar(forPlugin: "FLTPackageInfoPlugin")) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 329732075..fcbe1d733 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -23,11 +23,15 @@ PODS: - FlutterMacOS - devicelocale (0.0.1): - FlutterMacOS + - flutter_inappwebview_macos (0.0.1): + - FlutterMacOS + - OrderedSet (~> 5.0) - flutter_secure_storage_macos (6.1.1): - FlutterMacOS - FlutterMacOS (1.0.0) - in_app_review (0.2.0): - FlutterMacOS + - OrderedSet (5.0.0) - package_info (0.0.1): - FlutterMacOS - package_info_plus (0.0.1): @@ -51,6 +55,7 @@ DEPENDENCIES: - cw_monero (from `Flutter/ephemeral/.symlinks/plugins/cw_monero/macos`) - device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`) - devicelocale (from `Flutter/ephemeral/.symlinks/plugins/devicelocale/macos`) + - flutter_inappwebview_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos`) - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - FlutterMacOS (from `Flutter/ephemeral`) - in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`) @@ -64,6 +69,7 @@ DEPENDENCIES: SPEC REPOS: trunk: + - OrderedSet - ReachabilitySwift EXTERNAL SOURCES: @@ -75,6 +81,8 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos devicelocale: :path: Flutter/ephemeral/.symlinks/plugins/devicelocale/macos + flutter_inappwebview_macos: + :path: Flutter/ephemeral/.symlinks/plugins/flutter_inappwebview_macos/macos flutter_secure_storage_macos: :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos FlutterMacOS: @@ -98,18 +106,20 @@ EXTERNAL SOURCES: SPEC CHECKSUMS: connectivity_plus_macos: f6e86fd000e971d361e54b5afcadc8c8fa773308 - cw_monero: ec03de55a19c4a2b174ea687e0f4202edc716fa4 + cw_monero: f8b7f104508efba2591548e76b5c058d05cba3f0 device_info_plus: 5401765fde0b8d062a2f8eb65510fb17e77cf07f devicelocale: 9f0f36ac651cabae2c33f32dcff4f32b61c38225 + flutter_inappwebview_macos: 9600c9df9fdb346aaa8933812009f8d94304203d flutter_secure_storage_macos: d56e2d218c1130b262bef8b4a7d64f88d7f9c9ea FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 in_app_review: a850789fad746e89bce03d4aeee8078b45a53fd0 + OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c package_info: 6eba2fd8d3371dda2d85c8db6fe97488f24b74b2 package_info_plus: 02d7a575e80f194102bef286361c6c326e4c29ce - path_provider_foundation: 29f094ae23ebbca9d3d0cec13889cd9060c0e943 + path_provider_foundation: 3784922295ac71e43754bd15e0653ccfd36a147c ReachabilitySwift: 985039c6f7b23a1da463388634119492ff86c825 share_plus_macos: 853ee48e7dce06b633998ca0735d482dd671ade4 - shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 + shared_preferences_foundation: b4c3b4cddf1c21f02770737f147a3f5da9d39695 url_launcher_macos: d2691c7dd33ed713bf3544850a623080ec693d95 wakelock_plus: 4783562c9a43d209c458cb9b30692134af456269 diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfoBase.xcconfig similarity index 85% rename from macos/Runner/Configs/AppInfo.xcconfig rename to macos/Runner/Configs/AppInfoBase.xcconfig index 84f96bff7..aabf7b387 100644 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ b/macos/Runner/Configs/AppInfoBase.xcconfig @@ -5,10 +5,10 @@ // 'flutter create' template. // The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = Cake Wallet +PRODUCT_NAME = ${PRODUCT_NAME} // The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.fotolockr.cakewallet +PRODUCT_BUNDLE_IDENTIFIER = ${PRODUCT_BUNDLE_IDENTIFIER} // The copyright displayed in application information PRODUCT_COPYRIGHT = Copyright © 2022 com.fotolockr. All rights reserved. diff --git a/macos/Runner/InfoBase.plist b/macos/Runner/InfoBase.plist index 98d0ea9ee..c849a42c7 100644 --- a/macos/Runner/InfoBase.plist +++ b/macos/Runner/InfoBase.plist @@ -12,8 +12,10 @@ $(PRODUCT_BUNDLE_IDENTIFIER) CFBundleInfoDictionaryVersion 6.0 + CFBundleDisplayName + Cake Wallet CFBundleName - $(PRODUCT_NAME) + Cake Wallet CFBundlePackageType APPL CFBundleShortVersionString diff --git a/model_generator.sh b/model_generator.sh index 32d863aeb..e7231f19a 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,9 +1,8 @@ cd cw_core && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. +cd cw_evm && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_monero && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_bitcoin && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. cd cw_haven && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. -cd cw_ethereum && flutter pub get && flutter packages pub run build_runner build --delete-conflicting-outputs && cd .. 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 880b2abc0..554a16519 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -43,7 +43,7 @@ dependencies: auto_size_text: ^3.0.0 dotted_border: ^2.0.0+2 smooth_page_indicator: ^1.0.0+2 - flutter_inappwebview: ^5.7.2+3 + flutter_inappwebview: ^6.0.0 flutter_spinkit: ^5.1.0 uni_links: ^0.5.1 lottie: ^1.3.0 @@ -105,6 +105,8 @@ dependencies: socks5_proxy: ^1.0.4 flutter_svg: ^2.0.9 polyseed: ^0.0.2 + nostr_tools: ^1.0.9 + breez_sdk: git: url: https://github.com/breez/breez-sdk-flutter.git @@ -130,6 +132,11 @@ dev_dependencies: url: https://github.com/cake-tech/google-translator.git version: 1.0.0 +dependency_overrides: + bech32: + git: + url: https://github.com/cake-tech/bech32.git + flutter_icons: image_path: "assets/images/app_logo.png" android: true diff --git a/res/values/strings_ar.arb b/res/values/strings_ar.arb index 95871b904..7ee08460d 100644 --- a/res/values/strings_ar.arb +++ b/res/values/strings_ar.arb @@ -756,8 +756,20 @@ "dfx_option_description": "ﺎﺑﻭﺭﻭﺃ ﻲﻓ ﺕﺎﻛﺮﺸﻟﺍﻭ ﺔﺋﺰﺠﺘﻟﺍ ءﻼﻤﻌﻟ .ﻲﻓﺎﺿﺇ KYC ﻥﻭﺪﺑ ﻭﺭﻮﻳ 990 ﻰﻟﺇ ﻞﺼﻳ ﺎﻣ .ﻱﺮﺴﻳﻮﺴﻟﺍ", "polygonscan_history": "ﻥﺎﻜﺴﻧﻮﺠﻴﻟﻮﺑ ﺦﻳﺭﺎﺗ", "wallet_seed_legacy": "بذرة محفظة قديمة", - "default_sell_provider": " ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", + "default_sell_provider": "ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ", "select_sell_provider_notice": ".ﻖﻴﺒﻄﺘﻟﺍ ﺕﺍﺩﺍﺪﻋﺇ ﻲﻓ ﻚﺑ ﺹﺎﺨﻟﺍ ﻲﺿﺍﺮﺘﻓﻻﺍ ﻊﻴﺒﻟﺍ ﺩﻭﺰﻣ ﻦﻴﻴﻌﺗ ﻖﻳﺮﻃ ﻦﻋ ﺔﺷﺎﺸﻟﺍ ﻩﺬﻫ ﻲﻄﺨﺗ", "custom_drag": "مخصص (عقد وسحب)", - "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ" + "switchToEVMCompatibleWallet": " (Ethereum، Polygon) ﻯﺮﺧﺃ ﺓﺮﻣ ﺔﻟﻭﺎﺤﻤﻟﺍﻭ EVM ﻊﻣ ﺔﻘﻓﺍﻮﺘﻣ ﺔﻈﻔﺤﻣ ﻰﻟﺇ ﻞﻳﺪﺒﺘﻟﺍ ﻰﺟﺮﻳ", + "receivable_balance": "التوازن القادم", + "confirmed_tx": "مؤكد", + "transaction_details_source_address": "عنوان المصدر", + "pause_wallet_creation": ".ﺎﻴًﻟﺎﺣ ﺎﺘًﻗﺆﻣ ﺔﻔﻗﻮﺘﻣ Haven Wallet ءﺎﺸﻧﺇ ﻰﻠﻋ ﺓﺭﺪﻘﻟﺍ", + "contractName": "ﺪﻘﻌﻟﺍ ﻢﺳﺍ", + "contractSymbol": "ﺪﻘﻌﻟﺍ ﺰﻣﺭ", + "description": "ﻒﺻﻭ", + "camera_consent": ".ﻞﻴﺻﺎﻔﺘﻟﺍ ﻰﻠﻋ ﻝﻮﺼﺤﻠﻟ ﻢﻬﺑ ﺔﺻﺎﺨﻟﺍ ﺔﻴﺻﻮﺼﺨﻟﺍ ﺔﺳﺎﻴﺳ ﻦﻣ ﻖﻘﺤﺘﻟﺍ ﻰﺟﺮﻳ .${provider} ﻝﻮﻠ", + "no_relays": "ﺕﻼﺣﺮﻤﻟﺍ ﻻ", + "choose_relay": "ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ", + "no_relays_message": ".ﻪﺑ ﺹﺎﺨﻟﺍ Nostr ﻞﺠﺳ ﻰﻟﺇ ﺕﻼﺣﺮﻤﻟﺍ ﺔﻓﺎﺿﻹ ﻢﻠﺘﺴﻤﻟﺍ ﺩﺎﺷﺭﺇ ﻰﺟﺮﻳ .ﺕﻼﺣﺮﻣ ﻱﺃ ﻰﻠﻋ ﻱﻮﺘﺤﻳ ﻻ", + "no_relay_on_domain": ".ﻡﺍﺪﺨﺘﺳﻼﻟ ﻊﺑﺎﺘﺘﻟﺍ ﺭﺎﻴﺘﺧﺍ ءﺎﺟﺮﻟﺍ .ﺡﺎﺘﻣ ﺮﻴﻏ ﻞﻴﺣﺮﺘﻟﺍ ﻥﺃ ﻭﺃ ﻡﺪﺨﺘﺴﻤﻟﺍ ﻝﺎﺠﻤﻟ ﻞﻴﺣﺮﺗ ﺪ" } diff --git a/res/values/strings_bg.arb b/res/values/strings_bg.arb index 197dbe552..58c27d5e8 100644 --- a/res/values/strings_bg.arb +++ b/res/values/strings_bg.arb @@ -755,5 +755,17 @@ "default_sell_provider": "Доставчик за продажба по подразбиране", "select_sell_provider_notice": "Изберете доставчик на продажба по-горе. Можете да пропуснете този екран, като зададете своя доставчик на продажба по подразбиране в настройките на приложението.", "custom_drag": "Персонализиране (задръжте и плъзнете)", - "switchToEVMCompatibleWallet": "Моля, превключете към портфейл, съвместим с EVM, и опитайте отново (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Моля, превключете към портфейл, съвместим с EVM, и опитайте отново (Ethereum, Polygon)", + "receivable_balance": "Баланс за вземания", + "confirmed_tx": "Потвърдено", + "transaction_details_source_address": "Адрес на източника", + "pause_wallet_creation": "Възможността за създаване на Haven Wallet в момента е на пауза.", + "contractName": "Име на договора", + "contractSymbol": "Договор Символ", + "description": "Описание", + "camera_consent": "Вашият фотоапарат ще бъде използван за заснемане на изображение с цел идентификация от ${provider}. Моля, проверете тяхната политика за поверителност за подробности.", + "no_relays": "Без релета", + "choose_relay": "Моля, изберете реле, което да използвате", + "no_relays_message": "Намерихме запис Nostr NIP-05 за този потребител, но той не съдържа релета. Моля, инструктирайте получателя да добави релета към своя Nostr запис.", + "no_relay_on_domain": "Няма реле за домейна на потребителя или релето не е налично. Моля, изберете реле, което да използвате." } diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index bed4e3193..92874e315 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -755,5 +755,17 @@ "default_sell_provider": "Výchozí poskytovatel prodeje", "select_sell_provider_notice": "Výše vyberte poskytovatele prodeje. Tuto obrazovku můžete přeskočit nastavením výchozího poskytovatele prodeje v nastavení aplikace.", "custom_drag": "Custom (Hold and Drag)", - "switchToEVMCompatibleWallet": "Přepněte na peněženku kompatibilní s EVM a zkuste to znovu (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Přepněte na peněženku kompatibilní s EVM a zkuste to znovu (Ethereum, Polygon)", + "receivable_balance": "Zůstatek pohledávek", + "confirmed_tx": "Potvrzeno", + "transaction_details_source_address": "Zdrojová adresa", + "pause_wallet_creation": "Možnost vytvářet Haven Wallet je momentálně pozastavena.", + "contractName": "Název smlouvy", + "contractSymbol": "Symbol smlouvy", + "description": "Popis", + "camera_consent": "Váš fotoaparát použije k pořízení snímku pro účely identifikace ${provider}. Podrobnosti najdete v jejich Zásadách ochrany osobních údajů.", + "no_relays": "Žádná relé", + "choose_relay": "Vyberte relé, které chcete použít", + "no_relays_message": "Pro tohoto uživatele jsme našli záznam Nostr NIP-05, který však neobsahuje žádná relé. Požádejte příjemce, aby přidal přenosy do svého záznamu Nostr.", + "no_relay_on_domain": "Pro doménu uživatele neexistuje přenos nebo je přenos nedostupný. Vyberte relé, které chcete použít." } diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index 1d63bffc6..9aca5ba28 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Standard-Verkaufsanbieter", "select_sell_provider_notice": "Wählen Sie oben einen Verkaufsanbieter aus. Sie können diesen Bildschirm überspringen, indem Sie in den App-Einstellungen Ihren Standard-Verkaufsanbieter festlegen.", "custom_drag": "Custom (Hold and Drag)", - "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Bitte wechseln Sie zu einem EVM-kompatiblen Wallet und versuchen Sie es erneut (Ethereum, Polygon)", + "receivable_balance": "Forderungsbilanz", + "confirmed_tx": "Bestätigt", + "transaction_details_source_address": "Quelladresse", + "pause_wallet_creation": "Die Möglichkeit, Haven Wallet zu erstellen, ist derzeit pausiert.", + "contractName": "Vertragsname", + "contractSymbol": "Vertragssymbol", + "description": "Beschreibung", + "camera_consent": "Mit Ihrer Kamera wird bis zum ${provider} ein Bild zur Identifizierung aufgenommen. Weitere Informationen finden Sie in deren Datenschutzbestimmungen.", + "no_relays": "Keine Relais", + "choose_relay": "Bitte wählen Sie ein zu verwendendes Relais aus", + "no_relays_message": "Wir haben einen Nostr NIP-05-Eintrag für diesen Benutzer gefunden, der jedoch keine Relays enthält. Bitte weisen Sie den Empfänger an, Relays zu seinem Nostr-Datensatz hinzuzufügen.", + "no_relay_on_domain": "Es gibt kein Relay für die Domäne des Benutzers oder das Relay ist nicht verfügbar. Bitte wählen Sie ein zu verwendendes Relais aus." } diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index e6c95270e..d54b7baa4 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -764,5 +764,17 @@ "default_sell_provider": "Default Sell Provider", "select_sell_provider_notice": "Select a sell provider above. You can skip this screen by setting your default sell provider in app settings.", "custom_drag": "Custom (Hold and Drag)", - "switchToEVMCompatibleWallet": "Please switch to an EVM compatible wallet and try again (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Please switch to an EVM compatible wallet and try again (Ethereum, Polygon)", + "receivable_balance": "Receivable Balance", + "confirmed_tx": "Confirmed", + "transaction_details_source_address": "Source address", + "pause_wallet_creation": "Ability to create Haven Wallet is currently paused.", + "contractName": "Contract Name", + "contractSymbol": "Contract Symbol", + "description": "Description", + "camera_consent": "Your camera will be used to capture an image for identification purposes by ${provider}. Please check their Privacy Policy for details.", + "no_relays": "No relays", + "choose_relay": "Please choose a relay to use", + "no_relays_message": "We found a Nostr NIP-05 record for this user, but it does not contain any relays. Please instruct the recipient to add relays to their Nostr record.", + "no_relay_on_domain": "There isn't a relay for user's domain or the relay is unavailable. Please choose a relay to use." } diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 6ee0f509c..5f9a8e53b 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Proveedor de venta predeterminado", "select_sell_provider_notice": "Seleccione un proveedor de venta arriba. Puede omitir esta pantalla configurando su proveedor de venta predeterminado en la configuración de la aplicación.", "custom_drag": "Custom (mantenía y arrastre)", - "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Cambie a una billetera compatible con EVM e inténtelo nuevamente (Ethereum, Polygon)", + "receivable_balance": "Saldo de cuentas por cobrar", + "confirmed_tx": "Confirmado", + "transaction_details_source_address": "Dirección de la fuente", + "pause_wallet_creation": "La capacidad para crear Haven Wallet está actualmente pausada.", + "contractName": "Nombre del contrato", + "contractSymbol": "Símbolo de contrato", + "description": "Descripción", + "camera_consent": "Su cámara será utilizada para capturar una imagen con fines de identificación por ${provider}. Consulte su Política de privacidad para obtener más detalles.", + "no_relays": "Sin relevos", + "choose_relay": "Por favor elija un relé para usar", + "no_relays_message": "Encontramos un registro Nostr NIP-05 para este usuario, pero no contiene ningún relé. Indique al destinatario que agregue retransmisiones a su registro Nostr.", + "no_relay_on_domain": "No hay una retransmisión para el dominio del usuario o la retransmisión no está disponible. Elija un relé para usar." } diff --git a/res/values/strings_fr.arb b/res/values/strings_fr.arb index 862f9ae7d..523253bf4 100644 --- a/res/values/strings_fr.arb +++ b/res/values/strings_fr.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Fournisseur de vente par défaut", "select_sell_provider_notice": "Sélectionnez un fournisseur de vente ci-dessus. Vous pouvez ignorer cet écran en définissant votre fournisseur de vente par défaut dans les paramètres de l'application.", "custom_drag": "Custom (maintenir et traîner)", - "switchToEVMCompatibleWallet": "Veuillez passer à un portefeuille compatible EVM et réessayer (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Veuillez passer à un portefeuille compatible EVM et réessayer (Ethereum, Polygon)", + "receivable_balance": "Solde de créances", + "confirmed_tx": "Confirmé", + "transaction_details_source_address": "Adresse source", + "pause_wallet_creation": "La possibilité de créer Haven Wallet est actuellement suspendue.", + "contractName": "Nom du contrat", + "contractSymbol": "Symbole du contrat", + "description": "Description", + "camera_consent": "Votre appareil photo sera utilisé pour capturer une image à des fins d'identification par ${provider}. Veuillez consulter leur politique de confidentialité pour plus de détails.", + "no_relays": "Pas de relais", + "choose_relay": "Veuillez choisir un relais à utiliser", + "no_relays_message": "Nous avons trouvé un enregistrement Nostr NIP-05 pour cet utilisateur, mais il ne contient aucun relais. Veuillez demander au destinataire d'ajouter des relais à son enregistrement Nostr.", + "no_relay_on_domain": "Il n'existe pas de relais pour le domaine de l'utilisateur ou le relais n'est pas disponible. Veuillez choisir un relais à utiliser." } diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 9456085dc..2f2343434 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -745,5 +745,17 @@ "default_sell_provider": "Tsohuwar Mai Bayar Siyarwa", "select_sell_provider_notice": "Zaɓi mai bada siyarwa a sama. Kuna iya tsallake wannan allon ta saita mai bada siyar da ku a cikin saitunan app.", "custom_drag": "Al'ada (riƙe da ja)", - "switchToEVMCompatibleWallet": "Da fatan za a canza zuwa walat ɗin EVM mai jituwa kuma a sake gwadawa (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Da fatan za a canza zuwa walat ɗin EVM mai jituwa kuma a sake gwadawa (Ethereum, Polygon)", + "receivable_balance": "Daidaituwa da daidaituwa", + "confirmed_tx": "Tabbatar", + "transaction_details_source_address": "Adireshin Incord", + "pause_wallet_creation": "A halin yanzu an dakatar da ikon ƙirƙirar Haven Wallet.", + "contractName": "Sunan Kwangila", + "contractSymbol": "Alamar Kwangila", + "description": "Bayani", + "camera_consent": "Za a yi amfani da kyamarar ku don ɗaukar hoto don dalilai na tantancewa ta ${provider}. Da fatan za a duba Manufar Sirri don cikakkun bayanai.", + "no_relays": "Babu relays", + "choose_relay": "Da fatan za a zaɓi gudun ba da sanda don amfani", + "no_relays_message": "Mun sami rikodin Nostr NIP-05 don wannan mai amfani, amma ba ya ƙunshe da kowane relays. Da fatan za a umurci mai karɓa ya ƙara relays zuwa rikodin su na Nostr.", + "no_relay_on_domain": "Babu gudun ba da sanda ga yankin mai amfani ko kuma ba a samu ba. Da fatan za a zaɓi gudun ba da sanda don amfani." } diff --git a/res/values/strings_hi.arb b/res/values/strings_hi.arb index f6a7701ee..a3950b44e 100644 --- a/res/values/strings_hi.arb +++ b/res/values/strings_hi.arb @@ -763,5 +763,17 @@ "default_sell_provider": "डिफ़ॉल्ट विक्रय प्रदाता", "select_sell_provider_notice": "ऊपर एक विक्रय प्रदाता का चयन करें। आप ऐप सेटिंग में अपना डिफ़ॉल्ट विक्रय प्रदाता सेट करके इस स्क्रीन को छोड़ सकते हैं।", "custom_drag": "कस्टम (पकड़ और खींचें)", - "switchToEVMCompatibleWallet": "कृपया ईवीएम संगत वॉलेट पर स्विच करें और पुनः प्रयास करें (एथेरियम, पॉलीगॉन)" + "switchToEVMCompatibleWallet": "कृपया ईवीएम संगत वॉलेट पर स्विच करें और पुनः प्रयास करें (एथेरियम, पॉलीगॉन)", + "receivable_balance": "प्राप्य शेष", + "confirmed_tx": "की पुष्टि", + "transaction_details_source_address": "स्रोत पता", + "pause_wallet_creation": "हेवन वॉलेट बनाने की क्षमता फिलहाल रुकी हुई है।", + "contractName": "अनुबंध का नाम", + "contractSymbol": "अनुबंध चिह्न", + "description": "विवरण", + "camera_consent": "आपके कैमरे का उपयोग ${provider} द्वारा पहचान उद्देश्यों के लिए एक छवि कैप्चर करने के लिए किया जाएगा। विवरण के लिए कृपया उनकी गोपनीयता नीति जांचें।", + "no_relays": "कोई रिले नहीं", + "choose_relay": "कृपया उपयोग करने के लिए एक रिले चुनें", + "no_relays_message": "हमें इस उपयोगकर्ता के लिए एक Nostr NIP-05 रिकॉर्ड मिला, लेकिन इसमें कोई रिले नहीं है। कृपया प्राप्तकर्ता को अपने नॉस्ट्र रिकॉर्ड में रिले जोड़ने का निर्देश दें।", + "no_relay_on_domain": "उपयोगकर्ता के डोमेन के लिए कोई रिले नहीं है या रिले अनुपलब्ध है। कृपया उपयोग करने के लिए एक रिले चुनें।" } diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 3565c89a0..8e3365c96 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -761,5 +761,17 @@ "default_sell_provider": "Zadani dobavljač prodaje", "select_sell_provider_notice": "Gore odaberite pružatelja usluga prodaje. Ovaj zaslon možete preskočiti postavljanjem zadanog pružatelja usluga prodaje u postavkama aplikacije.", "custom_drag": "Prilagođeni (držite i povucite)", - "switchToEVMCompatibleWallet": "Prijeđite na novčanik kompatibilan s EVM-om i pokušajte ponovno (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Prijeđite na novčanik kompatibilan s EVM-om i pokušajte ponovno (Ethereum, Polygon)", + "receivable_balance": "Stanje potraživanja", + "confirmed_tx": "Potvrđen", + "transaction_details_source_address": "Adresa izvora", + "pause_wallet_creation": "Mogućnost stvaranja novčanika Haven trenutno je pauzirana.", + "contractName": "Naziv ugovora", + "contractSymbol": "Simbol ugovora", + "description": "Opis", + "camera_consent": "Vaš će fotoaparat koristiti za snimanje slike u svrhu identifikacije od strane ${provider}. Pojedinosti potražite u njihovoj politici privatnosti.", + "no_relays": "Nema releja", + "choose_relay": "Odaberite relej za korištenje", + "no_relays_message": "Pronašli smo zapis Nostr NIP-05 za ovog korisnika, ali on ne sadrži nikakve releje. Uputite primatelja da doda releje u svoj Nostr zapis.", + "no_relay_on_domain": "Ne postoji relej za korisničku domenu ili je relej nedostupan. Odaberite relej za korištenje." } diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 6744f0a38..27bfc4c7b 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -751,5 +751,17 @@ "default_sell_provider": "Penyedia Penjualan Default", "select_sell_provider_notice": "Pilih penyedia jual di atas. Anda dapat melewati layar ini dengan mengatur penyedia penjualan default Anda di pengaturan aplikasi.", "custom_drag": "Khusus (tahan dan seret)", - "switchToEVMCompatibleWallet": "Silakan beralih ke dompet yang kompatibel dengan EVM dan coba lagi (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Silakan beralih ke dompet yang kompatibel dengan EVM dan coba lagi (Ethereum, Polygon)", + "receivable_balance": "Saldo piutang", + "confirmed_tx": "Dikonfirmasi", + "transaction_details_source_address": "Alamat sumber", + "pause_wallet_creation": "Kemampuan untuk membuat Haven Wallet saat ini dijeda.", + "contractName": "Nama Kontrak", + "contractSymbol": "Simbol Kontrak", + "description": "Keterangan", + "camera_consent": "Kamera Anda akan digunakan untuk mengambil gambar untuk tujuan identifikasi oleh ${provider}. Silakan periksa Kebijakan Privasi mereka untuk detailnya.", + "no_relays": "Tidak ada relay", + "choose_relay": "Silakan pilih relai yang akan digunakan", + "no_relays_message": "Kami menemukan catatan Nostr NIP-05 untuk pengguna ini, tetapi tidak berisi relay apa pun. Harap instruksikan penerima untuk menambahkan relay ke catatan Nostr mereka.", + "no_relay_on_domain": "Tidak ada relai untuk domain pengguna atau relai tidak tersedia. Silakan pilih relai yang akan digunakan." } diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index bb909a7e0..a29ac5c93 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Fornitore di vendita predefinito", "select_sell_provider_notice": "Seleziona un fornitore di vendita sopra. Puoi saltare questa schermata impostando il tuo fornitore di vendita predefinito nelle impostazioni dell'app.", "custom_drag": "Custom (Hold and Drag)", - "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Passa a un portafoglio compatibile con EVM e riprova (Ethereum, Polygon)", + "receivable_balance": "Bilanciamento creditizio", + "confirmed_tx": "Confermato", + "transaction_details_source_address": "Indirizzo di partenza", + "pause_wallet_creation": "La possibilità di creare Haven Wallet è attualmente sospesa.", + "contractName": "Nome del contratto", + "contractSymbol": "Simbolo del contratto", + "description": "Descrizione", + "camera_consent": "La tua fotocamera verrà utilizzata per acquisire un'immagine a scopo identificativo da ${provider}. Si prega di controllare la loro Informativa sulla privacy per i dettagli.", + "no_relays": "Nessun relè", + "choose_relay": "Scegli un relè da utilizzare", + "no_relays_message": "Abbiamo trovato un record Nostr NIP-05 per questo utente, ma non contiene alcun relè. Si prega di indicare al destinatario di aggiungere inoltri al proprio record Nostr.", + "no_relay_on_domain": "Non esiste un inoltro per il dominio dell'utente oppure l'inoltro non è disponibile. Scegli un relè da utilizzare." } diff --git a/res/values/strings_ja.arb b/res/values/strings_ja.arb index 2152a3dcc..df317392f 100644 --- a/res/values/strings_ja.arb +++ b/res/values/strings_ja.arb @@ -763,5 +763,17 @@ "default_sell_provider": "デフォルトの販売プロバイダー", "select_sell_provider_notice": "上記の販売プロバイダーを選択してください。アプリ設定でデフォルトの販売プロバイダーを設定することで、この画面をスキップできます。", "custom_drag": "カスタム(ホールドとドラッグ)", - "switchToEVMCompatibleWallet": "EVM 互換のウォレットに切り替えて再試行してください (イーサリアム、ポリゴン)" + "switchToEVMCompatibleWallet": "EVM 互換のウォレットに切り替えて再試行してください (イーサリアム、ポリゴン)", + "receivable_balance": "売掛金残高", + "confirmed_tx": "確認済み", + "transaction_details_source_address": "ソースアドレス", + "pause_wallet_creation": "Haven Wallet を作成する機能は現在一時停止されています。", + "contractName": "契約名", + "contractSymbol": "契約記号", + "description": "説明", + "camera_consent": "あなたのカメラは、${provider}_ までに識別目的で画像を撮影するために使用されます。詳細については、プライバシー ポリシーをご確認ください。", + "no_relays": "リレーなし", + "choose_relay": "使用するリレーを選択してください", + "no_relays_message": "このユーザーの Nostr NIP-05 レコードが見つかりましたが、リレーは含まれていません。受信者に Nostr レコードにリレーを追加するよう指示してください。", + "no_relay_on_domain": "ユーザーのドメインのリレーが存在しないか、リレーが使用できません。使用するリレーを選択してください。" } diff --git a/res/values/strings_ko.arb b/res/values/strings_ko.arb index 5ad5482ed..5c3affbc5 100644 --- a/res/values/strings_ko.arb +++ b/res/values/strings_ko.arb @@ -761,5 +761,17 @@ "default_sell_provider": "기본 판매 공급자", "select_sell_provider_notice": "위에서 판매 공급자를 선택하세요. 앱 설정에서 기본 판매 공급자를 설정하면 이 화면을 건너뛸 수 있습니다.", "custom_drag": "사용자 정의 (홀드 앤 드래그)", - "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)" + "switchToEVMCompatibleWallet": "EVM 호환 지갑으로 전환 후 다시 시도해 주세요. (이더리움, 폴리곤)", + "receivable_balance": "채권 잔액", + "confirmed_tx": "확인", + "transaction_details_source_address": "소스 주소", + "pause_wallet_creation": "Haven Wallet 생성 기능이 현재 일시 중지되었습니다.", + "contractName": "계약명", + "contractSymbol": "계약 기호", + "description": "설명", + "camera_consent": "귀하의 카메라는 ${provider}의 식별 목적으로 이미지를 캡처하는 데 사용됩니다. 자세한 내용은 해당 개인정보 보호정책을 확인하세요.", + "no_relays": "릴레이 없음", + "choose_relay": "사용할 릴레이를 선택해주세요", + "no_relays_message": "이 사용자에 대한 Nostr NIP-05 레코드를 찾았지만 릴레이가 포함되어 있지 않습니다. 수신자에게 Nostr 기록에 릴레이를 추가하도록 지시하십시오.", + "no_relay_on_domain": "사용자 도메인에 릴레이가 없거나 릴레이를 사용할 수 없습니다. 사용할 릴레이를 선택해주세요." } diff --git a/res/values/strings_my.arb b/res/values/strings_my.arb index 73f4e5dcd..27c9bdeeb 100644 --- a/res/values/strings_my.arb +++ b/res/values/strings_my.arb @@ -761,5 +761,17 @@ "default_sell_provider": "ပုံသေရောင်းချပေးသူ", "select_sell_provider_notice": "အထက်ဖော်ပြပါ အရောင်းဝန်ဆောင်မှုပေးသူကို ရွေးပါ။ အက်ပ်ဆက်တင်များတွင် သင်၏မူလရောင်းချပေးသူကို သတ်မှတ်ခြင်းဖြင့် ဤစခရင်ကို ကျော်နိုင်သည်။", "custom_drag": "စိတ်ကြိုက် (Drag)", - "switchToEVMCompatibleWallet": "ကျေးဇူးပြု၍ EVM တွဲဖက်သုံးနိုင်သော ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ (Ethereum၊ Polygon)" + "switchToEVMCompatibleWallet": "ကျေးဇူးပြု၍ EVM တွဲဖက်သုံးနိုင်သော ပိုက်ဆံအိတ်သို့ ပြောင်းပြီး ထပ်စမ်းကြည့်ပါ (Ethereum၊ Polygon)", + "receivable_balance": "လက်ကျန်ငွေ", + "confirmed_tx": "အတည်ပြုသည်", + "transaction_details_source_address": "အရင်းအမြစ်လိပ်စာ", + "pause_wallet_creation": "Haven Wallet ဖန်တီးနိုင်မှုကို လောလောဆယ် ခေတ္တရပ်ထားသည်။", + "contractName": "စာချုပ်အမည်", + "contractSymbol": "စာချုပ်သင်္ကေတ", + "description": "ဖော်ပြချက်", + "camera_consent": "မှတ်ပုံတင်ခြင်းရည်ရွယ်ချက်များအတွက် ${provider} တွင် သင့်ကင်မရာကို အသုံးပြုပါမည်။ အသေးစိတ်အတွက် ၎င်းတို့၏ ကိုယ်ရေးကိုယ်တာမူဝါဒကို စစ်ဆေးပါ။", + "no_relays": "Relay မရှိပါ။", + "choose_relay": "အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။", + "no_relays_message": "ဤအသုံးပြုသူအတွက် Nostr NIP-05 မှတ်တမ်းကို ကျွန်ုပ်တို့တွေ့ရှိသော်လည်း ၎င်းတွင် မည်သည့် relays မှ မပါဝင်ပါ။ ကျေးဇူးပြု၍ လက်ခံသူကို ၎င်းတို့၏ Nostr မှတ်တမ်းတွင် ထပ်လောင်းထည့်ရန် ညွှန်ကြားပါ။", + "no_relay_on_domain": "အသုံးပြုသူ၏ဒိုမိန်းအတွက် ထပ်ဆင့်လွှင့်ခြင်း မရှိပါ သို့မဟုတ် ထပ်ဆင့်လွှင့်ခြင်း မရနိုင်ပါ။ အသုံးပြုရန် relay ကိုရွေးချယ်ပါ။" } diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 6cf41e0b9..51f25e64a 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Standaard verkoopaanbieder", "select_sell_provider_notice": "Selecteer hierboven een verkoopaanbieder. U kunt dit scherm overslaan door uw standaardverkoopprovider in te stellen in de app-instellingen.", "custom_drag": "Custom (vasthouden en slepen)", - "switchToEVMCompatibleWallet": "Schakel over naar een EVM-compatibele portemonnee en probeer het opnieuw (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Schakel over naar een EVM-compatibele portemonnee en probeer het opnieuw (Ethereum, Polygon)", + "receivable_balance": "Het saldo", + "confirmed_tx": "Bevestigd", + "transaction_details_source_address": "Bron adres", + "pause_wallet_creation": "De mogelijkheid om Haven Wallet te maken is momenteel onderbroken.", + "contractName": "Contractnaam", + "contractSymbol": "Contractsymbool", + "description": "Beschrijving", + "camera_consent": "Uw camera wordt gebruikt om vóór ${provider} een beeld vast te leggen voor identificatiedoeleinden. Raadpleeg hun privacybeleid voor meer informatie.", + "no_relays": "Geen relais", + "choose_relay": "Kies een relais dat u wilt gebruiken", + "no_relays_message": "We hebben een Nostr NIP-05-record voor deze gebruiker gevonden, maar deze bevat geen relays. Instrueer de ontvanger om relays toe te voegen aan zijn Nostr-record.", + "no_relay_on_domain": "Er is geen relay voor het domein van de gebruiker of de relay is niet beschikbaar. Kies een relais dat u wilt gebruiken." } diff --git a/res/values/strings_pl.arb b/res/values/strings_pl.arb index 9d3fb3f93..4363ef5ae 100644 --- a/res/values/strings_pl.arb +++ b/res/values/strings_pl.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Domyślny dostawca sprzedaży", "select_sell_provider_notice": "Wybierz dostawcę sprzedaży powyżej. Możesz pominąć ten ekran, ustawiając domyślnego dostawcę sprzedaży w ustawieniach aplikacji.", "custom_drag": "Niestandardowe (trzymaj i przeciągnij)", - "switchToEVMCompatibleWallet": "Przejdź na portfel zgodny z EVM i spróbuj ponownie (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Przejdź na portfel zgodny z EVM i spróbuj ponownie (Ethereum, Polygon)", + "receivable_balance": "Saldo należności", + "confirmed_tx": "Potwierdzony", + "transaction_details_source_address": "Adres źródłowy", + "pause_wallet_creation": "Możliwość utworzenia Portfela Haven jest obecnie wstrzymana.", + "contractName": "Nazwa umowy", + "contractSymbol": "Symbol kontraktu", + "description": "Opis", + "camera_consent": "Twój aparat zostanie użyty do przechwycenia obrazu w celach identyfikacyjnych przez ${provider}. Aby uzyskać szczegółowe informacje, sprawdź ich Politykę prywatności.", + "no_relays": "Żadnych przekaźników", + "choose_relay": "Wybierz przekaźnik, którego chcesz użyć", + "no_relays_message": "Znaleźliśmy rekord Nostr NIP-05 dla tego użytkownika, ale nie zawiera on żadnych przekaźników. Poinstruuj odbiorcę, aby dodał przekaźniki do swojego rekordu Nostr.", + "no_relay_on_domain": "Brak przekaźnika dla domeny użytkownika lub przekaźnik jest niedostępny. Wybierz przekaźnik, którego chcesz użyć." } diff --git a/res/values/strings_pt.arb b/res/values/strings_pt.arb index 5cf1fd370..7cfd261eb 100644 --- a/res/values/strings_pt.arb +++ b/res/values/strings_pt.arb @@ -762,5 +762,17 @@ "default_sell_provider": "Provedor de venda padrão", "select_sell_provider_notice": "Selecione um fornecedor de venda acima. Você pode pular esta tela definindo seu provedor de venda padrão nas configurações do aplicativo.", "custom_drag": "Personalizado (segure e arraste)", - "switchToEVMCompatibleWallet": "Mude para uma carteira compatível com EVM e tente novamente (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Mude para uma carteira compatível com EVM e tente novamente (Ethereum, Polygon)", + "receivable_balance": "Saldo a receber", + "confirmed_tx": "Confirmado", + "transaction_details_source_address": "Endereço de Origem", + "pause_wallet_creation": "A capacidade de criar a Haven Wallet está atualmente pausada.", + "contractName": "Nome do contrato", + "contractSymbol": "Símbolo do Contrato", + "description": "Descrição", + "camera_consent": "Sua câmera será usada para capturar uma imagem para fins de identificação por ${provider}. Por favor, verifique a Política de Privacidade para obter detalhes.", + "no_relays": "Sem relés", + "choose_relay": "Escolha um relé para usar", + "no_relays_message": "Encontramos um registro Nostr NIP-05 para este usuário, mas ele não contém nenhum relé. Instrua o destinatário a adicionar retransmissões ao seu registro Nostr.", + "no_relay_on_domain": "Não há uma retransmissão para o domínio do usuário ou a retransmissão está indisponível. Escolha um relé para usar." } diff --git a/res/values/strings_ru.arb b/res/values/strings_ru.arb index c81c0bd82..a4c34f5c5 100644 --- a/res/values/strings_ru.arb +++ b/res/values/strings_ru.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Поставщик продаж по умолчанию", "select_sell_provider_notice": "Выберите поставщика услуг продажи выше. Вы можете пропустить этот экран, установив поставщика услуг продаж по умолчанию в настройках приложения.", "custom_drag": "Пользователь (удерживайте и перетаскивайте)", - "switchToEVMCompatibleWallet": "Пожалуйста, переключитесь на кошелек, совместимый с EVM, и повторите попытку (Ethereum, Polygon)." + "switchToEVMCompatibleWallet": "Пожалуйста, переключитесь на кошелек, совместимый с EVM, и повторите попытку (Ethereum, Polygon).", + "receivable_balance": "Баланс дебиторской задолженности", + "confirmed_tx": "Подтвержденный", + "transaction_details_source_address": "Адрес источника", + "pause_wallet_creation": "Возможность создания Haven Wallet в настоящее время приостановлена.", + "contractName": "Название контракта", + "contractSymbol": "Символ контракта", + "description": "Описание", + "camera_consent": "Ваша камера будет использоваться для захвата изображения в целях идентификации ${provider}. Пожалуйста, ознакомьтесь с их Политикой конфиденциальности для получения подробной информации.", + "no_relays": "Нет реле", + "choose_relay": "Пожалуйста, выберите реле для использования", + "no_relays_message": "Мы нашли запись Nostr NIP-05 для этого пользователя, но она не содержит никаких реле. Попросите получателя добавить реле в свою запись Nostr.", + "no_relay_on_domain": "Для домена пользователя реле не существует или реле недоступно. Пожалуйста, выберите реле для использования." } diff --git a/res/values/strings_th.arb b/res/values/strings_th.arb index 0965d3979..603d04067 100644 --- a/res/values/strings_th.arb +++ b/res/values/strings_th.arb @@ -761,5 +761,17 @@ "default_sell_provider": "ผู้ให้บริการการขายเริ่มต้น", "select_sell_provider_notice": "เลือกผู้ให้บริการการขายด้านบน คุณสามารถข้ามหน้าจอนี้ได้โดยการตั้งค่าผู้ให้บริการการขายเริ่มต้นในการตั้งค่าแอป", "custom_drag": "กำหนดเอง (ค้างและลาก)", - "switchToEVMCompatibleWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงินที่รองรับ EVM แล้วลองอีกครั้ง (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "โปรดเปลี่ยนไปใช้กระเป๋าเงินที่รองรับ EVM แล้วลองอีกครั้ง (Ethereum, Polygon)", + "receivable_balance": "ยอดลูกหนี้", + "confirmed_tx": "ซึ่งยืนยันแล้ว", + "transaction_details_source_address": "ที่อยู่แหล่งกำเนิด", + "pause_wallet_creation": "ขณะนี้ความสามารถในการสร้าง Haven Wallet ถูกหยุดชั่วคราว", + "contractName": "ชื่อสัญญา", + "contractSymbol": "สัญลักษณ์สัญญา", + "description": "คำอธิบาย", + "camera_consent": "กล้องของคุณจะถูกนำมาใช้เพื่อจับภาพเพื่อวัตถุประสงค์ในการระบุตัวตนภายใน ${provider} โปรดตรวจสอบนโยบายความเป็นส่วนตัวเพื่อดูรายละเอียด", + "no_relays": "ไม่มีรีเลย์", + "choose_relay": "กรุณาเลือกรีเลย์ที่จะใช้", + "no_relays_message": "เราพบบันทึก Nostr NIP-05 สำหรับผู้ใช้รายนี้ แต่ไม่มีรีเลย์ใดๆ โปรดแนะนำให้ผู้รับเพิ่มรีเลย์ลงในบันทึก Nostr ของตน", + "no_relay_on_domain": "ไม่มีการส่งต่อสำหรับโดเมนของผู้ใช้ หรือการส่งต่อไม่พร้อมใช้งาน กรุณาเลือกรีเลย์ที่จะใช้" } diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index f9bbb5dea..a8ec4df3e 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -757,5 +757,17 @@ "default_sell_provider": "Default na Sell Provider", "select_sell_provider_notice": "Pumili ng provider ng nagbebenta sa itaas. Maaari mong laktawan ang screen na ito sa pamamagitan ng pagtatakda ng iyong default na sell provider sa mga setting ng app.", "custom_drag": "Pasadyang (hawakan at i -drag)", - "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Mangyaring lumipat sa isang EVM compatible na wallet at subukang muli (Ethereum, Polygon)", + "receivable_balance": "Natatanggap na balanse", + "confirmed_tx": "Nakumpirma", + "transaction_details_source_address": "SOURCE ADDRESS", + "pause_wallet_creation": "Kasalukuyang naka-pause ang kakayahang gumawa ng Haven Wallet.", + "contractName": "Pangalan ng Kontrata", + "contractSymbol": "Simbolo ng Kontrata", + "description": "Paglalarawan", + "camera_consent": "Gagamitin ang iyong camera upang kumuha ng larawan para sa mga layunin ng pagkakakilanlan sa pamamagitan ng ${provider}. Pakisuri ang kanilang Patakaran sa Privacy para sa mga detalye.", + "no_relays": "Walang mga relay", + "choose_relay": "Mangyaring pumili ng relay na gagamitin", + "no_relays_message": "Nakakita kami ng Nostr NIP-05 record para sa user na ito, ngunit hindi ito naglalaman ng anumang mga relay. Mangyaring atasan ang tatanggap na magdagdag ng mga relay sa kanilang Nostr record.", + "no_relay_on_domain": "Walang relay para sa domain ng user o hindi available ang relay. Mangyaring pumili ng relay na gagamitin." } diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 6fdb8bb99..337a85ff8 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -761,5 +761,17 @@ "default_sell_provider": "Varsayılan Satış Sağlayıcısı", "select_sell_provider_notice": "Yukarıdan bir satış sağlayıcısı seçin. Uygulama ayarlarında varsayılan satış sağlayıcınızı ayarlayarak bu ekranı atlayabilirsiniz.", "custom_drag": "Özel (Bekle ve Sürükle)", - "switchToEVMCompatibleWallet": "Lütfen EVM uyumlu bir cüzdana geçin ve tekrar deneyin (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Lütfen EVM uyumlu bir cüzdana geçin ve tekrar deneyin (Ethereum, Polygon)", + "receivable_balance": "Alacak bakiyesi", + "confirmed_tx": "Onaylanmış", + "transaction_details_source_address": "Kaynak adresi", + "pause_wallet_creation": "Haven Cüzdanı oluşturma yeteneği şu anda duraklatıldı.", + "contractName": "Sözleşme Adı", + "contractSymbol": "Sözleşme Sembolü", + "description": "Tanım", + "camera_consent": "Kameranız ${provider} tarihine kadar tanımlama amacıyla bir görüntü yakalamak için kullanılacaktır. Ayrıntılar için lütfen Gizlilik Politikalarını kontrol edin.", + "no_relays": "Röle yok", + "choose_relay": "Lütfen kullanmak için bir röle seçin", + "no_relays_message": "Bu kullanıcı için bir Nostr NIP-05 kaydı bulduk ancak bu kayıt herhangi bir aktarma içermiyor. Lütfen alıcıya Nostr kayıtlarına aktarma eklemesi talimatını verin.", + "no_relay_on_domain": "Kullanıcının alanı için bir geçiş yok veya geçiş kullanılamıyor. Lütfen kullanmak için bir röle seçin." } diff --git a/res/values/strings_uk.arb b/res/values/strings_uk.arb index 721a3970d..7cc41ab43 100644 --- a/res/values/strings_uk.arb +++ b/res/values/strings_uk.arb @@ -763,5 +763,17 @@ "default_sell_provider": "Постачальник продажу за замовчуванням", "select_sell_provider_notice": "Виберіть вище постачальника послуг продажу. Ви можете пропустити цей екран, встановивши постачальника послуг продажу за умовчанням у налаштуваннях програми.", "custom_drag": "На замовлення (утримуйте та перетягується)", - "switchToEVMCompatibleWallet": "Перейдіть на гаманець, сумісний з EVM, і повторіть спробу (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Перейдіть на гаманець, сумісний з EVM, і повторіть спробу (Ethereum, Polygon)", + "receivable_balance": "Баланс дебіторської заборгованості", + "confirmed_tx": "Підтверджений", + "transaction_details_source_address": "Адреса джерела", + "pause_wallet_creation": "Можливість створення гаманця Haven зараз призупинено.", + "contractName": "Назва контракту", + "contractSymbol": "Контракт символ", + "description": "опис", + "camera_consent": "Ваша камера використовуватиметься для зйомки зображення з метою ідентифікації ${provider}. Будь ласка, ознайомтеся з їхньою політикою конфіденційності, щоб дізнатися більше.", + "no_relays": "Без реле", + "choose_relay": "Будь ласка, виберіть реле для використання", + "no_relays_message": "Ми знайшли запис Nostr NIP-05 для цього користувача, але він не містить жодних реле. Будь ласка, попросіть одержувача додати реле до свого запису Nostr.", + "no_relay_on_domain": "Немає ретранслятора для домену користувача або ретранслятор недоступний. Будь ласка, виберіть реле для використання." } diff --git a/res/values/strings_ur.arb b/res/values/strings_ur.arb index e4a9f4590..11fc67c4e 100644 --- a/res/values/strings_ur.arb +++ b/res/values/strings_ur.arb @@ -755,5 +755,17 @@ "default_sell_provider": " ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ", "select_sell_provider_notice": "۔ﮟﯿﮨ ﮯﺘﮑﺳ ﮌﻮﮭﭼ ﻮﮐ ﻦﯾﺮﮑﺳﺍ ﺱﺍ ﺮﮐ ﮮﺩ ﺐﯿﺗﺮﺗ ﻮﮐ ﮦﺪﻨﻨﮐ ﻢﮨﺍﺮﻓ ﻞﯿﺳ ﭧﻟﺎﻔﯾﮈ ﮯﻨﭘﺍ ﮟﯿﻣ ﺕﺎﺒ", "custom_drag": "کسٹم (ہولڈ اینڈ ڈریگ)", - "switchToEVMCompatibleWallet": "(Ethereum, Polygon) ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ ﮯﻟﺍﻭ ﮯﻨﮭﮐﺭ ﺖﻘﺑﺎﻄﻣ " + "switchToEVMCompatibleWallet": "(Ethereum, Polygon) ﮟﯾﺮﮐ ﺶﺷﻮﮐ ﮦﺭﺎﺑﻭﺩ ﺭﻭﺍ ﮟﯾﺮﮐ ﭻﺋﻮﺳ ﺮﭘ ﭧﯿﻟﺍﻭ ﮯﻟﺍﻭ ﮯﻨﮭﮐﺭ ﺖﻘﺑﺎﻄﻣ ", + "receivable_balance": "قابل وصول توازن", + "confirmed_tx": "تصدیق", + "transaction_details_source_address": "ماخذ ایڈریس", + "pause_wallet_creation": "Haven Wallet ۔ﮯﮨ ﻑﻮﻗﻮﻣ ﻝﺎﺤﻟﺍ ﯽﻓ ﺖﯿﻠﮨﺍ ﯽﮐ ﮯﻧﺎﻨﺑ", + "contractName": "ﻡﺎﻧ ﺎﮐ ﮦﺪﮨﺎﻌﻣ", + "contractSymbol": "ﺖﻣﻼﻋ ﯽﮐ ﮦﺪﮨﺎﻌﻣ", + "description": "ﻞﯿﺼﻔﺗ", + "camera_consent": "۔ﮟﯿﮭﮑﯾﺩ ﯽﺴﯿﻟﺎﭘ ﯽﺴﯾﻮﯿﺋﺍﺮﭘ ﯽﮐ ﻥﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ﮯﯿﻟ ﮯﮐ ﺕﻼ${provider}ﯿﺼﻔﺗ ۔ﺎﮔ ﮯﺋﺎﺟ ﺎﯿﮐ ﻝﺎﻤﻌﺘﺳﺍ ﮯﯿﻟ", + "no_relays": "۔ﮟﯿﮩﻧ ﮯﻠﯾﺭ ﯽﺋﻮﮐ", + "choose_relay": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ", + "no_relays_message": "۔ﮟﯾﺮﮐ ﻞﻣﺎﺷ ﮯﻠﯾﺭ ﮟﯿﻣ ﮈﺭﺎﮑﯾﺭ ﺮﭩﺳﻮﻧ ﮯﻨﭘﺍ ﮦﻭ ﮧﮐ ﮟﯾﺩ ﺖﯾﺍﺪﮨ ﻮﮐ ﮦﺪﻨﻨﮐ ﻝﻮﺻﻭ ﻡﺮﮐ ﮦﺍﺮﺑ ۔", + "no_relay_on_domain": "۔ﮟﯾﺮﮐ ﺏﺎﺨﺘﻧﺍ ﺎﮐ ﮯﻠﯾﺭ ﮯﯿﻟ ﮯﮐ ﮯﻧﺮﮐ ﻝﺎﻤﻌﺘﺳﺍ ﻡﺮﮐ ﮦﺍﺮﺑ ۔ﮯﮨ ﮟﯿﮩﻧ ﺏﺎﯿﺘﺳﺩ ﮯﻠﯾﺭ ﺎﯾ ﮯﮨ ﮟ" } diff --git a/res/values/strings_yo.arb b/res/values/strings_yo.arb index 964307e6b..142d9f5bf 100644 --- a/res/values/strings_yo.arb +++ b/res/values/strings_yo.arb @@ -757,5 +757,17 @@ "default_sell_provider": "Aiyipada Olupese Tita", "select_sell_provider_notice": "Yan olupese ti o ta loke. O le foju iboju yii nipa tito olupese iṣẹ tita aiyipada rẹ ni awọn eto app.", "custom_drag": "Aṣa (mu ati fa)", - "switchToEVMCompatibleWallet": "Jọwọ yipada si apamọwọ ibaramu EVM ki o tun gbiyanju lẹẹkansi (Ethereum, Polygon)" + "switchToEVMCompatibleWallet": "Jọwọ yipada si apamọwọ ibaramu EVM ki o tun gbiyanju lẹẹkansi (Ethereum, Polygon)", + "receivable_balance": "Iwontunws.funfun ti o gba", + "confirmed_tx": "Jẹrisi", + "transaction_details_source_address": "Adirẹsi orisun", + "pause_wallet_creation": "Agbara lati ṣẹda Haven Wallet ti wa ni idaduro lọwọlọwọ.", + "contractName": "Orukọ adehun", + "contractSymbol": "Aami adehun", + "description": "Apejuwe", + "camera_consent": "Kamẹra rẹ yoo ṣee lo lati ya aworan kan fun awọn idi idanimọ nipasẹ ${provider}. Jọwọ ṣayẹwo Ilana Aṣiri wọn fun awọn alaye.", + "no_relays": "Ko si relays", + "choose_relay": "Jọwọ yan yii lati lo", + "no_relays_message": "A ri igbasilẹ Nostr NIP-05 fun olumulo yii, ṣugbọn ko ni eyikeyi awọn iṣipopada ninu. Jọwọ sọ fun olugba lati ṣafikun awọn isunmọ si igbasilẹ Nostr wọn.", + "no_relay_on_domain": "Ko si iṣipopada fun agbegbe olumulo tabi yiyi ko si. Jọwọ yan yii lati lo." } diff --git a/res/values/strings_zh.arb b/res/values/strings_zh.arb index 43e8c6ce1..92904034b 100644 --- a/res/values/strings_zh.arb +++ b/res/values/strings_zh.arb @@ -762,5 +762,17 @@ "default_sell_provider": "默认销售提供商", "select_sell_provider_notice": "选择上面的销售提供商。您可以通过在应用程序设置中设置默认销售提供商来跳过此屏幕。", "custom_drag": "定制(保持和拖动)", - "switchToEVMCompatibleWallet": "请切换到 EVM 兼容钱包并重试(以太坊、Polygon)" + "switchToEVMCompatibleWallet": "请切换到 EVM 兼容钱包并重试(以太坊、Polygon)", + "receivable_balance": "应收余额", + "confirmed_tx": "确认的", + "transaction_details_source_address": "源地址", + "pause_wallet_creation": "创建 Haven 钱包的功能当前已暂停。", + "contractName": "合约名称", + "contractSymbol": "合约符号", + "description": "描述", + "camera_consent": "${provider} 将使用您的相机拍摄图像以供识别之用。请查看他们的隐私政策了解详情。", + "no_relays": "无继电器", + "choose_relay": "请选择要使用的继电器", + "no_relays_message": "我们找到了该用户的 Nostr NIP-05 记录,但它不包含任何中继。请指示收件人将中继添加到他们的 Nostr 记录中。", + "no_relay_on_domain": "用户域没有中继或中继不可用。请选择要使用的继电器。" } diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 5feae289e..8de5be02c 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -15,15 +15,15 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_ANDROID_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.9.0" -MONERO_COM_BUILD_NUMBER=71 +MONERO_COM_VERSION="1.10.2" +MONERO_COM_BUILD_NUMBER=74 MONERO_COM_BUNDLE_ID="com.monero.app" MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.12.1" -CAKEWALLET_BUILD_NUMBER=188 +CAKEWALLET_VERSION="4.13.2" +CAKEWALLET_BUILD_NUMBER=191 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index ee59e837c..c7d62a40a 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -13,13 +13,13 @@ TYPES=($MONERO_COM $CAKEWALLET $HAVEN) APP_IOS_TYPE=$1 MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="1.9.0" -MONERO_COM_BUILD_NUMBER=69 +MONERO_COM_VERSION="1.10.2" +MONERO_COM_BUILD_NUMBER=72 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="4.12.1" -CAKEWALLET_BUILD_NUMBER=206 +CAKEWALLET_VERSION="4.13.2" +CAKEWALLET_BUILD_NUMBER=210 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" HAVEN_NAME="Haven" diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 2e6a945bf..cda367b9c 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -1,5 +1,6 @@ #!/bin/bash +MONERO_COM="monero.com" CAKEWALLET="cakewallet" DIR=`pwd` @@ -10,6 +11,8 @@ fi cd ../.. # go to root cp -rf ./macos/Runner/InfoBase.plist ./macos/Runner/Info.plist +/usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName ${APP_MACOS_NAME}" ./macos/Runner/Info.plist +/usr/libexec/PlistBuddy -c "Set :CFBundleName ${APP_MACOS_NAME}" ./macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier ${APP_MACOS_BUNDLE_ID}" ./macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${APP_MACOS_VERSION}" ./macos/Runner/Info.plist /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${APP_MACOS_BUILD_NUMBER}" ./macos/Runner/Info.plist @@ -17,11 +20,16 @@ cp -rf ./macos/Runner/InfoBase.plist ./macos/Runner/Info.plist # Fill entitlements Bundle ID cp -rf ./macos/Runner/DebugProfileBase.entitlements ./macos/Runner/DebugProfile.entitlements cp -rf ./macos/Runner/ReleaseBase.entitlements ./macos/Runner/Release.entitlements +cp -rf ./macos/Runner/Configs/AppInfoBase.xcconfig ./macos/Runner/Configs/AppInfo.xcconfig sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/DebugProfile.entitlements sed -i '' "s/\${BUNDLE_ID}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Release.entitlements +sed -i '' "s/\${PRODUCT_NAME}/${APP_MACOS_NAME}/g" ./macos/Runner/Configs/AppInfo.xcconfig +sed -i '' "s/\${PRODUCT_BUNDLE_IDENTIFIER}/${APP_MACOS_BUNDLE_ID}/g" ./macos/Runner/Configs/AppInfo.xcconfig CONFIG_ARGS="" case $APP_MACOS_TYPE in + $MONERO_COM) + CONFIG_ARGS="--monero";; $CAKEWALLET) CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash";; #--haven esac @@ -31,4 +39,5 @@ flutter pub get flutter pub run tool/generate_pubspec.dart flutter pub get flutter packages pub run tool/configure.dart $CONFIG_ARGS -cd $DIR \ No newline at end of file +cd $DIR +$DIR/app_icon.sh \ No newline at end of file diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index a37d67a82..c2c7493af 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -5,18 +5,24 @@ APP_MACOS_VERSION="" APP_MACOS_BUILD_VERSION="" APP_MACOS_BUNDLE_ID="" +MONERO_COM="monero.com" CAKEWALLET="cakewallet" -TYPES=($CAKEWALLET) -APP_MACOS_TYPE=$CAKEWALLET +TYPES=($MONERO_COM $CAKEWALLET) +APP_MACOS_TYPE=$1 if [ -n "$1" ]; then APP_MACOS_TYPE=$1 fi +MONERO_COM_NAME="Monero.com" +MONERO_COM_VERSION="1.0.2" +MONERO_COM_BUILD_NUMBER=4 +MONERO_COM_BUNDLE_ID="com.cakewallet.monero" + CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="1.5.1" -CAKEWALLET_BUILD_NUMBER=48 +CAKEWALLET_VERSION="1.6.2" +CAKEWALLET_BUILD_NUMBER=52 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then @@ -25,6 +31,11 @@ if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then fi case $APP_MACOS_TYPE in + $MONERO_COM) + APP_MACOS_NAME=$MONERO_COM_NAME + APP_MACOS_VERSION=$MONERO_COM_VERSION + APP_MACOS_BUILD_NUMBER=$MONERO_COM_BUILD_NUMBER + APP_MACOS_BUNDLE_ID=$MONERO_COM_BUNDLE_ID;; $CAKEWALLET) APP_MACOS_NAME=$CAKEWALLET_NAME APP_MACOS_VERSION=$CAKEWALLET_VERSION diff --git a/scripts/macos/app_icon.sh b/scripts/macos/app_icon.sh new file mode 100755 index 000000000..28ca9c17b --- /dev/null +++ b/scripts/macos/app_icon.sh @@ -0,0 +1,44 @@ +#!/bin/sh + +ICON_16_PATH="" +ICON_32_PATH="" +ICON_64_PATH="" +ICON_128_PATH="" +ICON_256_PATH="" +ICON_1024_PATH="" +DEST_DIR_PATH=`pwd`/../../macos/Runner/Assets.xcassets/AppIcon.appiconset + +case $APP_MACOS_TYPE in + "monero.com") + ICON_16_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_16.png + ICON_32_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_32.png + ICON_64_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_64.png + ICON_128_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_128.png + ICON_256_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_256.png + ICON_512_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_512.png + ICON_1024_PATH=`pwd`/../../assets/images/macos_icons/monero_macos_icons/monero_macos_1024.png;; + "cakewallet") + ICON_16_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_16.png + ICON_32_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_32.png + ICON_64_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_64.png + ICON_128_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_128.png + ICON_256_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_256.png + ICON_512_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_512.png + ICON_1024_PATH=`pwd`/../../assets/images/macos_icons/cakewallet_macos_icons/cakewallet_macos_1024.png;; +esac + +rm $DEST_DIR_PATH/app_icon_16.png +rm $DEST_DIR_PATH/app_icon_32.png +rm $DEST_DIR_PATH/app_icon_64.png +rm $DEST_DIR_PATH/app_icon_128.png +rm $DEST_DIR_PATH/app_icon_256.png +rm $DEST_DIR_PATH/app_icon_512.png +rm $DEST_DIR_PATH/app_icon_1024.png + +ln -s $ICON_16_PATH $DEST_DIR_PATH/app_icon_16.png +ln -s $ICON_32_PATH $DEST_DIR_PATH/app_icon_32.png +ln -s $ICON_64_PATH $DEST_DIR_PATH/app_icon_64.png +ln -s $ICON_128_PATH $DEST_DIR_PATH/app_icon_128.png +ln -s $ICON_256_PATH $DEST_DIR_PATH/app_icon_256.png +ln -s $ICON_512_PATH $DEST_DIR_PATH/app_icon_512.png +ln -s $ICON_1024_PATH $DEST_DIR_PATH/app_icon_1024.png \ No newline at end of file diff --git a/scripts/macos/build_openssl_common.sh b/scripts/macos/build_openssl_common.sh index fd07312fa..27cb1ef8c 100755 --- a/scripts/macos/build_openssl_common.sh +++ b/scripts/macos/build_openssl_common.sh @@ -10,7 +10,7 @@ OPEN_SSL_ARM_DIR_PATH="${EXTERNAL_MACOS_SOURCE_DIR}/${OPEN_SSL_ARM_DIR_NAME}" build_openssl_init_common() { DIR=$1 - # Use 1.1.1s becasue of https://github.com/openssl/openssl/issues/18720 + # Use 1.1.1s because of https://github.com/openssl/openssl/issues/18720 OPENSSL_VERSION="1.1.1s" echo "============================ OpenSSL ============================" diff --git a/tool/configure.dart b/tool/configure.dart index 67732faa9..8297d8a39 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -64,6 +64,7 @@ import 'package:cw_core/output_info.dart'; import 'package:cw_core/unspent_coins_info.dart'; import 'package:cw_core/wallet_service.dart'; import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart';"""; const bitcoinCWHeaders = """ import 'package:cw_bitcoin/electrum_wallet.dart'; @@ -76,9 +77,27 @@ import 'package:cw_bitcoin/bitcoin_amount_format.dart'; import 'package:cw_bitcoin/bitcoin_address_record.dart'; import 'package:cw_bitcoin/bitcoin_transaction_credentials.dart'; import 'package:cw_bitcoin/litecoin_wallet_service.dart'; +import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ + + class ElectrumSubAddress { + ElectrumSubAddress({ + required this.id, + required this.name, + required this.address, + required this.txCount, + required this.balance, + required this.isChange}); + final int id; + final String name; + final String address; + final int txCount; + final int balance; + final bool isChange; +} + abstract class Bitcoin { TransactionPriority getMediumTransactionPriority(); @@ -92,20 +111,23 @@ abstract class Bitcoin { TransactionPriority deserializeBitcoinTransactionPriority(int raw); TransactionPriority deserializeLitecoinTransactionPriority(int raw); int getFeeRate(Object wallet, TransactionPriority priority); - Future generateNewAddress(Object wallet); + Future generateNewAddress(Object wallet, String label); + Future updateAddress(Object wallet,String address, String label); Object createBitcoinTransactionCredentials(List outputs, {required TransactionPriority priority, int? feeRate}); Object createBitcoinTransactionCredentialsRaw(List outputs, {TransactionPriority? priority, required int feeRate}); List getAddresses(Object wallet); String getAddress(Object wallet); + List getSubAddresses(Object wallet); + String formatterBitcoinAmountToString({required int amount}); double formatterBitcoinAmountToDouble({required int amount}); int formatterStringDoubleToBitcoinAmount(String amount); String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate); List getUnspents(Object wallet); - void updateUnspents(Object wallet); + Future updateUnspents(Object wallet); WalletService createBitcoinWalletService(Box walletInfoSource, Box unspentCoinSource); WalletService createLitecoinWalletService(Box walletInfoSource, Box unspentCoinSource); TransactionPriority getBitcoinTransactionPriorityMedium(); @@ -248,7 +270,7 @@ abstract class Monero { List getMoneroWordList(String language); List getUnspents(Object wallet); - void updateUnspents(Object wallet); + Future updateUnspents(Object wallet); WalletCredentials createMoneroRestoreWalletFromKeysCredentials({ required String name, @@ -403,10 +425,10 @@ class AssetRate { } abstract class HavenWalletDetails { - // FIX-ME: it's abstruct class + // FIX-ME: it's abstract class @observable late Account account; - // FIX-ME: it's abstruct class + // FIX-ME: it's abstract class @observable late HavenBalance balance; } @@ -503,19 +525,24 @@ 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'; + +"""; + const ethereumCWHeaders = """ +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; + +import 'package:cw_ethereum/ethereum_client.dart'; +import 'package:cw_ethereum/ethereum_wallet.dart'; +import 'package:cw_ethereum/ethereum_wallet_service.dart'; + import 'package:eth_sig_util/util/utils.dart'; import 'package:hive/hive.dart'; import 'package:web3dart/web3dart.dart'; -"""; - const ethereumCWHeaders = """ -import 'package:cw_ethereum/ethereum_formatter.dart'; -import 'package:cw_ethereum/ethereum_mnemonics.dart'; -import 'package:cw_ethereum/ethereum_transaction_credentials.dart'; -import 'package:cw_ethereum/ethereum_transaction_info.dart'; -import 'package:cw_ethereum/ethereum_wallet.dart'; -import 'package:cw_ethereum/ethereum_wallet_creation_credentials.dart'; -import 'package:cw_ethereum/ethereum_wallet_service.dart'; -import 'package:cw_ethereum/ethereum_transaction_priority.dart'; + """; const ethereumCwPart = "part 'cw_ethereum.dart';"; const ethereumContent = """ @@ -590,19 +617,24 @@ import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_service.dart'; -import 'package:eth_sig_util/util/utils.dart'; -import 'package:hive/hive.dart'; -import 'package:web3dart/web3dart.dart'; + """; const polygonCWHeaders = """ -import 'package:cw_polygon/polygon_formatter.dart'; -import 'package:cw_polygon/polygon_transaction_credentials.dart'; -import 'package:cw_polygon/polygon_transaction_info.dart'; +import 'package:cw_evm/evm_chain_formatter.dart'; +import 'package:cw_evm/evm_chain_mnemonics.dart'; +import 'package:cw_evm/evm_chain_transaction_info.dart'; +import 'package:cw_evm/evm_chain_transaction_priority.dart'; +import 'package:cw_evm/evm_chain_transaction_credentials.dart'; +import 'package:cw_evm/evm_chain_wallet_creation_credentials.dart'; + +import 'package:cw_polygon/polygon_client.dart'; import 'package:cw_polygon/polygon_wallet.dart'; -import 'package:cw_polygon/polygon_wallet_creation_credentials.dart'; import 'package:cw_polygon/polygon_wallet_service.dart'; -import 'package:cw_polygon/polygon_transaction_priority.dart'; -import 'package:cw_ethereum/ethereum_mnemonics.dart'; + +import 'package:hive/hive.dart'; +import 'package:web3dart/web3dart.dart'; +import 'package:eth_sig_util/util/utils.dart'; + """; const polygonCwPart = "part 'cw_polygon.dart';"; const polygonContent = """ @@ -759,7 +791,7 @@ import 'package:convert/convert.dart'; import "package:ed25519_hd_key/ed25519_hd_key.dart"; import 'package:libcrypto/libcrypto.dart'; import 'package:nanodart/nanodart.dart' as ND; -import 'package:decimal/decimal.dart'; +import 'package:nanoutil/nanoutil.dart'; """; const nanoCwPart = "part 'cw_nano.dart';"; const nanoContent = """ @@ -795,7 +827,7 @@ abstract class Nano { Map getKeys(Object wallet); Object createNanoTransactionCredentials(List outputs); Future changeRep(Object wallet, String address); - Future updateTransactions(Object wallet); + Future updateTransactions(Object wallet); BigInt getTransactionAmountRaw(TransactionInfo transactionInfo); String getRepresentative(Object wallet); } @@ -810,20 +842,6 @@ abstract class NanoAccountList { } abstract class NanoUtil { - String seedToPrivate(String seed, int index); - String seedToAddress(String seed, int index); - String seedToMnemonic(String seed); - Future mnemonicToSeed(String mnemonic); - String privateKeyToPublic(String privateKey); - String addressToPublicKey(String publicAddress); - String privateKeyToAddress(String privateKey); - String publicKeyToAddress(String publicKey); - bool isValidSeed(String seed); - Future hdMnemonicListToSeed(List words); - Future hdSeedToPrivate(String seed, int index); - Future hdSeedToAddress(String seed, int index); - Future uniSeedToAddress(String seed, int index, String type); - Future uniSeedToPrivate(String seed, int index, String type); bool isValidBip39Seed(String seed); static const int maxDecimalDigits = 6; // Max digits after decimal BigInt rawPerNano = BigInt.parse("1000000000000000000000000000000"); @@ -831,7 +849,6 @@ abstract class NanoUtil { BigInt rawPerBanano = BigInt.parse("100000000000000000000000000000"); BigInt rawPerXMR = BigInt.parse("1000000000000"); BigInt convertXMRtoNano = BigInt.parse("1000000000000000000"); - String getRawAsDecimalString(String? raw, BigInt? rawPerCur); String getRawAsUsableString(String? raw, BigInt rawPerCur); String getRawAccuracy(String? raw, BigInt rawPerCur); String getAmountAsRaw(String amount, BigInt rawPerCur); @@ -917,6 +934,10 @@ Future generatePubspec( cw_polygon: path: ./cw_polygon """; + const cwEVM = """ + cw_evm: + path: ./cw_evm + """; final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); @@ -957,6 +978,10 @@ Future generatePubspec( output += '\n$cwHaven'; } + if (hasEthereum || hasPolygon) { + output += '\n$cwEVM'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); diff --git a/tool/generate_secrets_config.dart b/tool/generate_secrets_config.dart index 1d533b0b1..58e7b8839 100644 --- a/tool/generate_secrets_config.dart +++ b/tool/generate_secrets_config.dart @@ -4,7 +4,7 @@ import 'utils/secret_key.dart'; import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; -const ethereumConfigPath = 'tool/.ethereum-secrets-config.json'; +const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; Future main(List args) async => generateSecretsConfig(args); @@ -17,7 +17,7 @@ Future generateSecretsConfig(List args) async { }); final configFile = File(configPath); - final ethereumConfigFile = File(ethereumConfigPath); + final evmChainsConfigFile = File(evmChainsConfigPath); final secrets = {}; secrets.addAll(extraInfo); @@ -49,7 +49,7 @@ Future generateSecretsConfig(List args) async { await configFile.writeAsString(secretsJson); secrets.clear(); - SecretKey.ethereumSecrets.forEach((sec) { + SecretKey.evmChainsSecrets.forEach((sec) { if (secrets[sec.name] != null) { return; } @@ -59,5 +59,5 @@ Future generateSecretsConfig(List args) async { secretsJson = JsonEncoder.withIndent(' ').convert(secrets); - await ethereumConfigFile.writeAsString(secretsJson); + await evmChainsConfigFile.writeAsString(secretsJson); } diff --git a/tool/import_secrets_config.dart b/tool/import_secrets_config.dart index 5987b9005..83e345f78 100644 --- a/tool/import_secrets_config.dart +++ b/tool/import_secrets_config.dart @@ -5,8 +5,8 @@ import 'utils/utils.dart'; const configPath = 'tool/.secrets-config.json'; const outputPath = 'lib/.secrets.g.dart'; -const ethereumConfigPath = 'tool/.ethereum-secrets-config.json'; -const ethereumOutputPath = 'cw_ethereum/lib/.secrets.g.dart'; +const evmChainsConfigPath = 'tool/.evm-secrets-config.json'; +const evmChainsOutputPath = 'cw_evm/lib/.secrets.g.dart'; Future main(List args) async => importSecretsConfig(); @@ -15,11 +15,11 @@ Future importSecretsConfig() async { final input = json.decode(File(configPath).readAsStringSync()) as Map; final output = input.keys.fold('', (String acc, String val) => acc + generateConst(val, input)); - final ethereumOutputFile = File(ethereumOutputPath); - final ethereumInput = - json.decode(File(ethereumConfigPath).readAsStringSync()) as Map; - final ethereumOutput = ethereumInput.keys - .fold('', (String acc, String val) => acc + generateConst(val, ethereumInput)); + final evmChainsOutputFile = File(evmChainsOutputPath); + final evmChainsInput = + json.decode(File(evmChainsConfigPath).readAsStringSync()) as Map; + final evmChainsOutput = evmChainsInput.keys + .fold('', (String acc, String val) => acc + generateConst(val, evmChainsInput)); if (outputFile.existsSync()) { await outputFile.delete(); @@ -27,9 +27,9 @@ Future importSecretsConfig() async { await outputFile.writeAsString(output); - if (ethereumOutputFile.existsSync()) { - await ethereumOutputFile.delete(); + if (evmChainsOutputFile.existsSync()) { + await evmChainsOutputFile.delete(); } - await ethereumOutputFile.writeAsString(ethereumOutput); + await evmChainsOutputFile.writeAsString(evmChainsOutput); } diff --git a/tool/utils/secret_key.dart b/tool/utils/secret_key.dart index e6f625426..f991c43cf 100644 --- a/tool/utils/secret_key.dart +++ b/tool/utils/secret_key.dart @@ -36,10 +36,10 @@ class SecretKey { SecretKey('robinhoodApplicationId', () => ''), SecretKey('robinhoodCIdApiSecret', () => ''), SecretKey('walletConnectProjectId', () => ''), - SecretKey('moralisApiKey', () => '') + SecretKey('moralisApiKey', () => ''), ]; - static final ethereumSecrets = [ + static final evmChainsSecrets = [ SecretKey('etherScanApiKey', () => ''), SecretKey('polygonScanApiKey', () => ''), ];