From ef9b0fbae680ee14fbb2acbb4b7797e156daace6 Mon Sep 17 00:00:00 2001 From: Omar Hatem Date: Fri, 16 Jan 2026 03:16:41 +0100 Subject: [PATCH] Ywallet zcash (#2810) * feat(zcash): initial commit - initial work on cw_zcash - use single .db for everything zcash to speed up sync and scanning - implement silent autoshielding of non-orchard funds - use FFI for everything to fetch the data - add support for passphrase by adding extra word to seed phrase - enforce lints on CI for cw_zcash directory - use correct zcash name, and call transparent zcash tZEC - minor cleanups outsite of cw_zcash - gitmodules, gitignore - db debug - fix broken addressPageType in WalletInfo - enable debug options in kDebugMode and kProfileMode * feat: T address rotation (and cache) wip: minor fixes * wip: shield tx list * fix fiat amounts fix derrivation of rotation account addresses screen for zcash block height fix T address shields improvements Amount in disposable T shields UI improvements show pending outgoing tx in tx history * fix null check on passphrase page fix tx showing as pending rename auto shield txs fixed send all to include fee use rotating address in addressForExchange wrap all calls of WarpApi.getLatestHeight in try catch to prevent leaking errors other minor fixes from notion * wallet group debug cache sent tx address wallet T address rotation fixes automatically pick the fee for auto-shield * fix T address cache deserialization fix autoshield icon fix autoshield text getCachedDestinationAddress fix send address cache * address comments from review * wip: iOS fix * fix swap * Allow cakepay payment from zcash wallet * fix: ios (I'm stupid) fix: reorder addresses to make orchard default fix: send all balance * 5.6.6 bump fix: ios apple review add transport error to ignored exceptions * fix: leaving dust behind when sweeping all * Change wallets order * fix: reflect balance of pending out txs fix: restore height support * linux support (disabled) * fix: date picker for zcash on restore screen fix: build script for linux match WarpApi.warpSync args with upstream * fix: linter script * ensure all exchange providers support ZEC (#2802) * ensure all exchange providers support ZEC * remove zaddr and tzec currencies and refactor providers * Update lib/exchange/provider/near_Intents_exchange_provider.dart * add back old zcash currencies for backward compatibility, thanks Czarek * minor fix [skip ci] --------- Co-authored-by: Omar Hatem * Update android/app/src/main/AndroidManifestBase.xml [skip ci] * Update ios/Runner/InfoBase.plist [skip ci] * fix: remove unused code fix: macos fix: disable on linux and macos * update build numbers [skip ci] * Add zcash to birdpay --------- Co-authored-by: Czarek Nakamoto Co-authored-by: Serhii --- .github/workflows/pr_test_build_android.yml | 1 - .github/workflows/pr_test_build_linux.yml | 30 +- .github/workflows/reusable-build.yml | 14 +- .gitignore | 9 +- .gitmodules | 6 - android/app/src/main/AndroidManifestBase.xml | 6 +- assets/images/crypto/zcash.webp | Bin 0 -> 4134 bytes assets/images/tx_shield.svg | 1 + assets/images/zec_icon.png | Bin 6158 -> 32739 bytes assets/images/zec_icon_qr.svg | 5 + assets/text/Release_Notes.txt | 2 + assets/zcash_node_list.yml | 12 + cw_bitcoin/pubspec.lock | 4 +- cw_core/lib/crypto_currency.dart | 18 +- cw_core/lib/currency_for_wallet_type.dart | 2 + cw_core/lib/db/sqlite.dart | 14 +- cw_core/lib/node.dart | 16 + cw_core/lib/pathForWallet.dart | 16 +- cw_core/lib/wallet_info.dart | 4 + cw_core/lib/wallet_info_legacy.dart | 1 + cw_core/lib/wallet_type.dart | 14 + cw_decred/pubspec.yaml | 3 +- cw_zcash/.gitignore | 7 + cw_zcash/CHANGELOG.md | 3 + cw_zcash/README.md | 1 + cw_zcash/analysis_options.yaml | 31 + cw_zcash/lib/cw_zcash.dart | 15 + cw_zcash/lib/src/models.dart | 41 + .../lib/src/pending_zcash_transaction.dart | 103 + cw_zcash/lib/src/util/crc32.dart | 296 +++ cw_zcash/lib/src/zcash_balance.dart | 30 + ...zcash_mnemonic_is_incorrect_exception.dart | 5 + .../lib/src/zcash_receive_page_options.dart | 70 + cw_zcash/lib/src/zcash_taddress_rotation.dart | 421 +++ .../src/zcash_transaction_credentials.dart | 15 + cw_zcash/lib/src/zcash_transaction_info.dart | 106 + cw_zcash/lib/src/zcash_transactions.dart | 24 + cw_zcash/lib/src/zcash_wallet.dart | 976 +++++++ cw_zcash/lib/src/zcash_wallet_addresses.dart | 223 ++ cw_zcash/lib/src/zcash_wallet_service.dart | 181 ++ cw_zcash/pubspec.yaml | 29 + .../components/common_test_flows.dart | 2 + ios/Podfile.lock | 6 + ios/Runner.xcodeproj/project.pbxproj | 6 + ios/Runner/InfoBase.plist | 20 + lib/anonpay/anonpay_api.dart | 2 +- lib/buy/robinhood/robinhood_buy_provider.dart | 1 + lib/cake_pay/src/models/cake_pay_order.dart | 11 +- lib/core/address_validator.dart | 22 +- lib/core/payment_uris.dart | 15 + lib/core/seed_validator.dart | 3 + lib/core/wallet_creation_service.dart | 1 + lib/di.dart | 5 +- lib/entities/default_settings_migration.dart | 21 +- lib/entities/hash_wallet_identifier.dart | 5 +- lib/entities/node_list.dart | 7 +- lib/entities/parse_address_from_domain.dart | 2 +- lib/entities/preferences_key.dart | 1 + lib/entities/priority_for_wallet_type.dart | 3 + .../provider/chainflip_exchange_provider.dart | 4 +- .../provider/changenow_exchange_provider.dart | 7 +- lib/exchange/provider/exchange_provider.dart | 4 +- .../provider/exolix_exchange_provider.dart | 22 +- .../letsexchange_exchange_provider.dart | 4 +- .../near_Intents_exchange_provider.dart | 20 +- .../provider/sideshift_exchange_provider.dart | 18 +- .../simpleswap_exchange_provider.dart | 8 +- .../stealth_ex_exchange_provider.dart | 4 +- .../provider/swapsxyz_exchange_provider.dart | 4 +- .../provider/swaptrade_exchange_provider.dart | 27 +- .../provider/thorchain_exchange.provider.dart | 21 +- .../provider/trocador_exchange_provider.dart | 8 +- .../provider/xoswap_exchange_provider.dart | 4 +- lib/main.dart | 10 +- .../on_wallet_sync_status_change.dart | 2 + lib/reactions/wallet_utils.dart | 1 + .../desktop_wallet_selection_dropdown.dart | 6 +- .../desktop_widgets/dropdown_item_widget.dart | 2 +- .../screens/dashboard/pages/address_page.dart | 10 + .../dashboard/pages/transactions_page.dart | 2 +- .../dashboard/widgets/menu_widget.dart | 9 +- .../dashboard/widgets/transaction_raw.dart | 9 +- .../wallet_restore_from_seed_form.dart | 2 +- .../screens/restore/wallet_restore_page.dart | 2 +- .../screens/settings/other_settings_page.dart | 5 + lib/src/widgets/blockchain_height_widget.dart | 9 +- lib/store/settings_store.dart | 24 +- lib/utils/exception_handler.dart | 5 +- lib/utils/feature_flag.dart | 4 +- lib/utils/qr_util.dart | 2 + .../advanced_privacy_settings_view_model.dart | 2 + .../cake_pay_buy_card_view_model.dart | 13 +- .../dashboard/balance_view_model.dart | 1 + .../dashboard/dashboard_view_model.dart | 2 + .../dashboard/home_settings_view_model.dart | 1 + .../dashboard/receive_option_view_model.dart | 11 +- .../dashboard/transaction_list_item.dart | 9 + .../exchange/exchange_view_model.dart | 4 + .../node_create_or_edit_view_model.dart | 1 + lib/view_model/send/fees_view_model.dart | 5 + lib/view_model/send/output.dart | 8 + lib/view_model/send/send_view_model.dart | 11 +- .../settings/other_settings_view_model.dart | 1 + .../transaction_details_view_model.dart | 58 +- .../wallet_address_list_view_model.dart | 15 + lib/view_model/wallet_keys_view_model.dart | 3 + lib/view_model/wallet_new_vm.dart | 7 + lib/view_model/wallet_restore_view_model.dart | 23 +- lib/view_model/wallet_seed_view_model.dart | 2 +- lib/zcash/cw_zcash.dart | 2263 +++++++++++++++++ linux/CMakeLists.txt | 3 + macos/Runner.xcodeproj/project.pbxproj | 4 + model_generator.sh | 2 +- pubspec_base.yaml | 3 + res/values/strings_cs.arb | 4 +- res/values/strings_de.arb | 2 +- res/values/strings_en.arb | 4 +- res/values/strings_es.arb | 4 +- res/values/strings_ha.arb | 4 +- res/values/strings_hr.arb | 4 +- res/values/strings_id.arb | 4 +- res/values/strings_it.arb | 4 +- res/values/strings_nl.arb | 4 +- res/values/strings_tl.arb | 4 +- res/values/strings_tr.arb | 4 +- scripts/android/app_env.sh | 4 +- scripts/android/build_zcash.sh | 9 + scripts/android/pubspec_gen.sh | 2 +- scripts/build_bitbox_flutter.sh | 12 +- scripts/ios/app_config.sh | 2 +- scripts/ios/app_env.sh | 4 +- scripts/ios/build_zcash.sh | 9 + scripts/lint.sh | 18 + scripts/linux/build_zcash.sh | 9 + scripts/macos/app_config.sh | 2 +- scripts/macos/app_env.sh | 8 +- scripts/macos/build_zcash.sh | 9 + scripts/prepare_torch.sh | 2 +- scripts/prepare_zcash.sh | 28 + tool/configure.dart | 189 +- tool/fiat_api_check.dart | 0 141 files changed, 5664 insertions(+), 284 deletions(-) delete mode 100644 .gitmodules create mode 100644 assets/images/crypto/zcash.webp create mode 100644 assets/images/tx_shield.svg create mode 100644 assets/images/zec_icon_qr.svg create mode 100644 assets/zcash_node_list.yml create mode 100644 cw_zcash/.gitignore create mode 100644 cw_zcash/CHANGELOG.md create mode 100644 cw_zcash/README.md create mode 100644 cw_zcash/analysis_options.yaml create mode 100644 cw_zcash/lib/cw_zcash.dart create mode 100644 cw_zcash/lib/src/models.dart create mode 100644 cw_zcash/lib/src/pending_zcash_transaction.dart create mode 100644 cw_zcash/lib/src/util/crc32.dart create mode 100644 cw_zcash/lib/src/zcash_balance.dart create mode 100644 cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart create mode 100644 cw_zcash/lib/src/zcash_receive_page_options.dart create mode 100644 cw_zcash/lib/src/zcash_taddress_rotation.dart create mode 100644 cw_zcash/lib/src/zcash_transaction_credentials.dart create mode 100644 cw_zcash/lib/src/zcash_transaction_info.dart create mode 100644 cw_zcash/lib/src/zcash_transactions.dart create mode 100644 cw_zcash/lib/src/zcash_wallet.dart create mode 100644 cw_zcash/lib/src/zcash_wallet_addresses.dart create mode 100644 cw_zcash/lib/src/zcash_wallet_service.dart create mode 100644 cw_zcash/pubspec.yaml create mode 100644 lib/zcash/cw_zcash.dart create mode 100755 scripts/android/build_zcash.sh create mode 100755 scripts/ios/build_zcash.sh create mode 100755 scripts/lint.sh create mode 100755 scripts/linux/build_zcash.sh create mode 100755 scripts/macos/build_zcash.sh create mode 100755 scripts/prepare_zcash.sh mode change 100644 => 100755 tool/fiat_api_check.dart diff --git a/.github/workflows/pr_test_build_android.yml b/.github/workflows/pr_test_build_android.yml index 54b53cbb0..3743ed110 100644 --- a/.github/workflows/pr_test_build_android.yml +++ b/.github/workflows/pr_test_build_android.yml @@ -4,7 +4,6 @@ on: pull_request: jobs: - # ----------------------------------------- # PATH A: Internal PRs internal-build: diff --git a/.github/workflows/pr_test_build_linux.yml b/.github/workflows/pr_test_build_linux.yml index c3e7ecd88..67438ed82 100644 --- a/.github/workflows/pr_test_build_linux.yml +++ b/.github/workflows/pr_test_build_linux.yml @@ -25,7 +25,7 @@ jobs: steps: - name: Fix github actions messing up $HOME... - run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' + run: "echo HOME=/root | sudo tee -a $GITHUB_ENV" - uses: actions/checkout@v4 with: ref: ${{ github.event.pull_request.head.sha }} @@ -94,6 +94,13 @@ jobs: rm ./reown_flutter.tar.gz popd + - name: Build Zcash Lib + run: | + set -x -e + pushd scripts/linux + ./build_zcash.sh + popd + - name: Execute Build and Setup Commands run: | pushd scripts/linux @@ -108,7 +115,7 @@ jobs: wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.0-RC9/release-bundle.zip unzip release-bundle.zip rm release-bundle.zip - unxz -fv release/*/*.xz + unxz -fv release/*/*.xz popd - name: Build Bitbox Flutter @@ -138,7 +145,11 @@ jobs: - name: Generate localization run: | dart run tool/generate_localization.dart - + - name: Workaround zcash + run: | + mkdir -p scripts/zcash_lib/assets/ + touch scripts/zcash_lib/assets/sapling-spend.params + touch scripts/zcash_lib/assets/sapling-output.params - name: Build linux run: | flutter build linux --dart-define=hasDevOptions=true --release @@ -154,15 +165,18 @@ jobs: with: path: ${{ github.workspace }}/build/linux/x64/release/cakewallet_linux.zip name: cakewallet_linux + - name: Verify lint + run: | + exec env ABORT_ON_CHANGE=true ./scripts/lint.sh - name: Prepare virtual desktop if: ${{ contains(env.message, 'run tests') }} run: | - nohup Xvfb :99 -screen 0 720x1280x16 & - echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV - dbus-daemon --system --fork - nohup NetworkManager & - nohup ffmpeg -framerate 60 -video_size 720x1280 -f x11grab -i :99 -c:v libx264 -c:a aac /opt/screen_grab.mkv & + nohup Xvfb :99 -screen 0 720x1280x16 & + echo DISPLAY=:99 | sudo tee -a $GITHUB_ENV + dbus-daemon --system --fork + nohup NetworkManager & + nohup ffmpeg -framerate 60 -video_size 720x1280 -f x11grab -i :99 -c:v libx264 -c:a aac /opt/screen_grab.mkv & # Note for people adding tests: # - Tests are ran on Linux, with some things being mocked out. diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index 08bf24d67..7c218970c 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -40,7 +40,7 @@ jobs: steps: - name: Fix github actions messing up $HOME... - run: 'echo HOME=/root | sudo tee -a $GITHUB_ENV' + run: "echo HOME=/root | sudo tee -a $GITHUB_ENV" - uses: actions/checkout@v4 with: ref: ${{ inputs.ref }} @@ -112,7 +112,7 @@ jobs: # cleaning rm -rf reown_flutter rm -f reown_flutter.tar.gz - + wget https://github.com/cake-tech/reown_flutter/releases/download/v0.0.4/reown_flutter-v0.0.4.tar.gz -O reown_flutter.tar.gz mkdir reown_flutter pushd reown_flutter @@ -135,7 +135,7 @@ jobs: wget https://github.com/MrCyjaneK/monero_c/releases/download/v0.18.4.0-RC9/release-bundle.zip unzip release-bundle.zip rm release-bundle.zip - unxz -fv release/*/*.xz + unxz -fv release/*/*.xz popd - name: Build Bitbox Flutter @@ -144,6 +144,12 @@ jobs: pushd scripts ./build_bitbox_flutter.sh popd + - name: Build Zcash Lib + run: | + set -x -e + pushd scripts/android + ./build_zcash.sh + popd - name: Install Flutter dependencies run: | @@ -236,4 +242,4 @@ jobs: cd build/app/outputs/flutter-apk for i in arm64-v8a x86_64; do ../../../../scripts/android/check_16kb_align.sh app-$i-release.apk - done \ No newline at end of file + done diff --git a/.gitignore b/.gitignore index 5b17c0122..e9d8156de 100644 --- a/.gitignore +++ b/.gitignore @@ -144,6 +144,7 @@ lib/dogecoin/dogecoin.dart lib/base/base.dart lib/arbitrum/arbitrum.dart lib/evm/evm.dart +lib/zcash/zcash.dart ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json ios/Runner/Assets.xcassets/AppIcon.appiconset/*.png @@ -151,7 +152,7 @@ ios/Runner/Assets.xcassets/AppIcon.appiconset/*.png ios/Runner/Info.plist android/app/src/main/res/mipmap-* android/app/src/main/res/drawable/ic_launcher.png -!android/app/src/main/res/drawable/launch_background.xml +!android/app/src/main/res/drawable/launch_background.xml !android/app/src/main/res/values/styles.xml assets/images/app_logo.png @@ -215,4 +216,8 @@ flatpak-build/ **/linux/flutter/generated_plugins.cmake scripts/torch_dart/ -fiat-check-output.txt \ No newline at end of file +fiat-check-output.txt +scripts/zcash_lib +scripts/android/yttrium +scripts/bitbox_flutter +scripts/reown_flutter diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index cdd040e65..000000000 --- a/.gitmodules +++ /dev/null @@ -1,6 +0,0 @@ -[submodule "inject.dart"] - path = inject.dart - url = https://github.com/google/inject.dart -[submodule ".vendor/inject.dart"] - path = .vendor/inject.dart - url = https://github.com/google/inject.dart diff --git a/android/app/src/main/AndroidManifestBase.xml b/android/app/src/main/AndroidManifestBase.xml index 6b3b2cb2d..d2ffeef5f 100644 --- a/android/app/src/main/AndroidManifestBase.xml +++ b/android/app/src/main/AndroidManifestBase.xml @@ -39,7 +39,7 @@ android:versionName="__versionName__" android:requestLegacyExternalStorage="true" android:extractNativeLibs="true"> - @@ -112,6 +112,8 @@ + + @@ -139,7 +141,7 @@ android:resource="@xml/provider_paths" /> - + diff --git a/assets/images/crypto/zcash.webp b/assets/images/crypto/zcash.webp new file mode 100644 index 0000000000000000000000000000000000000000..08f2ec3c220b700a12cdec3e2c3e51de513ee34d GIT binary patch literal 4134 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 \ No newline at end of file diff --git a/assets/images/zec_icon.png b/assets/images/zec_icon.png index 095ad2c560c70f6a043c2db8f652574783f86254..009e174ebfabc99f4bf45953838cca4caeb4b700 100644 GIT binary patch literal 32739 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 g0}N_@2;toS0a;d3X + + + + diff --git a/assets/text/Release_Notes.txt b/assets/text/Release_Notes.txt index 677268508..7858a57d8 100644 --- a/assets/text/Release_Notes.txt +++ b/assets/text/Release_Notes.txt @@ -1 +1,3 @@ +Add Zcash wallet +Generic enhancements Bug fixes \ No newline at end of file diff --git a/assets/zcash_node_list.yml b/assets/zcash_node_list.yml new file mode 100644 index 000000000..88db5c54c --- /dev/null +++ b/assets/zcash_node_list.yml @@ -0,0 +1,12 @@ +- uri: zec.rocks:443 + is_default: false + useSSL: true + isEnabledForAutoSwitching: true +- uri: zec-node.cakewallet.com:443 + is_default: true + useSSL: true + isEnabledForAutoSwitching: true +- uri: 2c4whzg26j6hgjh22rxynj3oig4gm22ga7x74weclujywxye23v3u5id.onion:9067 + useSSL: false +- uri: bvp2l442g5ogma7rywzahm7eqyhpp3g26n3gvvxeioqn5csio2ir6myd.onion:9067 + useSSL: false diff --git a/cw_bitcoin/pubspec.lock b/cw_bitcoin/pubspec.lock index c4b45c776..90dbdf60f 100644 --- a/cw_bitcoin/pubspec.lock +++ b/cw_bitcoin/pubspec.lock @@ -604,8 +604,8 @@ packages: dependency: "direct main" description: path: "packages/ledger-bitcoin" - ref: dc42621f55702d0732681afc2f90bc0047f26633 - resolved-ref: dc42621f55702d0732681afc2f90bc0047f26633 + ref: "75bba8632c529debf30ee2b1858530009fa6440b" + resolved-ref: "75bba8632c529debf30ee2b1858530009fa6440b" url: "https://github.com/cake-tech/ledger-flutter-plus-plugins" source: git version: "0.0.3" diff --git a/cw_core/lib/crypto_currency.dart b/cw_core/lib/crypto_currency.dart index b581e803f..82451f228 100644 --- a/cw_core/lib/crypto_currency.dart +++ b/cw_core/lib/crypto_currency.dart @@ -62,7 +62,6 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen CryptoCurrency.hbar, CryptoCurrency.sc, CryptoCurrency.usdcsol, - CryptoCurrency.zaddr, CryptoCurrency.zec, CryptoCurrency.zen, CryptoCurrency.xvg, @@ -140,6 +139,10 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen xusd, ]; + // well technically not currencies but since we had them a long time ago and don't want to break + // the app for users who had contacts saved for them + static const zcashCurrencies = [ zec, zaddr, tzec ]; + // title, tag (if applicable), fullName (if unique), raw, name, iconPath static const xmr = CryptoCurrency(title: 'XMR', fullName: 'Monero', raw: 0, name: 'xmr', iconPath: 'assets/images/crypto/monero.webp', decimals: 12); static const ada = CryptoCurrency(title: 'ADA', fullName: 'Cardano', raw: 1, name: 'ada', iconPath: 'assets/images/ada_icon.png', decimals: 6); @@ -186,7 +189,7 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const usdc = CryptoCurrency(title: 'USDC', tag: 'ETH', fullName: 'USD Coin', raw: 40, name: 'usdc', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const usdcsol = CryptoCurrency(title: 'USDC', tag: 'SOL', fullName: 'USDC Coin', raw: 41, name: 'usdcsol', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static const zaddr = CryptoCurrency(title: 'ZZEC', tag: 'ZEC', fullName: 'Shielded Zcash', raw: 42, name: 'zaddr', iconPath: 'assets/images/zec_icon.png', decimals: 8); - static const zec = CryptoCurrency(title: 'TZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const tzec = CryptoCurrency(title: 'tZEC', tag: 'ZEC', fullName: 'Transparent Zcash', raw: 43, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); static const zen = CryptoCurrency(title: 'ZEN', fullName: 'Horizen', raw: 44, name: 'zen', iconPath: 'assets/images/zen_icon.png', decimals: 8); static const xvg = CryptoCurrency(title: 'XVG', fullName: 'Verge', raw: 45, name: 'xvg', iconPath: 'assets/images/xvg_icon.png', decimals: 8); @@ -250,23 +253,24 @@ class CryptoCurrency extends EnumerableItem with Serializable implemen static const cbbtc = CryptoCurrency(title: 'CBBTC', tag: 'ETH', fullName: 'Coinbase Wrapped BTC', raw: 103, name: 'cbbtc', iconPath: 'assets/images/cbbtc_icon.png', decimals: 8); static const baseEth = CryptoCurrency(title: 'ETH', tag: 'BASE', fullName: 'Ethereum', raw: 104, name: 'baseth', iconPath: 'assets/images/crypto/base_icon.webp', decimals: 18); static const usde = CryptoCurrency(title: 'USDE', tag: 'BASE', fullName: 'Ethena USDE', raw: 105, name: 'usde', iconPath: 'assets/images/crypto/ethena-usde-logo.png', decimals: 18); - static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Ethereum (Arbitrum One)', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); - static const usdcArb = CryptoCurrency(title: 'USDC', tag: 'ARB', fullName: 'USDC Coin', raw: 107, name: 'usdcarb', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); + static const arbEth = CryptoCurrency(title: 'ETH', tag: 'ARB', fullName: 'Ethereum (Arbitrum One)', raw: 106, name: 'arbeth', iconPath: 'assets/images/crypto/ethereum.webp', decimals: 18); + static const zec = CryptoCurrency(title: 'ZEC', fullName: 'Zcash', raw: 107, name: 'zec', iconPath: 'assets/images/zec_icon.png', decimals: 8); + static const usdcArb = CryptoCurrency(title: 'USDC', tag: 'ARB', fullName: 'USDC Coin', raw: 108, name: 'usdcarb', iconPath: 'assets/images/crypto/usdc.webp', decimals: 6); static final Map _rawCurrencyMap = - [...all, ...havenCurrencies].fold>({}, (acc, item) { + [...all, ...havenCurrencies, ...zcashCurrencies].fold>({}, (acc, item) { acc.addAll({item.raw: item}); return acc; }); static final Map _nameCurrencyMap = - [...all, ...havenCurrencies].fold>({}, (acc, item) { + [...all, ...havenCurrencies, ...zcashCurrencies].fold>({}, (acc, item) { acc.addAll({item.name: item}); return acc; }); static final Map _fullNameCurrencyMap = - [...all, ...havenCurrencies].fold>({}, (acc, item) { + [...all, ...havenCurrencies, ...zcashCurrencies].fold>({}, (acc, item) { if(item.fullName != null){ acc.addAll({item.fullName!.toLowerCase(): item}); } diff --git a/cw_core/lib/currency_for_wallet_type.dart b/cw_core/lib/currency_for_wallet_type.dart index e09abedf3..8928bfdb0 100644 --- a/cw_core/lib/currency_for_wallet_type.dart +++ b/cw_core/lib/currency_for_wallet_type.dart @@ -44,6 +44,8 @@ CryptoCurrency walletTypeToCryptoCurrency(WalletType type, {bool isTestnet = fal return CryptoCurrency.dcr; case WalletType.dogecoin: return CryptoCurrency.doge; + case WalletType.zcash: + return CryptoCurrency.zec; case WalletType.none: throw Exception( 'Unexpected wallet type: ${type.toString()} for CryptoCurrency walletTypeToCryptoCurrency'); diff --git a/cw_core/lib/db/sqlite.dart b/cw_core/lib/db/sqlite.dart index f4e235026..6c875e578 100644 --- a/cw_core/lib/db/sqlite.dart +++ b/cw_core/lib/db/sqlite.dart @@ -115,16 +115,26 @@ Future> dumpDb() async { } } -Future> _getTableNames() async { +Future> _getTableNames(Database db) async { final tableNames = await db.rawQuery('SELECT name FROM sqlite_master WHERE type = "table"'); return tableNames.map((e) => (e["name"]).toString()).toList(); } Future> _dumpDb() async { - final tableNames = await _getTableNames(); + final tableNames = await _getTableNames(db); final ret = {}; for (final tableName in tableNames) { ret[tableName] = await db.query(tableName); } return ret; } + +Future> dumpCustomDb(String path) async { + final db = await openDatabase(path); + final tableNames = await _getTableNames(db); + final ret = {}; + for (final tableName in tableNames) { + ret[tableName] = await db.query(tableName); + } + return ret; +} \ No newline at end of file diff --git a/cw_core/lib/node.dart b/cw_core/lib/node.dart index a8bd39aec..67ef15c63 100644 --- a/cw_core/lib/node.dart +++ b/cw_core/lib/node.dart @@ -38,6 +38,20 @@ class Node extends HiveObject with Keyable { } } + @override + String toString() { + return """Node( + uriRaw: $uriRaw, + path: $path, + login: $login, + password: $password, + useSSL: $useSSL, + trusted: $trusted, + socksProxyAddress: $socksProxyAddress, + isEnabledForAutoSwitching: $isEnabledForAutoSwitching, + })"""; + } + Node.fromMap(Map map) : uriRaw = map['uri'] as String? ?? '', path = map['path'] as String? ?? '', @@ -94,6 +108,7 @@ class Node extends HiveObject with Keyable { Uri get uri { switch (type) { case WalletType.monero: + case WalletType.zcash: case WalletType.haven: case WalletType.wownero: return Uri.http(uriRaw, ''); @@ -176,6 +191,7 @@ class Node extends HiveObject with Keyable { case WalletType.solana: case WalletType.tron: case WalletType.dogecoin: + case WalletType.zcash: return requestElectrumServer(); case WalletType.zano: return requestZanoNode(); diff --git a/cw_core/lib/pathForWallet.dart b/cw_core/lib/pathForWallet.dart index 9aa721923..cb5a4fe78 100644 --- a/cw_core/lib/pathForWallet.dart +++ b/cw_core/lib/pathForWallet.dart @@ -1,13 +1,23 @@ import 'dart:io'; import 'package:cw_core/root_dir.dart'; import 'package:cw_core/wallet_type.dart'; -import 'package:path_provider/path_provider.dart'; -Future pathForWalletDir({required String name, required WalletType type}) async { +Future pathForWalletTypeDir({required WalletType type}) async { final root = await getAppDir(); final prefix = walletTypeToString(type).toLowerCase(); final walletsDir = Directory('${root.path}/wallets'); - final walletDire = Directory('${walletsDir.path}/$prefix/$name'); + final walletDir = Directory('${walletsDir.path}/$prefix'); + + if (!walletDir.existsSync()) { + walletDir.createSync(recursive: true); + } + + return walletDir.path; +} + +Future pathForWalletDir({required String name, required WalletType type}) async { + final typeRoot = await pathForWalletTypeDir(type: type); + final walletDire = Directory('${typeRoot}/$name'); if (!walletDire.existsSync()) { walletDire.createSync(recursive: true); diff --git a/cw_core/lib/wallet_info.dart b/cw_core/lib/wallet_info.dart index 2fd18016b..a8d6c18cb 100644 --- a/cw_core/lib/wallet_info.dart +++ b/cw_core/lib/wallet_info.dart @@ -332,6 +332,7 @@ class WalletInfo { this.hashedWalletIdentifier, this.isNonSeedWallet, this.sortOrder, + this.addressPageType, ) : _yatLastUsedAddressController = StreamController.broadcast(); factory WalletInfo.external({ @@ -374,6 +375,7 @@ class WalletInfo { hashedWalletIdentifier, isNonSeedWallet ?? false, sortOrder ?? 0, + null, ); } @@ -546,6 +548,7 @@ class WalletInfo { "hashedWalletIdentifier": hashedWalletIdentifier, "isNonSeedWallet": isNonSeedWallet ? 1 : 0, "sortOrder": sortOrder, + "addressPageType": addressPageType, }; factory WalletInfo.fromJson(Map json) { @@ -569,6 +572,7 @@ class WalletInfo { json['hashedWalletIdentifier'] as String?, (json['isNonSeedWallet'] as int) == 1, json['sortOrder'] as int? ?? 0, + json['addressPageType'] as String? ?? null, ); } diff --git a/cw_core/lib/wallet_info_legacy.dart b/cw_core/lib/wallet_info_legacy.dart index 502edeb6b..08207f2b0 100644 --- a/cw_core/lib/wallet_info_legacy.dart +++ b/cw_core/lib/wallet_info_legacy.dart @@ -270,6 +270,7 @@ class WalletInfo extends HiveObject { hashedWalletIdentifier, isNonSeedWallet, 0, + addressPageType, ); final wiId = await walletInfo.save(); for (final address in usedAddresses ?? []) { diff --git a/cw_core/lib/wallet_type.dart b/cw_core/lib/wallet_type.dart index 1f8af52ac..0cb974cbb 100644 --- a/cw_core/lib/wallet_type.dart +++ b/cw_core/lib/wallet_type.dart @@ -21,6 +21,7 @@ const walletTypes = [ WalletType.dogecoin, WalletType.base, WalletType.arbitrum, + WalletType.zcash, ]; @HiveType(typeId: WALLET_TYPE_TYPE_ID) @@ -78,6 +79,9 @@ enum WalletType { @HiveField(17) arbitrum, + + @HiveField(18) + zcash, } int serializeToInt(WalletType type) { @@ -116,6 +120,8 @@ int serializeToInt(WalletType type) { return 15; case WalletType.arbitrum: return 16; + case WalletType.zcash: + return 17; case WalletType.none: return -1; } @@ -157,6 +163,8 @@ WalletType deserializeFromInt(int raw) { return WalletType.base; case 16: return WalletType.arbitrum; + case 17: + return WalletType.zcash; default: throw Exception('Unexpected token: $raw for WalletType deserializeFromInt'); } @@ -198,6 +206,8 @@ String walletTypeToString(WalletType type) { return 'Base'; case WalletType.arbitrum: return 'Arbitrum'; + case WalletType.zcash: + return 'Zcash'; case WalletType.none: return ''; } @@ -239,6 +249,8 @@ String walletTypeToDisplayName(WalletType type) { return 'Base (BASE)'; case WalletType.arbitrum: return 'Arbitrum (ARB)'; + case WalletType.zcash: + return 'Zcash (ZEC)'; case WalletType.none: return ''; } @@ -281,6 +293,8 @@ WalletType? cryptoCurrencyToWalletType(CryptoCurrency type) { return WalletType.decred; case CryptoCurrency.doge: return WalletType.dogecoin; + case CryptoCurrency.zec: + return WalletType.zcash; default: return null; } diff --git a/cw_decred/pubspec.yaml b/cw_decred/pubspec.yaml index 989831a89..781b38cc9 100644 --- a/cw_decred/pubspec.yaml +++ b/cw_decred/pubspec.yaml @@ -6,10 +6,9 @@ author: Cake Wallet homepage: https://cakewallet.com environment: - sdk: '>=3.2.0-0 <4.0.0' + sdk: ">=3.2.0-0 <4.0.0" flutter: ">=3.19.0" - dependencies: flutter: sdk: flutter diff --git a/cw_zcash/.gitignore b/cw_zcash/.gitignore new file mode 100644 index 000000000..3cceda557 --- /dev/null +++ b/cw_zcash/.gitignore @@ -0,0 +1,7 @@ +# https://dart.dev/guides/libraries/private-files +# Created by `dart pub` +.dart_tool/ + +# Avoid committing pubspec.lock for library packages; see +# https://dart.dev/guides/libraries/private-files#pubspeclock. +pubspec.lock diff --git a/cw_zcash/CHANGELOG.md b/cw_zcash/CHANGELOG.md new file mode 100644 index 000000000..effe43c82 --- /dev/null +++ b/cw_zcash/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.0.0 + +- Initial version. diff --git a/cw_zcash/README.md b/cw_zcash/README.md new file mode 100644 index 000000000..b6754b047 --- /dev/null +++ b/cw_zcash/README.md @@ -0,0 +1 @@ +# cw_zcash diff --git a/cw_zcash/analysis_options.yaml b/cw_zcash/analysis_options.yaml new file mode 100644 index 000000000..f43bd483d --- /dev/null +++ b/cw_zcash/analysis_options.yaml @@ -0,0 +1,31 @@ +analyzer: + errors: + implementation_imports: ignore + must_be_immutable: ignore + overridden_fields: ignore + exclude: + - "**.g.dart" + - "external/**" + +linter: + rules: + unawaited_futures: true + avoid_print: false + parameter_assignments: true + prefer_const_declarations: true + prefer_final_fields: true + prefer_final_in_for_each: true + prefer_final_locals: true + prefer_final_parameters: true + avoid_void_async: true + require_trailing_commas: true + sort_child_properties_last: true + sort_constructors_first: true + sort_unnamed_constructors_first: true + unnecessary_async: true + unnecessary_await_in_return: true + unnecessary_string_interpolations: true + always_declare_return_types: true + +formatter: + page_width: 100 diff --git a/cw_zcash/lib/cw_zcash.dart b/cw_zcash/lib/cw_zcash.dart new file mode 100644 index 000000000..c312ddb82 --- /dev/null +++ b/cw_zcash/lib/cw_zcash.dart @@ -0,0 +1,15 @@ +/// Support for doing something awesome. +/// +/// More dartdocs go here. +library; + +export 'src/zcash_balance.dart'; +export 'src/zcash_mnemonic_is_incorrect_exception.dart'; +export 'src/zcash_transaction_info.dart'; +export 'src/zcash_transactions.dart'; +export 'src/zcash_wallet.dart'; +export 'src/zcash_wallet_service.dart'; +export 'src/models.dart'; +export 'src/zcash_transaction_credentials.dart'; +export 'src/pending_zcash_transaction.dart'; +export 'src/zcash_receive_page_options.dart'; diff --git a/cw_zcash/lib/src/models.dart b/cw_zcash/lib/src/models.dart new file mode 100644 index 000000000..0dba32a47 --- /dev/null +++ b/cw_zcash/lib/src/models.dart @@ -0,0 +1,41 @@ +import 'package:cw_core/wallet_credentials.dart'; + +class ZcashNewWalletCredentials extends WalletCredentials { + ZcashNewWalletCredentials({ + required final String name, + final String? password, + required final String? passphrase, + final String? mnemonic, + final int? seedPhraseLength, + }) : super( + name: name, + password: password, + passphrase: passphrase, + seedPhraseLength: seedPhraseLength, + ) { + this.mnemonic = mnemonic; + } + + String? mnemonic; +} + +class ZcashFromSeedWalletCredentials extends WalletCredentials { + ZcashFromSeedWalletCredentials({ + required final String name, + final String? password, + required final String? passphrase, + required this.seed, + required super.height, + }) : super(name: name, password: password, passphrase: passphrase); + final String? seed; +} + +class ZcashFromKeysWalletCredentials extends WalletCredentials { + ZcashFromKeysWalletCredentials({ + required final String name, + final String? password, + required final String? passphrase, + required this.privateKey, + }) : super(name: name, password: password, passphrase: passphrase); + final String? privateKey; +} diff --git a/cw_zcash/lib/src/pending_zcash_transaction.dart b/cw_zcash/lib/src/pending_zcash_transaction.dart new file mode 100644 index 000000000..28be24652 --- /dev/null +++ b/cw_zcash/lib/src/pending_zcash_transaction.dart @@ -0,0 +1,103 @@ +import 'dart:convert'; +import 'dart:math'; + +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; +import 'package:warp_api/data_fb_generated.dart'; +import 'package:warp_api/warp_api.dart'; + +class PendingZcashTransaction with PendingTransaction { + PendingZcashTransaction({ + required this.zcashWallet, + required this.credentials, + required this.txPlan, + required this.fee, + required this.availableBalance, + }); + + final ZcashWallet zcashWallet; + final ZcashTransactionCredentials credentials; + final String txPlan; + String? _txId; + final int availableBalance; + + @override + String get id => _txId ?? ''; + + @override + String get hex => ''; + + @override + String get amountFormatted { + return walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(totalAmount)); + } + + int get totalAmount { + final isAll = credentials.outputs.fold(false, (final a, final b) => a || (b.sendAll)); + if (isAll) { + return availableBalance; + } + return credentials.outputs.fold( + 0, + (final a, final b) => a + (b.formattedCryptoAmount ?? 0), + ); + } + + @override + String get feeFormatted => + '$feeFormattedValue ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; + + @override + late String feeFormattedValue = walletTypeToCryptoCurrency( + WalletType.zcash, + ).formatAmount(BigInt.from(fee)); + + int fee; + + @override + Future commit() async { + _txId = await ZcashWalletService.runInDbMutex( + () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, zcashWallet.accountId, txPlan), + ); + ZcashWalletBase.temporarySentTx[zcashWallet.accountId] ??= []; + ZcashWalletBase.temporarySentTx[zcashWallet.accountId]?.add( + ShieldedTx( + base64.decode( + ZcashTaddressRotation.flatBuffersPack( + ShieldedTxT( + id: Random().nextInt(pow(2, 32).toInt()), + txId: _txId, + height: 0, + timestamp: DateTime.now().millisecondsSinceEpoch ~/ 1000, + value: -totalAmount, + ).pack, + ), + ), + ), + ); + await ZcashTransactionInfo.addCachedDestinationAddress( + _txId ?? '', + credentials.outputs.reduce((final o1, final o2) { + return OutputInfo( + address: o1.address + "," + o2.address, + sendAll: false, + isParsedAddress: false, + ); + }).address, + ); + await zcashWallet.updateTransactions(); + await zcashWallet.updateBalance(); + } + + @override + Future> commitUR() { + throw UnimplementedError('UR not supported for Zcash'); + } + + @override + bool shouldCommitUR() => false; +} diff --git a/cw_zcash/lib/src/util/crc32.dart b/cw_zcash/lib/src/util/crc32.dart new file mode 100644 index 000000000..c5fc23995 --- /dev/null +++ b/cw_zcash/lib/src/util/crc32.dart @@ -0,0 +1,296 @@ +/// CRC32 +/// Copyright (C) 2012, Kai Sellgren +/// Licensed under the MIT License. +/// http://www.opensource.org/licenses/mit-license.php +/// source: https://raw.githubusercontent.com/kaisellgren/CRC32/refs/heads/master/lib/crc32.dart +// ignore_for_file: parameter_assignments + +import 'dart:convert'; + +/// Computes Cyclic Redundancy Check values. +class CRC32 { + /// Computes a CRC32 value for the given input. + /// + /// The return value is an unsigned integer. + /// + /// You may optionally specify the beginning CRC value. + static int compute(dynamic input, [int crc = 0]) { + if (input == null) throw new ArgumentError.notNull('input'); + if (input is String) input = utf8.encode(input); + + crc = crc ^ (0xffffffff); + + for (final byte in input) { + if (!(byte >= -128 && byte <= 255)) + throw new FormatException("Invalid value in input: $byte"); + + final x = CRC32._table[(crc ^ byte) & 0xff]; + crc = (crc & 0xffffffff) >> 8; // crc >>> 8 (32-bit unsigned integer) + crc ^= x; + } + + crc = crc ^ (0xffffffff); + + // Turns the signed integer into an unsigned integer. + return crc & 0xffffffff; + } + + static const _table = const [ + 0x00000000, + 0x77073096, + 0xEE0E612C, + 0x990951BA, + 0x076DC419, + 0x706AF48F, + 0xE963A535, + 0x9E6495A3, + 0x0EDB8832, + 0x79DCB8A4, + 0xE0D5E91E, + 0x97D2D988, + 0x09B64C2B, + 0x7EB17CBD, + 0xE7B82D07, + 0x90BF1D91, + 0x1DB71064, + 0x6AB020F2, + 0xF3B97148, + 0x84BE41DE, + 0x1ADAD47D, + 0x6DDDE4EB, + 0xF4D4B551, + 0x83D385C7, + 0x136C9856, + 0x646BA8C0, + 0xFD62F97A, + 0x8A65C9EC, + 0x14015C4F, + 0x63066CD9, + 0xFA0F3D63, + 0x8D080DF5, + 0x3B6E20C8, + 0x4C69105E, + 0xD56041E4, + 0xA2677172, + 0x3C03E4D1, + 0x4B04D447, + 0xD20D85FD, + 0xA50AB56B, + 0x35B5A8FA, + 0x42B2986C, + 0xDBBBC9D6, + 0xACBCF940, + 0x32D86CE3, + 0x45DF5C75, + 0xDCD60DCF, + 0xABD13D59, + 0x26D930AC, + 0x51DE003A, + 0xC8D75180, + 0xBFD06116, + 0x21B4F4B5, + 0x56B3C423, + 0xCFBA9599, + 0xB8BDA50F, + 0x2802B89E, + 0x5F058808, + 0xC60CD9B2, + 0xB10BE924, + 0x2F6F7C87, + 0x58684C11, + 0xC1611DAB, + 0xB6662D3D, + 0x76DC4190, + 0x01DB7106, + 0x98D220BC, + 0xEFD5102A, + 0x71B18589, + 0x06B6B51F, + 0x9FBFE4A5, + 0xE8B8D433, + 0x7807C9A2, + 0x0F00F934, + 0x9609A88E, + 0xE10E9818, + 0x7F6A0DBB, + 0x086D3D2D, + 0x91646C97, + 0xE6635C01, + 0x6B6B51F4, + 0x1C6C6162, + 0x856530D8, + 0xF262004E, + 0x6C0695ED, + 0x1B01A57B, + 0x8208F4C1, + 0xF50FC457, + 0x65B0D9C6, + 0x12B7E950, + 0x8BBEB8EA, + 0xFCB9887C, + 0x62DD1DDF, + 0x15DA2D49, + 0x8CD37CF3, + 0xFBD44C65, + 0x4DB26158, + 0x3AB551CE, + 0xA3BC0074, + 0xD4BB30E2, + 0x4ADFA541, + 0x3DD895D7, + 0xA4D1C46D, + 0xD3D6F4FB, + 0x4369E96A, + 0x346ED9FC, + 0xAD678846, + 0xDA60B8D0, + 0x44042D73, + 0x33031DE5, + 0xAA0A4C5F, + 0xDD0D7CC9, + 0x5005713C, + 0x270241AA, + 0xBE0B1010, + 0xC90C2086, + 0x5768B525, + 0x206F85B3, + 0xB966D409, + 0xCE61E49F, + 0x5EDEF90E, + 0x29D9C998, + 0xB0D09822, + 0xC7D7A8B4, + 0x59B33D17, + 0x2EB40D81, + 0xB7BD5C3B, + 0xC0BA6CAD, + 0xEDB88320, + 0x9ABFB3B6, + 0x03B6E20C, + 0x74B1D29A, + 0xEAD54739, + 0x9DD277AF, + 0x04DB2615, + 0x73DC1683, + 0xE3630B12, + 0x94643B84, + 0x0D6D6A3E, + 0x7A6A5AA8, + 0xE40ECF0B, + 0x9309FF9D, + 0x0A00AE27, + 0x7D079EB1, + 0xF00F9344, + 0x8708A3D2, + 0x1E01F268, + 0x6906C2FE, + 0xF762575D, + 0x806567CB, + 0x196C3671, + 0x6E6B06E7, + 0xFED41B76, + 0x89D32BE0, + 0x10DA7A5A, + 0x67DD4ACC, + 0xF9B9DF6F, + 0x8EBEEFF9, + 0x17B7BE43, + 0x60B08ED5, + 0xD6D6A3E8, + 0xA1D1937E, + 0x38D8C2C4, + 0x4FDFF252, + 0xD1BB67F1, + 0xA6BC5767, + 0x3FB506DD, + 0x48B2364B, + 0xD80D2BDA, + 0xAF0A1B4C, + 0x36034AF6, + 0x41047A60, + 0xDF60EFC3, + 0xA867DF55, + 0x316E8EEF, + 0x4669BE79, + 0xCB61B38C, + 0xBC66831A, + 0x256FD2A0, + 0x5268E236, + 0xCC0C7795, + 0xBB0B4703, + 0x220216B9, + 0x5505262F, + 0xC5BA3BBE, + 0xB2BD0B28, + 0x2BB45A92, + 0x5CB36A04, + 0xC2D7FFA7, + 0xB5D0CF31, + 0x2CD99E8B, + 0x5BDEAE1D, + 0x9B64C2B0, + 0xEC63F226, + 0x756AA39C, + 0x026D930A, + 0x9C0906A9, + 0xEB0E363F, + 0x72076785, + 0x05005713, + 0x95BF4A82, + 0xE2B87A14, + 0x7BB12BAE, + 0x0CB61B38, + 0x92D28E9B, + 0xE5D5BE0D, + 0x7CDCEFB7, + 0x0BDBDF21, + 0x86D3D2D4, + 0xF1D4E242, + 0x68DDB3F8, + 0x1FDA836E, + 0x81BE16CD, + 0xF6B9265B, + 0x6FB077E1, + 0x18B74777, + 0x88085AE6, + 0xFF0F6A70, + 0x66063BCA, + 0x11010B5C, + 0x8F659EFF, + 0xF862AE69, + 0x616BFFD3, + 0x166CCF45, + 0xA00AE278, + 0xD70DD2EE, + 0x4E048354, + 0x3903B3C2, + 0xA7672661, + 0xD06016F7, + 0x4969474D, + 0x3E6E77DB, + 0xAED16A4A, + 0xD9D65ADC, + 0x40DF0B66, + 0x37D83BF0, + 0xA9BCAE53, + 0xDEBB9EC5, + 0x47B2CF7F, + 0x30B5FFE9, + 0xBDBDF21C, + 0xCABAC28A, + 0x53B39330, + 0x24B4A3A6, + 0xBAD03605, + 0xCDD70693, + 0x54DE5729, + 0x23D967BF, + 0xB3667A2E, + 0xC4614AB8, + 0x5D681B02, + 0x2A6F2B94, + 0xB40BBE37, + 0xC30C8EA1, + 0x5A05DF1B, + 0x2D02EF8D, + ]; +} diff --git a/cw_zcash/lib/src/zcash_balance.dart b/cw_zcash/lib/src/zcash_balance.dart new file mode 100644 index 000000000..5247ce9aa --- /dev/null +++ b/cw_zcash/lib/src/zcash_balance.dart @@ -0,0 +1,30 @@ +import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_currency.dart'; + +class ZcashBalance extends Balance { + ZcashBalance({required this.confirmed, required this.unconfirmed, required this.frozen}) + : super(confirmed, unconfirmed); + + factory ZcashBalance.zero() => ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0); + + final int confirmed; + final int unconfirmed; + final int frozen; + + @override + String get formattedAvailableBalance { + return CryptoCurrency.zec.formatAmount(BigInt.from(confirmed)); + } + + @override + String get formattedAdditionalBalance { + if (unconfirmed == 0) return '0.0'; + return CryptoCurrency.zec.formatAmount(BigInt.from(unconfirmed)); + } + + @override + String get formattedUnAvailableBalance { + if (frozen == 0) return ''; + return CryptoCurrency.zec.formatAmount(BigInt.from(frozen)); + } +} diff --git a/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart b/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart new file mode 100644 index 000000000..4986837c5 --- /dev/null +++ b/cw_zcash/lib/src/zcash_mnemonic_is_incorrect_exception.dart @@ -0,0 +1,5 @@ +class ZcashMnemonicIsIncorrectException implements Exception { + @override + String toString() => + 'Zcash mnemonic has incorrect format. Mnemonic should contain 12 or 24 words separated by space.'; +} diff --git a/cw_zcash/lib/src/zcash_receive_page_options.dart b/cw_zcash/lib/src/zcash_receive_page_options.dart new file mode 100644 index 000000000..400ade3bc --- /dev/null +++ b/cw_zcash/lib/src/zcash_receive_page_options.dart @@ -0,0 +1,70 @@ +import 'package:cw_core/receive_page_option.dart'; +import 'package:cw_core/utils/print_verbose.dart'; + +enum ZcashAddressType { + transparent, + transparentRotated, + shieldedSapling, + shieldedOrchard, + unifiedType, +} + +class ZcashReceivePageOption implements ReceivePageOption { + const ZcashReceivePageOption._(this.type); + + factory ZcashReceivePageOption.fromType(final ZcashAddressType type) { + switch (type) { + case ZcashAddressType.transparent: + return transparent; + case ZcashAddressType.transparentRotated: + return transparentRotated; + case ZcashAddressType.shieldedSapling: + return shieldedSapling; + case ZcashAddressType.shieldedOrchard: + return shieldedOrchard; + case ZcashAddressType.unifiedType: + return unified; + } + } + static const transparent = ZcashReceivePageOption._(ZcashAddressType.transparent); + static const transparentRotated = ZcashReceivePageOption._(ZcashAddressType.transparentRotated); + static const shieldedSapling = ZcashReceivePageOption._(ZcashAddressType.shieldedSapling); + static const shieldedOrchard = ZcashReceivePageOption._(ZcashAddressType.shieldedOrchard); + static const unified = ZcashReceivePageOption._(ZcashAddressType.unifiedType); + + final ZcashAddressType type; + + String get value => switch (type) { + ZcashAddressType.transparent => "Transparent (Static)", + ZcashAddressType.transparentRotated => "Transparent (Disposable)", + ZcashAddressType.shieldedSapling => "Shielded (Sapling)", + ZcashAddressType.shieldedOrchard => "Shielded (Orchard)", + ZcashAddressType.unifiedType => "Unified", + }; + + String toString() { + return value; + } + + static const all = [ + ZcashReceivePageOption.shieldedOrchard, + ZcashReceivePageOption.shieldedSapling, + ZcashReceivePageOption.unified, + ZcashReceivePageOption.transparentRotated, + ZcashReceivePageOption.transparent, + ]; + + ZcashAddressType toType() { + return type; + } + + static ZcashAddressType typeFromString(final String str) { + for (int i = 0; i < ZcashAddressType.values.length; i++) { + if (str == ZcashAddressType.values[i].toString()) { + return ZcashAddressType.values[i]; + } + } + printV("Not found for: $str"); + return ZcashAddressType.shieldedOrchard; + } +} diff --git a/cw_zcash/lib/src/zcash_taddress_rotation.dart b/cw_zcash/lib/src/zcash_taddress_rotation.dart new file mode 100644 index 000000000..faa867373 --- /dev/null +++ b/cw_zcash/lib/src/zcash_taddress_rotation.dart @@ -0,0 +1,421 @@ +// create offset wallets for T address rotation. +// Each address is considered one time use, if it receives less than 15000 zatoshi balance will be essentially burned. +// +// This class takes address index as constructor, and will create 10 addresses by default, if any of the addresses gets used +// it will be moved out of the pool +// +// T addresses are generated by creating new wallets with an extra seed word (or appending to "passphrase") the following +// :tgen:${CRC32(seed)} +// It is highly unlikely for users to create a wallet with that specific last word and add index to it +// We could leverage other fields such as native account index alone, but users may want to set that in future +// This solution is rather simple and if standard would get estabilished for this purpose it can be easily +// turned off and replaced with proper solution. + +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_zcash/src/util/crc32.dart'; +import 'package:cw_zcash/src/zcash_wallet.dart'; +import 'package:cw_zcash/src/zcash_wallet_service.dart'; +import 'package:flutter/foundation.dart'; +import 'package:warp_api/data_fb_generated.dart'; +import 'package:warp_api/warp_api.dart'; +import 'package:flat_buffers/flat_buffers.dart' as fb; +import 'package:path/path.dart' as p; + +class ZcashTaddressRotation { + ZcashTaddressRotation(this.accountIndex); + final int accountIndex; + + static bool _isStarted = false; + + static const coin = ZcashWalletBase.coin; + static String seedForOffset(final String seed) { + final seedWords = seed.split(" "); + if ([12, 24].contains(seedWords.length)) { + seedWords.add(""); + } + final lastI = seedWords.length - 1; + final crc = CRC32.compute(seed); + seedWords[lastI] += ":tgen:${crc}"; + return seedWords.join(" "); + } + + static bool isSeedForWallet(final String mainWallet, final String subWallet) { + return seedForOffset(mainWallet.trim()) == subWallet; + } + + static Map> rotationAccounts = {}; + static Map> rotationAccountsUsable = {}; + static Map> shieldedAccountsTx = {}; + static Future init() async { + printV("Deserializing previous state"); + if (_isStarted) { + return; + } + _isStarted = true; + final pfw = await pathForWalletTypeDir(type: WalletType.zcash); + try { + final f = File(p.join(pfw, "zec-taddr.json")); + if (!f.existsSync()) { + f.writeAsBytesSync(serialize()); + } + deserialize(f.readAsBytesSync()); + } catch (e) { + printV("Failed to deserialize T address DB for zcash, it doesn't really matter: $e"); + } + + unawaited( + (() async { + await Future.delayed(Duration(seconds: 5)); + return _jobRunner(); + })(), + ); + } + + static Future serializeToFile() async { + final pfw = await pathForWalletTypeDir(type: WalletType.zcash); + final f = File(p.join(pfw, "zec-taddr.json")); + f.writeAsBytesSync(serialize()); + } + + static Uint8List serialize() { + final data = { + "rotationAccounts": rotationAccounts.map( + (final k, final v) => MapEntry( + k.toRadixString(16), + v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), + ), + ), + if (kDebugMode) + "_rotationAccounts": rotationAccounts.map( + (final k, final v) => + MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), + ), + "rotationAccountsUsable": rotationAccountsUsable.map( + (final k, final v) => MapEntry( + k.toRadixString(16), + v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), + ), + ), + if (kDebugMode) + "_rotationAccountsUsable": rotationAccountsUsable.map( + (final k, final v) => + MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), + ), + "shieldedAccountsTx": shieldedAccountsTx.map( + (final k, final v) => MapEntry( + k.toRadixString(16), + v.map((final a) => flatBuffersPack(a.unpack().pack)).toList(), + ), + ), + if (kDebugMode) + "_shieldedAccountsTx": shieldedAccountsTx.map( + (final k, final v) => + MapEntry(k.toRadixString(16), v.map((final a) => a.toString()).toList()), + ), + }; + + if (kDebugMode) { + return utf8.encode(JsonEncoder.withIndent(" ").convert(data)); + } + return utf8.encode(jsonEncode(data)); + } + + static String flatBuffersPack(final int Function(fb.Builder fbBuilder) pack) { + final fbBuilder = fb.Builder(); + final offset = pack(fbBuilder); + fbBuilder.finish(offset); + return base64.encode(fbBuilder.buffer); + } + + static void deserialize(final Uint8List bytes) { + // todo: replace keys with sums and reverse in serialize + // final List seeds = []; + // for (int i = 0; i < accounts.length; i++) { + // final acc = accounts[i]; + // final backup = WarpApi.getBackup(coin, acc.id); + // await WarpApi.transparentSync(coin, acc.id, syncHeight); + // seeds.add(backup.seed!); + // } + try { + final Map data = jsonDecode(utf8.decode(bytes)); + rotationAccounts = (data["rotationAccounts"] as Map).map( + (final k, final v) => MapEntry( + int.parse(k, radix: 16), + (v as List).map((final a) => Account(atob(a))).toList(), + ), + ); + rotationAccountsUsable = (data["rotationAccountsUsable"] as Map).map( + (final k, final v) => MapEntry( + int.parse(k, radix: 16), + (v as List).map((final a) => Account(atob(a))).toList(), + ), + ); + shieldedAccountsTx = (data["shieldedAccountsTx"] as Map).map( + (final k, final v) => MapEntry( + int.parse(k, radix: 16), + (v as List).map((final a) => ShieldedTx(atob(a))).toList(), + ), + ); + } catch (e) { + rethrow; + } + } + + static Uint8List atob(final String value) => + Uint8List.fromList(List.from(base64.decode(value))); + + static Future createAndSweepTAddresses() async { + int chainHeight = 0; + try { + chainHeight = await WarpApi.getLatestHeight(coin); + } catch (e) { + printV("Error getting latest height: $e"); + } + final dbHeight = WarpApi.getDbHeight(coin); + final height = dbHeight.unpack(); + final syncHeight = height.height; + if (chainHeight != syncHeight) { + printV( + "Waiting for sync to finish: chainHeight(${chainHeight}) != syncHeight(${syncHeight})", + ); + return; + } + final Map> newShieldedAccountsTx = {}; + final accounts = WarpApi.getAccountList(coin); + if (accounts.isEmpty) return; + final Map seeds = {}; + for (int i = 0; i < accounts.length; i++) { + final acc = accounts[i]; + final backup = WarpApi.getBackup(coin, acc.id); + await WarpApi.transparentSync(coin, acc.id, syncHeight); + seeds[acc.id] = backup.seed!; + } + for (int i = 0; i < accounts.length; i++) { + final seed = seeds[accounts[i].id]!; + if ([12, 13, 24, 25].contains(seed.split(" ").length)) { + if (seed.split(" ").last.contains(":tgen:")) continue; + rotationAccounts[accounts[i].id] = []; + } + } + final raKeys = rotationAccounts.keys.toList(); + seeds.removeWhere((_, final val) => raKeys.contains(val)); + printV("raKeys: ${raKeys.length}"); + for (int i = 0; i < raKeys.length; i++) { + final accs = accounts.where((final a) { + final seed = WarpApi.getBackup(coin, a.id).seed; + return isSeedForWallet(seeds[raKeys[i]]!, seed!); + }).toList(); + rotationAccounts[raKeys[i]]!.addAll(accs); + final acc = accountForSeed(seeds[raKeys[i]]!)!; + final txs = WarpApi.getTxsSync(coin, acc.id); + newShieldedAccountsTx[acc.id] ??= []; + newShieldedAccountsTx[acc.id]!.addAll(txs); + } + rotationAccountsUsable = rotationAccounts.map((final k, final v) => MapEntry(k, v.toList())); + printV("rotationAccounts: ${rotationAccounts.length}"); + + for (int i = 0; i < raKeys.length; i++) { + final acc = accountForSeed(seeds[raKeys[i]]!)!; + newShieldedAccountsTx[acc.id] = []; + for (int j = 0; j < (rotationAccounts[raKeys[i]]?.length ?? 0); j++) { + final txs = WarpApi.getTxsSync(coin, rotationAccounts[raKeys[i]]![j].id); + rotationAccounts[raKeys[i]]![j]; + newShieldedAccountsTx[acc.id]!.addAll(txs); + } + rotationAccountsUsable[raKeys[i]]!.removeWhere((final a) { + final txs = WarpApi.getTxsSync(coin, a.id); + return txs.isNotEmpty; + }); + } + printV("rotationAccountsUsable: ${rotationAccountsUsable.length}"); + + bool didAddNewAccount = false; + for (int i = 0; i < raKeys.length; i++) { + if (rotationAccountsUsable[raKeys[i]]!.length < 5) { + final seed = seedForOffset(seeds[raKeys[i]]!); + final name = CRC32.compute(raKeys[i].toString()).toString(); + final id = await ZcashWalletService.runInDbMutex( + () => WarpApi.newAccount(coin, name, seed, rotationAccounts[raKeys[i]]!.length), + ); + printV("new id: $id / $seed"); + printV("${rotationAccounts[raKeys[i]]}"); + printV(raKeys[i]); + rotationAccounts[raKeys[i]]!.forEach((final a) { + final b = WarpApi.getBackup(coin, a.id); + printV("${a.id} / ${b.seed}"); + }); + didAddNewAccount = true; + } + } + printV("didAddNewAccount: ${didAddNewAccount}"); + if (didAddNewAccount) { + return createAndSweepTAddresses(); + } + + for (var i = 0; i < raKeys.length; i++) { + final accs = rotationAccounts[raKeys[i]]!; + inner: + for (var j = 0; j < accs.length; j++) { + int bal = 0; + try { + bal += await WarpApi.getTBalance(coin, accs[j].id); + } catch (e) { + printV("getTBalance: $e"); + return; + } + if (bal < 30000) continue inner; + final to = accountForSeed(seeds[raKeys[i]]!)!; + // final toAddress = WarpApi.getTAddr(coin, to.id); + // final fromSeed = WarpApi.getBackup(coin, to.id); + // final txId = await ZcashWalletService.runInDbMutex( + // () => WarpApi.sweepTransparentSeed( + // to.coin, + // to.id, + // chainHeight, + // fromSeed.seed!, + // 7, + // toAddress, + // 0, + // 1, + // FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0), + // ), + // ); + final recipientBuilder = RecipientObjectBuilder( + address: WarpApi.getAddress(coin, to.id, 4), + pools: 4, + feeIncluded: true, + amount: bal, + ); + + final recipient = Recipient(recipientBuilder.toBytes()); + final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0); + final txPlan = await ZcashWalletService.runInDbMutex( + () => WarpApi.prepareTx( + coin, + accs[j].id, + [recipient], + 3, // pools: (Transparent + Sapling) + 1, + 0, // anchorOffset + fee, + ), + ); + final _txId = await ZcashWalletService.runInDbMutex( + () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accs[j].id, txPlan), + ); + await ZcashWalletService.addShieldedTx(_txId); + printV("shielded: $_txId"); + + await Future.delayed(Duration(seconds: 120)); // let it be + return createAndSweepTAddresses(); + } + } + final nsatKeys = newShieldedAccountsTx.keys.toList(); + for (int i = 0; i < nsatKeys.length; i++) { + inner: + for (int j = 0; j < newShieldedAccountsTx[nsatKeys[i]]!.length; j++) { + final tx = newShieldedAccountsTx[nsatKeys[i]]?[j]; + if (tx == null) { + continue inner; + } + final _txId = tx.txId; + if (_txId == null) { + continue inner; + } + if (tx.value < 0) { + await ZcashWalletService.addShieldedTx(_txId); + // continue inner; + } + if (ZcashWalletService.autoshieldTx.contains(_txId)) { + continue inner; + } + } + } + shieldedAccountsTx = newShieldedAccountsTx.map((final k, final v) => MapEntry(k, v.toList())); + await serializeToFile(); + return; + } + + static ShieldedTx reverseAmount(final ShieldedTx t) { + final tt = t.unpack(); + tt.value = -tt.value; + return ShieldedTx(base64.decode(flatBuffersPack(tt.pack))); + } + + static Account? accountForSeed(final String seed) { + final accounts = WarpApi.getAccountList(coin); + if (accounts.isEmpty) return null; + for (int i = 0; i < accounts.length; i++) { + final acc = accounts[i]; + final backup = WarpApi.getBackup(coin, acc.id); + if (backup.seed == seed) { + return acc; + } + } + return null; + } + + static Future _jobRunner() async { + for (;;) { + try { + await Future.delayed(Duration(seconds: 5)); + await createAndSweepTAddresses(); + } catch (e, s) { + printV(e); + s.toString().split("\n").forEach(printV); + rethrow; + } finally { + await Future.delayed(Duration(seconds: 30)); + } + } + } + + static String? addressForAccount(final int accountId) { + final acc = rotationAccountsUsable[accountId]?.firstOrNull; + if (acc == null) { + return null; + } + return WarpApi.getTAddr(coin, acc.id); + } + + static List? allAddressesForAccount(final int accountId) { + final acc = rotationAccounts[accountId]; + if (acc == null) { + printV("Nothing found"); + return null; + } + for (int i = 0; i < acc.length; i++) { + final b = WarpApi.getBackup(coin, acc[i].id); + printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}"); + } + return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList(); + } + + static List? allUsedAddressesForAccount(final int accountId) { + final seed = WarpApi.getBackup(coin, accountId).seed!; + final acc = rotationAccounts[seed]?.toList(); + if (acc == null) { + printV("Nothing found"); + return null; + } + acc.removeWhere((final a1) { + for (int i = 0; i < (rotationAccountsUsable[seed]?.length ?? 0); i++) { + if (rotationAccountsUsable[seed]?[i].id == a1.id) { + return true; + } + } + return false; + }); + for (int i = 0; i < acc.length; i++) { + final b = WarpApi.getBackup(coin, acc[i].id); + printV("$i. ${b.seed?.split(" ").last}, ${b.index}, ${WarpApi.getTAddr(coin, acc[i].id)}"); + } + return acc.map((final a) => WarpApi.getTAddr(coin, a.id)).toList(); + } +} diff --git a/cw_zcash/lib/src/zcash_transaction_credentials.dart b/cw_zcash/lib/src/zcash_transaction_credentials.dart new file mode 100644 index 000000000..7ec3e5a81 --- /dev/null +++ b/cw_zcash/lib/src/zcash_transaction_credentials.dart @@ -0,0 +1,15 @@ +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/output_info.dart'; + +class ZcashTransactionCredentials { + ZcashTransactionCredentials({ + required this.outputs, + required this.priority, + required this.currency, + }); + + final List outputs; + final MoneroTransactionPriority priority; + final CryptoCurrency currency; +} diff --git a/cw_zcash/lib/src/zcash_transaction_info.dart b/cw_zcash/lib/src/zcash_transaction_info.dart new file mode 100644 index 000000000..62c9ad8dd --- /dev/null +++ b/cw_zcash/lib/src/zcash_transaction_info.dart @@ -0,0 +1,106 @@ +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/currency_for_wallet_type.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/format_amount.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_type.dart'; +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:path/path.dart' as p; + +class ZcashTransactionInfo extends TransactionInfo { + ZcashTransactionInfo({ + required final String id, + required final int amount, + required final int fee, + required final TransactionDirection direction, + required final bool isPending, + required final DateTime date, + required final int height, + required final int confirmations, + required final String to, + final String? memo, + }) { + this.id = id; + this.amount = amount; + this.fee = fee; + this.height = height; + this.direction = direction; + this.date = date; + this.isPending = isPending; + this.confirmations = confirmations; + this.to = getCachedDestinationAddress(id); + if (memo != null && memo.isNotEmpty) { + additionalInfo['memo'] = memo; + } + additionalInfo['autoShield'] = false; + // note: this won't work yet, fee is not in zcash_lib metadata, + // leaving here so it can start working automagically in future + if (amount == fee && to.startsWith("u")) { + additionalInfo['autoShield'] = true; + } + // remove below when above starts working + additionalInfo['autoShield'] = ZcashWalletService.autoshieldTx.contains(txHash); + // --- == === cut here === == --- + if (additionalInfo['autoShield'] == true) { + additionalInfo['memo'] ??= ''; + additionalInfo['memo'] += '\This is an auto-shielding transaction. Enjoy default privacy!'; + additionalInfo['memo'] = additionalInfo['memo'].trim(); + } + } + + String? _fiatAmount; + + @override + String amountFormatted() => + '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(amount))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; + + @override + String? feeFormatted() { + if (fee == null || fee == 0) return null; + return '${walletTypeToCryptoCurrency(WalletType.zcash).formatAmount(BigInt.from(fee!))} ${walletTypeToCryptoCurrency(WalletType.zcash).title}'; + } + + @override + String fiatAmount() => _fiatAmount ?? ''; + + @override + void changeFiatAmount(final String amount) => _fiatAmount = formatAmount(amount); + + String? get memo => additionalInfo['memo'] as String?; + + static final Map _destinationAddressMap = {}; + + static String? getCachedDestinationAddress(final String txId) { + printV("$txId -> ${_destinationAddressMap.keys.join(",")}"); + return _destinationAddressMap[txId] ?? + _destinationAddressMap['"$txId"'] ?? + _destinationAddressMap[txId.replaceAll('"', '')]; + } + + static Future addCachedDestinationAddress(final String txId, final String address) async { + _destinationAddressMap[txId] = address; + final pfwt = await pathForWalletTypeDir(type: WalletType.zcash); + final f = File(p.join(pfwt, "sent-tx-map.json")); + f.writeAsStringSync(json.encode(_destinationAddressMap)); + } + + static Future init() async { + try { + final pfwt = await pathForWalletTypeDir(type: WalletType.zcash); + final f = File(p.join(pfwt, "sent-tx-map.json")); + if (!f.existsSync()) { + f.writeAsStringSync('{}'); + } + final tmpMap = json.decode(f.readAsStringSync()); + tmpMap.forEach((final k, final v) { + _destinationAddressMap[k.toString()] = v.toString(); + }); + } catch (e, s) { + printV("failed to deserialize: $e, $s"); + } + } +} diff --git a/cw_zcash/lib/src/zcash_transactions.dart b/cw_zcash/lib/src/zcash_transactions.dart new file mode 100644 index 000000000..afb074314 --- /dev/null +++ b/cw_zcash/lib/src/zcash_transactions.dart @@ -0,0 +1,24 @@ +import 'package:mobx/mobx.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_history.dart'; + +class ZcashTransactionHistory extends TransactionHistoryBase { + ZcashTransactionHistory() { + transactions = ObservableMap(); + } + + @override + void addMany(final Map txs) { + transactions.addAll(txs); + } + + @override + void addOne(final TransactionInfo txs) { + transactions[txs.txHash] = txs; + } + + @override + Future save() async { + // No need to save anything, backend is taking care of that + } +} diff --git a/cw_zcash/lib/src/zcash_wallet.dart b/cw_zcash/lib/src/zcash_wallet.dart new file mode 100644 index 000000000..b94eb832f --- /dev/null +++ b/cw_zcash/lib/src/zcash_wallet.dart @@ -0,0 +1,976 @@ +import 'dart:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:cw_core/sync_status.dart'; +import 'package:cw_core/transaction_direction.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_addresses.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_type.dart'; +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:cw_zcash/src/util/crc32.dart'; +import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; +import 'package:cw_zcash/src/zcash_wallet_addresses.dart'; +import 'package:mobx/mobx.dart'; +import 'package:warp_api/warp_api.dart'; +import 'package:warp_api/data_fb_generated.dart'; +import 'package:flutter/services.dart'; +import 'package:mutex/mutex.dart'; +import 'package:path/path.dart' as p; + +part 'zcash_wallet.g.dart'; + +class ZcashWallet = ZcashWalletBase with _$ZcashWallet; + +abstract class ZcashWalletBase + extends WalletBase + with Store { + ZcashWalletBase(super.walletInfo, super.derivationInfo, {required this.accountId}) { + transactionHistory = ZcashTransactionHistory(); + } + + final int accountId; + static const int coin = 0; // Zcash mainnet coin ID in warp_api + @override + @observable + SyncStatus syncStatus = NotConnectedSyncStatus(); + + Timer? _syncStatusTimer; + Timer? _periodicSyncTimer; + int _initialSyncHeight = 0; + int _lastKnownBlockHeight = 0; + + @override + ObservableMap balance = ObservableMap.of({ + CryptoCurrency.zec: ZcashBalance(confirmed: 0, unconfirmed: 0, frozen: 0), + }); + + static int internalCalculateEstimatedFee(final TransactionPriority priority, final int? amount) { + const baseFee = 10000; + switch (priority) { + case MoneroTransactionPriority.slow: + case MoneroTransactionPriority.automatic: + return baseFee; + case MoneroTransactionPriority.medium: + return baseFee * 2; + case MoneroTransactionPriority.fast: + return baseFee * 4; + case MoneroTransactionPriority.fastest: + return baseFee * 10; + } + ; + return internalCalculateEstimatedFee(MoneroTransactionPriority.automatic, amount); + } + + @override + int calculateEstimatedFee(final TransactionPriority priority, final int? amount) { + return internalCalculateEstimatedFee(priority, amount); + } + + @override + Future changePassword(final String password) async { + // throw UnimplementedError(); + } + + static bool isNodeWorking = true; + + @override + Future checkNodeHealth() { + return Future.value(isNodeWorking); + } + + @override + Future close({final bool shouldCleanup = false}) async { + _stopSyncStatusUpdates(); + _stopPeriodicSync(); + await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync()); + } + + @override + @action + Future connectToNode({required final Node node}) async { + printV("connecting to node: ${node.uriRaw}"); + syncStatus = ConnectingSyncStatus(); + try { + String lwdUrl = node.uriRaw; + if (!lwdUrl.startsWith('http://') && !lwdUrl.startsWith('https://')) { + final protocol = node.useSSL == true ? 'https://' : 'http://'; + lwdUrl = '$protocol$lwdUrl'; + } + printV("Setting LWD URL to: $lwdUrl"); + WarpApi.updateLWD(coin, lwdUrl); + syncStatus = ConnectedSyncStatus(); + try { + await updateBalance(); + await updateTransactions(); + } catch (e) { + printV("Error updating balance/transactions after connect: $e"); + } + } catch (e) { + printV("Connection error: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + rethrow; + } + } + + @override + Future createTransaction(final Object credentials) async { + final creds = credentials as ZcashTransactionCredentials; + await updateBalance(); + + final zcashBalance = balance[CryptoCurrency.zec]; + final availableBalance = zcashBalance?.confirmed ?? 0; + + final recipients = []; + int totalAmount = 0; + + for (final output in creds.outputs) { + int amount; + if (output.sendAll) { + amount = availableBalance; + } else { + amount = output.formattedCryptoAmount ?? 0; + + if (amount == 0 && output.cryptoAmount != null && output.cryptoAmount!.isNotEmpty) { + try { + final parsedAmount = CryptoCurrency.zec.parseAmount( + output.cryptoAmount!.replaceAll(',', '.'), + ); + amount = parsedAmount.toInt(); + printV("Parsed amount from cryptoAmount '${output.cryptoAmount}': $amount"); + } catch (e) { + printV("Failed to parse cryptoAmount '${output.cryptoAmount}': $e"); + } + } + + if (amount <= 0) { + throw Exception( + 'Invalid amount for output. Amount: ${output.cryptoAmount}, Formatted: ${output.formattedCryptoAmount}', + ); + } + } + + totalAmount += amount; + + var address = (output.isParsedAddress ? output.extractedAddress! : output.address).trim(); + + if (address.isEmpty) { + throw Exception('Empty address for output'); + } + + final paymentUri = WarpApi.decodePaymentURI(coin, address); + String memo = output.note ?? ''; + if (paymentUri != null && paymentUri.address != null) { + address = paymentUri.address!; + if (memo.isEmpty && paymentUri.memo != null) { + memo = paymentUri.memo!; + } + } + + if (!WarpApi.validAddress(coin, address)) { + throw Exception('Invalid Zcash address: $address'); + } + + int recipientPools = 7; + + if (address.startsWith('t1') || address.startsWith('t3')) { + recipientPools = 1; // Transparent only + } else if (address.startsWith('zs')) { + recipientPools = 2; // Sapling only + } + // For unified addresses (u1...) and other types, use 7 (all pools) + + final builder = RecipientObjectBuilder( + address: address, + pools: recipientPools, + amount: amount, + feeIncluded: output.sendAll, + replyTo: false, + memo: memo.isNotEmpty ? memo : null, + ); + + recipients.add(Recipient(builder.toBytes())); + } + + if (totalAmount > availableBalance) { + throw Exception('Insufficient balance'); + } + + final fee = FeeT( + fee: internalCalculateEstimatedFee(creds.priority, null), + minFee: 0, + maxFee: 0, + scheme: 0, // Fixed fee scheme + ); + + // pools parameter: bitmask for which pools to use for sending + // 1=Transparent, 2=Sapling, 4=Orchard, 7=All pools + // Using 7 (all pools) allows spending from any pool type + final txPlan = await ZcashWalletService.runInDbMutex( + () => WarpApi.prepareTx( + coin, + accountId, + recipients, + 7, // pools: All pools (Transparent + Sapling + Orchard) - allows spending from any pool + 1, // senderUAType: 0 = unified address + 0, // anchorOffset + fee, + ), + ); + + return PendingZcashTransaction( + zcashWallet: this as ZcashWallet, + credentials: creds, + txPlan: txPlan, + fee: internalCalculateEstimatedFee(creds.priority, null), + availableBalance: availableBalance, + ); + } + + static const _dispPhrase = "Received to disposable address"; + Future> getShieldTxForUi() async { + final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? []) + .map((final v) { + final unpacked = v.unpack(); + unpacked.memo ??= ""; + unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim(); + final List buff = base64.decode( + ZcashTaddressRotation.flatBuffersPack(unpacked.pack), + ); + return ShieldedTx(buff); + }) + .where((final t) => t.value > 0); + + return tx.toList(); + } + + static Map> temporarySentTx = {}; + + static String txChecksumKey(final ShieldedTx tx) { + final direction = tx.value > 0 ? TransactionDirection.incoming : TransactionDirection.outgoing; + return 'tx${direction}_${tx.id}_${tx.timestamp}_${CRC32.compute(tx.toString())}'; + } + + @override + Future> fetchTransactions() async { + await ZcashWalletService.loadShieldTxs(); + final txs = (await ZcashWalletService.runInDbMutex( + () => WarpApi.getTxs(coin, accountId), + )).toList(); + // ShieldedTx{id: 26, txId: 4d1be06ce2c2debec8d98ce4e9434c8aac27c980488b459017d423fdcab37f93, height: 3195705, shortTxId: 4d1be06c, timestamp: 1767730944, name: null, value: 1000000, address: null, memo: , messages: MemoVec{memos: null}} + + final shieldTx = await getShieldTxForUi(); + + txs.addAll(shieldTx); + final txIds = txs.map((final tx) => tx.txId!.replaceAll('"', '')).toSet(); + temporarySentTx[accountId]?.removeWhere( + (final ttx) => txIds.contains(ttx.txId!.replaceAll('"', '')), + ); + txs.addAll(temporarySentTx[accountId] ?? []); + + txs.sort((final a, final b) => a.height.compareTo(b.height)); + final Map result = {}; + int currentHeight = 0; + try { + currentHeight = await WarpApi.getLatestHeight(coin); + } catch (e) { + printV("Error getting latest height: $e"); + } + + for (final tx in txs) { + final direction = tx.value > 0 + ? TransactionDirection.incoming + : TransactionDirection.outgoing; + + final confirmations = tx.height > 0 && currentHeight > 0 ? currentHeight - tx.height + 1 : 0; + + final txChecksum = txChecksumKey(tx); + final txId = tx.txId ?? tx.shortTxId ?? txChecksum; + + final txInfo = ZcashTransactionInfo( + id: txId.trim().replaceAll('"', ''), + amount: tx.value.abs(), + fee: 0, + direction: direction, + isPending: tx.height == 0, + date: DateTime.fromMillisecondsSinceEpoch(tx.timestamp * 1000), + height: tx.height, + confirmations: confirmations, + to: tx.address ?? '', + memo: tx.memo, + ); + // if (txInfo.additionalInfo['autoShield'] == true) { + // continue; + // } + result[txChecksum] = txInfo; + } + + return result; + } + + @override + Object get keys => {}; + + @override + String get password => _password!; + + @override + Future renameWalletFiles(final String newWalletName) async { + final currentWalletPath = await pathForWallet(name: name, type: type); + final currentCacheFile = File(currentWalletPath); + final newWalletPath = await pathForWallet(name: newWalletName, type: type); + if (currentCacheFile.existsSync()) { + await currentCacheFile.copy(newWalletPath); + } + Directory(currentWalletPath).deleteSync(recursive: true); + } + + @override + bool get hasRescan => true; + + @override + @action + Future rescan({required final int height}) async { + try { + syncStatus = StartingScanSyncStatus(height); + printV("rescanning from: $height"); + await ZcashWalletService.runInDbMutex(() async => WarpApi.rescanFrom(coin, height)); + await startSync(); + } catch (e) { + printV("Rescan error: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + rethrow; + } + } + + bool _isTransactionUpdating = false; + + Future updateTransactions() async { + try { + if (_isTransactionUpdating) { + return; + } + + _isTransactionUpdating = true; + final transactions = await fetchTransactions(); + + final currentIds = transactionHistory.transactions.keys.toSet(); + final newIds = transactions.keys.toSet(); + + currentIds + .difference(newIds) + .forEach((final id) => transactionHistory.transactions.remove(id)); + + transactions.forEach((final key, final tx) { + transactionHistory.transactions[key] = tx; + }); + await transactionHistory.save(); + _isTransactionUpdating = false; + } catch (e, stackTrace) { + printV("Update transactions error: $e"); + printV("Stack trace: $stackTrace"); + _isTransactionUpdating = false; + } + } + + Future updateTransactionsHistory() => updateTransactions(); + + @override + Future save() async {} + + Future init() async { + try { + await walletAddresses.init(); + + await updateBalance(); + await updateTransactions(); + } catch (e) { + printV("Wallet init error: $e"); + } + } + + @override + String? get seed { + try { + final backup = WarpApi.getBackup(coin, accountId); + final seed = backup.seed!.split(" "); + if ([13, 25].contains(seed.length)) { + seed.removeLast(); + } + return seed.join(" ").trim(); + } catch (e) { + return null; + } + } + + @override + String? get passphrase { + try { + final backup = WarpApi.getBackup(coin, accountId); + final seed = backup.seed!.split(" "); + if ([13, 25].contains(seed.length)) { + final passphrase = seed.removeLast(); + return passphrase; + } + return null; + } catch (e) { + return null; + } + } + + @override + Future signMessage(final String message, {final String? address = null}) { + throw UnimplementedError(); + } + + @override + @action + Future startSync() async { + try { + syncStatus = AttemptingSyncStatus(); + + _initialSyncHeight = 0; + _lastKnownBlockHeight = 0; + + _startSyncStatusUpdates(); + + syncStatus = SyncronizingSyncStatus(); + + unawaited( + _runWarpSync().catchError((final e) { + isNodeWorking = false; + printV("WarpSync error in startSync: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + _stopSyncStatusUpdates(); + }), + ); + } catch (e) { + isNodeWorking = false; + printV("Sync error: $e"); + syncStatus = FailedSyncStatus(error: e.toString()); + _stopSyncStatusUpdates(); + rethrow; + } + } + + static Mutex warpSyncMutex = Mutex(); + + static Future initialSyncCheck() async { + final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash); + final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker")); + if (!zcashInitialSync.existsSync()) { + int chainHeight = 3000000; // fallback if node is offline + try { + chainHeight = await WarpApi.getLatestHeight(coin); + } catch (e) { + printV("Error getting latest height: $e"); + } + await ZcashWalletService.runInDbMutex( + () async => await WarpApi.rescanFrom(coin, chainHeight - 150000), + ); + zcashInitialSync.writeAsBytesSync([0x00]); + zcashInitialSync.writeAsStringSync(chainHeight.toString(), mode: FileMode.writeOnlyAppend); + } + } + + @action + Future _runWarpSync() async { + Timer? _t; + try { + await warpSyncMutex.acquire(); + await initialSyncCheck(); + isNodeWorking = true; + printV("Starting warpSync for coin $coin, account $accountId"); + int? initialQueue = null; + void _cancelSyncIfShould(final Timer t) { + initialQueue ??= ZcashWalletService.dbMutexQueue + 2; + if (ZcashWalletService.dbMutexQueue <= initialQueue!) { + initialQueue = ZcashWalletService.dbMutexQueue; + return; + } + printV( + "Canceling sync! (ZcashWalletService.dbMutexQueue: ${ZcashWalletService.dbMutexQueue} > initialQueue: ${initialQueue})", + ); + WarpApi.cancelSync(); + t.cancel(); + _t = null; + } + + unawaited( + Future.delayed(Duration(seconds: 2)).then((_) { + _t = Timer.periodic(Duration(milliseconds: 100), _cancelSyncIfShould); + }), + ); + final result = await ZcashWalletService.runInDbMutex( + () => WarpApi.warpSync(coin, accountId, true, 0, 1000000, 0), + ); + printV("warpSync completed with result: $result"); + + await _updateSyncStatus(); + } catch (e) { + syncStatus = FailedSyncStatus(error: e.toString()); + unawaited(Future.delayed(Duration(seconds: 1)).then((_) => unawaited(_runWarpSync()))); + _stopSyncStatusUpdates(); + } finally { + isNodeWorking = false; + warpSyncMutex.release(); + _t?.cancel(); + } + } + + void _startSyncStatusUpdates() { + _stopSyncStatusUpdates(); + _updateSyncStatus(); + _syncStatusTimer = Timer.periodic(const Duration(milliseconds: 5000), (_) { + _updateSyncStatus().catchError((final e) { + printV("Error in sync status update timer: $e"); + }); + }); + } + + void _stopSyncStatusUpdates() { + _syncStatusTimer?.cancel(); + _syncStatusTimer = null; + } + + void _startPeriodicSync() { + printV("_startPeriodicSync"); + _stopPeriodicSync(); + _periodicSyncTimer = Timer.periodic(const Duration(seconds: 5), (_) async { + try { + final chainHeight = await WarpApi.getLatestHeight(coin); + final dbHeight = WarpApi.getDbHeight(coin); + final height = dbHeight.unpack(); + final syncHeight = height.height; + + if (syncHeight < chainHeight) { + printV("Periodic sync: chainHeight=$chainHeight, syncHeight=$syncHeight, starting sync"); + await _runWarpSync(); + } else { + await updateBalance(); + await updateTransactions(); + } + } catch (e) { + printV("Periodic sync error: $e"); + } + }); + } + + void _stopPeriodicSync() { + _periodicSyncTimer?.cancel(); + _periodicSyncTimer = null; + } + + @action + Future _updateSyncStatus() async { + try { + final dbHeight = WarpApi.getDbHeight(coin); + final height = dbHeight.unpack(); + final syncHeight = height.height; + + final chainHeight = await WarpApi.getLatestHeight(coin); + + if (_initialSyncHeight <= 0 && syncHeight > 0) { + _initialSyncHeight = syncHeight; + printV("Initialized sync height to: $_initialSyncHeight"); + if (syncHeight - 10 > dbHeight.height) {} + } + + if (chainHeight <= 0) { + if (syncStatus is! ConnectedSyncStatus && syncStatus is! ConnectingSyncStatus) { + syncStatus = ConnectedSyncStatus(); + } + try { + await updateBalance(); + await updateTransactions(); + } catch (e) { + printV("Error updating balance/transactions: $e"); + } + return; + } + + if (syncHeight <= 0) { + if (syncStatus is! ConnectedSyncStatus && + syncStatus is! ConnectingSyncStatus && + syncStatus is! AttemptingSyncStatus) { + syncStatus = ConnectedSyncStatus(); + } + try { + await updateBalance(); + await updateTransactions(); + } catch (e) { + printV("Error updating balance/transactions: $e"); + } + return; + } + + if (syncHeight >= chainHeight && syncHeight > 0) { + syncStatus = SyncedSyncStatus(); + _stopSyncStatusUpdates(); + await updateBalance(); + await updateTransactions(); + _startPeriodicSync(); + return; + } + + if (_lastKnownBlockHeight != syncHeight) { + _lastKnownBlockHeight = syncHeight; + } + + if (syncHeight < 0 || chainHeight < syncHeight) { + return; + } + + final blocksLeft = chainHeight - syncHeight; + if (blocksLeft <= 0) { + syncStatus = SyncedSyncStatus(); + _stopSyncStatusUpdates(); + await updateBalance(); + await updateTransactions(); + _startPeriodicSync(); + return; + } + + double ptc = 0.0; + if (_initialSyncHeight > 0) { + final track = chainHeight - _initialSyncHeight; + final diff = track > 0 ? track - (chainHeight - syncHeight) : 0; + ptc = track > 0 && diff >= 0 ? diff / track : 0.0; + } else { + ptc = syncHeight / chainHeight; + } + + syncStatus = SyncingSyncStatus(blocksLeft, ptc.clamp(0.0, 1.0)); + + await updateBalance(); + await updateTransactions(); + } catch (e) { + printV("Sync status update error: $e"); + } + } + + String getDiversifiedAddress(final int uaType, {final DateTime? time}) { + try { + final timestamp = (time ?? DateTime.now()).millisecondsSinceEpoch ~/ 1000; + return WarpApi.getDiversifiedAddress(coin, accountId, uaType, timestamp); + } catch (e) { + printV("Error getting diversified address: $e"); + return ""; + } + } + + static final autoShieldMutex = Mutex(); + Future _autoShield() async { + try { + await autoShieldMutex.acquire(); + await _$autoShield(); + } catch (e) { + printV("shielding failed: $e"); + await Future.delayed(Duration(seconds: 30)); + } finally { + autoShieldMutex.release(); + } + } + + Future _$autoShield() async { + final chainHeight = await WarpApi.getLatestHeight(coin); + final dbHeight = WarpApi.getDbHeight(coin); + final height = dbHeight.unpack(); + final syncHeight = height.height; + if (chainHeight != syncHeight) { + printV("Not autoshielding: chainHeight(${chainHeight}) != syncHeight(${syncHeight})"); + return; + } + final bpConfirmed = WarpApi.getPoolBalances(coin, accountId, 0, false); + if (bpConfirmed.transparent + bpConfirmed.sapling <= 20000) { + return; + } + + final recipientBuilder = RecipientObjectBuilder( + address: (walletAddresses as ZcashWalletAddresses).orchardAddress, + pools: 4, + feeIncluded: true, + amount: bpConfirmed.transparent + bpConfirmed.sapling, + ); + + final recipient = Recipient(recipientBuilder.toBytes()); + final fee = FeeT(fee: 10000, minFee: 0, maxFee: 0, scheme: 0); + final txPlan = await ZcashWalletService.runInDbMutex( + () => WarpApi.prepareTx( + coin, + accountId, + [recipient], + 3, // pools: (Transparent + Sapling) + 1, + 0, // anchorOffset + fee, + ), + ); + final _txId = await ZcashWalletService.runInDbMutex( + () => WarpApi.signAndBroadcast(ZcashWalletBase.coin, accountId, txPlan), + ); + await ZcashWalletService.addShieldedTx(_txId); + printV("shielded: $_txId"); + await updateTransactions(); + await updateBalance(); + await Future.delayed(Duration(seconds: 75 * 5)); // do not re-try doing that + } + + @override + @action + Future updateBalance() async { + try { + final poolBalances = WarpApi.getPoolBalances(coin, accountId, 0, true); + final balances = poolBalances.unpack(); + // final notes = WarpApi.getNotesSync(coin, accountId); + // int frozenBalance = 0; + // for (final note in notes) { + // if (note.excluded) { + // frozenBalance += note.value; + // } + // } + final total = balances.orchard + balances.sapling + balances.transparent; + final spendable = total - balances.transparent; + + final confirmedPoolBalances = WarpApi.getPoolBalances(coin, accountId, 3, true); + final confirmedBalances = confirmedPoolBalances.unpack(); + final confirmedTotal = + confirmedBalances.orchard + confirmedBalances.sapling + confirmedBalances.transparent; + + int knownOutPending = 0; + ZcashWalletBase.temporarySentTx[accountId]?.forEach((final sTx) { + knownOutPending += sTx.value; // it's negative + }); + final confirmedSpendable = confirmedTotal - balances.transparent + knownOutPending; + + unawaited(_autoShield()); + + balance[CryptoCurrency.zec] = ZcashBalance( + confirmed: confirmedSpendable, + unconfirmed: spendable - confirmedSpendable, + frozen: 0, + ); + } catch (e, stackTrace) { + printV("Balance update error: $e"); + printV("Stack trace: $stackTrace"); + } + } + + @override + Future verifyMessage( + final String message, + final String signature, { + final String? address = null, + }) { + throw UnimplementedError(); + } + + @observable + late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo); + + static Future create(final WalletCredentials credentials) async { + await _init(); + final newWalletCredentials = credentials as ZcashNewWalletCredentials; + + String mnemonic; + if (newWalletCredentials.mnemonic != null && newWalletCredentials.mnemonic!.isNotEmpty) { + mnemonic = newWalletCredentials.mnemonic!; + } else { + final strength = (newWalletCredentials.seedPhraseLength == 24) ? 256 : 128; + mnemonic = bip39.generateMnemonic(strength: strength); + } + + final accountId = await _restoreZcashWalletFromSeed( + name: credentials.name, + seed: mnemonic, + passphrase: newWalletCredentials.passphrase, + ); + await _saveAccountId(credentials.name, accountId); + final wallet = await open( + name: credentials.name, + password: credentials.password!, + walletInfo: credentials.walletInfo!, + ); + await wallet.walletAddresses.saveAddressesInBox(); + return wallet; + } + + static Future restore(final WalletCredentials credentials) async { + await _init(); + final fromSeedCredentials = credentials as ZcashFromSeedWalletCredentials; + final String? seed = fromSeedCredentials.seed; + if (seed == null || seed.isEmpty) { + throw Exception('Seed phrase is required for wallet restoration'); + } + + final accountId = await _restoreZcashWalletFromSeed( + name: credentials.name, + seed: seed, + passphrase: fromSeedCredentials.passphrase, + ); + await _saveAccountId(credentials.name, accountId); + final wallet = await open( + name: credentials.name, + password: credentials.password!, + walletInfo: credentials.walletInfo!, + ); + await wallet.walletAddresses.saveAddressesInBox(); + printV("height: ${credentials.height}"); + if (credentials.height != null) { + final zcashDir = await pathForWalletTypeDir(type: WalletType.zcash); + final zcashInitialSync = File(p.join(zcashDir, ".initial-sync-marker")); + zcashInitialSync.writeAsBytesSync([0x00]); + zcashInitialSync.writeAsStringSync( + credentials.height.toString(), + mode: FileMode.writeOnlyAppend, + ); + unawaited( + Future.delayed(Duration(seconds: 8)).then( + (_) => ZcashWalletService.runInDbMutex( + () async => await WarpApi.rescanFrom(coin, credentials.height ?? 0), + ), + ), + ); + } + return wallet; + } + + static Future open({ + required final String name, + required final String password, + required final WalletInfo walletInfo, + }) async { + await _init(); + if (password.isNotEmpty) { + WarpApi.setDbPasswd(coin, password); + } + final accountId = await getZcashAccountIdForName(name); + if (accountId == null) { + throw Exception("Wallet account not found for name: $name"); + } + final wallet = ZcashWallet( + walletInfo, + await walletInfo.getDerivationInfo(), + accountId: accountId, + ); + await wallet.walletAddresses.init(); + return wallet; + } + + static Future _restoreZcashWalletFromSeed({ + required final String name, + required String seed, + required String? passphrase, + }) async { + if (passphrase?.isNotEmpty == true) { + passphrase = passphrase!.replaceAll(" ", "_"); + seed = "${seed} ${passphrase}"; + } + final accountId = await ZcashWalletService.runInDbMutex( + () => WarpApi.newAccount(coin, name, seed, 0), + ); + return accountId; + } + + static Future getZcashAccountIdForName(final String name) async { + final wPath = await pathForWallet(name: name, type: _type); + final f = File(wPath); + if (!f.existsSync()) { + final accounts = WarpApi.getAccountList(coin); + for (final account in accounts) { + if (account.name == name) { + return account.id; + } + } + return null; + } + final content = f.readAsStringSync(); + return int.tryParse(content.trim()); + } + + static Future _saveAccountId(final String name, final int accountId) async { + final wPath = await pathForWallet(name: name, type: _type); + final f = File(wPath); + f.writeAsStringSync(accountId.toString()); + } + + static WalletType get _type => WalletType.zcash; + + static Future getDbDataPath() async { + final pathForWalletType = await pathForWalletTypeDir(type: _type); + final dbDataPath = "${pathForWalletType}/zec.db"; + if (!Directory(pathForWalletType).existsSync()) { + Directory(pathForWalletType).createSync(recursive: true); + } + return dbDataPath; + } + + static Future getTorDir() async { + final pathForWalletType = await pathForWalletTypeDir(type: _type); + final torPath = "${pathForWalletType}/tor"; + if (!Directory(torPath).existsSync()) { + Directory(torPath).createSync(recursive: true); + } + return torPath; + } + + static Future getFsBlockCacheDir() async { + final pathForWalletType = await pathForWalletTypeDir(type: _type); + final fsBlockCacheDir = "${pathForWalletType}/blockCache"; + if (!Directory(pathForWalletType).existsSync()) { + Directory(pathForWalletType).createSync(recursive: true); + } + return fsBlockCacheDir; + } + + static String? dbDataPath; + static bool _initialized = false; + + static void unlockDatabase(final String password) { + _password = password; + } + + static String? _password; + static Future _init() async { + if (_initialized) return; + dbDataPath = await getDbDataPath(); + printV("WarpApi.initWallet"); + if (_password == null) { + throw Exception("Zcash wallet locked! Please contact support"); + } + if (!File(dbDataPath!).existsSync()) { + //TODO(mrcyjanek): copy-encrypt + } + // coin+1 = ycash + WarpApi.setDbPasswd(coin, ''); + WarpApi.setDbPasswd(coin + 1, ''); + WarpApi.initWallet(coin, dbDataPath!); + WarpApi.initWallet(coin + 1, dbDataPath!); + try { + WarpApi.migrateData(coin); + WarpApi.migrateData(coin + 1); + } catch (e) { + printV("zec init failed: $e"); + } // do not fail on network exception + final spend = await rootBundle.load('scripts/zcash_lib/assets/sapling-spend.params'); + final output = await rootBundle.load('scripts/zcash_lib/assets/sapling-output.params'); + WarpApi.initProver(spend.buffer.asUint8List(), output.buffer.asUint8List()); + await ZcashTaddressRotation.init(); + await ZcashTransactionInfo.init(); + _initialized = true; + } + + static Future getHeightByDate(final DateTime date) { + return WarpApi.getBlockHeightByTime(coin, date); + } +} diff --git a/cw_zcash/lib/src/zcash_wallet_addresses.dart b/cw_zcash/lib/src/zcash_wallet_addresses.dart new file mode 100644 index 000000000..e05ee2ee5 --- /dev/null +++ b/cw_zcash/lib/src/zcash_wallet_addresses.dart @@ -0,0 +1,223 @@ +import 'package:cw_core/utils/print_verbose.dart'; +import 'package:cw_core/wallet_addresses.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:cw_zcash/src/zcash_taddress_rotation.dart'; +import 'package:mobx/mobx.dart'; +import 'package:warp_api/warp_api.dart'; + +part 'zcash_wallet_addresses.g.dart'; + +class ZcashWalletAddresses = ZcashWalletAddressesBase with _$ZcashWalletAddresses; + +abstract class ZcashWalletAddressesBase extends WalletAddresses with Store { + ZcashWalletAddressesBase(this.accountId, final WalletInfo walletInfo) : super(walletInfo) { + addressPageType = ZcashReceivePageOption.typeFromString(walletInfo.addressPageType ?? ""); + } + + final int accountId; + + static const int transparentType = 0; + static const int shieldedSaplingType = 1; + static const int shieldedOrchardType = 2; + static const int unifiedType = 3; + + String? _transparentAddress; + String? _saplingAddress; + String? _orchardAddress; + String? _unifiedAddress; + + @override + @computed + String get latestAddress { + switch (addressPageType) { + case ZcashAddressType.transparent: + return transparentAddress; + case ZcashAddressType.transparentRotated: + // just to display something, after the wallet is synced the address + // will start rotating, and since we can't rotate address until the wallet syncs + // we can't know if some T address already got used, so we would still default to + // the same address. + // So.. we can + // 1) display placeholder text (looks ugly) + // 2) display static address + // 3) display first address from rotated pool which is also static until synced + // 2 seems like an obvious winner simply because it offers little to no benefit over 3 + // and is noticably less complex in implementation + return transparentAddressRotated ?? transparentAddress; + case ZcashAddressType.shieldedSapling: + return saplingAddress; + case ZcashAddressType.shieldedOrchard: + return orchardAddress; + case ZcashAddressType.unifiedType: + if (_unifiedAddress == null) { + _unifiedAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 7); + } + return _unifiedAddress ?? ""; + } + } + + @observable + ZcashAddressType? _addressPageType; + + @computed + ZcashAddressType get addressPageType => _addressPageType ?? ZcashAddressType.shieldedOrchard; + + @computed + set addressPageType(final ZcashAddressType newZat) { + _addressPageType = newZat; + init(); + address = latestAddress; + } + + @action + Future setAddressType(final ZcashAddressType type) async { + addressPageType = ZcashReceivePageOption.typeFromString(type.toString()); + walletInfo.addressPageType = type.toString(); + await walletInfo.save(); + } + + String get transparentAddress { + try { + if (_transparentAddress == null) { + _transparentAddress = WarpApi.getTAddr(ZcashWalletBase.coin, accountId); + } + return _transparentAddress ?? ""; + } catch (e) { + return ""; + } + } + + @computed + String? get transparentAddressRotated { + try { + return ZcashTaddressRotation.addressForAccount(accountId); + } catch (e) { + return null; + } + } + + String get saplingAddress { + try { + if (_saplingAddress == null) { + _saplingAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 2); + } + return _saplingAddress ?? ""; + } catch (e) { + return ""; + } + } + + String get orchardAddress { + try { + if (_orchardAddress == null) { + _orchardAddress = WarpApi.getAddress(ZcashWalletBase.coin, accountId, 4); + } + return _orchardAddress ?? ""; + } catch (e) { + return ""; + } + } + + @override + String get addressForExchange => transparentAddressRotated ?? transparentAddress; + + @override + bool containsAddress(final String address) { + return this.address == address || addressesMap.values.contains(address); + } + + static int get coin => ZcashWalletBase.coin; + + @override + Future init() async { + addressesMap = await walletInfo.getAddresses(); + addressInfos = await walletInfo.getAddressInfos(); + usedAddresses = await walletInfo.getUsedAddresses(); + manualAddresses = await walletInfo.getManualAddresses(); + hiddenAddresses = await walletInfo.getHiddenAddresses(); + + addressesMap = {"a": "b"}; + + await ZcashTaddressRotation.init(); + printV(ZcashTaddressRotation.rotationAccounts.keys); + int accountIndex = 0; + addressInfos = { + 0: + ZcashTaddressRotation.allAddressesForAccount(accountId)?.map((final v) { + return WalletInfoAddressInfo( + walletInfoId: walletInfo.internalId, + mapKey: ++accountIndex, + accountIndex: 0, + address: v, + label: "", + ); + }).toList() ?? + [], + }; + hiddenAddresses = ZcashTaddressRotation.allUsedAddressesForAccount(accountId)?.toSet() ?? {}; + + addressInfos[0]?.removeWhere((final test) => hiddenAddresses.contains(test.address)); + if (_addressPageType == ZcashAddressType.transparentRotated) { + final addr = ZcashTaddressRotation.addressForAccount(accountId); + if (addr != null) { + address = addr; + } + } + await saveAddressesInBox(); + } + + @override + @observable + late String _address = latestAddress; + + String get address { + if (addressPageType == ZcashAddressType.transparentRotated) { + return transparentAddressRotated ?? transparentAddress; + } + return _address; + } + + void set address(final String _$address) => _address = _$address; + + @override + String get primaryAddress => address; + + @override + Future saveAddressesInBox() async { + try { + await walletInfo.setAddresses(addressesMap); + await walletInfo.setAddressInfos(addressInfos); + await walletInfo.setUsedAddresses(usedAddresses.toList()); + await walletInfo.setHiddenAddresses(hiddenAddresses.toList()); + await walletInfo.setManualAddresses(manualAddresses.toList()); + } catch (e) { + printV("Error saving addresses: $e"); + } + } + + @override + Future updateAddressesInBox() async { + _transparentAddress = null; + _saplingAddress = null; + _orchardAddress = null; + _unifiedAddress = null; + } + + @override + Map get usableAddressesMap => addressesMap; + + @override + Map get usableAllAddressesMap => allAddressesMap; + + List getAddressInfos() { + if (addressPageType != ZcashAddressType.transparentRotated) { + return []; + } + final List allInfos = []; + for (final entry in addressInfos.entries) { + allInfos.addAll(entry.value); + } + return allInfos; + } +} diff --git a/cw_zcash/lib/src/zcash_wallet_service.dart b/cw_zcash/lib/src/zcash_wallet_service.dart new file mode 100644 index 000000000..ac6fab53a --- /dev/null +++ b/cw_zcash/lib/src/zcash_wallet_service.dart @@ -0,0 +1,181 @@ +import 'dart:io'; + +import 'package:bip39/bip39.dart' as bip39; +import 'package:cw_core/pathForWallet.dart'; +import 'package:cw_core/utils/print_verbose.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_zcash/cw_zcash.dart'; +import 'package:mutex/mutex.dart'; +import 'package:path/path.dart' as p; + +class ZcashWalletService + extends + WalletService< + ZcashNewWalletCredentials, + ZcashFromSeedWalletCredentials, + ZcashFromKeysWalletCredentials, + ZcashNewWalletCredentials + > { + ZcashWalletService(); + + static Mutex dbMutex = Mutex(); + static int dbMutexQueue = 0; + static Future runInDbMutex(final Future Function() call) async { + try { + printV("dbMutexQueue: ${dbMutexQueue++}"); + await dbMutex.acquire(); + return await call(); + } finally { + dbMutex.release(); + dbMutexQueue--; + } + } + + static Set autoshieldTx = {}; + + static Future addShieldedTx(final String txId) async { + final pathForWalletType = await pathForWalletTypeDir(type: type); + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt"); + autoshieldTx.add(txId.replaceAll(RegExp(r'[^a-zA-Z0-9 ]'), '')); + final shieldedList = File(shieldedListFile); + shieldedList.writeAsStringSync(autoshieldTx.join("\n")); + } + + static Future loadShieldTxs() async { + try { + final pathForWalletType = await pathForWalletTypeDir(type: type); + final shieldedListFile = p.join(pathForWalletType, "autoshield_list.txt"); + final shieldedList = File(shieldedListFile); + if (!shieldedList.existsSync()) { + return; + } + autoshieldTx = shieldedList.readAsLinesSync().toSet(); + } catch (e) { + printV("loadShieldTxs failed: $e"); + } + } + + static bool walletFilesExist(final String path) => + !File(path).existsSync() && !File('$path.keys').existsSync(); + + @override + WalletType getType() => type; + + static WalletType get type => WalletType.zcash; + + @override + Future create(final WalletCredentials credentials, {final bool? isTestnet}) { + return ZcashWalletBase.create(credentials); + } + + @override + Future isWalletExit(final String name) async { + final path = await pathForWallet(name: name, type: getType()); + return File(path).existsSync(); + } + + @override + Future openWallet(final String name, final String password) async { + await loadShieldTxs(); + final walletInfo = await WalletInfo.get(name, getType()); + if (walletInfo == null) { + throw Exception('Wallet not found'); + } + try { + final wallet = await ZcashWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + await wallet.init(); + await saveBackup(name); + return wallet; + } catch (e) { + await restoreWalletFilesFromBackup(name); + final wallet = await ZcashWalletBase.open( + name: name, + password: password, + walletInfo: walletInfo, + ); + await wallet.init(); + return wallet; + } + } + + @override + Future remove(final String wallet) async { + final path = await pathForWalletDir(name: wallet, type: getType()); + final file = Directory(path); + final isExist = file.existsSync(); + + if (isExist) { + await file.delete(recursive: true); + } + + final walletInfo = await WalletInfo.get(wallet, getType()); + if (walletInfo == null) { + throw Exception('Wallet not found'); + } + await WalletInfo.delete(walletInfo); + } + + @override + Future rename(final String currentName, final String password, final String newName) async { + final currentWalletInfo = await WalletInfo.get(currentName, getType()); + if (currentWalletInfo == null) { + throw Exception('Wallet not found'); + } + final accountId = await ZcashWalletBase.getZcashAccountIdForName(currentName); + if (accountId == null) { + throw Exception('Wallet account not found for name: $currentName'); + } + final currentWallet = ZcashWallet( + currentWalletInfo, + await currentWalletInfo.getDerivationInfo(), + accountId: accountId, + ); + + await currentWallet.renameWalletFiles(newName); + + final newWalletInfo = currentWalletInfo; + newWalletInfo.id = WalletBase.idFor(newName, getType()); + newWalletInfo.name = newName; + + await newWalletInfo.save(); + } + + @override + Future restoreFromKeys( + final ZcashFromKeysWalletCredentials credentials, { + final bool? isTestnet, + }) async { + throw UnimplementedError(); + } + + @override + Future restoreFromSeed( + final ZcashFromSeedWalletCredentials credentials, { + final bool? isTestnet, + }) async { + if (credentials.seed == null || credentials.seed!.isEmpty) { + throw ZcashMnemonicIsIncorrectException(); + } + + if (!bip39.validateMnemonic(credentials.seed!)) { + throw ZcashMnemonicIsIncorrectException(); + } + + return ZcashWalletBase.restore(credentials); + } + + @override + Future restoreFromHardwareWallet(final ZcashNewWalletCredentials credentials) { + throw UnimplementedError( + "Restoring a Zcash wallet from a hardware wallet is not yet supported!", + ); + } +} diff --git a/cw_zcash/pubspec.yaml b/cw_zcash/pubspec.yaml new file mode 100644 index 000000000..70314f718 --- /dev/null +++ b/cw_zcash/pubspec.yaml @@ -0,0 +1,29 @@ +name: cw_zcash +description: Zcash library for cake wallet +version: 1.0.0 +publish_to: none + +environment: + sdk: ^3.8.0 + +dependencies: + flutter: + sdk: flutter + warp_api: + path: ../scripts/zcash_lib/packages/warp_api_ffi + cw_core: + path: ../cw_core + mobx: ^2.5.0 + bip39: ^1.0.6 + mutex: ^3.1.0 + + path: any + flat_buffers: any +dev_dependencies: + flutter_test: + sdk: flutter + build_runner: ^2.4.15 + build_resolvers: ^2.4.4 + mobx_codegen: ^2.0.7 + hive_generator: ^2.0.1 + ffigen: ^16.1.0 diff --git a/integration_test/components/common_test_flows.dart b/integration_test/components/common_test_flows.dart index 333a6599d..ba4210934 100644 --- a/integration_test/components/common_test_flows.dart +++ b/integration_test/components/common_test_flows.dart @@ -370,6 +370,8 @@ class CommonTestFlows { return secrets.decredTestWalletSeeds; case WalletType.dogecoin: return secrets.dogeTestWalletSeeds; + case WalletType.zcash: + return secrets.zcashTestWalletSeeds; case WalletType.none: case WalletType.haven: case WalletType.banano: diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 557e7c3dc..513aca591 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -139,6 +139,8 @@ PODS: - Flutter - wakelock_plus (0.0.1): - Flutter + - warp_api (0.0.1): + - Flutter - YttriumWrapper (0.8.35) DEPENDENCIES: @@ -179,6 +181,7 @@ DEPENDENCIES: - universal_ble (from `.symlinks/plugins/universal_ble/darwin`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - wakelock_plus (from `.symlinks/plugins/wakelock_plus/ios`) + - warp_api (from `.symlinks/plugins/warp_api/ios`) SPEC REPOS: https://github.com/CocoaPods/Specs.git: @@ -264,6 +267,8 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/url_launcher_ios/ios" wakelock_plus: :path: ".symlinks/plugins/wakelock_plus/ios" + warp_api: + :path: ".symlinks/plugins/warp_api/ios" SPEC CHECKSUMS: bitbox_flutter: 506f80b961ddf646b0d80cef9f6eadaab96d91b0 @@ -309,6 +314,7 @@ SPEC CHECKSUMS: universal_ble: cf52a7b3fd2e7c14d6d7262e9fdadb72ab6b88a6 url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe wakelock_plus: 76957ab028e12bfa4e66813c99e46637f367fc7e + warp_api: 4a60cc00440d5eb56ae4dbf75f89a34ed90ddf1b YttriumWrapper: 31e937fe9fbe0f1314d2ca6be9ce9b379a059966 PODFILE CHECKSUM: 5296465b1c6d14d506230356756826012f65d97a diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index c4818da16..0899da3a7 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -25,6 +25,7 @@ A1B4A70C9CFA13AB71662216 /* LnurlPay.swift in Resources */ = {isa = PBXBuildFile; fileRef = 7D3364C03978A8A74B6D586E /* LnurlPay.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; A3D5E17CC53DF13FA740DEFA /* RedeemSwap.swift in Resources */ = {isa = PBXBuildFile; fileRef = 9D2F2C9F2555316C95EE7EA3 /* RedeemSwap.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; B6C6E59403ACDE44724C12F4 /* ServiceConfig.swift in Resources */ = {isa = PBXBuildFile; fileRef = B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */; settings = {ASSET_TAGS = (BreezSDK, ); }; }; + CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -46,6 +47,7 @@ CE918BF82D533ECE007F186E /* MoneroWallet.xcframework in CopyFiles */, CE918BFA2D533ED4007F186E /* WowneroWallet.xcframework in CopyFiles */, CE918BFC2D533ED8007F186E /* ZanoWallet.xcframework in CopyFiles */, + CE3D5B792F142305004D767D /* WarpApiFFI.xcframework in CopyFiles */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -85,9 +87,11 @@ B3D5E78267F5F18D882FDC3B /* ServiceConfig.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = ServiceConfig.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/ServiceConfig.swift"; sourceTree = ""; }; B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; C58D93382C00FAC6004BCF69 /* libresolv.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libresolv.tbd; path = usr/lib/libresolv.tbd; sourceTree = SDKROOT; }; + CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = ""; }; CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = ""; }; CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = ""; }; CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = ""; }; + CEFC07A12EF2EEEB0012DB29 /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../../zcash_lib/WarpApiFFI.xcframework; sourceTree = ""; }; D67E45DEB6E9449D981B35E0 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; DCEA540E3586164FB47AD13E /* LnurlPayInvoice.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; name = LnurlPayInvoice.swift; path = "../.symlinks/plugins/breez_sdk/ios/bindings-swift/Sources/BreezSDK/Task/LnurlPayInvoice.swift"; sourceTree = ""; }; F2F2EF79B5A1520AF76225B9 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; @@ -113,6 +117,7 @@ CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */, CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */, CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */, + CEFC07A12EF2EEEB0012DB29 /* WarpApiFFI.xcframework */, C58D93382C00FAC6004BCF69 /* libresolv.tbd */, 0C9986A3251A932F00D566FD /* CryptoSwift.framework */, B478EFCC3C0760ED97FFE561 /* Pods_Runner.framework */, @@ -163,6 +168,7 @@ 97C146E51CF9000F007C117D = { isa = PBXGroup; children = ( + CE3D5B782F142305004D767D /* WarpApiFFI.xcframework */, 0C44A7182518EF4A00B570ED /* CakeWallet */, 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, diff --git a/ios/Runner/InfoBase.plist b/ios/Runner/InfoBase.plist index 59e3bb924..3486cc40e 100644 --- a/ios/Runner/InfoBase.plist +++ b/ios/Runner/InfoBase.plist @@ -342,6 +342,26 @@ arbitrum-wallet + + CFBundleTypeRole + Viewer + CFBundleURLName + zcash + CFBundleURLSchemes + + zcash + + + + CFBundleTypeRole + Viewer + CFBundleURLName + zcash-wallet + CFBundleURLSchemes + + zcash-wallet + + CFBundleVersion $(CURRENT_PROJECT_VERSION) diff --git a/lib/anonpay/anonpay_api.dart b/lib/anonpay/anonpay_api.dart index 6f401ae3f..02877e008 100644 --- a/lib/anonpay/anonpay_api.dart +++ b/lib/anonpay/anonpay_api.dart @@ -35,7 +35,7 @@ class AnonPayApi { clearnetUri: Uri.https(clearNetAuthority, "$anonPayStatus/$id"), onionUri: Uri.https(onionApiAuthority, "$anonPayStatus/$id"), ); - + final responseJSON = json.decode(response.body) as Map; final status = responseJSON['Status'] as String; final fiatAmount = responseJSON['Fiat_Amount'] as double?; diff --git a/lib/buy/robinhood/robinhood_buy_provider.dart b/lib/buy/robinhood/robinhood_buy_provider.dart index 57ce1ad87..48b46c7dd 100644 --- a/lib/buy/robinhood/robinhood_buy_provider.dart +++ b/lib/buy/robinhood/robinhood_buy_provider.dart @@ -122,6 +122,7 @@ class RobinhoodBuyProvider extends BuyProvider { case WalletType.banano: case WalletType.wownero: case WalletType.zano: + case WalletType.zcash: case WalletType.decred: throw Exception("Wallet Type ${wallet.type.name} is not available for Robinhood"); } diff --git a/lib/cake_pay/src/models/cake_pay_order.dart b/lib/cake_pay/src/models/cake_pay_order.dart index 0f70c50f5..d4cb37c6b 100644 --- a/lib/cake_pay/src/models/cake_pay_order.dart +++ b/lib/cake_pay/src/models/cake_pay_order.dart @@ -1,4 +1,4 @@ -enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB } +enum CakePayPaymentMethod { BTC, BTC_LN, XMR, LTC, LTC_MWEB, ZEC } extension CakePayPaymentMethodLabel on CakePayPaymentMethod { String get label => switch (this) { @@ -7,6 +7,7 @@ extension CakePayPaymentMethodLabel on CakePayPaymentMethod { CakePayPaymentMethod.XMR => 'Monero', CakePayPaymentMethod.LTC => 'Litecoin', CakePayPaymentMethod.LTC_MWEB => 'MWEB', + CakePayPaymentMethod.ZEC => 'Zcash', }; } @@ -63,7 +64,9 @@ class CakePayOrder { CakePayPaymentMethod.XMR => order.paymentData.xmr, CakePayPaymentMethod.LTC => order.paymentData.ltc, CakePayPaymentMethod.LTC_MWEB => order.paymentData.ltc_mweb, - _ => null + CakePayPaymentMethod.ZEC => order.paymentData.zec, + // TODO: Handle this case. + CakePayPaymentMethod.BTC_LN => throw UnimplementedError(), }; if (data == null) return null; @@ -87,6 +90,7 @@ class CakePayOrder { CakePayPaymentMethod.XMR => 'XMR', CakePayPaymentMethod.LTC => 'LTC', CakePayPaymentMethod.LTC_MWEB => 'LTC', + CakePayPaymentMethod.ZEC => 'ZEC', }; } } @@ -129,6 +133,7 @@ class PaymentData { final CryptoPaymentData? xmr; final CryptoPaymentData? ltc; final CryptoPaymentData? ltc_mweb; + final CryptoPaymentData? zec; final DateTime invoiceTime; final DateTime expirationTime; final int? commission; @@ -139,6 +144,7 @@ class PaymentData { required this.xmr, required this.ltc, required this.ltc_mweb, + required this.zec, required this.invoiceTime, required this.expirationTime, required this.commission, @@ -151,6 +157,7 @@ class PaymentData { xmr: map['XMR'] != null ? CryptoPaymentData.fromMap(map['XMR'] as Map) : null, ltc: map['LTC'] != null ? CryptoPaymentData.fromMap(map['LTC'] as Map) : null, ltc_mweb: map['LTC_MWEB'] != null ? CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map) : null, + zec: map['ZEC_CHAIN'] != null ? CryptoPaymentData.fromMap(map['ZEC_CHAIN'] as Map) : null, invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int), expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int), commission: map['commission'] as int?, diff --git a/lib/core/address_validator.dart b/lib/core/address_validator.dart index 62c62fab9..70b7dc956 100644 --- a/lib/core/address_validator.dart +++ b/lib/core/address_validator.dart @@ -135,10 +135,15 @@ class AddressValidator extends TextValidator { '|[13][a-km-zA-HJ-NP-Z1-9]{25,34}'; case CryptoCurrency.hbar: pattern = '[0-9a-zA-Z.]+'; - case CryptoCurrency.zaddr: - pattern = 'zs[0-9a-zA-Z]{75}'; case CryptoCurrency.zec: - pattern = 't1[0-9a-zA-Z]{33}|t3[0-9a-zA-Z]{33}'; + pattern = + '(?:' + 't1[0-9A-Za-z]{33}' + '|t3[0-9A-Za-z]{33}' + '|zs[a-z0-9]{76}' + '|zxviews[a-z0-9]{278}' + '|u1[a-z0-9]{1,300}' + ')'; case CryptoCurrency.dcr: pattern = '(D|T|S)[ksecS]([0-9a-zA-Z])+'; case CryptoCurrency.rvn: @@ -282,8 +287,6 @@ class AddressValidator extends TextValidator { return [34]; case CryptoCurrency.zen: return [35]; - case CryptoCurrency.zaddr: - return null; case CryptoCurrency.zec: return null; case CryptoCurrency.kmd: @@ -352,6 +355,15 @@ class AddressValidator extends TextValidator { pattern = '(T|t)[1-9A-HJ-NP-Za-km-z]{33}'; case CryptoCurrency.zano: pattern = '([1-9A-HJ-NP-Za-km-z]{90,200})|(@[\w\d.-]+)'; + case CryptoCurrency.zec: + pattern = + '(?:' + 't1[0-9A-Za-z]{33}' + '|t3[0-9A-Za-z]{33}' + '|zs[a-z0-9]{76}' + '|zxviews[a-z0-9]{278}' + '|u1[a-z0-9]{1,300}' + ')'; default: if (type.tag == CryptoCurrency.eth.title) { pattern = '0x[0-9a-zA-Z]+'; diff --git a/lib/core/payment_uris.dart b/lib/core/payment_uris.dart index 38f4f6d2e..74bd73763 100644 --- a/lib/core/payment_uris.dart +++ b/lib/core/payment_uris.dart @@ -253,6 +253,21 @@ class DogeURI extends PaymentURI { } } +class ZcashURI extends PaymentURI { + ZcashURI({required super.amount, required super.address}); + + @override + String toString() { + var base = 'zcash:$address'; + + if (amount.isNotEmpty) { + base += '?amount=${amount.replaceAll(',', '.')}'; + } + + return base; + } +} + class ERC681URI extends PaymentURI { final int chainId; final String? contractAddress; diff --git a/lib/core/seed_validator.dart b/lib/core/seed_validator.dart index dae50a2f9..893ebfad6 100644 --- a/lib/core/seed_validator.dart +++ b/lib/core/seed_validator.dart @@ -10,6 +10,7 @@ import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/decred/decred.dart'; import 'package:cake_wallet/utils/language_list.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/wallet_type.dart'; class SeedValidator extends Validator { @@ -52,6 +53,8 @@ class SeedValidator extends Validator { return zano!.getWordList(language); case WalletType.decred: return decred!.getDecredWordList(); + case WalletType.zcash: + return zcash!.getZcashWordList(language); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/core/wallet_creation_service.dart b/lib/core/wallet_creation_service.dart index 51f1b288f..69e9245e2 100644 --- a/lib/core/wallet_creation_service.dart +++ b/lib/core/wallet_creation_service.dart @@ -84,6 +84,7 @@ class WalletCreationService { case WalletType.tron: case WalletType.dogecoin: case WalletType.nano: + case WalletType.zcash: return true; case WalletType.monero: case WalletType.wownero: diff --git a/lib/di.dart b/lib/di.dart index 31703b4a2..a26af47fa 100644 --- a/lib/di.dart +++ b/lib/di.dart @@ -62,6 +62,7 @@ import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart'; import 'package:cake_wallet/view_model/start_tor_view_model.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cake_wallet/entities/wallet_edit_page_arguments.dart'; import 'package:cake_wallet/entities/wallet_manager.dart'; @@ -1109,7 +1110,7 @@ Future setup({ getIt.registerFactory(() => MoonPayProvider( appStore: getIt.get(), wallet: getIt.get().wallet!, - isTestEnvironment: kDebugMode, + isTestEnvironment: kDebugMode || kProfileMode, )); getIt.registerFactory(() => OnRamperBuyProvider( @@ -1224,6 +1225,8 @@ Future setup({ return decred!.createDecredWalletService(_unspentCoinsInfoSource); case WalletType.haven: return HavenWalletService(); + case WalletType.zcash: + return zcash!.createZcashWalletService(SettingsStoreBase.walletPasswordDirectInput); case WalletType.none: throw Exception('Unexpected token: ${param1.toString()} for generating of WalletService'); } diff --git a/lib/entities/default_settings_migration.dart b/lib/entities/default_settings_migration.dart index 9e57fd873..c176ebe57 100644 --- a/lib/entities/default_settings_migration.dart +++ b/lib/entities/default_settings_migration.dart @@ -52,6 +52,7 @@ const decredDefaultUri = "default-spv-nodes"; const dogecoinDefaultNodeUri = 'dogecoin.stackwallet.com:50022'; const baseDefaultNodeUri = 'base.nownodes.io'; const arbitrumDefaultNodeUri = 'arbitrum.nownodes.io'; +const zcashDefaultNodeUri = 'zec-node.cakewallet.com:443'; Future defaultSettingsMigration( {required int version, @@ -559,7 +560,14 @@ Future defaultSettingsMigration( case 54: await _backupWowneroSeeds(havenSeedStore); break; - + case 55: + await addWalletNodeList(nodes: nodes, type: WalletType.zcash); + await _changeDefaultNode( + nodes: nodes, + sharedPreferences: sharedPreferences, + type: WalletType.zcash, + currentNodePreferenceKey: PreferencesKey.currentZcashNodeIdKey, + ); default: break; } @@ -672,6 +680,8 @@ String _getDefaultNodeUri(WalletType type) { return baseDefaultNodeUri; case WalletType.arbitrum: return arbitrumDefaultNodeUri; + case WalletType.zcash: + return zcashDefaultNodeUri; case WalletType.banano: case WalletType.none: return ''; @@ -1092,6 +1102,7 @@ Future checkCurrentNodes( final currentTronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final currentWowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final currentZanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final currentZcashNodeId = sharedPreferences.getInt(PreferencesKey.currentZcashNodeIdKey); final currentMoneroNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentMoneroNodeId); final currentBitcoinElectrumServer = @@ -1126,6 +1137,8 @@ Future checkCurrentNodes( nodeSource.values.firstWhereOrNull((node) => node.key == currentWowneroNodeId); final currentZanoNode = nodeSource.values.firstWhereOrNull((node) => node.key == currentZanoNodeId); + final currentZcashNode = + nodeSource.values.firstWhereOrNull((node) => node.key == currentZcashNodeId); if (currentMoneroNode == null) { final newCakeWalletNode = Node(uri: newCakeWalletMoneroUri, type: WalletType.monero); @@ -1240,6 +1253,12 @@ Future checkCurrentNodes( await nodeSource.add(node); await sharedPreferences.setInt(PreferencesKey.currentDecredNodeIdKey, node.key as int); } + + if (currentZcashNode == null) { + final node = Node(uri: zcashDefaultNodeUri, type: WalletType.zcash, useSSL: true); + await nodeSource.add(node); + await sharedPreferences.setInt(PreferencesKey.currentZcashNodeIdKey, node.key as int); + } } Future resetBitcoinElectrumServer( diff --git a/lib/entities/hash_wallet_identifier.dart b/lib/entities/hash_wallet_identifier.dart index 175063eb6..204faec8c 100644 --- a/lib/entities/hash_wallet_identifier.dart +++ b/lib/entities/hash_wallet_identifier.dart @@ -2,6 +2,7 @@ import 'dart:convert'; import 'package:cake_wallet/.secrets.g.dart' as secrets; import 'package:cw_core/wallet_base.dart'; +import 'package:flutter/foundation.dart'; import 'package:hashlib/hashlib.dart'; String createHashedWalletIdentifier(WalletBase wallet) { @@ -9,8 +10,10 @@ String createHashedWalletIdentifier(WalletBase wallet) { final salt = secrets.walletGroupSalt; final combined = '$salt.$hashContent'; - final bytes = utf8.encode(combined); final digest = sha256.convert(bytes); + if (kDebugMode) { + return "$combined;$digest"; + } return digest.toString(); } diff --git a/lib/entities/node_list.dart b/lib/entities/node_list.dart index 1b7a1e0d3..68a99e026 100644 --- a/lib/entities/node_list.dart +++ b/lib/entities/node_list.dart @@ -56,6 +56,9 @@ Future> loadDefaultNodes(WalletType type) async { case WalletType.arbitrum: path = 'assets/arbitrum_node_list.yml'; break; + case WalletType.zcash: + path = 'assets/zcash_node_list.yml'; + break; case WalletType.banano: case WalletType.none: path = ''; @@ -109,6 +112,7 @@ Future resetToDefault(Box nodeSource) async { final dogecoinElectrumServerList = await loadDefaultNodes(WalletType.dogecoin); final baseNodes = await loadDefaultNodes(WalletType.base); final arbitrumNodes = await loadDefaultNodes(WalletType.arbitrum); + final zcashNodes = await loadDefaultNodes(WalletType.zcash); final nodes = moneroNodes + bitcoinElectrumServerList + @@ -124,7 +128,8 @@ Future resetToDefault(Box nodeSource) async { decredNodes + dogecoinElectrumServerList + baseNodes + - arbitrumNodes; + arbitrumNodes + + zcashNodes; await nodeSource.clear(); await nodeSource.addAll(nodes); diff --git a/lib/entities/parse_address_from_domain.dart b/lib/entities/parse_address_from_domain.dart index 59683ba9e..451a4f4b4 100644 --- a/lib/entities/parse_address_from_domain.dart +++ b/lib/entities/parse_address_from_domain.dart @@ -175,7 +175,7 @@ class AddressResolver { var addressPattern = AddressValidator.getAddressFromStringPattern(type); if (addressPattern == null) { - throw Exception('Unexpected token: $type for getAddressFromStringPattern'); + return null; } if (requireSurroundingWhitespaces) diff --git a/lib/entities/preferences_key.dart b/lib/entities/preferences_key.dart index 8109c4ea2..8cc6be1d4 100644 --- a/lib/entities/preferences_key.dart +++ b/lib/entities/preferences_key.dart @@ -22,6 +22,7 @@ class PreferencesKey { static const currentSolanaNodeIdKey = 'current_node_id_sol'; static const currentTronNodeIdKey = 'current_node_id_trx'; static const currentWowneroNodeIdKey = 'current_node_id_wow'; + static const currentZcashNodeIdKey = 'current_node_id_zec'; static const currentTransactionPriorityKeyLegacy = 'current_fee_priority'; static const currentBalanceDisplayModeKey = 'current_balance_display_mode'; static const shouldSaveRecipientAddressKey = 'save_recipient_address'; diff --git a/lib/entities/priority_for_wallet_type.dart b/lib/entities/priority_for_wallet_type.dart index d346e3460..a7ae6fcbd 100644 --- a/lib/entities/priority_for_wallet_type.dart +++ b/lib/entities/priority_for_wallet_type.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; import 'package:cake_wallet/decred/decred.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/transaction_priority.dart'; import 'package:cw_core/wallet_type.dart'; @@ -37,6 +38,8 @@ List priorityForWalletType(WalletType type) { return zano!.getTransactionPriorities(); case WalletType.decred: return decred!.getTransactionPriorities(); + case WalletType.zcash: + return zcash!.getTransactionPriorities(); case WalletType.none: case WalletType.haven: return []; diff --git a/lib/exchange/provider/chainflip_exchange_provider.dart b/lib/exchange/provider/chainflip_exchange_provider.dart index 8e2413c8a..2fa44a4c1 100644 --- a/lib/exchange/provider/chainflip_exchange_provider.dart +++ b/lib/exchange/provider/chainflip_exchange_provider.dart @@ -16,10 +16,8 @@ import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class ChainflipExchangeProvider extends ExchangeProvider { - ChainflipExchangeProvider({required this.tradesStore}) - : super(pairList: supportedPairs(_notSupported)); + ChainflipExchangeProvider({required this.tradesStore}); - static final List _notSupported = []; static final List _supported = [ CryptoCurrency.btc, CryptoCurrency.eth, diff --git a/lib/exchange/provider/changenow_exchange_provider.dart b/lib/exchange/provider/changenow_exchange_provider.dart index 31ab6d55b..24f1ec812 100644 --- a/lib/exchange/provider/changenow_exchange_provider.dart +++ b/lib/exchange/provider/changenow_exchange_provider.dart @@ -21,13 +21,8 @@ import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class ChangeNowExchangeProvider extends ExchangeProvider { ChangeNowExchangeProvider({required SettingsStore settingsStore}) : _settingsStore = settingsStore, - _lastUsedRateId = '', - super(pairList: supportedPairs(_notSupported)); + _lastUsedRateId = ''; - static const List _notSupported = [ - CryptoCurrency.zaddr, - CryptoCurrency.xhv, - ]; static final apiKey = isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey; diff --git a/lib/exchange/provider/exchange_provider.dart b/lib/exchange/provider/exchange_provider.dart index 45c61d7c8..2f2bfafb9 100644 --- a/lib/exchange/provider/exchange_provider.dart +++ b/lib/exchange/provider/exchange_provider.dart @@ -6,12 +6,10 @@ import 'package:cake_wallet/exchange/trade_request.dart'; import 'package:cw_core/crypto_currency.dart'; abstract class ExchangeProvider { - ExchangeProvider({required this.pairList}); + ExchangeProvider(); String get title; - List pairList; - ExchangeProviderDescription get description; bool get isAvailable; diff --git a/lib/exchange/provider/exolix_exchange_provider.dart b/lib/exchange/provider/exolix_exchange_provider.dart index 6065b05fc..b7e8df135 100644 --- a/lib/exchange/provider/exolix_exchange_provider.dart +++ b/lib/exchange/provider/exolix_exchange_provider.dart @@ -16,33 +16,13 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class ExolixExchangeProvider extends ExchangeProvider { - ExolixExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + ExolixExchangeProvider(); static final apiKey = isMoneroOnly ? secrets.exolixMoneroApiKey : secrets.exolixCakeWalletApiKey; static const apiBaseUrl = 'exolix.com'; static const transactionsPath = '/api/v2/transactions'; static const ratePath = '/api/v2/rate'; - static const List _notSupported = [ - CryptoCurrency.usdt, - CryptoCurrency.xhv, - CryptoCurrency.btt, - CryptoCurrency.firo, - CryptoCurrency.zaddr, - CryptoCurrency.xvg, - CryptoCurrency.kmd, - CryptoCurrency.paxg, - CryptoCurrency.rune, - CryptoCurrency.scrt, - CryptoCurrency.btcln, - CryptoCurrency.cro, - CryptoCurrency.ftm, - CryptoCurrency.frax, - CryptoCurrency.gusd, - CryptoCurrency.gtc, - CryptoCurrency.weth, - ]; - @override String get title => 'Exolix'; diff --git a/lib/exchange/provider/letsexchange_exchange_provider.dart b/lib/exchange/provider/letsexchange_exchange_provider.dart index b2915ee84..b57912370 100644 --- a/lib/exchange/provider/letsexchange_exchange_provider.dart +++ b/lib/exchange/provider/letsexchange_exchange_provider.dart @@ -16,9 +16,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class LetsExchangeExchangeProvider extends ExchangeProvider { - LetsExchangeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = []; + LetsExchangeExchangeProvider(); static const apiKey = secrets.letsExchangeBearerToken; static const _baseUrl = 'api.letsexchange.io'; diff --git a/lib/exchange/provider/near_Intents_exchange_provider.dart b/lib/exchange/provider/near_Intents_exchange_provider.dart index 71a4005c0..b36a42026 100644 --- a/lib/exchange/provider/near_Intents_exchange_provider.dart +++ b/lib/exchange/provider/near_Intents_exchange_provider.dart @@ -17,10 +17,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class NearIntentsExchangeProvider extends ExchangeProvider { - NearIntentsExchangeProvider() - : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = []; + NearIntentsExchangeProvider(); static const apiKey = secrets.nearIntentsBearerToken; static const _baseUrl = '1click.chaindefuser.com'; @@ -44,7 +41,6 @@ class NearIntentsExchangeProvider extends ExchangeProvider { // UTXO 'LTC': 'ltc1qhdwz74m3wuuhppv2mckagqk9e2e49z5j4kucnv', 'BTC': 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kygt080', - 'ZZEC': 't1Zcash4hPS2bq8rTQkN9bKpZV1X4W3pZxK', 'DOGE': 'D9t7rGQ9mE3hJ2z1w8pGQxkGmKjYwYc8pQ', 'BCH': 'qpm2qsznhks23z7629mms6s4cwef74vcwvy22gdx6a', @@ -63,6 +59,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider { 'TON': 'UQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAM9c', 'XLM': 'GA3D5O2W7YQZJQ3H4Y5QW6N7V8X9Z0A1B2C3D4E5F6G7H8I9J0', 'ADA': 'addr1vyc33hdv5vag52f3d8h0qsngu52vm27x28zkzru333jma9gaxd38v', + 'ZEC': 't1VdfFUbyTT7ZSdeEaHuwB7veGD1NXoUhGS', }; String getNearDummyAddress(CryptoCurrency currency) { @@ -430,8 +427,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider { try { final uri = Uri.https(_baseUrl, '$_versionPath$_tokenPath'); - final response = - await ProxyWrapper().get(clearnetUri: uri, headers: _headers); + final response = await ProxyWrapper().get(clearnetUri: uri, headers: _headers); if (response.statusCode != 200) return []; final data = json.decode(response.body) as List; @@ -532,14 +528,6 @@ class NearIntentsExchangeProvider extends ExchangeProvider { }; } - String _normalizeTitleToNearSymbol(CryptoCurrency currency) { - final title = currency.title.toUpperCase(); - return switch (title) { - 'TZEC' => 'ZEC', - _ => title, - }; - } - String? _normalizeTagToNearBlockchain(String? tag) { return switch (tag) { 'TRX' => 'tron', @@ -559,7 +547,7 @@ class NearIntentsExchangeProvider extends ExchangeProvider { Token? currencyToNearAssetId(CryptoCurrency currency, List supported) { if (supported.isEmpty) return null; - final symbol = _normalizeTitleToNearSymbol(currency).toUpperCase(); + final symbol = currency.title.toUpperCase(); final blockchain = _normalizeTagToNearBlockchain(currency.tag); // Native asset (no contract) diff --git a/lib/exchange/provider/sideshift_exchange_provider.dart b/lib/exchange/provider/sideshift_exchange_provider.dart index 629597b34..340842244 100644 --- a/lib/exchange/provider/sideshift_exchange_provider.dart +++ b/lib/exchange/provider/sideshift_exchange_provider.dart @@ -16,23 +16,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class SideShiftExchangeProvider extends ExchangeProvider { - SideShiftExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = [ - CryptoCurrency.xhv, - CryptoCurrency.dcr, - CryptoCurrency.kmd, - CryptoCurrency.oxt, - CryptoCurrency.pivx, - CryptoCurrency.rune, - CryptoCurrency.rvn, - CryptoCurrency.scrt, - CryptoCurrency.stx, - CryptoCurrency.bttc, - CryptoCurrency.usdt, - CryptoCurrency.eos, - CryptoCurrency.xmr, - ]; + SideShiftExchangeProvider(); static const affiliateId = secrets.sideShiftAffiliateId; static const apiBaseUrl = 'https://sideshift.ai/api'; diff --git a/lib/exchange/provider/simpleswap_exchange_provider.dart b/lib/exchange/provider/simpleswap_exchange_provider.dart index 1118e7ba7..3d14c6918 100644 --- a/lib/exchange/provider/simpleswap_exchange_provider.dart +++ b/lib/exchange/provider/simpleswap_exchange_provider.dart @@ -16,12 +16,8 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class SimpleSwapExchangeProvider extends ExchangeProvider { - SimpleSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); + SimpleSwapExchangeProvider(); - static const List _notSupported = [ - CryptoCurrency.zaddr, - CryptoCurrency.xhv, - ]; static final apiKey = DeviceInfo.instance.isMobile ? secrets.simpleSwapApiKey : secrets.simpleSwapApiKeyDesktop; @@ -347,8 +343,6 @@ class SimpleSwapExchangeProvider extends ExchangeProvider { static String _normalizeCurrency(CryptoCurrency currency) { switch (currency) { - case CryptoCurrency.zaddr: - return 'zec'; case CryptoCurrency.zec: return 'zec'; case CryptoCurrency.bnb: diff --git a/lib/exchange/provider/stealth_ex_exchange_provider.dart b/lib/exchange/provider/stealth_ex_exchange_provider.dart index 1b9e6a4f1..a10affa39 100644 --- a/lib/exchange/provider/stealth_ex_exchange_provider.dart +++ b/lib/exchange/provider/stealth_ex_exchange_provider.dart @@ -15,9 +15,7 @@ import 'package:cw_core/crypto_currency.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class StealthExExchangeProvider extends ExchangeProvider { - StealthExExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = []; + StealthExExchangeProvider(); static final apiKey = secrets.stealthExBearerToken; static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent); diff --git a/lib/exchange/provider/swapsxyz_exchange_provider.dart b/lib/exchange/provider/swapsxyz_exchange_provider.dart index 4b399d6db..b93b23621 100644 --- a/lib/exchange/provider/swapsxyz_exchange_provider.dart +++ b/lib/exchange/provider/swapsxyz_exchange_provider.dart @@ -15,9 +15,7 @@ import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cw_core/crypto_currency.dart'; class SwapsXyzExchangeProvider extends ExchangeProvider { - SwapsXyzExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = []; + SwapsXyzExchangeProvider(); static final _apiKey = secrets.swapsXyzApiKey; static const _baseUrl = 'api-v2.swaps.xyz'; diff --git a/lib/exchange/provider/swaptrade_exchange_provider.dart b/lib/exchange/provider/swaptrade_exchange_provider.dart index 539c026a8..ff37165a0 100644 --- a/lib/exchange/provider/swaptrade_exchange_provider.dart +++ b/lib/exchange/provider/swaptrade_exchange_provider.dart @@ -16,25 +16,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class SwapTradeExchangeProvider extends ExchangeProvider { - SwapTradeExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static final List _notSupported = [ - ...(CryptoCurrency.all - .where((element) => ![ - CryptoCurrency.btc, - CryptoCurrency.sol, - CryptoCurrency.eth, - CryptoCurrency.ltc, - CryptoCurrency.ada, - CryptoCurrency.bch, - CryptoCurrency.usdterc20, - CryptoCurrency.usdttrc20, - CryptoCurrency.bnb, - CryptoCurrency.xmr, - CryptoCurrency.zec, - ].contains(element)) - .toList()) - ]; + SwapTradeExchangeProvider(); static final markup = secrets.swapTradeExchangeMarkup; @@ -220,8 +202,11 @@ class SwapTradeExchangeProvider extends ExchangeProvider { final responseBody = json.decode(response.body) as Map; if (response.statusCode == 400 || responseBody["success"] == false) { - final error = responseBody['errors'][0]['msg'] as String; - + final List errorsList = responseBody['errors'] as List? ?? []; + final error = errorsList.isNotEmpty + ? (errorsList[0]['msg'] as String?) ?? responseBody.toString() + : responseBody.toString(); + ExchangeProviderLogger.logError( provider: description, function: 'createTrade', diff --git a/lib/exchange/provider/thorchain_exchange.provider.dart b/lib/exchange/provider/thorchain_exchange.provider.dart index 023b021ee..69b11ad57 100644 --- a/lib/exchange/provider/thorchain_exchange.provider.dart +++ b/lib/exchange/provider/thorchain_exchange.provider.dart @@ -14,26 +14,7 @@ import 'package:hive/hive.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class ThorChainExchangeProvider extends ExchangeProvider { - ThorChainExchangeProvider({required this.tradesStore}) - : super(pairList: supportedPairs(_notSupported)); - - static final List _notSupported = [ - ...(CryptoCurrency.all - .where((element) => ![ - CryptoCurrency.btc, - // CryptoCurrency.eth, - CryptoCurrency.ltc, - CryptoCurrency.bch, - CryptoCurrency.usdtbsc, - // CryptoCurrency.aave, - // CryptoCurrency.dai, - // CryptoCurrency.gusd, - // CryptoCurrency.usdc, - // CryptoCurrency.usdterc20, - // CryptoCurrency.wbtc, // TODO: temporarily commented until https://github.com/cake-tech/cake_wallet/pull/1436 is merged - ].contains(element)) - .toList()) - ]; + ThorChainExchangeProvider({required this.tradesStore}); static final isRefundAddressSupported = [CryptoCurrency.eth]; diff --git a/lib/exchange/provider/trocador_exchange_provider.dart b/lib/exchange/provider/trocador_exchange_provider.dart index 1b27d7598..a8a13b3de 100644 --- a/lib/exchange/provider/trocador_exchange_provider.dart +++ b/lib/exchange/provider/trocador_exchange_provider.dart @@ -17,8 +17,7 @@ import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class TrocadorExchangeProvider extends ExchangeProvider { TrocadorExchangeProvider({this.useTorOnly = false, this.providerStates = const {}}) : _lastUsedRateId = '', - _provider = [], - super(pairList: supportedPairs(_notSupported)); + _provider = []; bool useTorOnly; Map providerStates; @@ -48,11 +47,6 @@ class TrocadorExchangeProvider extends ExchangeProvider { 'SwapSpace', ]; - static const List _notSupported = [ - CryptoCurrency.stx, - CryptoCurrency.zaddr, - ]; - static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey; static const clearNetAuthority = 'api.trocador.app'; static const onionApiAuthority = clearNetAuthority; diff --git a/lib/exchange/provider/xoswap_exchange_provider.dart b/lib/exchange/provider/xoswap_exchange_provider.dart index d05ed4c09..80559a7cb 100644 --- a/lib/exchange/provider/xoswap_exchange_provider.dart +++ b/lib/exchange/provider/xoswap_exchange_provider.dart @@ -13,9 +13,7 @@ import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/utils/proxy_wrapper.dart'; import 'package:cake_wallet/utils/exchange_provider_logger.dart'; class XOSwapExchangeProvider extends ExchangeProvider { - XOSwapExchangeProvider() : super(pairList: supportedPairs(_notSupported)); - - static const List _notSupported = []; + XOSwapExchangeProvider(); static const _apiAuthority = 'exchange.exodus.io'; static const _apiPath = '/v3'; diff --git a/lib/main.dart b/lib/main.dart index 6d96e6787..f30f09e95 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -34,10 +34,12 @@ import 'package:cake_wallet/utils/exception_handler.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/link_view_model.dart'; import 'package:cake_wallet/utils/responsive_layout_util.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/address_info.dart'; import 'package:cw_core/cake_hive.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/hive_type_ids.dart'; +import 'package:cw_core/key.dart'; import 'package:cw_core/mweb_utxo.dart'; import 'package:cw_core/node.dart'; import 'package:cw_core/payjoin_session.dart'; @@ -119,6 +121,12 @@ Future runAppWithZone({Key? topLevelKey}) async { if (FeatureFlag.hasDevOptions) { ProxyWrapper.logger = MemoryProxyLogger(); } + var zcashPassword = await secureStorageShared.read(key: "com.cakewallet.cw_zcash/zec.db"); + if (zcashPassword == null || zcashPassword.isEmpty) { + zcashPassword = generateKey().substring(0,32); + secureStorageShared.write(key: "com.cakewallet.cw_zcash/zec.db", value: zcashPassword); + } + zcash?.unlockDatabase(zcashPassword); // Basically when we're running a test if (topLevelKey != null) { @@ -266,7 +274,7 @@ Future initializeAppConfigs({bool loadWallet = true}) async { payjoinSessionSource: payjoinSessionSource, anonpayInvoiceInfo: anonpayInvoiceInfo, havenSeedStore: havenSeedStore, - initialMigrationVersion: 54, + initialMigrationVersion: 55, ); } diff --git a/lib/reactions/on_wallet_sync_status_change.dart b/lib/reactions/on_wallet_sync_status_change.dart index 6d31cb227..93807554b 100644 --- a/lib/reactions/on_wallet_sync_status_change.dart +++ b/lib/reactions/on_wallet_sync_status_change.dart @@ -1,5 +1,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/store/settings_store.dart'; +import 'package:cake_wallet/di.dart'; +import 'package:cake_wallet/view_model/wallet_list/wallet_list_view_model.dart'; import 'package:cw_core/utils/print_verbose.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:mobx/mobx.dart'; diff --git a/lib/reactions/wallet_utils.dart b/lib/reactions/wallet_utils.dart index 2b48a49ea..9dff92f2e 100644 --- a/lib/reactions/wallet_utils.dart +++ b/lib/reactions/wallet_utils.dart @@ -15,6 +15,7 @@ bool isBIP39Wallet(WalletType walletType) { case WalletType.banano: case WalletType.monero: case WalletType.dogecoin: + case WalletType.zcash: return true; case WalletType.wownero: case WalletType.haven: diff --git a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart index 99cecc5dc..4ab6e20b1 100644 --- a/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart +++ b/lib/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart @@ -17,6 +17,7 @@ import 'package:cake_wallet/wallet_type_utils.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; class DesktopWalletSelectionDropDown extends StatefulWidget { final WalletListViewModel walletListViewModel; @@ -48,6 +49,7 @@ class _DesktopWalletSelectionDropDownState extends State Image.asset( @@ -153,7 +155,7 @@ class _DesktopWalletSelectionDropDownState extends State TransactionRow( key: item.key, onTap: () => Navigator.of(context) .pushNamed(Routes.transactionDetails, arguments: transaction), + isShield: transaction.additionalInfo['autoShield'] == true, direction: transaction.direction, formattedDate: DateFormat('HH:mm').format(transaction.date), formattedAmount: item.formattedCryptoAmount, diff --git a/lib/src/screens/dashboard/widgets/menu_widget.dart b/lib/src/screens/dashboard/widgets/menu_widget.dart index 9c67c2ab9..d16319448 100644 --- a/lib/src/screens/dashboard/widgets/menu_widget.dart +++ b/lib/src/screens/dashboard/widgets/menu_widget.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:flutter_svg/svg.dart'; class MenuWidget extends StatefulWidget { MenuWidget(this.dashboardViewModel, Key? key); @@ -40,7 +41,8 @@ class MenuWidgetState extends State { this.wowneroIcon = Image.asset('assets/images/crypto/wownero.webp'), this.zanoIcon = Image.asset('assets/images/crypto/zano.webp'), this.decredIcon = Image.asset('assets/images/crypto/decred.webp'), - this.dogecoinIcon = Image.asset('assets/images/crypto/dogecoin.webp'); + this.dogecoinIcon = Image.asset('assets/images/crypto/dogecoin.webp'), + this.zcashIcon = Image.asset('assets/images/crypto/zcash.webp'); final largeScreen = 731; @@ -70,6 +72,7 @@ class MenuWidgetState extends State { Image zanoIcon; Image decredIcon; Image dogecoinIcon; + Image zcashIcon; @override void initState() { @@ -229,7 +232,7 @@ class MenuWidgetState extends State { ); } - Image _iconFor({required WalletType type}) { + Widget _iconFor({required WalletType type}) { switch (type) { case WalletType.monero: return moneroIcon; @@ -265,6 +268,8 @@ class MenuWidgetState extends State { return decredIcon; case WalletType.dogecoin: return dogecoinIcon; + case WalletType.zcash: + return zcashIcon; default: throw Exception('No icon for ${type.toString()}'); } diff --git a/lib/src/screens/dashboard/widgets/transaction_raw.dart b/lib/src/screens/dashboard/widgets/transaction_raw.dart index ed8ccd352..7abb0f420 100644 --- a/lib/src/screens/dashboard/widgets/transaction_raw.dart +++ b/lib/src/screens/dashboard/widgets/transaction_raw.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:cw_core/transaction_direction.dart'; +import 'package:flutter_svg/svg.dart'; class TransactionRow extends StatelessWidget { TransactionRow({ @@ -10,6 +11,7 @@ class TransactionRow extends StatelessWidget { required this.tags, required this.title, required this.onTap, + this.isShield = false, super.key, }); @@ -20,6 +22,7 @@ class TransactionRow extends StatelessWidget { final String formattedFiatAmount; final String title; final List tags; + final bool isShield; @override Widget build(BuildContext context) { @@ -39,7 +42,11 @@ class TransactionRow extends StatelessWidget { shape: BoxShape.circle, color: Theme.of(context).colorScheme.surfaceContainerHighest, ), - child: Image.asset(direction == TransactionDirection.incoming + child: (isShield) ? Padding( + padding: const EdgeInsets.all(8.0), + child: SvgPicture.asset('assets/images/tx_shield.svg'), + ) : + Image.asset(direction == TransactionDirection.incoming ? 'assets/images/down_arrow.png' : 'assets/images/up_arrow.png'), ), diff --git a/lib/src/screens/restore/wallet_restore_from_seed_form.dart b/lib/src/screens/restore/wallet_restore_from_seed_form.dart index 426ea662e..166a46cd1 100644 --- a/lib/src/screens/restore/wallet_restore_from_seed_form.dart +++ b/lib/src/screens/restore/wallet_restore_from_seed_form.dart @@ -286,7 +286,7 @@ class WalletRestoreFromSeedFormState extends State { 'wallet_restore_from_seed_blockheight_textfield_key', ), onHeightOrDateEntered: widget.onHeightOrDateEntered, - hasDatePicker: [WalletType.monero, WalletType.wownero].contains( + hasDatePicker: [WalletType.monero, WalletType.wownero, WalletType.zcash].contains( widget.type, ), walletType: widget.type, diff --git a/lib/src/screens/restore/wallet_restore_page.dart b/lib/src/screens/restore/wallet_restore_page.dart index d673319e5..14e2d5a74 100644 --- a/lib/src/screens/restore/wallet_restore_page.dart +++ b/lib/src/screens/restore/wallet_restore_page.dart @@ -146,7 +146,7 @@ class WalletRestorePage extends BasePage { bottom: MediaQuery.of(bottomSheetContext).viewInsets.bottom, ), child: AddPassphraseBottomSheet( - titleText: S.of(context).add_passphrase, + titleText: S.of(bottomSheetContext).add_passphrase, onRestoreButtonPressed: (passphrase) async { await _onPassphraseBottomSheetRestoreButtonPressed( passphrase, diff --git a/lib/src/screens/settings/other_settings_page.dart b/lib/src/screens/settings/other_settings_page.dart index 1c39d8f87..850f566b1 100644 --- a/lib/src/screens/settings/other_settings_page.dart +++ b/lib/src/screens/settings/other_settings_page.dart @@ -10,11 +10,16 @@ import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.da import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart'; import 'package:cake_wallet/utils/feature_flag.dart'; import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart'; +import 'package:cw_core/node.dart'; +import 'package:cw_core/pathForWallet.dart'; import 'package:cw_core/wallet_info.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:cw_core/db/sqlite.dart'; import 'package:flutter/material.dart'; import 'package:flutter_mobx/flutter_mobx.dart'; +import 'package:get_it/get_it.dart'; +import 'package:hive/hive.dart'; +import 'package:path/path.dart' as p; class OtherSettingsPage extends BasePage { OtherSettingsPage(this._otherSettingsViewModel) { diff --git a/lib/src/widgets/blockchain_height_widget.dart b/lib/src/widgets/blockchain_height_widget.dart index a835048fd..6473891d2 100644 --- a/lib/src/widgets/blockchain_height_widget.dart +++ b/lib/src/widgets/blockchain_height_widget.dart @@ -2,6 +2,7 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/src/widgets/standard_switch.dart'; import 'package:cake_wallet/utils/date_picker.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/wallet_type.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; @@ -189,10 +190,12 @@ class BlockchainHeightState extends State { height = decred!.heightByDate(date); } else if (widget.walletType == WalletType.monero) { height = monero!.getHeightByDate(date: date); - } else { - assert(widget.walletType == WalletType.wownero, - "unknown currency in BlockchainHeightWidget"); + } else if (widget.walletType == WalletType.wownero){ height = wownero!.getHeightByDate(date: date); + } else if (widget.walletType == WalletType.zcash) { + height = await zcash!.getHeightByDate(date); + } else { + throw Exception("unknown currency in BlockchainHeightWidget"); } } if (mounted) { diff --git a/lib/store/settings_store.dart b/lib/store/settings_store.dart index bac343ccb..0a57047ba 100644 --- a/lib/store/settings_store.dart +++ b/lib/store/settings_store.dart @@ -964,6 +964,10 @@ abstract class SettingsStoreBase with Store { if (node == null) { throw Exception('No node found for wallet type: ${walletType.toString()}'); } + if (node.type != walletType) { + throw Exception("Node is not valid for $walletType (found: ${node.type}"); + } + return node; } @@ -1202,6 +1206,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final zcashNodeId = sharedPreferences.getInt(PreferencesKey.currentZcashNodeIdKey); final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final dogecoinNodeId = sharedPreferences.getInt(PreferencesKey.currentDogecoinNodeIdKey); @@ -1236,6 +1241,8 @@ abstract class SettingsStoreBase with Store { nodeSource.values.firstWhereOrNull((e) => e.uriRaw == wowneroDefaultNodeUri); final zanoNode = nodeSource.get(zanoNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == zanoDefaultNodeUri); + final zcashNode = nodeSource.get(zcashNodeId) ?? + nodeSource.values.firstWhereOrNull((e) => e.uriRaw == zcashDefaultNodeUri); final dogecoinNode = nodeSource.get(dogecoinNodeId) ?? nodeSource.values.firstWhereOrNull((e) => e.uriRaw == dogecoinDefaultNodeUri); @@ -1330,6 +1337,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (zcashNode != null) { + nodes[WalletType.zcash] = zcashNode; + } + if (decredNode != null) { nodes[WalletType.decred] = decredNode; } @@ -1738,6 +1749,7 @@ abstract class SettingsStoreBase with Store { final tronNodeId = sharedPreferences.getInt(PreferencesKey.currentTronNodeIdKey); final wowneroNodeId = sharedPreferences.getInt(PreferencesKey.currentWowneroNodeIdKey); final zanoNodeId = sharedPreferences.getInt(PreferencesKey.currentZanoNodeIdKey); + final zcashNodeId = sharedPreferences.getInt(PreferencesKey.currentZcashNodeIdKey); final decredNodeId = sharedPreferences.getInt(PreferencesKey.currentDecredNodeIdKey); final dogecoinNodeId = sharedPreferences.getInt(PreferencesKey.currentDogecoinNodeIdKey); final moneroNode = nodeSource.get(nodeId); @@ -1754,6 +1766,7 @@ abstract class SettingsStoreBase with Store { final tronNode = nodeSource.get(tronNodeId); final wowneroNode = nodeSource.get(wowneroNodeId); final zanoNode = nodeSource.get(zanoNodeId); + final zcashNode = nodeSource.get(zcashNodeId); final decredNode = nodeSource.get(decredNodeId); final dogecoinNode = nodeSource.get(dogecoinNodeId); @@ -1813,6 +1826,10 @@ abstract class SettingsStoreBase with Store { nodes[WalletType.zano] = zanoNode; } + if (zcashNode != null) { + nodes[WalletType.zcash] = zcashNode; + } + if (decredNode != null) { nodes[WalletType.decred] = decredNode; } @@ -1969,8 +1986,13 @@ abstract class SettingsStoreBase with Store { case WalletType.dogecoin: await _sharedPreferences.setInt(PreferencesKey.currentDogecoinNodeIdKey, node.key as int); break; - default: + case WalletType.zcash: + await _sharedPreferences.setInt(PreferencesKey.currentZcashNodeIdKey, node.key as int); break; + case WalletType.none: + throw UnimplementedError(); + case WalletType.banano: + throw UnimplementedError(); } nodes[walletType] = node; diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index 67ead274b..1b327466d 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -22,7 +22,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class ExceptionHandler { static bool _hasError = false; static const _coolDownDurationInDays = - bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode) ? 0 : 7; + bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode || kProfileMode) ? 0 : 7; static File? _file; static Future _saveException(String? error, StackTrace? stackTrace, @@ -118,7 +118,7 @@ class ExceptionHandler { if (kDebugMode || kProfileMode) { FlutterError.presentError(errorDetails); - printV(errorDetails.toString()); + errorDetails.toString().split("\n").forEach(printV); return; } @@ -198,6 +198,7 @@ class ExceptionHandler { 'Exception: 6e00', 'Exception: 6985', 'Exception: 5515', + 'transport error' ]; static bool isLedgerError(Object exception) => diff --git a/lib/utils/feature_flag.dart b/lib/utils/feature_flag.dart index 81329cb6d..340eb0d1f 100644 --- a/lib/utils/feature_flag.dart +++ b/lib/utils/feature_flag.dart @@ -9,8 +9,8 @@ class FeatureFlag { static const bool isExolixEnabled = true; static const bool isBackgroundSyncEnabled = true; static bool get isInAppTorEnabled => CakeTor.instance is! CakeTorDisabled; - static const int verificationWordsCount = kDebugMode ? 0 : 2; - static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode); + static const int verificationWordsCount = kDebugMode || kProfileMode ? 0 : 2; + static const bool hasDevOptions = bool.fromEnvironment('hasDevOptions', defaultValue: kDebugMode || kProfileMode); static const bool hasBitcoinViewOnly = true; static const bool customBackgroundEnabled = false; static const bool duressPinEnabled = true; diff --git a/lib/utils/qr_util.dart b/lib/utils/qr_util.dart index 36ef209c2..076239511 100644 --- a/lib/utils/qr_util.dart +++ b/lib/utils/qr_util.dart @@ -41,6 +41,8 @@ String getQrImage(WalletType type, {int? selectedChainId}) { return 'assets/images/dcr_chain_qr.svg'; case WalletType.dogecoin: return 'assets/images/doge_chain_qr.svg'; + case WalletType.zcash: + return 'assets/images/zec_icon_qr.svg'; case WalletType.banano: case WalletType.haven: case WalletType.none: diff --git a/lib/view_model/advanced_privacy_settings_view_model.dart b/lib/view_model/advanced_privacy_settings_view_model.dart index c946d2eef..6a491f986 100644 --- a/lib/view_model/advanced_privacy_settings_view_model.dart +++ b/lib/view_model/advanced_privacy_settings_view_model.dart @@ -57,6 +57,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { case WalletType.arbitrum: case WalletType.solana: case WalletType.tron: + case WalletType.zcash: return true; case WalletType.bitcoin: @@ -103,6 +104,7 @@ abstract class AdvancedPrivacySettingsViewModelBase with Store { WalletType.wownero, WalletType.zano, WalletType.dogecoin, + WalletType.zcash, ].contains(type); @computed diff --git a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart index 2f4ccc502..8148e647c 100644 --- a/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart +++ b/lib/view_model/cake_pay/cake_pay_buy_card_view_model.dart @@ -115,6 +115,8 @@ abstract class CakePayBuyCardViewModelBase with Store { ]; case WalletType.monero: return [CakePayPaymentMethod.XMR]; + case WalletType.zcash: + return [CakePayPaymentMethod.ZEC]; default: return const []; } @@ -139,11 +141,14 @@ abstract class CakePayBuyCardViewModelBase with Store { @action Future createOrder() async { - if (walletType != WalletType.bitcoin && - walletType != WalletType.monero && - walletType != WalletType.litecoin) { + if (![ + WalletType.bitcoin, + WalletType.monero, + WalletType.litecoin, + WalletType.zcash, + ].contains(walletType)) { sendViewModel.state = - FailureState('Unsupported wallet type, please use Bitcoin, Monero, or Litecoin.'); + FailureState('Unsupported wallet type, please use Bitcoin, Monero, Litecoin or Zcash.'); } try { order = await _cakePayService.createOrder( diff --git a/lib/view_model/dashboard/balance_view_model.dart b/lib/view_model/dashboard/balance_view_model.dart index 88b651e63..36987916d 100644 --- a/lib/view_model/dashboard/balance_view_model.dart +++ b/lib/view_model/dashboard/balance_view_model.dart @@ -320,6 +320,7 @@ abstract class BalanceViewModelBase with Store { case WalletType.wownero: case WalletType.zano: case WalletType.decred: + case WalletType.zcash: return true; default: return false; diff --git a/lib/view_model/dashboard/dashboard_view_model.dart b/lib/view_model/dashboard/dashboard_view_model.dart index 671c20f57..d01c39f4e 100644 --- a/lib/view_model/dashboard/dashboard_view_model.dart +++ b/lib/view_model/dashboard/dashboard_view_model.dart @@ -957,6 +957,7 @@ abstract class DashboardViewModelBase with Store { return true; case WalletType.zano: case WalletType.haven: + case WalletType.zcash: case WalletType.none: return false; } @@ -1284,6 +1285,7 @@ abstract class DashboardViewModelBase with Store { if (wallet.chainId == 1 && tx.evmSignatureName == 'approval') return ' (${tx.evmSignatureName})'; + return ''; } diff --git a/lib/view_model/dashboard/home_settings_view_model.dart b/lib/view_model/dashboard/home_settings_view_model.dart index 020733605..dc6d54dd4 100644 --- a/lib/view_model/dashboard/home_settings_view_model.dart +++ b/lib/view_model/dashboard/home_settings_view_model.dart @@ -231,6 +231,7 @@ abstract class HomeSettingsViewModelBase with Store { case WalletType.bitcoinCash: case WalletType.decred: case WalletType.dogecoin: + case WalletType.zcash: return false; } diff --git a/lib/view_model/dashboard/receive_option_view_model.dart b/lib/view_model/dashboard/receive_option_view_model.dart index 8aab2736d..c3d4f20ff 100644 --- a/lib/view_model/dashboard/receive_option_view_model.dart +++ b/lib/view_model/dashboard/receive_option_view_model.dart @@ -1,4 +1,5 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/receive_page_option.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_type.dart'; @@ -16,7 +17,9 @@ abstract class ReceiveOptionViewModelBase with Store { ? bitcoin!.getSelectedAddressType(_wallet) : (_wallet.type == WalletType.decred && _wallet.isTestnet) ? ReceivePageOption.testnet - : ReceivePageOption.mainnet), + : _wallet.type == WalletType.zcash + ? zcash!.getSelectedAddressType(_wallet) + : ReceivePageOption.mainnet), _options = [] { final walletType = _wallet.type; switch (walletType) { @@ -46,6 +49,12 @@ abstract class ReceiveOptionViewModelBase with Store { _options = ReceivePageOptions; } break; + case WalletType.zcash: + _options = [ + ...zcash!.getZcashReceivePageOptions(_wallet), + ...ReceivePageOptions.where((element) => element != ReceivePageOption.mainnet) + ]; + break; default: _options = ReceivePageOptions; } diff --git a/lib/view_model/dashboard/transaction_list_item.dart b/lib/view_model/dashboard/transaction_list_item.dart index da86968e3..8aef7a8b7 100644 --- a/lib/view_model/dashboard/transaction_list_item.dart +++ b/lib/view_model/dashboard/transaction_list_item.dart @@ -9,6 +9,7 @@ import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/transaction_direction.dart'; import 'package:cw_core/transaction_info.dart'; @@ -52,6 +53,9 @@ class TransactionListItem extends ActionListItem with Keyable { } String get formattedTitle { + if (transaction.additionalInfo['autoShield'] == true) { + return "Autoshield"; + } if (transaction.direction == TransactionDirection.incoming) { return S.current.received; } @@ -217,6 +221,11 @@ class TransactionListItem extends ActionListItem with Keyable { cryptoAmount: decred!.formatterDecredAmountToDouble(amount: transaction.amount), price: price); break; + case WalletType.zcash: + amount = calculateFiatAmountRaw( + cryptoAmount: zcash!.formatterZcashAmountToDouble(amount: BigInt.from(transaction.amount)), + price: price); + case WalletType.none: case WalletType.banano: case WalletType.haven: diff --git a/lib/view_model/exchange/exchange_view_model.dart b/lib/view_model/exchange/exchange_view_model.dart index 58b49157b..ac6ba2d6a 100644 --- a/lib/view_model/exchange/exchange_view_model.dart +++ b/lib/view_model/exchange/exchange_view_model.dart @@ -922,6 +922,10 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with depositCurrency = CryptoCurrency.dcr; receiveCurrency = CryptoCurrency.xmr; break; + case WalletType.zcash: + depositCurrency = CryptoCurrency.zec; + receiveCurrency = CryptoCurrency.xmr; + break; case WalletType.none: break; } diff --git a/lib/view_model/node_list/node_create_or_edit_view_model.dart b/lib/view_model/node_list/node_create_or_edit_view_model.dart index dffa690f8..c833b37fa 100644 --- a/lib/view_model/node_list/node_create_or_edit_view_model.dart +++ b/lib/view_model/node_list/node_create_or_edit_view_model.dart @@ -97,6 +97,7 @@ abstract class NodeCreateOrEditViewModelBase with Store { case WalletType.dogecoin: case WalletType.zano: case WalletType.decred: + case WalletType.zcash: return false; } } diff --git a/lib/view_model/send/fees_view_model.dart b/lib/view_model/send/fees_view_model.dart index 588bdc5fb..6bda3396b 100644 --- a/lib/view_model/send/fees_view_model.dart +++ b/lib/view_model/send/fees_view_model.dart @@ -5,6 +5,7 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; import 'package:cake_wallet/core/wallet_change_listener_view_model.dart'; import 'package:cake_wallet/evm/evm.dart'; import 'package:cake_wallet/monero/monero.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cake_wallet/store/app_store.dart'; import 'package:cake_wallet/view_model/dashboard/balance_view_model.dart'; import 'package:cw_core/crypto_currency.dart'; @@ -102,6 +103,7 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.solana: case WalletType.tron: case WalletType.arbitrum: + case WalletType.zcash: return false; } } @@ -185,6 +187,9 @@ abstract class FeesViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.zano: _settingsStore.setPriority(wallet.type, monero!.getMoneroTransactionPriorityAutomatic()); break; + case WalletType.zcash: + _settingsStore.setPriority[wallet.type] = zcash!.getZcashTransactionPriorityAutomatic(); + break; case WalletType.bitcoin: _settingsStore.setPriority(wallet.type, bitcoin!.getBitcoinTransactionPriorityMedium()); break; diff --git a/lib/view_model/send/output.dart b/lib/view_model/send/output.dart index 233011579..16864d450 100644 --- a/lib/view_model/send/output.dart +++ b/lib/view_model/send/output.dart @@ -18,6 +18,7 @@ import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/balance.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/format_fixed.dart'; @@ -125,6 +126,9 @@ abstract class OutputBase with Store { _amount = zano! .formatterParseAmount(amount: _cryptoAmount, currency: cryptoCurrencyHandler()); break; + case WalletType.zcash: + _amount = zcash!.formatterZcashParseAmount(_cryptoAmount); + break; case WalletType.none: case WalletType.haven: case WalletType.nano: @@ -204,6 +208,10 @@ abstract class OutputBase with Store { estimatedFee = tron!.getTronTRC20EstimatedFee(_wallet).toString(); } break; + + case WalletType.zcash: + estimatedFee = zcash!.formatterZcashAmountToDouble(amount: BigInt.from(fee)).toString(); + break; /// EVMs case WalletType.ethereum: diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 55a604bac..a4697587e 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -45,6 +45,7 @@ import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_list_view_model.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/crypto_currency.dart'; import 'package:cw_core/erc20_token.dart'; import 'package:cw_core/exceptions.dart'; @@ -803,8 +804,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor wallet.type != WalletType.banano && wallet.type != WalletType.solana && wallet.type != WalletType.tron && - wallet.type != WalletType.arbitrum) { - // Wallet type is generic for all EVM chains, so we need to check the chainId + wallet.type != WalletType.arbitrum && + wallet.type != WalletType.zcash) { throw Exception('Priority is null for wallet type: ${wallet.type}'); } @@ -861,6 +862,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor case WalletType.decred: this.coinTypeToSpendFrom = UnspentCoinType.any; return decred!.createDecredTransactionCredentials(outputs, priority!); + case WalletType.zcash: + return zcash!.createZcashTransactionCredentials( + outputs, + currency: selectedCryptoCurrency, + // priority: priority, + ); default: throw Exception('Unexpected wallet type: ${wallet.type}'); } diff --git a/lib/view_model/settings/other_settings_view_model.dart b/lib/view_model/settings/other_settings_view_model.dart index 3977d4f77..39c23f3a1 100644 --- a/lib/view_model/settings/other_settings_view_model.dart +++ b/lib/view_model/settings/other_settings_view_model.dart @@ -1,5 +1,6 @@ import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/entities/priority_for_wallet_type.dart'; +import 'package:cake_wallet/monero/monero.dart'; import 'package:cake_wallet/store/settings_store.dart'; import 'package:cake_wallet/utils/package_info.dart'; import 'package:cake_wallet/view_model/send/send_view_model.dart'; diff --git a/lib/view_model/transaction_details_view_model.dart b/lib/view_model/transaction_details_view_model.dart index 489057f84..c5e49436e 100644 --- a/lib/view_model/transaction_details_view_model.dart +++ b/lib/view_model/transaction_details_view_model.dart @@ -95,6 +95,8 @@ abstract class TransactionDetailsViewModelBase with Store { case WalletType.dogecoin: _addDogecoinListItems(tx, dateFormat); break; + case WalletType.zcash: + _addZcashListItems(tx, dateFormat); case WalletType.none: case WalletType.banano: break; @@ -211,7 +213,8 @@ abstract class TransactionDetailsViewModelBase with Store { return 'https://${wallet.isTestnet ? "testnet" : "dcrdata"}.decred.org/tx/${txId.split(':')[0]}'; case WalletType.dogecoin: return 'https://blockchair.com/dogecoin/transaction/${txId}'; - + case WalletType.zcash: + return 'https://blockchair.com/zcash/transaction/${txId}'; case WalletType.none: return ''; } @@ -257,6 +260,8 @@ abstract class TransactionDetailsViewModelBase with Store { return S.current.view_transaction_on + 'basescan.org'; case WalletType.arbitrum: return S.current.view_transaction_on + 'arbiscan.io'; + case WalletType.zcash: + return S.current.view_transaction_on + 'blockchair.com'; case WalletType.none: return ''; } @@ -755,6 +760,57 @@ abstract class TransactionDetailsViewModelBase with Store { items.addAll(_items); } + void _addZcashListItems(TransactionInfo tx, DateFormat dateFormat) { + final memo = tx.additionalInfo['memo'] as String?; + + final _items = [ + StandartListItem( + title: S.current.transaction_details_transaction_id, + value: tx.txHash, + key: ValueKey('standard_list_item_transaction_details_id_key'), + ), + if (tx.to != null) + StandartListItem( + title: S.current.transaction_details_recipient_address, + value: tx.to!, + key: ValueKey('standard_list_item_transaction_details_recipient_address_key'), + ), + StandartListItem( + title: S.current.transaction_details_date, + value: dateFormat.format(tx.date), + key: ValueKey('standard_list_item_transaction_details_date_key'), + ), + StandartListItem( + title: S.current.confirmations, + value: tx.confirmations.toString(), + key: ValueKey('standard_list_item_transaction_confirmations_key'), + ), + StandartListItem( + title: S.current.transaction_details_height, + value: '${tx.height ?? 0}', + key: ValueKey('standard_list_item_transaction_details_height_key'), + ), + StandartListItem( + title: S.current.transaction_details_amount, + value: tx.amountFormatted(), + key: ValueKey('standard_list_item_transaction_details_amount_key'), + ), + if (tx.feeFormatted() != null && tx.feeFormatted()!.isNotEmpty) + StandartListItem( + title: S.current.transaction_details_fee, + value: tx.feeFormatted()!, + key: ValueKey('standard_list_item_transaction_details_fee_key'), + ), + if (memo != null && memo.isNotEmpty) + StandartListItem( + title: S.current.memo, + value: memo, + key: ValueKey('standard_list_item_transaction_details_memo_key'), + ), + ]; + items.addAll(_items); + } + void _addDogecoinListItems(TransactionInfo tx, DateFormat dateFormat) { final _items = [ StandartListItem( diff --git a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart index 1ceb9849e..1ad9c927b 100644 --- a/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart +++ b/lib/view_model/wallet_address_list/wallet_address_list_view_model.dart @@ -28,6 +28,7 @@ import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart'; import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/amount_converter.dart'; import 'package:cw_core/currency.dart'; import 'package:cw_core/currency_for_wallet_type.dart'; @@ -165,6 +166,8 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo return BaseURI(amount: amount, address: address.address); case WalletType.arbitrum: return ArbitrumURI(amount: amount, address: address.address); + case WalletType.zcash: + return ZcashURI(amount: amount, address: address.address); case WalletType.none: throw Exception('Unexpected type: ${type.toString()}'); } @@ -313,6 +316,14 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo }); } + if (wallet.type == WalletType.zcash) { + final addrInfos = zcash!.getAddressInfos(wallet); + addrInfos.forEach((info) { + addressList.add( + new WalletAddressListItem(isPrimary: false, address: info.address, name: info.label)); + }); + } + for (var i = 0; i < addressList.length; i++) { if (!(addressList[i] is WalletAddressListItem)) continue; (addressList[i] as WalletAddressListItem).isHidden = wallet.walletAddresses.hiddenAddresses @@ -409,6 +420,7 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo WalletType.litecoin, WalletType.decred, WalletType.dogecoin, + WalletType.zcash, ].contains(wallet.type); @computed @@ -528,6 +540,9 @@ abstract class WalletAddressListViewModelBase extends WalletChangeListenerViewMo if ([WalletType.bitcoin, WalletType.litecoin].contains(wallet.type)) { await bitcoin!.setAddressType(wallet, option); } + if (wallet.type == WalletType.zcash) { + await zcash!.setAddressType(wallet, option); + } } void _init() { diff --git a/lib/view_model/wallet_keys_view_model.dart b/lib/view_model/wallet_keys_view_model.dart index a09a9c59d..c8257d8c4 100644 --- a/lib/view_model/wallet_keys_view_model.dart +++ b/lib/view_model/wallet_keys_view_model.dart @@ -144,6 +144,7 @@ abstract class WalletKeysViewModelBase with Store { case WalletType.base: case WalletType.arbitrum: case WalletType.solana: + case WalletType.zcash: case WalletType.tron: items.addAll([ if (_wallet.privateKey != null) @@ -285,6 +286,8 @@ abstract class WalletKeysViewModelBase with Store { return 'decred-wallet'; case WalletType.dogecoin: return 'dogecoin-wallet'; + case WalletType.zcash: + return 'zcash-wallet'; default: throw Exception('Unexpected wallet type: ${_wallet.type.toString()}'); } diff --git a/lib/view_model/wallet_new_vm.dart b/lib/view_model/wallet_new_vm.dart index 26ac3569f..8e077214a 100644 --- a/lib/view_model/wallet_new_vm.dart +++ b/lib/view_model/wallet_new_vm.dart @@ -6,6 +6,7 @@ import 'package:cake_wallet/bitcoin_cash/bitcoin_cash.dart'; import 'package:cake_wallet/solana/solana.dart'; import 'package:cake_wallet/tron/tron.dart'; import 'package:cake_wallet/wownero/wownero.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:mobx/mobx.dart'; import 'package:cake_wallet/bitcoin/bitcoin.dart'; import 'package:cake_wallet/core/wallet_creation_service.dart'; @@ -138,6 +139,12 @@ abstract class WalletNewVMBase extends WalletCreationVM with Store { password: walletPassword, passphrase: passphrase, ); + case WalletType.zcash: + return zcash!.createZcashNewWalletCredentials( + name: name, + password: walletPassword, + passphrase: passphrase, + ); case WalletType.decred: return decred!.createDecredNewWalletCredentials(name: name); case WalletType.none: diff --git a/lib/view_model/wallet_restore_view_model.dart b/lib/view_model/wallet_restore_view_model.dart index 0227b8954..d7e668f72 100644 --- a/lib/view_model/wallet_restore_view_model.dart +++ b/lib/view_model/wallet_restore_view_model.dart @@ -19,6 +19,7 @@ import 'package:cake_wallet/view_model/seed_settings_view_model.dart'; import 'package:cake_wallet/view_model/wallet_creation_vm.dart'; import 'package:cake_wallet/wownero/wownero.dart'; import 'package:cake_wallet/zano/zano.dart'; +import 'package:cake_wallet/zcash/zcash.dart'; import 'package:cw_core/wallet_base.dart'; import 'package:cw_core/wallet_credentials.dart'; import 'package:cw_core/wallet_info.dart'; @@ -59,10 +60,12 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { break; case WalletType.bitcoinCash: case WalletType.zano: - case WalletType.none: + case WalletType.zcash: case WalletType.dogecoin: availableModes = [WalletRestoreMode.seed]; break; + case WalletType.none: + availableModes = []; } walletCreationService.changeWalletType(type: type); if (restoredWallet != null) { @@ -79,9 +82,13 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { late final bool hasSeedLanguageSelector = [WalletType.monero, WalletType.haven, WalletType.wownero].contains(type); - late final bool hasBlockchainHeightSelector = - [WalletType.monero, WalletType.haven, WalletType.wownero].contains(type); - + late final bool hasBlockchainHeightSelector = [ + WalletType.monero, + WalletType.haven, + WalletType.wownero, + WalletType.zcash + ].contains(type); + late final bool hasRestoreFromPrivateKey = [ WalletType.ethereum, WalletType.polygon, @@ -202,9 +209,17 @@ abstract class WalletRestoreViewModelBase extends WalletCreationVM with Store { ); case WalletType.decred: return decred!.createDecredRestoreWalletFromSeedCredentials( + name: name, + mnemonic: seed, + password: password, + ); + case WalletType.zcash: + return zcash!.createZcashRestoreWalletFromSeedCredentials( name: name, mnemonic: seed, password: password, + passphrase: passphrase, + height: height, ); case WalletType.none: case WalletType.haven: diff --git a/lib/view_model/wallet_seed_view_model.dart b/lib/view_model/wallet_seed_view_model.dart index 3cf05bf34..70813c35a 100644 --- a/lib/view_model/wallet_seed_view_model.dart +++ b/lib/view_model/wallet_seed_view_model.dart @@ -84,7 +84,7 @@ abstract class WalletSeedViewModelBase with Store { bool shouldPerformVerification() { bool isCI = bool.fromEnvironment('CI_BUILD', defaultValue: false); - bool isDebug = kDebugMode; + bool isDebug = kDebugMode || kProfileMode; if (isDebug && !isCI) { printV("Skipping verification in debug mode - $isDebug (and when it's not in CI - $isCI)."); diff --git a/lib/zcash/cw_zcash.dart b/lib/zcash/cw_zcash.dart new file mode 100644 index 000000000..4d082a1a0 --- /dev/null +++ b/lib/zcash/cw_zcash.dart @@ -0,0 +1,2263 @@ +part of 'zcash.dart'; + +class CWZcash extends Zcash { + @override + WalletCredentials createZcashNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + String? password, + String? mnemonic, + required String? passphrase}) { + return ZcashNewWalletCredentials( + name: name, + passphrase: passphrase, + password: password, + mnemonic: mnemonic, + ); + } + + @override + WalletCredentials createZcashRestoreWalletFromPrivateKey( + {required String name, + required String privateKey, + required String password}) { + throw UnimplementedError(); + } + + @override + WalletCredentials createZcashRestoreWalletFromSeedCredentials( + {required String name, + required String mnemonic, + required String password, + String? passphrase, + required int? height}) { + return ZcashFromSeedWalletCredentials( + name: name, seed: mnemonic, passphrase: passphrase, password: password, height: height); + } + + @override + Object createZcashTransactionCredentials(List outputs, + {required CryptoCurrency currency, + int? feeRate, + TransactionPriority? priority}) { + final txPriority = (priority ?? getZcashTransactionPriorityAutomatic()) + as MoneroTransactionPriority; + return ZcashTransactionCredentials( + outputs: outputs + .map((out) => OutputInfo( + fiatAmount: out.fiatAmount, + cryptoAmount: out.cryptoAmount, + address: out.address, + note: out.note, + sendAll: out.sendAll, + extractedAddress: out.extractedAddress, + isParsedAddress: out.isParsedAddress, + formattedCryptoAmount: out.formattedCryptoAmount)) + .toList(), + priority: txPriority, + currency: currency, + ); + } + + @override + Object createZcashTransactionCredentialsRaw(List outputs, + {required CryptoCurrency currency, required int feeRate}) { + return ZcashTransactionCredentials( + outputs: outputs, + priority: + getZcashTransactionPriorityAutomatic() as MoneroTransactionPriority, + currency: currency, + ); + } + + @override + WalletService createZcashWalletService(bool isDirect) { + return ZcashWalletService(); + } + + @override + double formatterZcashAmountToDouble({TransactionInfo? transaction, BigInt? amount}) { + return cryptoAmountToDouble(amount: amount?.toInt()??0, divider: 1e8); + } + + @override + int formatterZcashParseAmount(String amount) { + return CryptoCurrency.zec.parseAmount(amount).toInt(); + } + + @override + String formatterZcashAmountToString({required int amount}) { + return CryptoCurrency.zec.formatAmount(BigInt.from(amount)); + } + + @override + String getAddress( + WalletBase, + TransactionInfo> + wallet) { + final zcashWallet = wallet as ZcashWallet; + return zcashWallet.walletAddresses.address; + } + + @override + String getPrivateKey( + WalletBase, + TransactionInfo> + wallet) { + final zcashWallet = wallet as ZcashWallet; + final seed = zcashWallet.seed; + return seed ?? ''; + } + + @override + String getPublicKey( + WalletBase, + TransactionInfo> + wallet) { + return getAddress(wallet); + } + + @override + Map getKeys(Object wallet) { + final zcashWallet = wallet as ZcashWallet; + final seed = zcashWallet.seed; + final address = zcashWallet.walletAddresses.address; + + return { + if (seed != null) 'seed': seed, + 'primaryAddress': address, + }; + } + + @override + List getZcashWordList(String language) { + return wordList; + } + + @override + List getAddressInfos(Object wallet) { + final zcashWallet = wallet as ZcashWallet; + return (zcashWallet.walletAddresses as ZcashWalletAddresses) + .getAddressInfos(); + } + + @override + TransactionPriority getDefaultTransactionPriority() { + return MoneroTransactionPriority.automatic; + } + + @override + TransactionPriority getZcashTransactionPriorityAutomatic() { + return MoneroTransactionPriority.automatic; + } + + @override + TransactionPriority deserializeZcashTransactionPriority({required int raw}) { + return MoneroTransactionPriority.deserialize(raw: raw); + } + + @override + List getTransactionPriorities() { + return MoneroTransactionPriority.all; + } + + @override + List getZcashReceivePageOptions(Object wallet) { + // final zcashWallet = wallet as ZcashWallet; + return ZcashReceivePageOption.all; + } + + @override + ReceivePageOption getSelectedAddressType(Object wallet) { + final zcashWallet = wallet as ZcashWallet; + final t = (zcashWallet.walletAddresses as ZcashWalletAddresses).walletInfo.addressPageType??""; + return ZcashReceivePageOption.fromType(ZcashReceivePageOption.typeFromString(t)); + } + + @override + ZcashAddressType getZcashAddressType(ReceivePageOption option) { + switch (option) { + case ZcashReceivePageOption.unified: + return ZcashAddressType.unifiedType; + case ZcashReceivePageOption.transparent: + return ZcashAddressType.transparent; + case ZcashReceivePageOption.shieldedSapling: + return ZcashAddressType.shieldedSapling; + case ZcashReceivePageOption.shieldedOrchard: + return ZcashAddressType.shieldedOrchard; + default: + throw Exception("Unknown ReceivePageOption!"); + } + } + Future setAddressType(Object wallet, dynamic option) async { + final zcashWallet = wallet as ZcashWallet; + await (zcashWallet.walletAddresses as ZcashWalletAddresses).setAddressType(option as ZcashAddressType); + } + + @override + ZcashAddressType getOptionToType(ReceivePageOption option) { + return (option as ZcashReceivePageOption).toType(); + } + + @override + void unlockDatabase(String password) { + return ZcashWalletBase.unlockDatabase(password); + } + + @override + Future getHeightByDate(DateTime date) { + return ZcashWalletBase.getHeightByDate(date); + } +} + +const wordList = [ + 'abandon', + 'ability', + 'able', + 'about', + 'above', + 'absent', + 'absorb', + 'abstract', + 'absurd', + 'abuse', + 'access', + 'accident', + 'account', + 'accuse', + 'achieve', + 'acid', + 'acoustic', + 'acquire', + 'across', + 'act', + 'action', + 'actor', + 'actress', + 'actual', + 'adapt', + 'add', + 'addict', + 'address', + 'adjust', + 'admit', + 'adult', + 'advance', + 'advice', + 'aerobic', + 'affair', + 'afford', + 'afraid', + 'again', + 'age', + 'agent', + 'agree', + 'ahead', + 'aim', + 'air', + 'airport', + 'aisle', + 'alarm', + 'album', + 'alcohol', + 'alert', + 'alien', + 'all', + 'alley', + 'allow', + 'almost', + 'alone', + 'alpha', + 'already', + 'also', + 'alter', + 'always', + 'amateur', + 'amazing', + 'among', + 'amount', + 'amused', + 'analyst', + 'anchor', + 'ancient', + 'anger', + 'angle', + 'angry', + 'animal', + 'ankle', + 'announce', + 'annual', + 'another', + 'answer', + 'antenna', + 'antique', + 'anxiety', + 'any', + 'apart', + 'apology', + 'appear', + 'apple', + 'approve', + 'april', + 'arch', + 'arctic', + 'area', + 'arena', + 'argue', + 'arm', + 'armed', + 'armor', + 'army', + 'around', + 'arrange', + 'arrest', + 'arrive', + 'arrow', + 'art', + 'artefact', + 'artist', + 'artwork', + 'ask', + 'aspect', + 'assault', + 'asset', + 'assist', + 'assume', + 'asthma', + 'athlete', + 'atom', + 'attack', + 'attend', + 'attitude', + 'attract', + 'auction', + 'audit', + 'august', + 'aunt', + 'author', + 'auto', + 'autumn', + 'average', + 'avocado', + 'avoid', + 'awake', + 'aware', + 'away', + 'awesome', + 'awful', + 'awkward', + 'axis', + 'baby', + 'bachelor', + 'bacon', + 'badge', + 'bag', + 'balance', + 'balcony', + 'ball', + 'bamboo', + 'banana', + 'banner', + 'bar', + 'barely', + 'bargain', + 'barrel', + 'base', + 'basic', + 'basket', + 'battle', + 'beach', + 'bean', + 'beauty', + 'because', + 'become', + 'beef', + 'before', + 'begin', + 'behave', + 'behind', + 'believe', + 'below', + 'belt', + 'bench', + 'benefit', + 'best', + 'betray', + 'better', + 'between', + 'beyond', + 'bicycle', + 'bid', + 'bike', + 'bind', + 'biology', + 'bird', + 'birth', + 'bitter', + 'black', + 'blade', + 'blame', + 'blanket', + 'blast', + 'bleak', + 'bless', + 'blind', + 'blood', + 'blossom', + 'blouse', + 'blue', + 'blur', + 'blush', + 'board', + 'boat', + 'body', + 'boil', + 'bomb', + 'bone', + 'bonus', + 'book', + 'boost', + 'border', + 'boring', + 'borrow', + 'boss', + 'bottom', + 'bounce', + 'box', + 'boy', + 'bracket', + 'brain', + 'brand', + 'brass', + 'brave', + 'bread', + 'breeze', + 'brick', + 'bridge', + 'brief', + 'bright', + 'bring', + 'brisk', + 'broccoli', + 'broken', + 'bronze', + 'broom', + 'brother', + 'brown', + 'brush', + 'bubble', + 'buddy', + 'budget', + 'buffalo', + 'build', + 'bulb', + 'bulk', + 'bullet', + 'bundle', + 'bunker', + 'burden', + 'burger', + 'burst', + 'bus', + 'business', + 'busy', + 'butter', + 'buyer', + 'buzz', + 'cabbage', + 'cabin', + 'cable', + 'cactus', + 'cage', + 'cake', + 'call', + 'calm', + 'camera', + 'camp', + 'can', + 'canal', + 'cancel', + 'candy', + 'cannon', + 'canoe', + 'canvas', + 'canyon', + 'capable', + 'capital', + 'captain', + 'car', + 'carbon', + 'card', + 'cargo', + 'carpet', + 'carry', + 'cart', + 'case', + 'cash', + 'casino', + 'castle', + 'casual', + 'cat', + 'catalog', + 'catch', + 'category', + 'cattle', + 'caught', + 'cause', + 'caution', + 'cave', + 'ceiling', + 'celery', + 'cement', + 'census', + 'century', + 'cereal', + 'certain', + 'chair', + 'chalk', + 'champion', + 'change', + 'chaos', + 'chapter', + 'charge', + 'chase', + 'chat', + 'cheap', + 'check', + 'cheese', + 'chef', + 'cherry', + 'chest', + 'chicken', + 'chief', + 'child', + 'chimney', + 'choice', + 'choose', + 'chronic', + 'chuckle', + 'chunk', + 'churn', + 'cigar', + 'cinnamon', + 'circle', + 'citizen', + 'city', + 'civil', + 'claim', + 'clap', + 'clarify', + 'claw', + 'clay', + 'clean', + 'clerk', + 'clever', + 'click', + 'client', + 'cliff', + 'climb', + 'clinic', + 'clip', + 'clock', + 'clog', + 'close', + 'cloth', + 'cloud', + 'clown', + 'club', + 'clump', + 'cluster', + 'clutch', + 'coach', + 'coast', + 'coconut', + 'code', + 'coffee', + 'coil', + 'coin', + 'collect', + 'color', + 'column', + 'combine', + 'come', + 'comfort', + 'comic', + 'common', + 'company', + 'concert', + 'conduct', + 'confirm', + 'congress', + 'connect', + 'consider', + 'control', + 'convince', + 'cook', + 'cool', + 'copper', + 'copy', + 'coral', + 'core', + 'corn', + 'correct', + 'cost', + 'cotton', + 'couch', + 'country', + 'couple', + 'course', + 'cousin', + 'cover', + 'coyote', + 'crack', + 'cradle', + 'craft', + 'cram', + 'crane', + 'crash', + 'crater', + 'crawl', + 'crazy', + 'cream', + 'credit', + 'creek', + 'crew', + 'cricket', + 'crime', + 'crisp', + 'critic', + 'crop', + 'cross', + 'crouch', + 'crowd', + 'crucial', + 'cruel', + 'cruise', + 'crumble', + 'crunch', + 'crush', + 'cry', + 'crystal', + 'cube', + 'culture', + 'cup', + 'cupboard', + 'curious', + 'current', + 'curtain', + 'curve', + 'cushion', + 'custom', + 'cute', + 'cycle', + 'dad', + 'damage', + 'damp', + 'dance', + 'danger', + 'daring', + 'dash', + 'daughter', + 'dawn', + 'day', + 'deal', + 'debate', + 'debris', + 'decade', + 'december', + 'decide', + 'decline', + 'decorate', + 'decrease', + 'deer', + 'defense', + 'define', + 'defy', + 'degree', + 'delay', + 'deliver', + 'demand', + 'demise', + 'denial', + 'dentist', + 'deny', + 'depart', + 'depend', + 'deposit', + 'depth', + 'deputy', + 'derive', + 'describe', + 'desert', + 'design', + 'desk', + 'despair', + 'destroy', + 'detail', + 'detect', + 'develop', + 'device', + 'devote', + 'diagram', + 'dial', + 'diamond', + 'diary', + 'dice', + 'diesel', + 'diet', + 'differ', + 'digital', + 'dignity', + 'dilemma', + 'dinner', + 'dinosaur', + 'direct', + 'dirt', + 'disagree', + 'discover', + 'disease', + 'dish', + 'dismiss', + 'disorder', + 'display', + 'distance', + 'divert', + 'divide', + 'divorce', + 'dizzy', + 'doctor', + 'document', + 'dog', + 'doll', + 'dolphin', + 'domain', + 'donate', + 'donkey', + 'donor', + 'door', + 'dose', + 'double', + 'dove', + 'draft', + 'dragon', + 'drama', + 'drastic', + 'draw', + 'dream', + 'dress', + 'drift', + 'drill', + 'drink', + 'drip', + 'drive', + 'drop', + 'drum', + 'dry', + 'duck', + 'dumb', + 'dune', + 'during', + 'dust', + 'dutch', + 'duty', + 'dwarf', + 'dynamic', + 'eager', + 'eagle', + 'early', + 'earn', + 'earth', + 'easily', + 'east', + 'easy', + 'echo', + 'ecology', + 'economy', + 'edge', + 'edit', + 'educate', + 'effort', + 'egg', + 'eight', + 'either', + 'elbow', + 'elder', + 'electric', + 'elegant', + 'element', + 'elephant', + 'elevator', + 'elite', + 'else', + 'embark', + 'embody', + 'embrace', + 'emerge', + 'emotion', + 'employ', + 'empower', + 'empty', + 'enable', + 'enact', + 'end', + 'endless', + 'endorse', + 'enemy', + 'energy', + 'enforce', + 'engage', + 'engine', + 'enhance', + 'enjoy', + 'enlist', + 'enough', + 'enrich', + 'enroll', + 'ensure', + 'enter', + 'entire', + 'entry', + 'envelope', + 'episode', + 'equal', + 'equip', + 'era', + 'erase', + 'erode', + 'erosion', + 'error', + 'erupt', + 'escape', + 'essay', + 'essence', + 'estate', + 'eternal', + 'ethics', + 'evidence', + 'evil', + 'evoke', + 'evolve', + 'exact', + 'example', + 'excess', + 'exchange', + 'excite', + 'exclude', + 'excuse', + 'execute', + 'exercise', + 'exhaust', + 'exhibit', + 'exile', + 'exist', + 'exit', + 'exotic', + 'expand', + 'expect', + 'expire', + 'explain', + 'expose', + 'express', + 'extend', + 'extra', + 'eye', + 'eyebrow', + 'fabric', + 'face', + 'faculty', + 'fade', + 'faint', + 'faith', + 'fall', + 'false', + 'fame', + 'family', + 'famous', + 'fan', + 'fancy', + 'fantasy', + 'farm', + 'fashion', + 'fat', + 'fatal', + 'father', + 'fatigue', + 'fault', + 'favorite', + 'feature', + 'february', + 'federal', + 'fee', + 'feed', + 'feel', + 'female', + 'fence', + 'festival', + 'fetch', + 'fever', + 'few', + 'fiber', + 'fiction', + 'field', + 'figure', + 'file', + 'film', + 'filter', + 'final', + 'find', + 'fine', + 'finger', + 'finish', + 'fire', + 'firm', + 'first', + 'fiscal', + 'fish', + 'fit', + 'fitness', + 'fix', + 'flag', + 'flame', + 'flash', + 'flat', + 'flavor', + 'flee', + 'flight', + 'flip', + 'float', + 'flock', + 'floor', + 'flower', + 'fluid', + 'flush', + 'fly', + 'foam', + 'focus', + 'fog', + 'foil', + 'fold', + 'follow', + 'food', + 'foot', + 'force', + 'forest', + 'forget', + 'fork', + 'fortune', + 'forum', + 'forward', + 'fossil', + 'foster', + 'found', + 'fox', + 'fragile', + 'frame', + 'frequent', + 'fresh', + 'friend', + 'fringe', + 'frog', + 'front', + 'frost', + 'frown', + 'frozen', + 'fruit', + 'fuel', + 'fun', + 'funny', + 'furnace', + 'fury', + 'future', + 'gadget', + 'gain', + 'galaxy', + 'gallery', + 'game', + 'gap', + 'garage', + 'garbage', + 'garden', + 'garlic', + 'garment', + 'gas', + 'gasp', + 'gate', + 'gather', + 'gauge', + 'gaze', + 'general', + 'genius', + 'genre', + 'gentle', + 'genuine', + 'gesture', + 'ghost', + 'giant', + 'gift', + 'giggle', + 'ginger', + 'giraffe', + 'girl', + 'give', + 'glad', + 'glance', + 'glare', + 'glass', + 'glide', + 'glimpse', + 'globe', + 'gloom', + 'glory', + 'glove', + 'glow', + 'glue', + 'goat', + 'goddess', + 'gold', + 'good', + 'goose', + 'gorilla', + 'gospel', + 'gossip', + 'govern', + 'gown', + 'grab', + 'grace', + 'grain', + 'grant', + 'grape', + 'grass', + 'gravity', + 'great', + 'green', + 'grid', + 'grief', + 'grit', + 'grocery', + 'group', + 'grow', + 'grunt', + 'guard', + 'guess', + 'guide', + 'guilt', + 'guitar', + 'gun', + 'gym', + 'habit', + 'hair', + 'half', + 'hammer', + 'hamster', + 'hand', + 'happy', + 'harbor', + 'hard', + 'harsh', + 'harvest', + 'hat', + 'have', + 'hawk', + 'hazard', + 'head', + 'health', + 'heart', + 'heavy', + 'hedgehog', + 'height', + 'hello', + 'helmet', + 'help', + 'hen', + 'hero', + 'hidden', + 'high', + 'hill', + 'hint', + 'hip', + 'hire', + 'history', + 'hobby', + 'hockey', + 'hold', + 'hole', + 'holiday', + 'hollow', + 'home', + 'honey', + 'hood', + 'hope', + 'horn', + 'horror', + 'horse', + 'hospital', + 'host', + 'hotel', + 'hour', + 'hover', + 'hub', + 'huge', + 'human', + 'humble', + 'humor', + 'hundred', + 'hungry', + 'hunt', + 'hurdle', + 'hurry', + 'hurt', + 'husband', + 'hybrid', + 'ice', + 'icon', + 'idea', + 'identify', + 'idle', + 'ignore', + 'ill', + 'illegal', + 'illness', + 'image', + 'imitate', + 'immense', + 'immune', + 'impact', + 'impose', + 'improve', + 'impulse', + 'inch', + 'include', + 'income', + 'increase', + 'index', + 'indicate', + 'indoor', + 'industry', + 'infant', + 'inflict', + 'inform', + 'inhale', + 'inherit', + 'initial', + 'inject', + 'injury', + 'inmate', + 'inner', + 'innocent', + 'input', + 'inquiry', + 'insane', + 'insect', + 'inside', + 'inspire', + 'install', + 'intact', + 'interest', + 'into', + 'invest', + 'invite', + 'involve', + 'iron', + 'island', + 'isolate', + 'issue', + 'item', + 'ivory', + 'jacket', + 'jaguar', + 'jar', + 'jazz', + 'jealous', + 'jeans', + 'jelly', + 'jewel', + 'job', + 'join', + 'joke', + 'journey', + 'joy', + 'judge', + 'juice', + 'jump', + 'jungle', + 'junior', + 'junk', + 'just', + 'kangaroo', + 'keen', + 'keep', + 'ketchup', + 'key', + 'kick', + 'kid', + 'kidney', + 'kind', + 'kingdom', + 'kiss', + 'kit', + 'kitchen', + 'kite', + 'kitten', + 'kiwi', + 'knee', + 'knife', + 'knock', + 'know', + 'lab', + 'label', + 'labor', + 'ladder', + 'lady', + 'lake', + 'lamp', + 'language', + 'laptop', + 'large', + 'later', + 'latin', + 'laugh', + 'laundry', + 'lava', + 'law', + 'lawn', + 'lawsuit', + 'layer', + 'lazy', + 'leader', + 'leaf', + 'learn', + 'leave', + 'lecture', + 'left', + 'leg', + 'legal', + 'legend', + 'leisure', + 'lemon', + 'lend', + 'length', + 'lens', + 'leopard', + 'lesson', + 'letter', + 'level', + 'liar', + 'liberty', + 'library', + 'license', + 'life', + 'lift', + 'light', + 'like', + 'limb', + 'limit', + 'link', + 'lion', + 'liquid', + 'list', + 'little', + 'live', + 'lizard', + 'load', + 'loan', + 'lobster', + 'local', + 'lock', + 'logic', + 'lonely', + 'long', + 'loop', + 'lottery', + 'loud', + 'lounge', + 'love', + 'loyal', + 'lucky', + 'luggage', + 'lumber', + 'lunar', + 'lunch', + 'luxury', + 'lyrics', + 'machine', + 'mad', + 'magic', + 'magnet', + 'maid', + 'mail', + 'main', + 'major', + 'make', + 'mammal', + 'man', + 'manage', + 'mandate', + 'mango', + 'mansion', + 'manual', + 'maple', + 'marble', + 'march', + 'margin', + 'marine', + 'market', + 'marriage', + 'mask', + 'mass', + 'master', + 'match', + 'material', + 'math', + 'matrix', + 'matter', + 'maximum', + 'maze', + 'meadow', + 'mean', + 'measure', + 'meat', + 'mechanic', + 'medal', + 'media', + 'melody', + 'melt', + 'member', + 'memory', + 'mention', + 'menu', + 'mercy', + 'merge', + 'merit', + 'merry', + 'mesh', + 'message', + 'metal', + 'method', + 'middle', + 'midnight', + 'milk', + 'million', + 'mimic', + 'mind', + 'minimum', + 'minor', + 'minute', + 'miracle', + 'mirror', + 'misery', + 'miss', + 'mistake', + 'mix', + 'mixed', + 'mixture', + 'mobile', + 'model', + 'modify', + 'mom', + 'moment', + 'monitor', + 'monkey', + 'monster', + 'month', + 'moon', + 'moral', + 'more', + 'morning', + 'mosquito', + 'mother', + 'motion', + 'motor', + 'mountain', + 'mouse', + 'move', + 'movie', + 'much', + 'muffin', + 'mule', + 'multiply', + 'muscle', + 'museum', + 'mushroom', + 'music', + 'must', + 'mutual', + 'myself', + 'mystery', + 'myth', + 'naive', + 'name', + 'napkin', + 'narrow', + 'nasty', + 'nation', + 'nature', + 'near', + 'neck', + 'need', + 'negative', + 'neglect', + 'neither', + 'nephew', + 'nerve', + 'nest', + 'net', + 'network', + 'neutral', + 'never', + 'news', + 'next', + 'nice', + 'night', + 'noble', + 'noise', + 'nominee', + 'noodle', + 'normal', + 'north', + 'nose', + 'notable', + 'note', + 'nothing', + 'notice', + 'novel', + 'now', + 'nuclear', + 'number', + 'nurse', + 'nut', + 'oak', + 'obey', + 'object', + 'oblige', + 'obscure', + 'observe', + 'obtain', + 'obvious', + 'occur', + 'ocean', + 'october', + 'odor', + 'off', + 'offer', + 'office', + 'often', + 'oil', + 'okay', + 'old', + 'olive', + 'olympic', + 'omit', + 'once', + 'one', + 'onion', + 'online', + 'only', + 'open', + 'opera', + 'opinion', + 'oppose', + 'option', + 'orange', + 'orbit', + 'orchard', + 'order', + 'ordinary', + 'organ', + 'orient', + 'original', + 'orphan', + 'ostrich', + 'other', + 'outdoor', + 'outer', + 'output', + 'outside', + 'oval', + 'oven', + 'over', + 'own', + 'owner', + 'oxygen', + 'oyster', + 'ozone', + 'pact', + 'paddle', + 'page', + 'pair', + 'palace', + 'palm', + 'panda', + 'panel', + 'panic', + 'panther', + 'paper', + 'parade', + 'parent', + 'park', + 'parrot', + 'party', + 'pass', + 'patch', + 'path', + 'patient', + 'patrol', + 'pattern', + 'pause', + 'pave', + 'payment', + 'peace', + 'peanut', + 'pear', + 'peasant', + 'pelican', + 'pen', + 'penalty', + 'pencil', + 'people', + 'pepper', + 'perfect', + 'permit', + 'person', + 'pet', + 'phone', + 'photo', + 'phrase', + 'physical', + 'piano', + 'picnic', + 'picture', + 'piece', + 'pig', + 'pigeon', + 'pill', + 'pilot', + 'pink', + 'pioneer', + 'pipe', + 'pistol', + 'pitch', + 'pizza', + 'place', + 'planet', + 'plastic', + 'plate', + 'play', + 'please', + 'pledge', + 'pluck', + 'plug', + 'plunge', + 'poem', + 'poet', + 'point', + 'polar', + 'pole', + 'police', + 'pond', + 'pony', + 'pool', + 'popular', + 'portion', + 'position', + 'possible', + 'post', + 'potato', + 'pottery', + 'poverty', + 'powder', + 'power', + 'practice', + 'praise', + 'predict', + 'prefer', + 'prepare', + 'present', + 'pretty', + 'prevent', + 'price', + 'pride', + 'primary', + 'print', + 'priority', + 'prison', + 'private', + 'prize', + 'problem', + 'process', + 'produce', + 'profit', + 'program', + 'project', + 'promote', + 'proof', + 'property', + 'prosper', + 'protect', + 'proud', + 'provide', + 'public', + 'pudding', + 'pull', + 'pulp', + 'pulse', + 'pumpkin', + 'punch', + 'pupil', + 'puppy', + 'purchase', + 'purity', + 'purpose', + 'purse', + 'push', + 'put', + 'puzzle', + 'pyramid', + 'quality', + 'quantum', + 'quarter', + 'question', + 'quick', + 'quit', + 'quiz', + 'quote', + 'rabbit', + 'raccoon', + 'race', + 'rack', + 'radar', + 'radio', + 'rail', + 'rain', + 'raise', + 'rally', + 'ramp', + 'ranch', + 'random', + 'range', + 'rapid', + 'rare', + 'rate', + 'rather', + 'raven', + 'raw', + 'razor', + 'ready', + 'real', + 'reason', + 'rebel', + 'rebuild', + 'recall', + 'receive', + 'recipe', + 'record', + 'recycle', + 'reduce', + 'reflect', + 'reform', + 'refuse', + 'region', + 'regret', + 'regular', + 'reject', + 'relax', + 'release', + 'relief', + 'rely', + 'remain', + 'remember', + 'remind', + 'remove', + 'render', + 'renew', + 'rent', + 'reopen', + 'repair', + 'repeat', + 'replace', + 'report', + 'require', + 'rescue', + 'resemble', + 'resist', + 'resource', + 'response', + 'result', + 'retire', + 'retreat', + 'return', + 'reunion', + 'reveal', + 'review', + 'reward', + 'rhythm', + 'rib', + 'ribbon', + 'rice', + 'rich', + 'ride', + 'ridge', + 'rifle', + 'right', + 'rigid', + 'ring', + 'riot', + 'ripple', + 'risk', + 'ritual', + 'rival', + 'river', + 'road', + 'roast', + 'robot', + 'robust', + 'rocket', + 'romance', + 'roof', + 'rookie', + 'room', + 'rose', + 'rotate', + 'rough', + 'round', + 'route', + 'royal', + 'rubber', + 'rude', + 'rug', + 'rule', + 'run', + 'runway', + 'rural', + 'sad', + 'saddle', + 'sadness', + 'safe', + 'sail', + 'salad', + 'salmon', + 'salon', + 'salt', + 'salute', + 'same', + 'sample', + 'sand', + 'satisfy', + 'satoshi', + 'sauce', + 'sausage', + 'save', + 'say', + 'scale', + 'scan', + 'scare', + 'scatter', + 'scene', + 'scheme', + 'school', + 'science', + 'scissors', + 'scorpion', + 'scout', + 'scrap', + 'screen', + 'script', + 'scrub', + 'sea', + 'search', + 'season', + 'seat', + 'second', + 'secret', + 'section', + 'security', + 'seed', + 'seek', + 'segment', + 'select', + 'sell', + 'seminar', + 'senior', + 'sense', + 'sentence', + 'series', + 'service', + 'session', + 'settle', + 'setup', + 'seven', + 'shadow', + 'shaft', + 'shallow', + 'share', + 'shed', + 'shell', + 'sheriff', + 'shield', + 'shift', + 'shine', + 'ship', + 'shiver', + 'shock', + 'shoe', + 'shoot', + 'shop', + 'short', + 'shoulder', + 'shove', + 'shrimp', + 'shrug', + 'shuffle', + 'shy', + 'sibling', + 'sick', + 'side', + 'siege', + 'sight', + 'sign', + 'silent', + 'silk', + 'silly', + 'silver', + 'similar', + 'simple', + 'since', + 'sing', + 'siren', + 'sister', + 'situate', + 'six', + 'size', + 'skate', + 'sketch', + 'ski', + 'skill', + 'skin', + 'skirt', + 'skull', + 'slab', + 'slam', + 'sleep', + 'slender', + 'slice', + 'slide', + 'slight', + 'slim', + 'slogan', + 'slot', + 'slow', + 'slush', + 'small', + 'smart', + 'smile', + 'smoke', + 'smooth', + 'snack', + 'snake', + 'snap', + 'sniff', + 'snow', + 'soap', + 'soccer', + 'social', + 'sock', + 'soda', + 'soft', + 'solar', + 'soldier', + 'solid', + 'solution', + 'solve', + 'someone', + 'song', + 'soon', + 'sorry', + 'sort', + 'soul', + 'sound', + 'soup', + 'source', + 'south', + 'space', + 'spare', + 'spatial', + 'spawn', + 'speak', + 'special', + 'speed', + 'spell', + 'spend', + 'sphere', + 'spice', + 'spider', + 'spike', + 'spin', + 'spirit', + 'split', + 'spoil', + 'sponsor', + 'spoon', + 'sport', + 'spot', + 'spray', + 'spread', + 'spring', + 'spy', + 'square', + 'squeeze', + 'squirrel', + 'stable', + 'stadium', + 'staff', + 'stage', + 'stairs', + 'stamp', + 'stand', + 'start', + 'state', + 'stay', + 'steak', + 'steel', + 'stem', + 'step', + 'stereo', + 'stick', + 'still', + 'sting', + 'stock', + 'stomach', + 'stone', + 'stool', + 'story', + 'stove', + 'strategy', + 'street', + 'strike', + 'strong', + 'struggle', + 'student', + 'stuff', + 'stumble', + 'style', + 'subject', + 'submit', + 'subway', + 'success', + 'such', + 'sudden', + 'suffer', + 'sugar', + 'suggest', + 'suit', + 'summer', + 'sun', + 'sunny', + 'sunset', + 'super', + 'supply', + 'supreme', + 'sure', + 'surface', + 'surge', + 'surprise', + 'surround', + 'survey', + 'suspect', + 'sustain', + 'swallow', + 'swamp', + 'swap', + 'swarm', + 'swear', + 'sweet', + 'swift', + 'swim', + 'swing', + 'switch', + 'sword', + 'symbol', + 'symptom', + 'syrup', + 'system', + 'table', + 'tackle', + 'tag', + 'tail', + 'talent', + 'talk', + 'tank', + 'tape', + 'target', + 'task', + 'taste', + 'tattoo', + 'taxi', + 'teach', + 'team', + 'tell', + 'ten', + 'tenant', + 'tennis', + 'tent', + 'term', + 'test', + 'text', + 'thank', + 'that', + 'theme', + 'then', + 'theory', + 'there', + 'they', + 'thing', + 'this', + 'thought', + 'three', + 'thrive', + 'throw', + 'thumb', + 'thunder', + 'ticket', + 'tide', + 'tiger', + 'tilt', + 'timber', + 'time', + 'tiny', + 'tip', + 'tired', + 'tissue', + 'title', + 'toast', + 'tobacco', + 'today', + 'toddler', + 'toe', + 'together', + 'toilet', + 'token', + 'tomato', + 'tomorrow', + 'tone', + 'tongue', + 'tonight', + 'tool', + 'tooth', + 'top', + 'topic', + 'topple', + 'torch', + 'tornado', + 'tortoise', + 'toss', + 'total', + 'tourist', + 'toward', + 'tower', + 'town', + 'toy', + 'track', + 'trade', + 'traffic', + 'tragic', + 'train', + 'transfer', + 'trap', + 'trash', + 'travel', + 'tray', + 'treat', + 'tree', + 'trend', + 'trial', + 'tribe', + 'trick', + 'trigger', + 'trim', + 'trip', + 'trophy', + 'trouble', + 'truck', + 'true', + 'truly', + 'trumpet', + 'trust', + 'truth', + 'try', + 'tube', + 'tuition', + 'tumble', + 'tuna', + 'tunnel', + 'turkey', + 'turn', + 'turtle', + 'twelve', + 'twenty', + 'twice', + 'twin', + 'twist', + 'two', + 'type', + 'typical', + 'ugly', + 'umbrella', + 'unable', + 'unaware', + 'uncle', + 'uncover', + 'under', + 'undo', + 'unfair', + 'unfold', + 'unhappy', + 'uniform', + 'unique', + 'unit', + 'universe', + 'unknown', + 'unlock', + 'until', + 'unusual', + 'unveil', + 'update', + 'upgrade', + 'uphold', + 'upon', + 'upper', + 'upset', + 'urban', + 'urge', + 'usage', + 'use', + 'used', + 'useful', + 'useless', + 'usual', + 'utility', + 'vacant', + 'vacuum', + 'vague', + 'valid', + 'valley', + 'valve', + 'van', + 'vanish', + 'vapor', + 'various', + 'vast', + 'vault', + 'vehicle', + 'velvet', + 'vendor', + 'venture', + 'venue', + 'verb', + 'verify', + 'version', + 'very', + 'vessel', + 'veteran', + 'viable', + 'vibrant', + 'vicious', + 'victory', + 'video', + 'view', + 'village', + 'vintage', + 'violin', + 'virtual', + 'virus', + 'visa', + 'visit', + 'visual', + 'vital', + 'vivid', + 'vocal', + 'voice', + 'void', + 'volcano', + 'volume', + 'vote', + 'voyage', + 'wage', + 'wagon', + 'wait', + 'walk', + 'wall', + 'walnut', + 'want', + 'warfare', + 'warm', + 'warrior', + 'wash', + 'wasp', + 'waste', + 'water', + 'wave', + 'way', + 'wealth', + 'weapon', + 'wear', + 'weasel', + 'weather', + 'web', + 'wedding', + 'weekend', + 'weird', + 'welcome', + 'west', + 'wet', + 'whale', + 'what', + 'wheat', + 'wheel', + 'when', + 'where', + 'whip', + 'whisper', + 'wide', + 'width', + 'wife', + 'wild', + 'will', + 'win', + 'window', + 'wine', + 'wing', + 'wink', + 'winner', + 'winter', + 'wire', + 'wisdom', + 'wise', + 'wish', + 'witness', + 'wolf', + 'woman', + 'wonder', + 'wood', + 'wool', + 'word', + 'work', + 'world', + 'worry', + 'worth', + 'wrap', + 'wreck', + 'wrestle', + 'wrist', + 'write', + 'wrong', + 'yard', + 'year', + 'yellow', + 'you', + 'young', + 'youth', + 'zebra', + 'zero', + 'zone', + 'zoo' +]; diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index b551d629d..2356c844b 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -113,6 +113,9 @@ else() set(LIB_TRIPLET "x86_64-linux-gnu") endif() +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/zcash_lib/target/release/libwarp_api_ffi.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "libwarp_api_ffi.so" + COMPONENT Runtime) + install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/monero_c/release/monero/${LIB_TRIPLET}_libwallet2_api_c.so" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" RENAME "monero_libwallet2_api_c.so" COMPONENT Runtime) diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index f1b1fda98..e29989c8d 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -28,6 +28,7 @@ 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; 83FF6BF911B29965D3589BE7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = BD06F8F6E3797AC031038136 /* Pods_Runner.framework */; }; 9F565D5929954F53009A75FB /* secRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F565D5729954F53009A75FB /* secRandom.swift */; }; + CE46BF642F15C020000A6140 /* WarpApiFFI.xcframework in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE46BF632F15C020000A6140 /* WarpApiFFI.xcframework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; CE75FC4A2C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; CED5DBE42BE59BBF0065028F /* monero_libwallet2_api_c.dylib in CopyFiles */ = {isa = PBXBuildFile; fileRef = CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */; settings = {ATTRIBUTES = (CodeSignOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -59,6 +60,7 @@ dstPath = ""; dstSubfolderSpec = 10; files = ( + CE46BF642F15C020000A6140 /* WarpApiFFI.xcframework in CopyFiles */, CE75FC4A2C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib in CopyFiles */, CED5DBE42BE59BBF0065028F /* monero_libwallet2_api_c.dylib in CopyFiles */, ); @@ -90,6 +92,7 @@ 9F1E7BFB2BF2D27500C28C9A /* Runner.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Runner.entitlements; sourceTree = ""; }; 9F565D5729954F53009A75FB /* secRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; name = secRandom.swift; path = CakeWallet/secRandom.swift; sourceTree = ""; }; BD06F8F6E3797AC031038136 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + CE46BF632F15C020000A6140 /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../scripts/zcash_lib/WarpApiFFI.xcframework; sourceTree = ""; }; CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = wownero_libwallet2_api_c.dylib; sourceTree = ""; }; CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */ = {isa = PBXFileReference; lastKnownFileType = "compiled.mach-o.dylib"; path = monero_libwallet2_api_c.dylib; sourceTree = ""; }; /* End PBXFileReference section */ @@ -120,6 +123,7 @@ 33CC10E42044A3C60003C045 = { isa = PBXGroup; children = ( + CE46BF632F15C020000A6140 /* WarpApiFFI.xcframework */, CE75FC492C147EBA00CCC46E /* wownero_libwallet2_api_c.dylib */, CED5DBE32BE59BBF0065028F /* monero_libwallet2_api_c.dylib */, 9F565D5729954F53009A75FB /* secRandom.swift */, diff --git a/model_generator.sh b/model_generator.sh index e5efb06c6..20de89b39 100755 --- a/model_generator.sh +++ b/model_generator.sh @@ -1,7 +1,7 @@ #!/bin/bash set -x -e -for cwcoin in cw_{core,evm,monero,bitcoin,nano,bitcoin_cash,solana,tron,wownero,zano,decred,dogecoin} +for cwcoin in cw_{core,evm,monero,bitcoin,nano,bitcoin_cash,solana,tron,wownero,zano,decred,dogecoin,zcash} do if [[ "x$1" == "xasync" ]]; then diff --git a/pubspec_base.yaml b/pubspec_base.yaml index 4552b0057..f5ecd6e38 100644 --- a/pubspec_base.yaml +++ b/pubspec_base.yaml @@ -241,9 +241,12 @@ flutter: - assets/dogecoin_electrum_server_list.yml - assets/base_node_list.yml - assets/arbitrum_node_list.yml + - assets/zcash_node_list.yml - assets/text/ - assets/faq/ - assets/animation/ + - scripts/zcash_lib/assets/sapling-spend.params + - scripts/zcash_lib/assets/sapling-output.params fonts: - family: Lato diff --git a/res/values/strings_cs.arb b/res/values/strings_cs.arb index 8923ad414..4381285f9 100644 --- a/res/values/strings_cs.arb +++ b/res/values/strings_cs.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Tmavé téma Matrix Green", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Zpráva", "message_verified": "Zpráva byla úspěšně ověřena", "messages": "Zprávy", @@ -1172,4 +1172,4 @@ "youCanGoBackToYourDapp": "Nyní se můžete vrátit do svého dappu", "your": "Vaše", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_de.arb b/res/values/strings_de.arb index eacc31699..8f0d7eb51 100644 --- a/res/values/strings_de.arb +++ b/res/values/strings_de.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Green Dark Theme", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Nachricht", "message_verified": "Die Nachricht wurde erfolgreich überprüft", "messages": "Nachrichten", diff --git a/res/values/strings_en.arb b/res/values/strings_en.arb index 6ae55d780..fdc515e91 100644 --- a/res/values/strings_en.arb +++ b/res/values/strings_en.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Green Dark Theme", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Message", "message_verified": "The message was successfully verified", "messages": "Messages", @@ -1173,4 +1173,4 @@ "youCanGoBackToYourDapp": "You can go back to your dApp now", "your": "Your", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_es.arb b/res/values/strings_es.arb index 738e9569b..d0a443777 100644 --- a/res/values/strings_es.arb +++ b/res/values/strings_es.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Tema oscuro Matrix Green", "max_amount": "Máx: ${value}", "max_value": "Máx: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Mensaje", "message_verified": "El mensaje se verificó con éxito", "messages": "Mensajes", @@ -1174,4 +1174,4 @@ "youCanGoBackToYourDapp": "Ahora puedes volver a tu dApp", "your": "Tu", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_ha.arb b/res/values/strings_ha.arb index 5c9082434..4601874e1 100644 --- a/res/values/strings_ha.arb +++ b/res/values/strings_ha.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Green Dark Jigo", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Sako", "message_verified": "An yi nasarar tabbatar da sakon", "messages": "Manegiya", @@ -1175,4 +1175,4 @@ "youCanGoBackToYourDapp": "Kuna iya komawa zuwa DPP ɗinku yanzu", "your": "Naku", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_hr.arb b/res/values/strings_hr.arb index 5ea9c5ecf..a27d7e27d 100644 --- a/res/values/strings_hr.arb +++ b/res/values/strings_hr.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Green Dark Theme", "max_amount": "Maksimum: ${value}", "max_value": "Maks.: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Poruka", "message_verified": "Poruka je uspješno provjerena", "messages": "Poruke", @@ -1172,4 +1172,4 @@ "youCanGoBackToYourDapp": "Sada se možete vratiti na svoj dapp", "your": "Vaš", "yy": "GG" -} \ No newline at end of file +} diff --git a/res/values/strings_id.arb b/res/values/strings_id.arb index 39b070d1a..ca333a118 100644 --- a/res/values/strings_id.arb +++ b/res/values/strings_id.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Tema Matrix Green Dark", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Pesan", "message_verified": "Pesan itu berhasil diverifikasi", "messages": "Pesan", @@ -1175,4 +1175,4 @@ "youCanGoBackToYourDapp": "Anda dapat kembali ke dapp Anda sekarang", "your": "Milikmu", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_it.arb b/res/values/strings_it.arb index 93a1dd9db..f37f4fa3c 100644 --- a/res/values/strings_it.arb +++ b/res/values/strings_it.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Tema Matrix verde scuro", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Messaggio", "message_verified": "Il messaggio è stato verificato con successo", "messages": "Messaggi", @@ -1175,4 +1175,4 @@ "youCanGoBackToYourDapp": "Puoi tornare al tuo DApp ora", "your": "Tuo", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_nl.arb b/res/values/strings_nl.arb index 813e49375..ac50d866b 100644 --- a/res/values/strings_nl.arb +++ b/res/values/strings_nl.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix groen donker thema", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Bericht", "message_verified": "Het bericht is succesvol geverifieerd", "messages": "Berichten", @@ -1173,4 +1173,4 @@ "youCanGoBackToYourDapp": "U kunt nu terug naar uw DApp gaan", "your": "Jouw", "yy": "JJ" -} \ No newline at end of file +} diff --git a/res/values/strings_tl.arb b/res/values/strings_tl.arb index 14bbad0ed..c9cdf5f14 100644 --- a/res/values/strings_tl.arb +++ b/res/values/strings_tl.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Green Dark Theme", "max_amount": "Max: ${value}", "max_value": "Max: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "Mensahe", "message_verified": "Ang mensahe ay matagumpay na na -verify", "messages": "Mga mensahe", @@ -1172,4 +1172,4 @@ "youCanGoBackToYourDapp": "Maaari kang bumalik sa iyong dapp ngayon", "your": "IYONG", "yy": "YY" -} \ No newline at end of file +} diff --git a/res/values/strings_tr.arb b/res/values/strings_tr.arb index 7ffb8a27a..c9f8680b4 100644 --- a/res/values/strings_tr.arb +++ b/res/values/strings_tr.arb @@ -515,7 +515,7 @@ "matrix_green_dark_theme": "Matrix Yeşil Koyu Tema", "max_amount": "Maks: ${value}", "max_value": "En fazla: ${value} ${currency}", - "memo": "Memo:", + "memo": "Memo", "message": "İleti", "message_verified": "Mesaj başarıyla doğrulandı", "messages": "Mesaj", @@ -1172,4 +1172,4 @@ "youCanGoBackToYourDapp": "Şimdi Dapp'ınıza geri dönebilirsin", "your": "Senin", "yy": "YY" -} \ No newline at end of file +} diff --git a/scripts/android/app_env.sh b/scripts/android/app_env.sh index 5e7eaefad..c739ffa02 100644 --- a/scripts/android/app_env.sh +++ b/scripts/android/app_env.sh @@ -21,8 +21,8 @@ MONERO_COM_PACKAGE="com.monero.app" MONERO_COM_SCHEME="monero.com" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.6.5" -CAKEWALLET_BUILD_NUMBER=4296 +CAKEWALLET_VERSION="5.7.0" +CAKEWALLET_BUILD_NUMBER=4298 CAKEWALLET_BUNDLE_ID="com.cakewallet.cake_wallet" CAKEWALLET_PACKAGE="com.cakewallet.cake_wallet" CAKEWALLET_SCHEME="cakewallet" diff --git a/scripts/android/build_zcash.sh b/scripts/android/build_zcash.sh new file mode 100755 index 000000000..eca46936a --- /dev/null +++ b/scripts/android/build_zcash.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -x -e +cd "$(dirname "$0")" + +../prepare_zcash.sh + +pushd ../zcash_lib + ./build-scripts/build-android.sh +popd diff --git a/scripts/android/pubspec_gen.sh b/scripts/android/pubspec_gen.sh index 1d4dcadc0..f95f63532 100755 --- a/scripts/android/pubspec_gen.sh +++ b/scripts/android/pubspec_gen.sh @@ -10,7 +10,7 @@ case $APP_ANDROID_TYPE in CONFIG_ARGS="--monero" ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --zcash --arbitrum" ;; esac diff --git a/scripts/build_bitbox_flutter.sh b/scripts/build_bitbox_flutter.sh index 00693abbb..7d790a540 100755 --- a/scripts/build_bitbox_flutter.sh +++ b/scripts/build_bitbox_flutter.sh @@ -1,9 +1,17 @@ #!/bin/bash +set -x -e + +if [[ ! -d "bitbox_flutter" ]]; +then + git clone https://github.com/konstantinullrich/bitbox_flutter +fi -git clone https://github.com/konstantinullrich/bitbox_flutter cd bitbox_flutter - +git fetch -a +git reset --hard git checkout 5a6e6dd388ef64003f86094af80d5453518b601d +git reset --hard + bash ./build_bindings.sh --dont-install FILE=api.aar diff --git a/scripts/ios/app_config.sh b/scripts/ios/app_config.sh index 9a8ee840c..8991921e0 100755 --- a/scripts/ios/app_config.sh +++ b/scripts/ios/app_config.sh @@ -31,7 +31,7 @@ case $APP_IOS_TYPE in ;; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --arbitrum" + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --zano --decred --dogecoin --base --zcash --arbitrum" ;; esac diff --git a/scripts/ios/app_env.sh b/scripts/ios/app_env.sh index b3b2d8dd3..36cb36ae1 100644 --- a/scripts/ios/app_env.sh +++ b/scripts/ios/app_env.sh @@ -17,8 +17,8 @@ MONERO_COM_BUILD_NUMBER=153 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.6.5" -CAKEWALLET_BUILD_NUMBER=360 +CAKEWALLET_VERSION="5.7.0" +CAKEWALLET_BUILD_NUMBER=363 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" diff --git a/scripts/ios/build_zcash.sh b/scripts/ios/build_zcash.sh new file mode 100755 index 000000000..96a8e4992 --- /dev/null +++ b/scripts/ios/build_zcash.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -x -e +cd "$(dirname "$0")" + +../prepare_zcash.sh + +pushd ../zcash_lib + ./build-scripts/mac/build-mac.sh +popd diff --git a/scripts/lint.sh b/scripts/lint.sh new file mode 100755 index 000000000..c4b1b4e4f --- /dev/null +++ b/scripts/lint.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -x -e +cd $(dirname $0) +cd .. + +for i in cw_zcash; +do + dart fix --apply $i/ + dart format --line-length=100 $i/ + if [[ ! "x$ABORT_ON_CHANGE" == "x" ]]; + then + if [[ ! -z "$(git status --porcelain -- $(find $i))" ]]; + then + echo "Please run scripts/lint.sh ($i has changes)" + exit 1 + fi + fi +done diff --git a/scripts/linux/build_zcash.sh b/scripts/linux/build_zcash.sh new file mode 100755 index 000000000..07f85f018 --- /dev/null +++ b/scripts/linux/build_zcash.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -x -e +cd "$(dirname "$0")" + +../prepare_zcash.sh + +pushd ../zcash_lib + ./build-scripts/build-linux.sh +popd diff --git a/scripts/macos/app_config.sh b/scripts/macos/app_config.sh index 589b5ec1d..21e8ada95 100755 --- a/scripts/macos/app_config.sh +++ b/scripts/macos/app_config.sh @@ -36,7 +36,7 @@ case $APP_MACOS_TYPE in $MONERO_COM) CONFIG_ARGS="--monero";; $CAKEWALLET) - CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --arbitrum";; + CONFIG_ARGS="--monero --bitcoin --ethereum --polygon --nano --bitcoinCash --solana --tron --wownero --dogecoin --base --zcash --arbitrum";; esac cp -rf pubspec_description.yaml pubspec.yaml diff --git a/scripts/macos/app_env.sh b/scripts/macos/app_env.sh index 5916e0606..d1c41b6d1 100755 --- a/scripts/macos/app_env.sh +++ b/scripts/macos/app_env.sh @@ -16,13 +16,13 @@ if [ -n "$1" ]; then fi MONERO_COM_NAME="Monero.com" -MONERO_COM_VERSION="5.6.5" -MONERO_COM_BUILD_NUMBER=73 +MONERO_COM_VERSION="5.6.6" +MONERO_COM_BUILD_NUMBER=74 MONERO_COM_BUNDLE_ID="com.cakewallet.monero" CAKEWALLET_NAME="Cake Wallet" -CAKEWALLET_VERSION="5.6.5" -CAKEWALLET_BUILD_NUMBER=140 +CAKEWALLET_VERSION="5.7.0" +CAKEWALLET_BUILD_NUMBER=142 CAKEWALLET_BUNDLE_ID="com.fotolockr.cakewallet" if ! [[ " ${TYPES[*]} " =~ " ${APP_MACOS_TYPE} " ]]; then diff --git a/scripts/macos/build_zcash.sh b/scripts/macos/build_zcash.sh new file mode 100755 index 000000000..96a8e4992 --- /dev/null +++ b/scripts/macos/build_zcash.sh @@ -0,0 +1,9 @@ +#!/bin/bash +set -x -e +cd "$(dirname "$0")" + +../prepare_zcash.sh + +pushd ../zcash_lib + ./build-scripts/mac/build-mac.sh +popd diff --git a/scripts/prepare_torch.sh b/scripts/prepare_torch.sh index d91805114..d8ecbf25b 100755 --- a/scripts/prepare_torch.sh +++ b/scripts/prepare_torch.sh @@ -7,7 +7,7 @@ cd "$(dirname "$0")" # - .github/workflows/pr_test_build_linux.yml # https://github.com/MrCyjaneK/torch_dart/releases/download/v1.0.17/torch_dart-v1.0.17.tar.gz -HASH=85130c85ad80f4596896f5fa4dc1a160cf0f0487 +HASH=d52b9ebcc2e3677520e51bd8a54d771c353587b8 if [[ ! -d "torch_dart/.git" ]]; then diff --git a/scripts/prepare_zcash.sh b/scripts/prepare_zcash.sh new file mode 100755 index 000000000..5abc3212d --- /dev/null +++ b/scripts/prepare_zcash.sh @@ -0,0 +1,28 @@ +#!/bin/bash +set -x -e +cd "$(dirname "$0")" + +HASH=647231e2e6d93c1d6c16d4019cd3a11a3aaa9504 + +if [[ ! -d "zcash_lib/.git" ]]; +then + rm -rf zcash_lib + git clone https://github.com/MrCyjaneK/zwallet.git zcash_lib + cd zcash_lib +else + cd zcash_lib + git fetch -a +fi + + +git reset --hard +git checkout $HASH +git reset --hard +git submodule update --init --force --recursive + +# in go I could nicely replace => the heck out of it +# sadly rust is not go +find . -name Cargo.toml -exec sed -i.bak -E ' +s|rusqlite[[:space:]]*=[[:space:]]*\{[^}]*\}|rusqlite = { version = "0.29.0", features = ["bundled", "modern_sqlite", "backup"] }|g +' {} + +# find . -name Cargo.toml -exec sed -i.bak -E 's|r2d2_sqlite[[:space:]]*=[[:space:]]*([^\n]*)|r2d2_sqlite = "0.32.0"|g' {} + diff --git a/tool/configure.dart b/tool/configure.dart index f8d40a98b..37814836e 100644 --- a/tool/configure.dart +++ b/tool/configure.dart @@ -11,6 +11,7 @@ const zanoOutputPath = 'lib/zano/zano.dart'; const decredOutputPath = 'lib/decred/decred.dart'; const dogecoinOutputPath = 'lib/dogecoin/dogecoin.dart'; const evmOutputPath = 'lib/evm/evm.dart'; +const zcashOutputPath = 'lib/zcash/zcash.dart'; const walletTypesPath = 'lib/wallet_types.g.dart'; const secureStoragePath = 'lib/core/secure_storage.dart'; const pubspecDefaultPath = 'pubspec_default.yaml'; @@ -33,6 +34,7 @@ Future main(List args) async { final hasDogecoin = args.contains('${prefix}dogecoin'); final hasBase = args.contains('${prefix}base'); final hasArbitrum = args.contains('${prefix}arbitrum'); + final hasZcash = args.contains('${prefix}zcash'); final hasEVM = hasEthereum || hasPolygon || hasBase || hasArbitrum; final excludeFlutterSecureStorage = args.contains('${prefix}excludeFlutterSecureStorage'); @@ -48,6 +50,7 @@ Future main(List args) async { await generateDecred(hasDecred); await generateDogecoin(hasDogecoin); await generateEVM(hasEVM); + await generateZcash(hasZcash); await generatePubspec( hasMonero: hasMonero, @@ -66,6 +69,7 @@ Future main(List args) async { hasDogecoin: hasDogecoin, hasBase: hasBase, hasArbitrum: hasArbitrum, + hasZcash: hasZcash, ); await generateWalletTypes( hasMonero: hasMonero, @@ -83,6 +87,7 @@ Future main(List args) async { hasDogecoin: hasDogecoin, hasBase: hasBase, hasArbitrum: hasArbitrum, + hasZcash: hasZcash, ); await injectSecureStorage(!excludeFlutterSecureStorage); } @@ -149,7 +154,7 @@ import 'package:mobx/mobx.dart'; """; const bitcoinCwPart = "part 'cw_bitcoin.dart';"; const bitcoinContent = """ - + class ElectrumSubAddress { ElectrumSubAddress({ required this.id, @@ -396,7 +401,7 @@ abstract class MoneroWalletDetails { abstract class Monero { MoneroAccountList getAccountList(Object wallet); - + MoneroSubaddressList getSubaddressList(Object wallet); TransactionHistoryBase getTransactionHistory(Object wallet); @@ -414,7 +419,7 @@ abstract class Monero { TransactionPriority deserializeMoneroTransactionPriority({required int raw}); List getTransactionPriorities(); List getMoneroWordList(String language); - + List getUnspents(Object wallet); Future updateUnspents(Object wallet); @@ -603,7 +608,7 @@ abstract class WowneroWalletDetails { abstract class Wownero { WowneroAccountList getAccountList(Object wallet); - + WowneroSubaddressList getSubaddressList(Object wallet); TransactionHistoryBase getTransactionHistory(Object wallet); @@ -621,7 +626,7 @@ abstract class Wownero { TransactionPriority deserializeWowneroTransactionPriority({required int raw}); List getTransactionPriorities(); List getWowneroWordList(String language); - + List getUnspents(Object wallet); Future updateUnspents(Object wallet); @@ -729,18 +734,21 @@ abstract class BitcoinCash { TransactionPriority getDefaultTransactionPriority(); List getTransactionPriorities(); - + TransactionPriority getBitcoinCashTransactionPrioritySlow(); } """; const bitcoinCashEmptyDefinition = 'BitcoinCash? bitcoinCash;\n'; - const bitcoinCashCWDefinition = 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; + const bitcoinCashCWDefinition = + 'BitcoinCash? bitcoinCash = CWBitcoinCash();\n'; final output = '$bitcoinCashCommonHeaders\n' + (hasImplementation ? '$bitcoinCashCWHeaders\n' : '\n') + (hasImplementation ? '$bitcoinCashCwPart\n\n' : '\n') + - (hasImplementation ? bitcoinCashCWDefinition : bitcoinCashEmptyDefinition) + + (hasImplementation + ? bitcoinCashCWDefinition + : bitcoinCashEmptyDefinition) + '\n' + bitcoinCashContent; @@ -806,7 +814,7 @@ abstract class Nano { WalletInfo? walletInfo, String? passphrase, }); - + WalletCredentials createNanoRestoreWalletFromSeedCredentials({ required String name, required String password, @@ -875,7 +883,8 @@ abstract class NanoUtil { """; const nanoEmptyDefinition = 'Nano? nano;\nNanoUtil? nanoUtil;\n'; - const nanoCWDefinition = 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; + const nanoCWDefinition = + 'Nano? nano = CWNano();\nNanoUtil? nanoUtil = CWNanoUtil();\n'; final output = '$nanoCommonHeaders\n' + (hasImplementation ? '$nanoCWHeaders\n' : '\n') + @@ -1023,7 +1032,7 @@ abstract class Tron { Future addTronToken(WalletBase wallet, CryptoCurrency token, String contractAddress); Future deleteTronToken(WalletBase wallet, CryptoCurrency token); Future getTronToken(WalletBase wallet, String contractAddress); - + double getTransactionAmountRaw(TransactionInfo transactionInfo); CryptoCurrency assetOfTransaction(WalletBase wallet, TransactionInfo transaction); String getTokenAddress(CryptoCurrency asset); @@ -1031,7 +1040,7 @@ abstract class Tron { String? getTronNativeEstimatedFee(WalletBase wallet); String? getTronTRC20EstimatedFee(WalletBase wallet); - + void updateTronGridUsageState(WalletBase wallet, bool isEnabled); List getDefaultTokenContractAddresses(); bool isTokenAlreadyAdded(WalletBase wallet, String contractAddress); @@ -1498,6 +1507,104 @@ class ChainInfo { await outputFile.writeAsString(output); } +Future generateZcash(bool hasImplementation) async { + final outputFile = File(zcashOutputPath); + const zcashCommonHeaders = """ +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/balance.dart'; +import 'package:cw_core/crypto_amount_format.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/output_info.dart'; +import 'package:cw_core/transaction_history.dart'; +import 'package:cw_core/transaction_info.dart'; +import 'package:cw_core/transaction_priority.dart'; +import 'package:cw_core/monero_transaction_priority.dart'; +import 'package:cw_core/wallet_base.dart'; +import 'package:cw_core/wallet_credentials.dart'; +import 'package:cw_core/wallet_info.dart'; +import 'package:cw_core/wallet_service.dart'; +import 'package:cw_core/receive_page_option.dart'; + +"""; + const zcashCWHeaders = """ +import 'package:cw_zcash/cw_zcash.dart'; +import 'package:cw_zcash/src/zcash_wallet_addresses.dart'; + +"""; + const zcashCwPart = "part 'cw_zcash.dart';"; + const zcashContent = """ +abstract class Zcash { + List getZcashWordList(String language); + WalletService createZcashWalletService(bool isDirect); + WalletCredentials createZcashNewWalletCredentials( + {required String name, + WalletInfo? walletInfo, + String? password, + String? mnemonic, + required String? passphrase}); + WalletCredentials createZcashRestoreWalletFromSeedCredentials( + {required String name, + required String mnemonic, + required String password, + String? passphrase, + required int? height}); + WalletCredentials createZcashRestoreWalletFromPrivateKey( + {required String name, required String privateKey, required String password}); + String getAddress(WalletBase wallet); + String getPrivateKey(WalletBase wallet); + String getPublicKey(WalletBase wallet); + Map getKeys(Object wallet); + + Object createZcashTransactionCredentials( + List outputs, { + required CryptoCurrency currency, + int? feeRate, + }); + + Object createZcashTransactionCredentialsRaw( + List outputs, { + required CryptoCurrency currency, + required int feeRate, + }); + + int formatterZcashParseAmount(String amount); + double formatterZcashAmountToDouble({TransactionInfo? transaction, BigInt? amount}); + String formatterZcashAmountToString({required int amount}); + + List getAddressInfos(Object wallet); + + TransactionPriority getDefaultTransactionPriority(); + TransactionPriority getZcashTransactionPriorityAutomatic(); + TransactionPriority deserializeZcashTransactionPriority({required int raw}); + List getTransactionPriorities(); + List getZcashReceivePageOptions(Object wallet); + ReceivePageOption getSelectedAddressType(Object wallet); + ZcashAddressType getZcashAddressType(ReceivePageOption option); + Future setAddressType(Object wallet, dynamic option); + ZcashAddressType getOptionToType(ReceivePageOption option); + void unlockDatabase(String password); + Future getHeightByDate(DateTime date); +} + + """; + + const zcashEmptyDefinition = 'Zcash? zcash;\n'; + const zcashCWDefinition = 'Zcash? zcash = CWZcash();\n'; + + final output = '$zcashCommonHeaders\n' + + (hasImplementation ? '$zcashCWHeaders\n' : '\n') + + (hasImplementation ? '$zcashCwPart\n\n' : '\n') + + (hasImplementation ? zcashCWDefinition : zcashEmptyDefinition) + + '\n' + + zcashContent; + + if (outputFile.existsSync()) { + await outputFile.delete(); + } + + await outputFile.writeAsString(output); +} + Future generatePubspec({ required bool hasMonero, required bool hasBitcoin, @@ -1515,6 +1622,7 @@ Future generatePubspec({ required bool hasDogecoin, required bool hasBase, required bool hasArbitrum, + required bool hasZcash, }) async { const cwCore = """ cw_core: @@ -1575,12 +1683,18 @@ Future generatePubspec({ cw_dogecoin: path: ./cw_dogecoin """; + const cwZcash = """ + cw_zcash: + path: ./cw_zcash + """; + final inputFile = File(pubspecOutputPath); final inputText = await inputFile.readAsString(); final inputLines = inputText.split('\n'); final dependenciesIndex = inputLines.indexWhere((line) => Platform.isWindows // On Windows it could contains `\r` (Carriage Return). It could be fixed in newer dart versions. - ? line.toLowerCase() == 'dependencies:\r' || line.toLowerCase() == 'dependencies:' + ? line.toLowerCase() == 'dependencies:\r' || + line.toLowerCase() == 'dependencies:' : line.toLowerCase() == 'dependencies:'); var output = cwCore; @@ -1636,6 +1750,10 @@ Future generatePubspec({ output += '\n$cwDogecoin'; } + if (hasZcash) { + output += '\n$cwZcash'; + } + final outputLines = output.split('\n'); inputLines.insertAll(dependenciesIndex + 1, outputLines); final outputContent = inputLines.join('\n'); @@ -1664,6 +1782,7 @@ Future generateWalletTypes({ required bool hasDogecoin, required bool hasBase, required bool hasArbitrum, + required bool hasZcash, }) async { final walletTypesFile = File(walletTypesPath); @@ -1687,14 +1806,30 @@ Future generateWalletTypes({ outputContent += '\tWalletType.ethereum,\n'; } - if (hasBitcoin) { - outputContent += '\tWalletType.litecoin,\n'; + if (hasSolana) { + outputContent += '\tWalletType.solana,\n'; + } + + if (hasTron) { + outputContent += '\tWalletType.tron,\n'; } if (hasDogecoin) { outputContent += '\tWalletType.dogecoin,\n'; } + if (hasBitcoinCash) { + outputContent += '\tWalletType.bitcoinCash,\n'; + } + + if (hasZcash) { + outputContent += '\tWalletType.zcash,\n'; + } + + if (hasBitcoin) { + outputContent += '\tWalletType.litecoin,\n'; + } + if (hasBase) { outputContent += '\tWalletType.base,\n'; } @@ -1703,26 +1838,18 @@ Future generateWalletTypes({ outputContent += '\tWalletType.arbitrum,\n'; } - if (hasBitcoinCash) { - outputContent += '\tWalletType.bitcoinCash,\n'; - } - if (hasPolygon) { outputContent += '\tWalletType.polygon,\n'; } - if (hasSolana) { - outputContent += '\tWalletType.solana,\n'; - } - - if (hasTron) { - outputContent += '\tWalletType.tron,\n'; - } - if (hasNano) { outputContent += '\tWalletType.nano,\n'; } + if (hasDecred) { + outputContent += '\tWalletType.decred,\n'; + } + if (hasZano) { outputContent += '\tWalletType.zano,\n'; } @@ -1731,10 +1858,6 @@ Future generateWalletTypes({ outputContent += '\tWalletType.banano,\n'; } - if (hasDecred) { - outputContent += '\tWalletType.decred,\n'; - } - // if (hasWownero) { // outputContent += '\tWalletType.wownero,\n'; // } @@ -1769,7 +1892,7 @@ class DefaultSecureStorage extends SecureStorage { iOptions: IOSOptions(accessibility: KeychainAccessibility.first_unlock), aOptions: AndroidOptions(encryptedSharedPreferences: true), )); - + final FlutterSecureStorage _secureStorage; @override @@ -1787,7 +1910,7 @@ class DefaultSecureStorage extends SecureStorage { @override Future delete({required String key}) async => _secureStorage.delete(key: key); - + @override Future deleteAll() async => _secureStorage.deleteAll(); diff --git a/tool/fiat_api_check.dart b/tool/fiat_api_check.dart old mode 100644 new mode 100755