diff --git a/.github/workflows/cache_dependencies.yml b/.github/workflows/cache_dependencies.yml index 4d2dc136c..5373bbc87 100644 --- a/.github/workflows/cache_dependencies.yml +++ b/.github/workflows/cache_dependencies.yml @@ -1,6 +1,7 @@ name: Cache Dependencies on: + workflow_dispatch: push: branches: [ main ] @@ -18,7 +19,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: '3.3.x' + flutter-version: '3.10.x' channel: stable - name: Install package dependencies @@ -45,7 +46,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh') }} + key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals diff --git a/.github/workflows/pr_test_build.yml b/.github/workflows/pr_test_build.yml index 47378eef5..30bf3058e 100644 --- a/.github/workflows/pr_test_build.yml +++ b/.github/workflows/pr_test_build.yml @@ -28,7 +28,7 @@ jobs: - name: Flutter action uses: subosito/flutter-action@v1 with: - flutter-version: '3.7.x' + flutter-version: '3.10.x' channel: stable - name: Install package dependencies @@ -55,7 +55,7 @@ jobs: /opt/android/cake_wallet/cw_monero/android/.cxx /opt/android/cake_wallet/cw_monero/ios/External /opt/android/cake_wallet/cw_shared_external/ios/External - key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh') }} + key: ${{ hashFiles('**/build_monero.sh', '**/build_haven.sh', '**/monero_api.cpp') }} - if: ${{ steps.cache-externals.outputs.cache-hit != 'true' }} name: Generate Externals @@ -163,7 +163,11 @@ jobs: - name: Send Test APK continue-on-error: true - run: | - cd /opt/android/cake_wallet - var=$(curl --upload-file build/app/outputs/apk/release/app-release.apk https://transfer.sh/$GITHUB_HEAD_REF.apk -H "Max-Days: 10") - curl ${{ secrets.SLACK_WEB_HOOK }} -H "Content-Type: application/json" -d '{"apk_link": "'"$var"'","ticket": "'"$GITHUB_HEAD_REF"'"}' + uses: adrey/slack-file-upload-action@1.0.5 + with: + token: ${{ secrets.SLACK_APP_TOKEN }} + path: /opt/android/cake_wallet/build/app/outputs/apk/release/app-release.apk + channel: ${{ secrets.SLACK_APK_CHANNEL }} + title: '${{github.head_ref}}.apk' + filename: ${{github.head_ref}}.apk + initial_comment: ${{ github.event.head_commit.message }} diff --git a/.gitignore b/.gitignore index 6fd8f33d6..09583004b 100644 --- a/.gitignore +++ b/.gitignore @@ -143,4 +143,4 @@ assets/images/app_logo.png macos/Runner/Info.plist macos/Runner/DebugProfile.entitlements -macos/Runner/Release.entitlements \ No newline at end of file +macos/Runner/Release.entitlements diff --git a/android/build.gradle b/android/build.gradle index 692e8dfb1..bd4ebd770 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -27,6 +27,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/assets/text/Monerocom_Release_Notes.txt b/assets/text/Monerocom_Release_Notes.txt index 403bb9f08..62f649704 100644 --- a/assets/text/Monerocom_Release_Notes.txt +++ b/assets/text/Monerocom_Release_Notes.txt @@ -1,8 +1,2 @@ -Monero background syncing! See https://guides.cakewallet.com/docs/monero/#background-syncing -Cake 2FA access control settings! See https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa-presets-and-access-control-settings -Support Monero node proxy -UI improvements when sending to Address Book entry -Allow renaming Monero account names -Send templates now support multiple recipients (try using to make Monero change) -Onramper improvements -Scan node QR codes (for Umbrel) \ No newline at end of file +Bug fixes +Fiat Onramp improvements \ No newline at end of file diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index cedec7b7f..62f649704 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1,9 +1,2 @@ -Ethereum! Store ETH and ERC-20 tokens -Monero background syncing! See https://guides.cakewallet.com/docs/monero/#background-syncing -Cake 2FA access control settings! See https://guides.cakewallet.com/docs/advanced-features/authentication/#cake-2fa-presets-and-access-control-settings -Support Monero node proxy -UI improvements when sending to Address Book entry -Allow renaming Monero/Haven account names -Send templates now support multiple recipients (try using to make Monero change) -Onramper improvements -Scan node QR codes (for Umbrel) \ No newline at end of file +Bug fixes +Fiat Onramp improvements \ No newline at end of file diff --git a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart index d4397dead..fac7e93c4 100644 --- a/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart +++ b/cw_bitcoin/lib/bitcoin_transaction_no_inputs_exception.dart @@ -1,4 +1,4 @@ class BitcoinTransactionNoInputsException implements Exception { @override - String toString() => 'Not enough inputs available'; + String toString() => 'Not enough inputs available. Please select more under Coin Control'; } diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index 4d864059f..843daa771 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -37,19 +37,19 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" bech32: dependency: transitive description: path: "." - ref: "cake-0.2.1" - resolved-ref: cafd1c270641e95017d57d69f55cca9831d4db56 + ref: "cake-0.2.2" + resolved-ref: "05755063b593aa6cca0a4820a318e0ce17de6192" url: "https://github.com/cake-tech/bech32.git" source: git - version: "0.2.1" + version: "0.2.2" bip32: dependency: transitive description: @@ -70,8 +70,8 @@ packages: dependency: "direct main" description: path: "." - ref: cake-update-v2 - resolved-ref: "8f86453761c0c26e368392d0ff2c6f12f3b7397b" + ref: cake-update-v3 + resolved-ref: df9204144011ed9419eff7d9ef3143102a40252d url: "https://github.com/cake-tech/bitcoin_flutter.git" source: git version: "2.0.2" @@ -159,10 +159,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -191,18 +191,18 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: name: convert - sha256: "196284f26f69444b7f5c50692b55ec25da86d9e500451dc09333bf2e3ad69259" + sha256: "0f08b14755d163f6e2134cb58222dd25ea2a2ee8a195e53983d57c075324d592" url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.1.1" crypto: dependency: transitive description: @@ -344,10 +344,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -368,10 +368,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -384,10 +384,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -408,10 +408,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -424,10 +424,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -464,10 +464,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: @@ -669,10 +669,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -746,5 +746,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_bitcoin/pubspec.yaml b/cw_bitcoin/pubspec.yaml index 481a41ac5..dae0af39b 100644 --- a/cw_bitcoin/pubspec.yaml +++ b/cw_bitcoin/pubspec.yaml @@ -13,16 +13,16 @@ dependencies: flutter: sdk: flutter path_provider: ^2.0.11 - http: ^0.13.4 + http: ^1.1.0 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 cw_core: path: ../cw_core bitcoin_flutter: git: url: https://github.com/cake-tech/bitcoin_flutter.git - ref: cake-update-v2 + ref: cake-update-v3 rxdart: ^0.27.5 unorm_dart: ^0.2.0 cryptography: ^2.0.5 diff --git a/cw_core/lib/address_info.dart b/cw_core/lib/address_info.dart new file mode 100644 index 000000000..63dc023ab --- /dev/null +++ b/cw_core/lib/address_info.dart @@ -0,0 +1,21 @@ +import 'package:cw_core/hive_type_ids.dart'; +import 'package:hive/hive.dart'; + +part 'address_info.g.dart'; + +@HiveType(typeId: ADDRESS_INFO_TYPE_ID) +class AddressInfo extends HiveObject { + AddressInfo({required this.address, this.accountIndex, required this.label}); + + static const typeId = ADDRESS_INFO_TYPE_ID; + static const boxName = 'AddressInfo'; + + @HiveField(0) + int? accountIndex; + + @HiveField(1, defaultValue: '') + String address; + + @HiveField(2, defaultValue: '') + String label; +} diff --git a/cw_core/lib/hive_type_ids.dart b/cw_core/lib/hive_type_ids.dart index 0961182bc..950f39e1f 100644 --- a/cw_core/lib/hive_type_ids.dart +++ b/cw_core/lib/hive_type_ids.dart @@ -9,5 +9,5 @@ const EXCHANGE_TEMPLATE_TYPE_ID = 7; const ORDER_TYPE_ID = 8; const UNSPENT_COINS_INFO_TYPE_ID = 9; const ANONPAY_INVOICE_INFO_TYPE_ID = 10; - +const ADDRESS_INFO_TYPE_ID = 11; const ERC20_TOKEN_TYPE_ID = 12; diff --git a/cw_core/lib/monero_balance.dart b/cw_core/lib/monero_balance.dart index 7d569ef2f..bf30110a3 100644 --- a/cw_core/lib/monero_balance.dart +++ b/cw_core/lib/monero_balance.dart @@ -2,24 +2,31 @@ import 'package:cw_core/balance.dart'; import 'package:cw_core/monero_amount_format.dart'; class MoneroBalance extends Balance { - MoneroBalance({required this.fullBalance, required this.unlockedBalance}) + MoneroBalance({required this.fullBalance, required this.unlockedBalance, this.frozenBalance = 0}) : formattedFullBalance = moneroAmountToString(amount: fullBalance), - formattedUnlockedBalance = - moneroAmountToString(amount: unlockedBalance), + formattedUnlockedBalance = moneroAmountToString(amount: unlockedBalance), + frozenFormatted = moneroAmountToString(amount: frozenBalance), super(unlockedBalance, fullBalance); MoneroBalance.fromString( {required this.formattedFullBalance, - required this.formattedUnlockedBalance}) + required this.formattedUnlockedBalance, + this.frozenFormatted = '0.0'}) : fullBalance = moneroParseAmount(amount: formattedFullBalance), unlockedBalance = moneroParseAmount(amount: formattedUnlockedBalance), + frozenBalance = moneroParseAmount(amount: frozenFormatted), super(moneroParseAmount(amount: formattedUnlockedBalance), moneroParseAmount(amount: formattedFullBalance)); final int fullBalance; final int unlockedBalance; + final int frozenBalance; final String formattedFullBalance; final String formattedUnlockedBalance; + final String frozenFormatted; + + @override + String get formattedFrozenBalance => frozenFormatted == '0.0' ? '' : frozenFormatted; @override String get formattedAvailableBalance => formattedUnlockedBalance; diff --git a/cw_core/lib/unspent_coins_info.dart b/cw_core/lib/unspent_coins_info.dart index 33be2eb2c..68bbcbfd2 100644 --- a/cw_core/lib/unspent_coins_info.dart +++ b/cw_core/lib/unspent_coins_info.dart @@ -13,7 +13,9 @@ class UnspentCoinsInfo extends HiveObject { required this.noteRaw, required this.address, required this.vout, - required this.value}); + required this.value, + this.keyImage = null + }); static const typeId = UNSPENT_COINS_INFO_TYPE_ID; static const boxName = 'Unspent'; @@ -43,6 +45,9 @@ class UnspentCoinsInfo extends HiveObject { @HiveField(7, defaultValue: 0) int vout; + @HiveField(8, defaultValue: null) + String? keyImage; + String get note => noteRaw ?? ''; set note(String value) => noteRaw = value; diff --git a/cw_core/lib/wallet_addresses.dart b/cw_core/lib/wallet_addresses.dart index a34101a88..27b5468c5 100644 --- a/cw_core/lib/wallet_addresses.dart +++ b/cw_core/lib/wallet_addresses.dart @@ -1,8 +1,10 @@ +import 'package:cw_core/address_info.dart'; import 'package:cw_core/wallet_info.dart'; abstract class WalletAddresses { WalletAddresses(this.walletInfo) - : addressesMap = {}; + : addressesMap = {}, + addressInfos = {}; final WalletInfo walletInfo; @@ -12,6 +14,10 @@ abstract class WalletAddresses { Map addressesMap; + Map> addressInfos; + + Set usedAddresses = {}; + Future init(); Future updateAddressesInBox(); @@ -20,6 +26,8 @@ abstract class WalletAddresses { try { walletInfo.address = address; walletInfo.addresses = addressesMap; + walletInfo.addressInfos = addressInfos; + walletInfo.usedAddresses = usedAddresses.toList(); if (walletInfo.isInBox) { await walletInfo.save(); diff --git a/cw_core/lib/wallet_base.dart b/cw_core/lib/wallet_base.dart index 019f87631..e24fdb4b0 100644 --- a/cw_core/lib/wallet_base.dart +++ b/cw_core/lib/wallet_base.dart @@ -42,7 +42,9 @@ abstract class WalletBase< set syncStatus(SyncStatus status); - String get seed; + String? get seed; + + String? get privateKey => null; Object get keys; @@ -50,6 +52,10 @@ abstract class WalletBase< late HistoryType transactionHistory; + set isEnabledAutoGenerateSubaddress(bool value) {} + + bool get isEnabledAutoGenerateSubaddress => false; + Future connectToNode({required Node node}); Future startSync(); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 6b3fa9e98..210adb9a4 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'package:cw_core/address_info.dart'; import 'package:cw_core/hive_type_ids.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:hive/hive.dart'; @@ -72,6 +73,12 @@ class WalletInfo extends HiveObject { @HiveField(13) bool? showIntroCakePayCard; + @HiveField(14) + Map>? addressInfos; + + @HiveField(15) + List? usedAddresses; + String get yatLastUsedAddress => yatLastUsedAddressRaw ?? ''; set yatLastUsedAddress(String address) { diff --git a/cw_core/pubspec.lock b/cw_core/pubspec.lock index 70652ec35..e399526fd 100644 --- a/cw_core/pubspec.lock +++ b/cw_core/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -279,10 +279,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -303,10 +303,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -319,10 +319,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -343,10 +343,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -359,10 +359,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -399,10 +399,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: @@ -596,10 +596,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -665,5 +665,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_core/pubspec.yaml b/cw_core/pubspec.yaml index e33aeb803..9dcb7eaba 100644 --- a/cw_core/pubspec.yaml +++ b/cw_core/pubspec.yaml @@ -12,12 +12,12 @@ environment: dependencies: flutter: sdk: flutter - http: ^0.13.4 + http: ^1.1.0 file: ^6.1.4 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 encrypt: ^5.0.1 dev_dependencies: diff --git a/cw_ethereum/lib/ethereum_client.dart b/cw_ethereum/lib/ethereum_client.dart index f00e2ef7b..7eba43aa7 100644 --- a/cw_ethereum/lib/ethereum_client.dart +++ b/cw_ethereum/lib/ethereum_client.dart @@ -9,7 +9,7 @@ import 'package:cw_ethereum/pending_ethereum_transaction.dart'; import 'package:flutter/services.dart'; import 'package:http/http.dart'; import 'package:web3dart/web3dart.dart'; -import 'package:web3dart/contracts/erc20.dart'; +import 'package: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; @@ -72,7 +72,7 @@ class EthereumClient { to: EthereumAddress.fromHex(toAddress), maxGas: gas, gasPrice: price, - maxPriorityFeePerGas: EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip), + maxPriorityFeePerGas: EtherAmount.fromInt(EtherUnit.gwei, priority.tip), value: _isEthereum ? EtherAmount.inWei(BigInt.parse(amount)) : EtherAmount.zero(), ); @@ -83,7 +83,7 @@ class EthereumClient { if (_isEthereum) { _sendTransaction = () async => await sendTransaction(signedTransaction); } else { - final erc20 = Erc20( + final erc20 = ERC20( client: _client!, address: EthereumAddress.fromHex(contractAddress!), ); @@ -153,7 +153,7 @@ I/flutter ( 4474): Gas Used: 53000 Future fetchERC20Balances( EthereumAddress userAddress, String contractAddress) async { - final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); final balance = await erc20.balanceOf(userAddress); int exponent = (await erc20.decimals()).toInt(); @@ -163,7 +163,7 @@ I/flutter ( 4474): Gas Used: 53000 Future getErc20Token(String contractAddress) async { try { - final erc20 = Erc20(address: EthereumAddress.fromHex(contractAddress), client: _client!); + final erc20 = ERC20(address: EthereumAddress.fromHex(contractAddress), client: _client!); final name = await erc20.name(); final symbol = await erc20.symbol(); final decimal = await erc20.decimals(); diff --git a/cw_ethereum/lib/ethereum_wallet.dart b/cw_ethereum/lib/ethereum_wallet.dart index 404b78ca2..8d7c477e1 100644 --- a/cw_ethereum/lib/ethereum_wallet.dart +++ b/cw_ethereum/lib/ethereum_wallet.dart @@ -44,12 +44,14 @@ abstract class EthereumWalletBase with Store { EthereumWalletBase({ required WalletInfo walletInfo, - required String mnemonic, + String? mnemonic, + String? privateKey, required String password, ERC20Balance? initialBalance, }) : syncStatus = NotConnectedSyncStatus(), _password = password, _mnemonic = mnemonic, + _hexPrivateKey = privateKey, _isTransactionUpdating = false, _client = EthereumClient(), walletAddresses = EthereumWalletAddresses(walletInfo), @@ -66,12 +68,13 @@ abstract class EthereumWalletBase _sharedPrefs.complete(SharedPreferences.getInstance()); } - final String _mnemonic; + final String? _mnemonic; + final String? _hexPrivateKey; final String _password; late final Box erc20TokensBox; - late final EthPrivateKey _privateKey; + late final EthPrivateKey _ethPrivateKey; late EthereumClient _client; @@ -99,8 +102,12 @@ abstract class EthereumWalletBase erc20TokensBox = await CakeHive.openBox(Erc20Token.boxName); await walletAddresses.init(); await transactionHistory.init(); - _privateKey = await getPrivateKey(_mnemonic, _password); - walletAddresses.address = _privateKey.address.toString(); + _ethPrivateKey = await getPrivateKey( + mnemonic: _mnemonic, + privateKey: _hexPrivateKey, + password: _password, + ); + walletAddresses.address = _ethPrivateKey.address.toString(); await save(); } @@ -108,8 +115,7 @@ abstract class EthereumWalletBase int calculateEstimatedFee(TransactionPriority priority, int? amount) { try { if (priority is EthereumTransactionPriority) { - final priorityFee = - EtherAmount.fromUnitAndValue(EtherUnit.gwei, priority.tip).getInWei.toInt(); + final priorityFee = EtherAmount.fromInt(EtherUnit.gwei, priority.tip).getInWei.toInt(); return (_gasPrice! + priorityFee) * (_estimatedGas ?? 0); } @@ -142,7 +148,7 @@ abstract class EthereumWalletBase throw Exception("Ethereum Node connection failed"); } - _client.setListeners(_privateKey.address, _onNewTransaction); + _client.setListeners(_ethPrivateKey.address, _onNewTransaction); _setTransactionUpdateTimer(); @@ -202,7 +208,7 @@ abstract class EthereumWalletBase } final pendingEthereumTransaction = await _client.signTransaction( - privateKey: _privateKey, + privateKey: _ethPrivateKey, toAddress: _credentials.outputs.first.isParsedAddress ? _credentials.outputs.first.extractedAddress! : _credentials.outputs.first.address, @@ -240,7 +246,7 @@ abstract class EthereumWalletBase @override Future> fetchTransactions() async { - final address = _privateKey.address.hex; + final address = _ethPrivateKey.address.hex; final transactions = await _client.fetchTransactions(address); final List>> erc20TokensTransactions = []; @@ -300,7 +306,10 @@ abstract class EthereumWalletBase } @override - String get seed => _mnemonic; + String? get seed => _mnemonic; + + @override + String get privateKey => HEX.encode(_ethPrivateKey.privateKey); @action @override @@ -327,6 +336,7 @@ abstract class EthereumWalletBase String toJSON() => json.encode({ 'mnemonic': _mnemonic, + 'private_key': privateKey, 'balance': balance[currency]!.toJSON(), }); @@ -338,13 +348,15 @@ abstract class EthereumWalletBase 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 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, ); } @@ -357,7 +369,7 @@ abstract class EthereumWalletBase } Future _fetchEthBalance() async { - final balance = await _client.getBalance(_privateKey.address); + final balance = await _client.getBalance(_ethPrivateKey.address); return ERC20Balance(balance.getInWei); } @@ -366,7 +378,7 @@ abstract class EthereumWalletBase try { if (token.enabled) { balance[token] = await _client.fetchERC20Balances( - _privateKey.address, + _ethPrivateKey.address, token.contractAddress, ); } else { @@ -376,8 +388,15 @@ abstract class EthereumWalletBase } } - Future getPrivateKey(String mnemonic, String password) async { - final seed = bip39.mnemonicToSeed(mnemonic); + 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); @@ -413,7 +432,7 @@ abstract class EthereumWalletBase if (_token.enabled) { balance[_token] = await _client.fetchERC20Balances( - _privateKey.address, + _ethPrivateKey.address, _token.contractAddress, ); } else { diff --git a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart index 12d0d53e2..6546f2fae 100644 --- a/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart +++ b/cw_ethereum/lib/ethereum_wallet_creation_credentials.dart @@ -8,16 +8,22 @@ class EthereumNewWalletCredentials extends WalletCredentials { class EthereumRestoreWalletFromSeedCredentials extends WalletCredentials { EthereumRestoreWalletFromSeedCredentials( - {required String name, required String password, required this.mnemonic, WalletInfo? walletInfo}) + {required String name, + required String password, + required this.mnemonic, + WalletInfo? walletInfo}) : super(name: name, password: password, walletInfo: walletInfo); final String mnemonic; } -class EthereumRestoreWalletFromWIFCredentials extends WalletCredentials { - EthereumRestoreWalletFromWIFCredentials( - {required String name, required String password, required this.wif, WalletInfo? walletInfo}) +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 wif; + final String privateKey; } diff --git a/cw_ethereum/lib/ethereum_wallet_service.dart b/cw_ethereum/lib/ethereum_wallet_service.dart index 318f287fc..16dbc0b04 100644 --- a/cw_ethereum/lib/ethereum_wallet_service.dart +++ b/cw_ethereum/lib/ethereum_wallet_service.dart @@ -13,7 +13,7 @@ import 'package:bip39/bip39.dart' as bip39; import 'package:collection/collection.dart'; class EthereumWalletService extends WalletService { + EthereumRestoreWalletFromSeedCredentials, EthereumRestoreWalletFromPrivateKey> { EthereumWalletService(this.walletInfoSource); final Box walletInfoSource; @@ -66,8 +66,18 @@ class EthereumWalletService extends WalletService restoreFromKeys(credentials) { - throw UnimplementedError(); + Future restoreFromKeys(EthereumRestoreWalletFromPrivateKey credentials) async { + final wallet = EthereumWallet( + password: credentials.password!, + privateKey: credentials.privateKey, + walletInfo: credentials.walletInfo!, + ); + + await wallet.init(); + wallet.addInitialTokens(); + await wallet.save(); + + return wallet; } @override diff --git a/cw_ethereum/pubspec.yaml b/cw_ethereum/pubspec.yaml index cb1046d5a..5d19589f3 100644 --- a/cw_ethereum/pubspec.yaml +++ b/cw_ethereum/pubspec.yaml @@ -12,13 +12,14 @@ environment: dependencies: flutter: sdk: flutter - web3dart: 2.3.5 + web3dart: ^2.7.1 + erc20: ^1.0.1 mobx: ^2.0.7+4 bip39: ^1.0.6 bip32: ^2.0.0 ed25519_hd_key: ^2.2.0 hex: ^0.2.0 - http: ^0.13.4 + http: ^1.1.0 shared_preferences: ^2.0.15 cw_core: path: ../cw_core diff --git a/cw_haven/lib/haven_wallet.dart b/cw_haven/lib/haven_wallet.dart index 226ace6a1..e639be4b9 100644 --- a/cw_haven/lib/haven_wallet.dart +++ b/cw_haven/lib/haven_wallet.dart @@ -12,8 +12,7 @@ import 'package:cw_core/monero_wallet_utils.dart'; import 'package:cw_haven/api/structs/pending_transaction.dart'; import 'package:flutter/foundation.dart'; import 'package:mobx/mobx.dart'; -import 'package:cw_haven/api/transaction_history.dart' - as haven_transaction_history; +import 'package:cw_haven/api/transaction_history.dart' as haven_transaction_history; //import 'package:cw_haven/wallet.dart'; import 'package:cw_haven/api/wallet.dart' as haven_wallet; import 'package:cw_haven/api/transaction_history.dart' as transaction_history; @@ -37,8 +36,8 @@ const moneroBlockSize = 1000; class HavenWallet = HavenWalletBase with _$HavenWallet; -abstract class HavenWalletBase extends WalletBase with Store { +abstract class HavenWalletBase + extends WalletBase with Store { HavenWalletBase({required WalletInfo walletInfo}) : balance = ObservableMap.of(getHavenBalance(accountIndex: 0)), _isTransactionUpdating = false, @@ -47,8 +46,7 @@ abstract class HavenWalletBase extends WalletBase walletAddresses.account, - (Account? account) { + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } @@ -96,14 +94,12 @@ abstract class HavenWalletBase extends WalletBase await save()); + _autoSaveTimer = + Timer.periodic(Duration(seconds: _autoSaveInterval), (_) async => await save()); } @override @@ -115,7 +111,7 @@ abstract class HavenWalletBase extends WalletBase connectToNode({required Node node}) async { try { @@ -170,26 +166,25 @@ abstract class HavenWalletBase extends WalletBase item.sendAll - || (item.formattedCryptoAmount ?? 0) <= 0)) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + if (outputs.any((item) => item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final int totalAmount = outputs.fold(0, (acc, value) => - acc + (value.formattedCryptoAmount ?? 0)); + final int totalAmount = + outputs.fold(0, (acc, value) => acc + (value.formattedCryptoAmount ?? 0)); if (unlockedBalance < totalAmount) { - throw HavenTransactionCreationException('You do not have enough coins to send this amount.'); + throw HavenTransactionCreationException( + 'You do not have enough coins to send this amount.'); } - final moneroOutputs = outputs.map((output) => - MoneroOutput( - address: output.address, - amount: output.cryptoAmount!.replaceAll(',', '.'))) + final moneroOutputs = outputs + .map((output) => MoneroOutput( + address: output.address, amount: output.cryptoAmount!.replaceAll(',', '.'))) .toList(); - pendingTransactionDescription = - await transaction_history.createTransactionMultDest( + pendingTransactionDescription = await transaction_history.createTransactionMultDest( outputs: moneroOutputs, priorityRaw: _credentials.priority.serialize(), accountIndex: walletAddresses.account!.id); @@ -198,12 +193,8 @@ abstract class HavenWalletBase extends WalletBase - haven_wallet.getAddress( - accountIndex: accountIndex, - addressIndex: addressIndex); + haven_wallet.getAddress(accountIndex: accountIndex, addressIndex: addressIndex); @override Future> fetchTransactions() async { haven_transaction_history.refreshTransactions(); - return _getAllTransactions(null).fold>( - {}, - (Map acc, HavenTransactionInfo tx) { + return _getAllTransactions(null) + .fold>({}, + (Map acc, HavenTransactionInfo tx) { acc[tx.id] = tx; return acc; }); @@ -340,9 +328,9 @@ abstract class HavenWalletBase extends WalletBase _getAllTransactions(dynamic _) => haven_transaction_history - .getAllTransations() - .map((row) => HavenTransactionInfo.fromRow(row)) - .toList(); + .getAllTransations() + .map((row) => HavenTransactionInfo.fromRow(row)) + .toList(); void _setListeners() { _listener?.stop(); @@ -364,8 +352,7 @@ abstract class HavenWalletBase extends WalletBase balance.addAll(getHavenBalance(accountIndex: walletAddresses.account!.id)); - Future _askForUpdateTransactionHistory() async => - await updateTransactions(); + Future _askForUpdateTransactionHistory() async => await updateTransactions(); void _onNewBlock(int height, int blocksLeft, double ptc) async { try { @@ -404,9 +390,9 @@ abstract class HavenWalletBase extends WalletBase=2.19.0 <3.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_haven/pubspec.yaml b/cw_haven/pubspec.yaml index 7a5ac6aa4..c215ab779 100644 --- a/cw_haven/pubspec.yaml +++ b/cw_haven/pubspec.yaml @@ -13,11 +13,11 @@ dependencies: flutter: sdk: flutter ffi: ^2.0.1 - http: ^0.13.4 + http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 cw_core: path: ../cw_core diff --git a/cw_monero/example/pubspec.lock b/cw_monero/example/pubspec.lock index 19d9cef8f..1aae6b0c9 100644 --- a/cw_monero/example/pubspec.lock +++ b/cw_monero/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" clock: dependency: transitive description: @@ -53,10 +53,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_parser: dependency: transitive description: @@ -173,18 +173,18 @@ packages: dependency: transitive description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" js: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" lints: dependency: transitive description: @@ -197,10 +197,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -213,10 +213,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mobx: dependency: transitive description: @@ -229,10 +229,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: transitive description: @@ -362,10 +362,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" typed_data: dependency: transitive description: @@ -399,5 +399,5 @@ packages: source: hosted version: "0.2.0+3" sdks: - dart: ">=2.18.1 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_monero/ios/Classes/monero_api.cpp b/cw_monero/ios/Classes/monero_api.cpp index 780fc5b14..1cb01fac7 100644 --- a/cw_monero/ios/Classes/monero_api.cpp +++ b/cw_monero/ios/Classes/monero_api.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "thread" #include "CwWalletListener.h" #if __APPLE__ @@ -138,7 +139,7 @@ extern "C" int8_t direction; int8_t isPending; uint32_t subaddrIndex; - + char *hash; char *paymentId; @@ -153,7 +154,7 @@ extern "C" std::set::iterator it = transaction->subaddrIndex().begin(); subaddrIndex = *it; confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); + datetime = static_cast(transaction->timestamp()); direction = transaction->direction(); isPending = static_cast(transaction->isPending()); std::string *hash_str = new std::string(transaction->hash()); @@ -182,6 +183,62 @@ extern "C" } }; + struct CoinsInfoRow + { + uint64_t blockHeight; + char *hash; + uint64_t internalOutputIndex; + uint64_t globalOutputIndex; + bool spent; + bool frozen; + uint64_t spentHeight; + uint64_t amount; + bool rct; + bool keyImageKnown; + uint64_t pkIndex; + uint32_t subaddrIndex; + uint32_t subaddrAccount; + char *address; + char *addressLabel; + char *keyImage; + uint64_t unlockTime; + bool unlocked; + char *pubKey; + bool coinbase; + char *description; + + CoinsInfoRow(Monero::CoinsInfo *coinsInfo) + { + blockHeight = coinsInfo->blockHeight(); + std::string *hash_str = new std::string(coinsInfo->hash()); + hash = strdup(hash_str->c_str()); + internalOutputIndex = coinsInfo->internalOutputIndex(); + globalOutputIndex = coinsInfo->globalOutputIndex(); + spent = coinsInfo->spent(); + frozen = coinsInfo->frozen(); + spentHeight = coinsInfo->spentHeight(); + amount = coinsInfo->amount(); + rct = coinsInfo->rct(); + keyImageKnown = coinsInfo->keyImageKnown(); + pkIndex = coinsInfo->pkIndex(); + subaddrIndex = coinsInfo->subaddrIndex(); + subaddrAccount = coinsInfo->subaddrAccount(); + address = strdup(coinsInfo->address().c_str()) ; + addressLabel = strdup(coinsInfo->addressLabel().c_str()); + keyImage = strdup(coinsInfo->keyImage().c_str()); + unlockTime = coinsInfo->unlockTime(); + unlocked = coinsInfo->unlocked(); + pubKey = strdup(coinsInfo->pubKey().c_str()); + coinbase = coinsInfo->coinbase(); + description = strdup(coinsInfo->description().c_str()); + } + + void setUnlocked(bool unlocked); + + }; + + Monero::Coins *m_coins; + Monero::Wallet *m_wallet; Monero::TransactionHistory *m_transaction_history; MoneroWalletListener *m_listener; @@ -189,6 +246,7 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; + std::list m_coins_info; std::mutex store_lock; bool is_storing = false; @@ -196,7 +254,7 @@ extern "C" { m_wallet = wallet; m_listener = nullptr; - + if (wallet != nullptr) { @@ -224,6 +282,17 @@ extern "C" { m_subaddress = nullptr; } + + m_coins_info = std::list(); + + if (wallet != nullptr) + { + m_coins = wallet->coins(); + } + else + { + m_coins = nullptr; + } } Monero::Wallet *get_current_wallet() @@ -410,7 +479,7 @@ extern "C" { nice(19); Monero::Wallet *wallet = get_current_wallet(); - + std::string _login = ""; std::string _password = ""; std::string _socksProxyAddress = ""; @@ -487,10 +556,19 @@ extern "C" } bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); - + + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -503,13 +581,13 @@ extern "C" if (amount != nullptr) { uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } else { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } - + int status = transaction->status(); if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) @@ -527,7 +605,9 @@ extern "C" } bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); @@ -541,6 +621,13 @@ extern "C" amounts++; } + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -800,6 +887,91 @@ extern "C" return m_wallet->trustedDaemon(); } + CoinsInfoRow* coin(int index) + { + if (index >= 0 && index < m_coins_info.size()) { + std::list::iterator it = m_coins_info.begin(); + std::advance(it, index); + Monero::CoinsInfo* element = *it; + std::cout << "Element at index " << index << ": " << element << std::endl; + return new CoinsInfoRow(element); + } else { + std::cout << "Invalid index." << std::endl; + return nullptr; // Return a default value (nullptr) for invalid index + } + } + + void refresh_coins(uint32_t accountIndex) + { + m_coins_info.clear(); + + m_coins->refresh(); + for (const auto i : m_coins->getAll()) { + if (i->subaddrAccount() == accountIndex && !(i->spent())) { + m_coins_info.push_back(i); + } + } + } + + uint64_t coins_count() + { + return m_coins_info.size(); + } + + CoinsInfoRow** coins_from_account(uint32_t accountIndex) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (coinInfo->subaddrAccount == accountIndex) { + matchingCoins.push_back(coinInfo); + } + } + + CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (std::string(coinInfo->hash) == txid) { + matchingCoins.push_back(coinInfo); + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinsInfoRow = coin(i); + for (size_t j = 0; j < keyimageCount; j++) { + if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { + matchingCoins.push_back(coinsInfoRow); + break; + } + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + #ifdef __cplusplus } #endif diff --git a/cw_monero/lib/api/coins_info.dart b/cw_monero/lib/api/coins_info.dart new file mode 100644 index 000000000..9a5303f9d --- /dev/null +++ b/cw_monero/lib/api/coins_info.dart @@ -0,0 +1,23 @@ +import 'dart:ffi'; +import 'package:cw_monero/api/signatures.dart'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; +import 'package:cw_monero/api/types.dart'; +import 'package:cw_monero/api/monero_api.dart'; + +final refreshCoinsNative = moneroApi + .lookup>('refresh_coins') + .asFunction(); + +final coinsCountNative = moneroApi + .lookup>('coins_count') + .asFunction(); + +final coinNative = moneroApi + .lookup>('coin') + .asFunction(); + +void refreshCoins(int accountIndex) => refreshCoinsNative(accountIndex); + +int countOfCoins() => coinsCountNative(); + +CoinsInfoRow getCoin(int index) => coinNative(index).ref; diff --git a/cw_monero/lib/api/signatures.dart b/cw_monero/lib/api/signatures.dart index 82bc7801e..e208414c8 100644 --- a/cw_monero/lib/api/signatures.dart +++ b/cw_monero/lib/api/signatures.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -9,8 +10,8 @@ typedef create_wallet = Int8 Function( typedef restore_wallet_from_seed = Int8 Function( Pointer, Pointer, Pointer, Int32, Int64, Pointer); -typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, - Pointer, Pointer, Pointer, Pointer, Int32, Int64, Pointer); +typedef restore_wallet_from_keys = Int8 Function(Pointer, Pointer, Pointer, + Pointer, Pointer, Pointer, Int32, Int64, Pointer); typedef is_wallet_exist = Int8 Function(Pointer); @@ -63,8 +64,7 @@ typedef subaddrress_refresh = Void Function(Int32); typedef subaddress_get_all = Pointer Function(); -typedef subaddress_add_new = Void Function( - Int32 accountIndex, Pointer label); +typedef subaddress_add_new = Void Function(Int32 accountIndex, Pointer label); typedef subaddress_set_label = Void Function( Int32 accountIndex, Int32 addressIndex, Pointer label); @@ -77,8 +77,7 @@ typedef account_get_all = Pointer Function(); typedef account_add_new = Void Function(Pointer label); -typedef account_set_label = Void Function( - Int32 accountIndex, Pointer label); +typedef account_set_label = Void Function(Int32 accountIndex, Pointer label); typedef transactions_refresh = Void Function(); @@ -94,6 +93,8 @@ typedef transaction_create = Int8 Function( Pointer amount, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -104,6 +105,8 @@ typedef transaction_create_mult_dest = Int8 Function( Int32 size, Int8 priorityRaw, Int32 subaddrAccount, + Pointer> preferredInputs, + Int32 preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -123,10 +126,16 @@ typedef on_startup = Void Function(); typedef rescan_blockchain = Void Function(); -typedef get_subaddress_label = Pointer Function( - Int32 accountIndex, - Int32 addressIndex); +typedef get_subaddress_label = Pointer Function(Int32 accountIndex, Int32 addressIndex); typedef set_trusted_daemon = Void Function(Int8 trusted); -typedef trusted_daemon = Int8 Function(); \ No newline at end of file +typedef trusted_daemon = Int8 Function(); + +typedef refresh_coins = Void Function(Int32 accountIndex); + +typedef coins_count = Int64 Function(); + +// typedef coins_from_txid = Pointer Function(Pointer txid); + +typedef coin = Pointer Function(Int32 index); diff --git a/cw_monero/lib/api/structs/coins_info_row.dart b/cw_monero/lib/api/structs/coins_info_row.dart new file mode 100644 index 000000000..ff6f6ce73 --- /dev/null +++ b/cw_monero/lib/api/structs/coins_info_row.dart @@ -0,0 +1,73 @@ +import 'dart:ffi'; +import 'package:ffi/ffi.dart'; + +class CoinsInfoRow extends Struct { + @Int64() + external int blockHeight; + + external Pointer hash; + + @Uint64() + external int internalOutputIndex; + + @Uint64() + external int globalOutputIndex; + + @Int8() + external int spent; + + @Int8() + external int frozen; + + @Uint64() + external int spentHeight; + + @Uint64() + external int amount; + + @Int8() + external int rct; + + @Int8() + external int keyImageKnown; + + @Uint64() + external int pkIndex; + + @Uint32() + external int subaddrIndex; + + @Uint32() + external int subaddrAccount; + + external Pointer address; + + external Pointer addressLabel; + + external Pointer keyImage; + + @Uint64() + external int unlockTime; + + @Int8() + external int unlocked; + + external Pointer pubKey; + + @Int8() + external int coinbase; + + external Pointer description; + + String getHash() => hash.toDartString(); + + String getAddress() => address.toDartString(); + + String getAddressLabel() => addressLabel.toDartString(); + + String getKeyImage() => keyImage.toDartString(); + + String getPubKey() => pubKey.toDartString(); + + String getDescription() => description.toDartString(); +} diff --git a/cw_monero/lib/api/transaction_history.dart b/cw_monero/lib/api/transaction_history.dart index 0fc507500..6f848cb3d 100644 --- a/cw_monero/lib/api/transaction_history.dart +++ b/cw_monero/lib/api/transaction_history.dart @@ -35,9 +35,8 @@ final transactionCommitNative = moneroApi .lookup>('transaction_commit') .asFunction(); -final getTxKeyNative = moneroApi - .lookup>('get_tx_key') - .asFunction(); +final getTxKeyNative = + moneroApi.lookup>('get_tx_key').asFunction(); String getTxKey(String txId) { final txIdPointer = txId.toNativeUtf8(); @@ -71,10 +70,21 @@ PendingTransactionDescription createTransactionSync( required String paymentId, required int priorityRaw, String? amount, - int accountIndex = 0}) { + int accountIndex = 0, + List preferredInputs = const []}) { final addressPointer = address.toNativeUtf8(); final paymentIdPointer = paymentId.toNativeUtf8(); final amountPointer = amount != null ? amount.toNativeUtf8() : nullptr; + + final int preferredInputsSize = preferredInputs.length; + final List> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); final created = transactionCreateNative( @@ -83,10 +93,16 @@ PendingTransactionDescription createTransactionSync( amountPointer, priorityRaw, accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, errorMessagePointer, pendingTransactionRawPointer) != 0; + calloc.free(preferredInputsPointerPointer); + + preferredInputsPointers.forEach((element) => calloc.free(element)); + calloc.free(addressPointer); calloc.free(paymentIdPointer); @@ -111,15 +127,16 @@ PendingTransactionDescription createTransactionSync( PendingTransactionDescription createTransactionMultDestSync( {required List outputs, - required String paymentId, - required int priorityRaw, - int accountIndex = 0}) { + required String paymentId, + required int priorityRaw, + int accountIndex = 0, + List preferredInputs = const []}) { final int size = outputs.length; - final List> addressesPointers = outputs.map((output) => - output.address.toNativeUtf8()).toList(); + final List> addressesPointers = + outputs.map((output) => output.address.toNativeUtf8()).toList(); final Pointer> addressesPointerPointer = calloc(size); - final List> amountsPointers = outputs.map((output) => - output.amount.toNativeUtf8()).toList(); + final List> amountsPointers = + outputs.map((output) => output.amount.toNativeUtf8()).toList(); final Pointer> amountsPointerPointer = calloc(size); for (int i = 0; i < size; i++) { @@ -127,25 +144,38 @@ PendingTransactionDescription createTransactionMultDestSync( amountsPointerPointer[i] = amountsPointers[i]; } + final int preferredInputsSize = preferredInputs.length; + final List> preferredInputsPointers = + preferredInputs.map((output) => output.toNativeUtf8()).toList(); + final Pointer> preferredInputsPointerPointer = calloc(preferredInputsSize); + + for (int i = 0; i < preferredInputsSize; i++) { + preferredInputsPointerPointer[i] = preferredInputsPointers[i]; + } + final paymentIdPointer = paymentId.toNativeUtf8(); final errorMessagePointer = calloc(); final pendingTransactionRawPointer = calloc(); final created = transactionCreateMultDestNative( - addressesPointerPointer, - paymentIdPointer, - amountsPointerPointer, - size, - priorityRaw, - accountIndex, - errorMessagePointer, - pendingTransactionRawPointer) != + addressesPointerPointer, + paymentIdPointer, + amountsPointerPointer, + size, + priorityRaw, + accountIndex, + preferredInputsPointerPointer, + preferredInputsSize, + errorMessagePointer, + pendingTransactionRawPointer) != 0; calloc.free(addressesPointerPointer); calloc.free(amountsPointerPointer); + calloc.free(preferredInputsPointerPointer); addressesPointers.forEach((element) => calloc.free(element)); amountsPointers.forEach((element) => calloc.free(element)); + preferredInputsPointers.forEach((element) => calloc.free(element)); calloc.free(paymentIdPointer); @@ -164,13 +194,12 @@ PendingTransactionDescription createTransactionMultDestSync( pointerAddress: pendingTransactionRawPointer.address); } -void commitTransactionFromPointerAddress({required int address}) => commitTransaction( - transactionPointer: Pointer.fromAddress(address)); +void commitTransactionFromPointerAddress({required int address}) => + commitTransaction(transactionPointer: Pointer.fromAddress(address)); void commitTransaction({required Pointer transactionPointer}) { final errorMessagePointer = calloc(); - final isCommited = - transactionCommitNative(transactionPointer, errorMessagePointer) != 0; + final isCommited = transactionCommitNative(transactionPointer, errorMessagePointer) != 0; if (!isCommited) { final message = errorMessagePointer.ref.getValue(); @@ -185,13 +214,15 @@ PendingTransactionDescription _createTransactionSync(Map args) { final amount = args['amount'] as String?; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; return createTransactionSync( address: address, paymentId: paymentId, amount: amount, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } PendingTransactionDescription _createTransactionMultDestSync(Map args) { @@ -199,12 +230,14 @@ PendingTransactionDescription _createTransactionMultDestSync(Map args) { final paymentId = args['paymentId'] as String; final priorityRaw = args['priorityRaw'] as int; final accountIndex = args['accountIndex'] as int; + final preferredInputs = args['preferredInputs'] as List; return createTransactionMultDestSync( outputs: outputs, paymentId: paymentId, priorityRaw: priorityRaw, - accountIndex: accountIndex); + accountIndex: accountIndex, + preferredInputs: preferredInputs); } Future createTransaction( @@ -212,23 +245,27 @@ Future createTransaction( required int priorityRaw, String? amount, String paymentId = '', - int accountIndex = 0}) => + int accountIndex = 0, + List preferredInputs = const []}) => compute(_createTransactionSync, { 'address': address, 'paymentId': paymentId, 'amount': amount, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); Future createTransactionMultDest( - {required List outputs, - required int priorityRaw, - String paymentId = '', - int accountIndex = 0}) => + {required List outputs, + required int priorityRaw, + String paymentId = '', + int accountIndex = 0, + List preferredInputs = const []}) => compute(_createTransactionMultDestSync, { 'outputs': outputs, 'paymentId': paymentId, 'priorityRaw': priorityRaw, - 'accountIndex': accountIndex + 'accountIndex': accountIndex, + 'preferredInputs': preferredInputs }); diff --git a/cw_monero/lib/api/types.dart b/cw_monero/lib/api/types.dart index 051f317c9..2c92f2d80 100644 --- a/cw_monero/lib/api/types.dart +++ b/cw_monero/lib/api/types.dart @@ -1,4 +1,5 @@ import 'dart:ffi'; +import 'package:cw_monero/api/structs/coins_info_row.dart'; import 'package:cw_monero/api/structs/pending_transaction.dart'; import 'package:cw_monero/api/structs/ut8_box.dart'; import 'package:ffi/ffi.dart'; @@ -92,6 +93,8 @@ typedef TransactionCreate = int Function( Pointer amount, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -102,6 +105,8 @@ typedef TransactionCreateMultDest = int Function( int size, int priorityRaw, int subaddrAccount, + Pointer> preferredInputs, + int preferredInputsSize, Pointer error, Pointer pendingTransaction); @@ -127,4 +132,10 @@ typedef GetSubaddressLabel = Pointer Function( typedef SetTrustedDaemon = void Function(int); -typedef TrustedDaemon = int Function(); \ No newline at end of file +typedef TrustedDaemon = int Function(); + +typedef RefreshCoins = void Function(int); + +typedef CoinsCount = int Function(); + +typedef GetCoin = Pointer Function(int); diff --git a/cw_monero/lib/monero_transaction_creation_exception.dart b/cw_monero/lib/exceptions/monero_transaction_creation_exception.dart similarity index 100% rename from cw_monero/lib/monero_transaction_creation_exception.dart rename to cw_monero/lib/exceptions/monero_transaction_creation_exception.dart diff --git a/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart new file mode 100644 index 000000000..5d808be8f --- /dev/null +++ b/cw_monero/lib/exceptions/monero_transaction_no_inputs_exception.dart @@ -0,0 +1,4 @@ +class MoneroTransactionNoInputsException implements Exception { + @override + String toString() => 'Not enough inputs available. Please select more under Coin Control'; +} diff --git a/cw_monero/lib/monero_subaddress_list.dart b/cw_monero/lib/monero_subaddress_list.dart index 8d8eeb469..dbd1a89ae 100644 --- a/cw_monero/lib/monero_subaddress_list.dart +++ b/cw_monero/lib/monero_subaddress_list.dart @@ -1,19 +1,20 @@ -import 'package:cw_monero/api/structs/subaddress_row.dart'; import 'package:flutter/services.dart'; import 'package:mobx/mobx.dart'; +import 'package:cw_monero/api/coins_info.dart'; import 'package:cw_monero/api/subaddress_list.dart' as subaddress_list; import 'package:cw_core/subaddress.dart'; part 'monero_subaddress_list.g.dart'; -class MoneroSubaddressList = MoneroSubaddressListBase - with _$MoneroSubaddressList; +class MoneroSubaddressList = MoneroSubaddressListBase with _$MoneroSubaddressList; abstract class MoneroSubaddressListBase with Store { MoneroSubaddressListBase() - : _isRefreshing = false, - _isUpdating = false, - subaddresses = ObservableList(); + : _isRefreshing = false, + _isUpdating = false, + subaddresses = ObservableList(); + + final List _usedAddresses = []; @observable ObservableList subaddresses; @@ -22,6 +23,8 @@ abstract class MoneroSubaddressListBase with Store { bool _isUpdating; void update({required int accountIndex}) { + refreshCoins(accountIndex); + if (_isUpdating) { return; } @@ -47,20 +50,24 @@ abstract class MoneroSubaddressListBase with Store { subaddresses = [primary] + rest.toList(); } - return subaddresses - .map((subaddressRow) => Subaddress( + return subaddresses.map((subaddressRow) { + final hasDefaultAddressName = + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() || + subaddressRow.getLabel().toLowerCase() == 'Untitled account'.toLowerCase(); + final isPrimaryAddress = subaddressRow.getId() == 0 && hasDefaultAddressName; + return Subaddress( id: subaddressRow.getId(), address: subaddressRow.getAddress(), - label: subaddressRow.getId() == 0 && - subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() - ? 'Primary address' - : subaddressRow.getLabel())) - .toList(); + label: isPrimaryAddress + ? 'Primary address' + : hasDefaultAddressName + ? '' + : subaddressRow.getLabel()); + }).toList(); } Future addSubaddress({required int accountIndex, required String label}) async { - await subaddress_list.addSubaddress( - accountIndex: accountIndex, label: label); + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); update(accountIndex: accountIndex); } @@ -86,4 +93,59 @@ abstract class MoneroSubaddressListBase with Store { rethrow; } } + + Future updateWithAutoGenerate({ + required int accountIndex, + required String defaultLabel, + required List usedAddresses, + }) async { + _usedAddresses.addAll(usedAddresses); + if (_isUpdating) { + return; + } + + try { + _isUpdating = true; + refresh(accountIndex: accountIndex); + subaddresses.clear(); + final newSubAddresses = + await _getAllUnusedAddresses(accountIndex: accountIndex, label: defaultLabel); + subaddresses.addAll(newSubAddresses); + } catch (e) { + rethrow; + } finally { + _isUpdating = false; + } + } + + Future> _getAllUnusedAddresses( + {required int accountIndex, required String label}) async { + final allAddresses = subaddress_list.getAllSubaddresses(); + + if (allAddresses.isEmpty || _usedAddresses.contains(allAddresses.last.getAddress())) { + final isAddressUnused = await _newSubaddress(accountIndex: accountIndex, label: label); + if (!isAddressUnused) { + return await _getAllUnusedAddresses(accountIndex: accountIndex, label: label); + } + } + + return allAddresses + .map((subaddressRow) => Subaddress( + id: subaddressRow.getId(), + address: subaddressRow.getAddress(), + label: subaddressRow.getId() == 0 && + subaddressRow.getLabel().toLowerCase() == 'Primary account'.toLowerCase() + ? 'Primary address' + : subaddressRow.getLabel())) + .toList(); + } + + Future _newSubaddress({required int accountIndex, required String label}) async { + await subaddress_list.addSubaddress(accountIndex: accountIndex, label: label); + + return subaddress_list + .getAllSubaddresses() + .where((subaddressRow) => !_usedAddresses.contains(subaddressRow.getAddress())) + .isNotEmpty; + } } diff --git a/cw_monero/lib/monero_unspent.dart b/cw_monero/lib/monero_unspent.dart new file mode 100644 index 000000000..c2ff9f9db --- /dev/null +++ b/cw_monero/lib/monero_unspent.dart @@ -0,0 +1,28 @@ +import 'package:cw_monero/api/structs/coins_info_row.dart'; + +class MoneroUnspent { + MoneroUnspent(this.address, this.hash, this.keyImage, this.value, this.isFrozen, this.isUnlocked) + : isSending = true, + note = ''; + + MoneroUnspent.fromCoinsInfoRow(CoinsInfoRow coinsInfoRow) + : address = coinsInfoRow.getAddress(), + hash = coinsInfoRow.getHash(), + keyImage = coinsInfoRow.getKeyImage(), + value = coinsInfoRow.amount, + isFrozen = coinsInfoRow.frozen == 1, + isUnlocked = coinsInfoRow.unlocked == 1, + isSending = true, + note = ''; + + final String address; + final String hash; + final String keyImage; + final int value; + + final bool isUnlocked; + + bool isFrozen; + bool isSending; + String note; +} diff --git a/cw_monero/lib/monero_wallet.dart b/cw_monero/lib/monero_wallet.dart index ef25b6b93..39c0604a3 100644 --- a/cw_monero/lib/monero_wallet.dart +++ b/cw_monero/lib/monero_wallet.dart @@ -1,33 +1,35 @@ import 'dart:async'; import 'dart:io'; -import 'package:cw_core/pathForWallet.dart'; -import 'package:cw_core/transaction_priority.dart'; -import 'package:cw_core/monero_amount_format.dart'; -import 'package:cw_monero/monero_transaction_creation_exception.dart'; -import 'package:cw_monero/monero_transaction_info.dart'; -import 'package:cw_monero/monero_wallet_addresses.dart'; -import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:cw_monero/api/structs/pending_transaction.dart'; -import 'package:mobx/mobx.dart'; -import 'package:cw_monero/api/transaction_history.dart' - as monero_transaction_history; -import 'package:cw_monero/api/wallet.dart'; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/transaction_history.dart' as transaction_history; -import 'package:cw_monero/api/monero_output.dart'; -import 'package:cw_monero/monero_transaction_creation_credentials.dart'; -import 'package:cw_monero/pending_monero_transaction.dart'; -import 'package:cw_core/monero_wallet_keys.dart'; -import 'package:cw_core/monero_balance.dart'; -import 'package:cw_monero/monero_transaction_history.dart'; import 'package:cw_core/account.dart'; -import 'package:cw_core/pending_transaction.dart'; -import 'package:cw_core/wallet_base.dart'; -import 'package:cw_core/sync_status.dart'; -import 'package:cw_core/wallet_info.dart'; -import 'package:cw_core/node.dart'; -import 'package:cw_core/monero_transaction_priority.dart'; import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_amount_format.dart'; +import 'package:cw_core/monero_balance.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/monero_wallet_keys.dart'; +import 'package:cw_core/monero_wallet_utils.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/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_monero/api/coins_info.dart'; +import 'package:cw_monero/api/monero_output.dart'; +import 'package:cw_monero/api/structs/pending_transaction.dart'; +import 'package:cw_monero/api/transaction_history.dart' as transaction_history; +import 'package:cw_monero/api/wallet.dart' as monero_wallet; +import 'package:cw_monero/exceptions/monero_transaction_creation_exception.dart'; +import 'package:cw_monero/exceptions/monero_transaction_no_inputs_exception.dart'; +import 'package:cw_monero/pending_monero_transaction.dart'; +import 'package:cw_monero/monero_transaction_creation_credentials.dart'; +import 'package:cw_monero/monero_transaction_history.dart'; +import 'package:cw_monero/monero_transaction_info.dart'; +import 'package:cw_monero/monero_unspent.dart'; +import 'package:cw_monero/monero_wallet_addresses.dart'; +import 'package:mobx/mobx.dart'; +import 'package:hive/hive.dart'; part 'monero_wallet.g.dart'; @@ -37,39 +39,51 @@ class MoneroWallet = MoneroWalletBase with _$MoneroWallet; abstract class MoneroWalletBase extends WalletBase with Store { - MoneroWalletBase({required WalletInfo walletInfo}) + MoneroWalletBase({required WalletInfo walletInfo, + required Box unspentCoinsInfo}) : balance = ObservableMap.of({ - CryptoCurrency.xmr: MoneroBalance( + CryptoCurrency.xmr: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: 0), unlockedBalance: monero_wallet.getFullBalance(accountIndex: 0)) - }), + }), _isTransactionUpdating = false, _hasSyncAfterStartup = false, - walletAddresses = MoneroWalletAddresses(walletInfo), + isEnabledAutoGenerateSubaddress = false, syncStatus = NotConnectedSyncStatus(), + unspentCoins = [], + this.unspentCoinsInfo = unspentCoinsInfo, super(walletInfo) { transactionHistory = MoneroTransactionHistory(); - _onAccountChangeReaction = reaction((_) => walletAddresses.account, - (Account? account) { + walletAddresses = MoneroWalletAddresses(walletInfo, transactionHistory); + + _onAccountChangeReaction = reaction((_) => walletAddresses.account, (Account? account) { if (account == null) { return; } - balance = ObservableMap.of( - { - currency: MoneroBalance( + balance = ObservableMap.of({ + currency: MoneroBalance( fullBalance: monero_wallet.getFullBalance(accountIndex: account.id), - unlockedBalance: - monero_wallet.getUnlockedBalance(accountIndex: account.id)) - }); - walletAddresses.updateSubaddressList(accountIndex: account.id); + unlockedBalance: monero_wallet.getUnlockedBalance(accountIndex: account.id)) + }); + _updateSubAddress(isEnabledAutoGenerateSubaddress, account: account); + }); + + reaction((_) => isEnabledAutoGenerateSubaddress, (bool enabled) { + _updateSubAddress(enabled, account: walletAddresses.account); }); } static const int _autoSaveInterval = 30; + Box unspentCoinsInfo; + @override - MoneroWalletAddresses walletAddresses; + late MoneroWalletAddresses walletAddresses; + + @override + @observable + bool isEnabledAutoGenerateSubaddress; @override @observable @@ -89,11 +103,12 @@ abstract class MoneroWalletBase extends WalletBase unspentCoins; Future init() async { await walletAddresses.init(); @@ -170,10 +185,12 @@ abstract class MoneroWalletBase extends WalletBase createTransaction(Object credentials) async { final _credentials = credentials as MoneroTransactionCreationCredentials; + final inputs = []; final outputs = _credentials.outputs; final hasMultiDestination = outputs.length > 1; final unlockedBalance = monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + var allInputsAmount = 0; PendingTransactionDescription pendingTransactionDescription; @@ -181,6 +198,21 @@ abstract class MoneroWalletBase extends WalletBase item.sendAll || (item.formattedCryptoAmount ?? 0) <= 0)) { @@ -208,7 +240,8 @@ abstract class MoneroWalletBase extends WalletBase save() async { + await walletAddresses.updateUsedSubaddress(); + + if (isEnabledAutoGenerateSubaddress) { + walletAddresses.updateUnusedSubaddress( + accountIndex: walletAddresses.account?.id ?? 0, + defaultLabel: walletAddresses.account?.label ?? ''); + } + await walletAddresses.updateAddressesInBox(); await backupWalletFiles(name); await monero_wallet.store(); @@ -354,6 +395,85 @@ abstract class MoneroWalletBase extends WalletBase updateUnspent() async { + refreshCoins(walletAddresses.account!.id); + + unspentCoins.clear(); + + final coinCount = countOfCoins(); + for (var i = 0; i < coinCount; i++) { + final coin = getCoin(i); + if (coin.spent == 0) { + unspentCoins.add(MoneroUnspent.fromCoinsInfoRow(coin)); + } + } + + if (unspentCoinsInfo.isEmpty) { + unspentCoins.forEach((coin) => _addCoinInfo(coin)); + return; + } + + if (unspentCoins.isNotEmpty) { + unspentCoins.forEach((coin) { + final coinInfoList = unspentCoinsInfo.values.where((element) => + element.walletId.contains(id) && element.hash.contains(coin.hash)); + + if (coinInfoList.isNotEmpty) { + final coinInfo = coinInfoList.first; + + coin.isFrozen = coinInfo.isFrozen; + coin.isSending = coinInfo.isSending; + coin.note = coinInfo.note; + } else { + _addCoinInfo(coin); + } + }); + } + + await _refreshUnspentCoinsInfo(); + _askForUpdateBalance(); + } + + Future _addCoinInfo(MoneroUnspent coin) async { + final newInfo = UnspentCoinsInfo( + walletId: id, + hash: coin.hash, + isFrozen: coin.isFrozen, + isSending: coin.isSending, + noteRaw: coin.note, + address: coin.address, + value: coin.value, + vout: 0, + keyImage: coin.keyImage + ); + + await unspentCoinsInfo.add(newInfo); + } + + Future _refreshUnspentCoinsInfo() async { + try { + final List keys = []; + final currentWalletUnspentCoins = unspentCoinsInfo.values + .where((element) => element.walletId.contains(id)); + + if (currentWalletUnspentCoins.isNotEmpty) { + currentWalletUnspentCoins.forEach((element) { + final existUnspentCoins = unspentCoins.where((coin) => element.hash.contains(coin.hash)); + + if (existUnspentCoins.isEmpty) { + keys.add(element.key); + } + }); + } + + if (keys.isNotEmpty) { + await unspentCoinsInfo.deleteAll(keys); + } + } catch (e) { + print(e.toString()); + } + } + String getTransactionAddress(int accountIndex, int addressIndex) => monero_wallet.getAddress( accountIndex: accountIndex, @@ -361,7 +481,7 @@ abstract class MoneroWalletBase extends WalletBase> fetchTransactions() async { - monero_transaction_history.refreshTransactions(); + transaction_history.refreshTransactions(); return _getAllTransactions(null).fold>( {}, (Map acc, MoneroTransactionInfo tx) { @@ -392,7 +512,7 @@ abstract class MoneroWalletBase extends WalletBase _getAllTransactions(dynamic _) => - monero_transaction_history + transaction_history .getAllTransations() .map((row) => MoneroTransactionInfo.fromRow(row)) .toList(); @@ -407,7 +527,7 @@ abstract class MoneroWalletBase extends WalletBase monero_wallet.getUnlockedBalance(accountIndex: walletAddresses.account!.id); + int _getFrozenBalance() { + var frozenBalance = 0; + + for (var coin in unspentCoinsInfo.values) { + if (coin.isFrozen) + frozenBalance += coin.value; + } + + return frozenBalance; + } + void _onNewBlock(int height, int blocksLeft, double ptc) async { try { if (walletInfo.isRecovery) { @@ -495,4 +628,15 @@ abstract class MoneroWalletBase extends WalletBase updateUsedSubaddress() async { + final transactions = _moneroTransactionHistory.transactions.values.toList(); + + transactions.forEach((element) { + final accountIndex = element.accountIndex; + final addressIndex = element.addressIndex; + usedAddresses.add(getAddress(accountIndex: accountIndex, addressIndex: addressIndex)); + }); + } + + Future updateUnusedSubaddress( + {required int accountIndex, required String defaultLabel}) async { + await subaddressList.updateWithAutoGenerate( + accountIndex: accountIndex, + defaultLabel: defaultLabel, + usedAddresses: usedAddresses.toList()); + subaddress = subaddressList.subaddresses.last; + address = subaddress!.address; + } +} diff --git a/cw_monero/lib/monero_wallet_service.dart b/cw_monero/lib/monero_wallet_service.dart index 6539d58a5..9d1a0b3e4 100644 --- a/cw_monero/lib/monero_wallet_service.dart +++ b/cw_monero/lib/monero_wallet_service.dart @@ -1,16 +1,16 @@ import 'dart:io'; -import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/monero_wallet_utils.dart'; -import 'package:hive/hive.dart'; -import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; -import 'package:cw_monero/api/wallet.dart' as monero_wallet; -import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; -import 'package:cw_monero/monero_wallet.dart'; -import 'package:cw_core/wallet_credentials.dart'; -import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/unspent_coins_info.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; import 'package:cw_core/wallet_type.dart'; +import 'package:cw_monero/api/exceptions/wallet_opening_exception.dart'; +import 'package:cw_monero/api/wallet_manager.dart' as monero_wallet_manager; +import 'package:cw_monero/monero_wallet.dart'; +import 'package:hive/hive.dart'; class MoneroNewWalletCredentials extends WalletCredentials { MoneroNewWalletCredentials({required String name, required this.language, String? password}) @@ -53,10 +53,11 @@ class MoneroWalletService extends WalletService< MoneroNewWalletCredentials, MoneroRestoreWalletFromSeedCredentials, MoneroRestoreWalletFromKeysCredentials> { - MoneroWalletService(this.walletInfoSource); + MoneroWalletService(this.walletInfoSource, this.unspentCoinsInfoSource); final Box walletInfoSource; - + final Box unspentCoinsInfoSource; + static bool walletFilesExist(String path) => !File(path).existsSync() && !File('$path.keys').existsSync(); @@ -71,7 +72,8 @@ class MoneroWalletService extends WalletService< path: path, password: credentials.password!, language: credentials.language); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -107,7 +109,7 @@ class MoneroWalletService extends WalletService< .openWalletAsync({'path': path, 'password': password}); final walletInfo = walletInfoSource.values.firstWhere( (info) => info.id == WalletBase.idFor(name, getType())); - final wallet = MoneroWallet(walletInfo: walletInfo); + final wallet = MoneroWallet(walletInfo: walletInfo, unspentCoinsInfo: unspentCoinsInfoSource); final isValid = wallet.walletAddresses.validate(); if (!isValid) { @@ -122,13 +124,20 @@ class MoneroWalletService extends WalletService< } catch (e) { // TODO: Implement Exception for wallet list service. - if ((e.toString().contains('bad_alloc') || + final bool isBadAlloc = e.toString().contains('bad_alloc') || (e is WalletOpeningException && - (e.message == 'std::bad_alloc' || - e.message.contains('bad_alloc')))) || - (e.toString().contains('does not correspond') || - (e is WalletOpeningException && - e.message.contains('does not correspond')))) { + (e.message == 'std::bad_alloc' || e.message.contains('bad_alloc'))); + + final bool doesNotCorrespond = e.toString().contains('does not correspond') || + (e is WalletOpeningException && e.message.contains('does not correspond')); + + final bool isMissingCacheFilesIOS = e.toString().contains('basic_string') || + (e is WalletOpeningException && e.message.contains('basic_string')); + + final bool isMissingCacheFilesAndroid = e.toString().contains('input_stream') || + (e is WalletOpeningException && e.message.contains('input_stream')); + + if (isBadAlloc || doesNotCorrespond || isMissingCacheFilesIOS || isMissingCacheFilesAndroid) { await restoreOrResetWalletFiles(name); return openWallet(name, password); } @@ -157,7 +166,8 @@ class MoneroWalletService extends WalletService< String currentName, String password, String newName) async { final currentWalletInfo = walletInfoSource.values.firstWhere( (info) => info.id == WalletBase.idFor(currentName, getType())); - final currentWallet = MoneroWallet(walletInfo: currentWalletInfo); + final currentWallet = + MoneroWallet(walletInfo: currentWalletInfo, unspentCoinsInfo: unspentCoinsInfoSource); await currentWallet.renameWalletFiles(newName); @@ -181,7 +191,8 @@ class MoneroWalletService extends WalletService< address: credentials.address, viewKey: credentials.viewKey, spendKey: credentials.spendKey); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; @@ -202,7 +213,8 @@ class MoneroWalletService extends WalletService< password: credentials.password!, seed: credentials.mnemonic, restoreHeight: credentials.height!); - final wallet = MoneroWallet(walletInfo: credentials.walletInfo!); + final wallet = MoneroWallet( + walletInfo: credentials.walletInfo!, unspentCoinsInfo: unspentCoinsInfoSource); await wallet.init(); return wallet; diff --git a/cw_monero/macos/Classes/monero_api.cpp b/cw_monero/macos/Classes/monero_api.cpp index 56548e79e..ac8a64861 100644 --- a/cw_monero/macos/Classes/monero_api.cpp +++ b/cw_monero/macos/Classes/monero_api.cpp @@ -3,8 +3,10 @@ #include #include #include +#include #include #include +#include #include "thread" #include "CwWalletListener.h" #if __APPLE__ @@ -137,7 +139,7 @@ extern "C" int8_t direction; int8_t isPending; uint32_t subaddrIndex; - + char *hash; char *paymentId; @@ -152,7 +154,7 @@ extern "C" std::set::iterator it = transaction->subaddrIndex().begin(); subaddrIndex = *it; confirmations = transaction->confirmations(); - datetime = static_cast(transaction->timestamp()); + datetime = static_cast(transaction->timestamp()); direction = transaction->direction(); isPending = static_cast(transaction->isPending()); std::string *hash_str = new std::string(transaction->hash()); @@ -181,6 +183,62 @@ extern "C" } }; + struct CoinsInfoRow + { + uint64_t blockHeight; + char *hash; + uint64_t internalOutputIndex; + uint64_t globalOutputIndex; + bool spent; + bool frozen; + uint64_t spentHeight; + uint64_t amount; + bool rct; + bool keyImageKnown; + uint64_t pkIndex; + uint32_t subaddrIndex; + uint32_t subaddrAccount; + char *address; + char *addressLabel; + char *keyImage; + uint64_t unlockTime; + bool unlocked; + char *pubKey; + bool coinbase; + char *description; + + CoinsInfoRow(Monero::CoinsInfo *coinsInfo) + { + blockHeight = coinsInfo->blockHeight(); + std::string *hash_str = new std::string(coinsInfo->hash()); + hash = strdup(hash_str->c_str()); + internalOutputIndex = coinsInfo->internalOutputIndex(); + globalOutputIndex = coinsInfo->globalOutputIndex(); + spent = coinsInfo->spent(); + frozen = coinsInfo->frozen(); + spentHeight = coinsInfo->spentHeight(); + amount = coinsInfo->amount(); + rct = coinsInfo->rct(); + keyImageKnown = coinsInfo->keyImageKnown(); + pkIndex = coinsInfo->pkIndex(); + subaddrIndex = coinsInfo->subaddrIndex(); + subaddrAccount = coinsInfo->subaddrAccount(); + address = strdup(coinsInfo->address().c_str()) ; + addressLabel = strdup(coinsInfo->addressLabel().c_str()); + keyImage = strdup(coinsInfo->keyImage().c_str()); + unlockTime = coinsInfo->unlockTime(); + unlocked = coinsInfo->unlocked(); + pubKey = strdup(coinsInfo->pubKey().c_str()); + coinbase = coinsInfo->coinbase(); + description = strdup(coinsInfo->description().c_str()); + } + + void setUnlocked(bool unlocked); + + }; + + Monero::Coins *m_coins; + Monero::Wallet *m_wallet; Monero::TransactionHistory *m_transaction_history; MoneroWalletListener *m_listener; @@ -188,6 +246,7 @@ extern "C" Monero::SubaddressAccount *m_account; uint64_t m_last_known_wallet_height; uint64_t m_cached_syncing_blockchain_height = 0; + std::list m_coins_info; std::mutex store_lock; bool is_storing = false; @@ -195,7 +254,7 @@ extern "C" { m_wallet = wallet; m_listener = nullptr; - + if (wallet != nullptr) { @@ -223,6 +282,17 @@ extern "C" { m_subaddress = nullptr; } + + m_coins_info = std::list(); + + if (wallet != nullptr) + { + m_coins = wallet->coins(); + } + else + { + m_coins = nullptr; + } } Monero::Wallet *get_current_wallet() @@ -405,13 +475,14 @@ extern "C" return is_connected; } - bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *error) + bool setup_node(char *address, char *login, char *password, bool use_ssl, bool is_light_wallet, char *socksProxyAddress, char *error) { nice(19); Monero::Wallet *wallet = get_current_wallet(); - + std::string _login = ""; std::string _password = ""; + std::string _socksProxyAddress = ""; if (login != nullptr) { @@ -423,7 +494,12 @@ extern "C" _password = std::string(password); } - bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet); + if (socksProxyAddress != nullptr) + { + _socksProxyAddress = std::string(socksProxyAddress); + } + + bool inited = wallet->init(std::string(address), 0, _login, _password, use_ssl, is_light_wallet, _socksProxyAddress); if (!inited) { @@ -480,10 +556,19 @@ extern "C" } bool transaction_create(char *address, char *payment_id, char *amount, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); - + + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -496,13 +581,13 @@ extern "C" if (amount != nullptr) { uint64_t _amount = Monero::Wallet::amountFromString(std::string(amount)); - transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, _amount, m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } else { - transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account); + transaction = m_wallet->createTransaction(std::string(address), _payment_id, Monero::optional(), m_wallet->defaultMixin(), priority, subaddr_account, {}, _preferred_inputs); } - + int status = transaction->status(); if (status == Monero::PendingTransaction::Status::Status_Error || status == Monero::PendingTransaction::Status::Status_Critical) @@ -520,7 +605,9 @@ extern "C" } bool transaction_create_mult_dest(char **addresses, char *payment_id, char **amounts, uint32_t size, - uint8_t priority_raw, uint32_t subaddr_account, Utf8Box &error, PendingTransactionRaw &pendingTransaction) + uint8_t priority_raw, uint32_t subaddr_account, + char **preferred_inputs, uint32_t preferred_inputs_size, + Utf8Box &error, PendingTransactionRaw &pendingTransaction) { nice(19); @@ -534,6 +621,13 @@ extern "C" amounts++; } + std::set _preferred_inputs; + + for (int i = 0; i < preferred_inputs_size; i++) { + _preferred_inputs.insert(std::string(*preferred_inputs)); + preferred_inputs++; + } + auto priority = static_cast(priority_raw); std::string _payment_id; Monero::PendingTransaction *transaction; @@ -793,6 +887,91 @@ extern "C" return m_wallet->trustedDaemon(); } + CoinsInfoRow* coin(int index) + { + if (index >= 0 && index < m_coins_info.size()) { + std::list::iterator it = m_coins_info.begin(); + std::advance(it, index); + Monero::CoinsInfo* element = *it; + std::cout << "Element at index " << index << ": " << element << std::endl; + return new CoinsInfoRow(element); + } else { + std::cout << "Invalid index." << std::endl; + return nullptr; // Return a default value (nullptr) for invalid index + } + } + + void refresh_coins(uint32_t accountIndex) + { + m_coins_info.clear(); + + m_coins->refresh(); + for (const auto i : m_coins->getAll()) { + if (i->subaddrAccount() == accountIndex && !(i->spent())) { + m_coins_info.push_back(i); + } + } + } + + uint64_t coins_count() + { + return m_coins_info.size(); + } + + CoinsInfoRow** coins_from_account(uint32_t accountIndex) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (coinInfo->subaddrAccount == accountIndex) { + matchingCoins.push_back(coinInfo); + } + } + + CoinsInfoRow** result = new CoinsInfoRow*[matchingCoins.size()]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_txid(const char* txid, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinInfo = coin(i); + if (std::string(coinInfo->hash) == txid) { + matchingCoins.push_back(coinInfo); + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + CoinsInfoRow** coins_from_key_image(const char** keyimages, size_t keyimageCount, size_t* count) + { + std::vector matchingCoins; + + for (int i = 0; i < coins_count(); i++) { + CoinsInfoRow* coinsInfoRow = coin(i); + for (size_t j = 0; j < keyimageCount; j++) { + if (coinsInfoRow->keyImageKnown && std::string(coinsInfoRow->keyImage) == keyimages[j]) { + matchingCoins.push_back(coinsInfoRow); + break; + } + } + } + + *count = matchingCoins.size(); + CoinsInfoRow** result = new CoinsInfoRow*[*count]; + std::copy(matchingCoins.begin(), matchingCoins.end(), result); + return result; + } + + #ifdef __cplusplus } #endif diff --git a/cw_monero/pubspec.lock b/cw_monero/pubspec.lock index 1e33631d5..37e08e7ca 100644 --- a/cw_monero/pubspec.lock +++ b/cw_monero/pubspec.lock @@ -37,10 +37,10 @@ packages: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" boolean_selector: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -149,10 +149,10 @@ packages: dependency: transitive description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -286,10 +286,10 @@ packages: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "759d1a329847dd0f39226c688d3e06a6b8679668e350e2891a6474f8b4bb8525" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "1.1.0" http_multi_server: dependency: transitive description: @@ -310,10 +310,10 @@ packages: dependency: "direct main" description: name: intl - sha256: "910f85bce16fb5c6f614e117efa303e85a1731bb0081edf3604a2ae6e9a3cc91" + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.17.0" + version: "0.18.1" io: dependency: transitive description: @@ -326,10 +326,10 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: @@ -350,10 +350,10 @@ packages: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -366,10 +366,10 @@ packages: dependency: transitive description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -406,10 +406,10 @@ packages: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_provider: dependency: "direct main" description: @@ -603,10 +603,10 @@ packages: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" timing: dependency: transitive description: @@ -672,5 +672,5 @@ packages: source: hosted version: "3.1.1" sdks: - dart: ">=2.19.0 <4.0.0" + dart: ">=3.0.0 <4.0.0" flutter: ">=3.0.0" diff --git a/cw_monero/pubspec.yaml b/cw_monero/pubspec.yaml index 066a0d4c3..cf2993ef3 100644 --- a/cw_monero/pubspec.yaml +++ b/cw_monero/pubspec.yaml @@ -13,11 +13,11 @@ dependencies: flutter: sdk: flutter ffi: ^2.0.1 - http: ^0.13.4 + http: ^1.1.0 path_provider: ^2.0.11 mobx: ^2.0.7+4 flutter_mobx: ^2.0.6+1 - intl: ^0.17.0 + intl: ^0.18.0 encrypt: ^5.0.1 cw_core: path: ../cw_core diff --git a/cw_shared_external/pubspec.yaml b/cw_shared_external/pubspec.yaml index b9a8ca1e0..71d5fcd5a 100644 --- a/cw_shared_external/pubspec.yaml +++ b/cw_shared_external/pubspec.yaml @@ -5,7 +5,7 @@ author: Cake Walelt homepage: https://cakewallet.com environment: - sdk: ">=2.7.0 <3.0.0" + sdk: ">=2.12.0 <3.0.0" flutter: ">=1.20.0" dependencies: diff --git a/howto-build-android.md b/howto-build-android.md index d37f1b417..a2a4e4d9f 100644 --- a/howto-build-android.md +++ b/howto-build-android.md @@ -5,10 +5,10 @@ The following are the system requirements to build CakeWallet for your Android device. ``` -Ubuntu >= 16.04 +Ubuntu >= 20.04 Android SDK 29 or higher (better to have the latest one 33) Android NDK 17c -Flutter 3.7.x +Flutter 3.10.x or earlier ``` ## Building CakeWallet on Android @@ -66,7 +66,7 @@ Verify that the Android toolchain, Flutter, and Android Studio have been correct The output of this command will appear like this, indicating successful installations. If there are problems with your installation, they **must** be corrected before proceeding. ``` Doctor summary (to see all details, run flutter doctor -v): -[✓] Flutter (Channel stable, 3.7.x, on Linux, locale en_US.UTF-8) +[✓] Flutter (Channel stable, 3.10.x, on Linux, locale en_US.UTF-8) [✓] Android toolchain - develop for Android devices (Android SDK version 29 or higher) [✓] Android Studio (version 4.0 or higher) ``` diff --git a/ios/Podfile.lock b/ios/Podfile.lock index f13c68629..6f441c587 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -174,12 +174,12 @@ DEPENDENCIES: - in_app_review (from `.symlinks/plugins/in_app_review/ios`) - local_auth_ios (from `.symlinks/plugins/local_auth_ios/ios`) - package_info (from `.symlinks/plugins/package_info/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - permission_handler_apple (from `.symlinks/plugins/permission_handler_apple/ios`) - platform_device_id (from `.symlinks/plugins/platform_device_id/ios`) - sensitive_clipboard (from `.symlinks/plugins/sensitive_clipboard/ios`) - share_plus (from `.symlinks/plugins/share_plus/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - uni_links (from `.symlinks/plugins/uni_links/ios`) - UnstoppableDomainsResolution (~> 4.0.0) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -236,7 +236,7 @@ EXTERNAL SOURCES: package_info: :path: ".symlinks/plugins/package_info/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/ios" + :path: ".symlinks/plugins/path_provider_foundation/darwin" permission_handler_apple: :path: ".symlinks/plugins/permission_handler_apple/ios" platform_device_id: @@ -246,7 +246,7 @@ EXTERNAL SOURCES: share_plus: :path: ".symlinks/plugins/share_plus/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/ios" + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" uni_links: :path: ".symlinks/plugins/uni_links/ios" url_launcher_ios: diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index bc6abc6e2..e46499407 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -182,6 +182,8 @@ class AnonPayApi { switch (currency) { case CryptoCurrency.usdt: return CryptoCurrency.btc.title.toLowerCase(); + case CryptoCurrency.eth: + return 'ERC20'; default: return currency.tag != null ? _normalizeTag(currency.tag!) : 'Mainnet'; } diff --git a/lib/bitcoin/cw_bitcoin.dart b/lib/bitcoin/cw_bitcoin.dart index dfd3b1538..3164aacaa 100644 --- a/lib/bitcoin/cw_bitcoin.dart +++ b/lib/bitcoin/cw_bitcoin.dart @@ -128,7 +128,8 @@ class CWBitcoin extends Bitcoin { bitcoinUnspent.address.address, bitcoinUnspent.hash, bitcoinUnspent.value, - bitcoinUnspent.vout)) + bitcoinUnspent.vout, + null)) .toList(); } @@ -160,4 +161,4 @@ class CWBitcoin extends Bitcoin { @override TransactionPriority getLitecoinTransactionPrioritySlow() => LitecoinTransactionPriority.slow; -} \ No newline at end of file +} diff --git a/lib/buy/onramper/onramper_buy_provider.dart b/lib/buy/onramper/onramper_buy_provider.dart index 68be59f4e..cf4cbd124 100644 --- a/lib/buy/onramper/onramper_buy_provider.dart +++ b/lib/buy/onramper/onramper_buy_provider.dart @@ -1,8 +1,10 @@ import 'package:cake_wallet/.secrets.g.dart' as secrets; +import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/store/settings_store.dart'; -import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/material.dart'; class OnRamperBuyProvider { OnRamperBuyProvider({required SettingsStore settingsStore, required WalletBase wallet}) @@ -27,7 +29,11 @@ class OnRamperBuyProvider { } } - Uri requestUrl() { + String getColorStr(Color color) { + return color.value.toRadixString(16).replaceAll(RegExp(r'^ff'), ""); + } + + Uri requestUrl(BuildContext context) { String primaryColor, secondaryColor, primaryTextColor, @@ -35,31 +41,16 @@ class OnRamperBuyProvider { containerColor, cardColor; - switch (_settingsStore.currentTheme.type) { - case ThemeType.bright: - primaryColor = '815dfbff'; - secondaryColor = 'ffffff'; - primaryTextColor = '141519'; - secondaryTextColor = '6b6f80'; - containerColor = 'ffffff'; - cardColor = 'f2f0faff'; - break; - case ThemeType.light: - primaryColor = '2194ffff'; - secondaryColor = 'ffffff'; - primaryTextColor = '141519'; - secondaryTextColor = '6b6f80'; - containerColor = 'ffffff'; - cardColor = 'e5f7ff'; - break; - case ThemeType.dark: - primaryColor = '456effff'; - secondaryColor = '1b2747ff'; - primaryTextColor = 'ffffff'; - secondaryTextColor = 'ffffff'; - containerColor = '19233C'; - cardColor = '232f4fff'; - break; + primaryColor = getColorStr(Theme.of(context).primaryColor); + secondaryColor = getColorStr(Theme.of(context).colorScheme.background); + primaryTextColor = getColorStr(Theme.of(context).extension()!.titleColor); + secondaryTextColor = + getColorStr(Theme.of(context).extension()!.secondaryTextColor); + containerColor = getColorStr(Theme.of(context).colorScheme.background); + cardColor = getColorStr(Theme.of(context).cardColor); + + if (_settingsStore.currentTheme.title == S.current.high_contrast_theme) { + cardColor = getColorStr(Colors.white); } final networkName = _wallet.currency.fullName?.toUpperCase().replaceAll(" ", ""); diff --git a/lib/core/backup_service.dart b/lib/core/backup_service.dart index 6476891ed..3f430b7e9 100644 --- a/lib/core/backup_service.dart +++ b/lib/core/backup_service.dart @@ -246,6 +246,7 @@ class BackupService { final useEtherscan = data[PreferencesKey.useEtherscan] as bool?; final syncAll = data[PreferencesKey.syncAllKey] as bool?; final syncMode = data[PreferencesKey.syncModeKey] as int?; + final autoGenerateSubaddressStatus = data[PreferencesKey.autoGenerateSubaddressStatusKey] as int?; await _sharedPreferences.setString(PreferencesKey.currentWalletName, currentWalletName); @@ -296,6 +297,9 @@ class BackupService { if (fiatApiMode != null) await _sharedPreferences.setInt(PreferencesKey.currentFiatApiModeKey, fiatApiMode); + if (autoGenerateSubaddressStatus != null) + await _sharedPreferences.setInt(PreferencesKey.autoGenerateSubaddressStatusKey, + autoGenerateSubaddressStatus); if (currentPinLength != null) await _sharedPreferences.setInt(PreferencesKey.currentPinLength, currentPinLength); @@ -523,6 +527,8 @@ class BackupService { _sharedPreferences.getInt(PreferencesKey.syncModeKey), PreferencesKey.syncAllKey: _sharedPreferences.getBool(PreferencesKey.syncAllKey), + PreferencesKey.autoGenerateSubaddressStatusKey: + _sharedPreferences.getInt(PreferencesKey.autoGenerateSubaddressStatusKey), }; return json.encode(preferences); diff --git a/lib/core/fiat_conversion_service.dart b/lib/core/fiat_conversion_service.dart index 9690c430a..479aa3b82 100644 --- a/lib/core/fiat_conversion_service.dart +++ b/lib/core/fiat_conversion_service.dart @@ -21,7 +21,7 @@ Future _fetchPrice(Map args) async { 'key': secrets.fiatApiKey, }; - double price = 0.0; + num price = 0.0; try { late final Uri uri; @@ -41,12 +41,12 @@ Future _fetchPrice(Map args) async { final results = responseJSON['results'] as Map; if (results.isNotEmpty) { - price = results.values.first as double; + price = results.values.first as num; } - return price; + return price.toDouble(); } catch (e) { - return price; + return price.toDouble(); } } diff --git a/lib/core/generate_wallet_password.dart b/lib/core/generate_wallet_password.dart index c9a9fac57..93803dd9d 100644 --- a/lib/core/generate_wallet_password.dart +++ b/lib/core/generate_wallet_password.dart @@ -1,4 +1,3 @@ -import 'package:uuid/uuid.dart'; import 'package:cw_core/key.dart'; String generateWalletPassword() { diff --git a/lib/core/wallet_change_listener_view_model.dart b/lib/core/wallet_change_listener_view_model.dart new file mode 100644 index 000000000..6735afee5 --- /dev/null +++ b/lib/core/wallet_change_listener_view_model.dart @@ -0,0 +1,30 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:mobx/mobx.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cake_wallet/store/app_store.dart'; + +part 'wallet_change_listener_view_model.g.dart'; + +class WalletChangeListenerViewModel = WalletChangeListenerViewModelBase + with _$WalletChangeListenerViewModel; + +abstract class WalletChangeListenerViewModelBase with Store { + WalletChangeListenerViewModelBase({ + required AppStore appStore, + }) : _wallet = appStore.wallet! { + reaction((_) => appStore.wallet, (WalletBase? wallet) { + _wallet = wallet!; + onWalletChange(wallet); + }); + } + + void onWalletChange(WalletBase wallet) {} + + @observable + WalletBase, TransactionInfo> _wallet; + @computed + WalletBase, TransactionInfo> get wallet => + _wallet; +} diff --git a/lib/di.dart b/lib/di.dart index 9ef8e7193..6eb0b3263 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/buy/onramper/onramper_buy_provider.dart'; import 'package:cake_wallet/buy/payfura/payfura_buy_provider.dart'; import 'package:cake_wallet/core/yat_service.dart'; import 'package:cake_wallet/entities/background_tasks.dart'; +import 'package:cake_wallet/entities/auto_generate_subaddress_status.dart'; import 'package:cake_wallet/entities/exchange_api_mode.dart'; import 'package:cake_wallet/entities/parse_address_from_domain.dart'; import 'package:cake_wallet/entities/receive_page_option.dart'; @@ -178,8 +179,6 @@ import 'package:hive/hive.dart'; import 'package:mobx/mobx.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -import 'package:cake_wallet/view_model/wallet_restoration_from_seed_vm.dart'; -import 'package:cake_wallet/view_model/wallet_restoration_from_keys_vm.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cw_core/wallet_type.dart'; @@ -219,10 +218,10 @@ late Box