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 000000000..08f2ec3c2 Binary files /dev/null and b/assets/images/crypto/zcash.webp differ diff --git a/assets/images/tx_shield.svg b/assets/images/tx_shield.svg new file mode 100644 index 000000000..bf0073e0c --- /dev/null +++ b/assets/images/tx_shield.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/assets/images/zec_icon.png b/assets/images/zec_icon.png index 095ad2c56..009e174eb 100644 Binary files a/assets/images/zec_icon.png and b/assets/images/zec_icon.png differ diff --git a/assets/images/zec_icon_qr.svg b/assets/images/zec_icon_qr.svg new file mode 100644 index 000000000..54ceab824 --- /dev/null +++ b/assets/images/zec_icon_qr.svg @@ -0,0 +1,5 @@ + + + + + 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