Ywallet zcash (#2810)

* feat(zcash): initial commit
- initial work on cw_zcash
  - use single .db for everything zcash to speed up sync and scanning
  - implement silent autoshielding of non-orchard funds
  - use FFI for everything to fetch the data
  - add support for passphrase by adding extra word to seed phrase
  - enforce lints on CI for cw_zcash directory
- use correct zcash name, and call transparent zcash tZEC
- minor cleanups outsite of cw_zcash
  - gitmodules, gitignore
  - db debug
  - fix broken addressPageType in WalletInfo
  - enable debug options in kDebugMode and kProfileMode

* feat: T address rotation (and cache) wip: minor fixes

* wip: shield tx list

* fix fiat amounts fix derrivation of rotation account addresses screen
for zcash block height fix T address shields improvements Amount in
disposable T shields UI improvements
show pending outgoing tx in tx history

* fix null check on passphrase page fix tx showing as pending rename auto
shield txs fixed send all to include fee use rotating address in
addressForExchange wrap all calls of WarpApi.getLatestHeight in try
catch to prevent leaking errors

other minor fixes from notion

* wallet group debug cache sent tx address wallet T address rotation fixes
automatically pick the fee for auto-shield

* fix T address cache deserialization fix autoshield icon fix autoshield
text getCachedDestinationAddress fix send address cache

* address comments from review

* wip: iOS fix

* fix swap

* Allow cakepay payment from zcash wallet

* fix: ios (I'm stupid) fix: reorder addresses to make orchard default
fix: send all balance

* 5.6.6 bump fix: ios apple review add transport error to ignored
exceptions

* fix: leaving dust behind when sweeping all

* Change wallets order

* fix: reflect balance of pending out txs fix: restore height support

* linux support (disabled)

* fix: date picker for zcash on restore screen fix: build script for linux
match WarpApi.warpSync args with upstream

* fix: linter script

* ensure all exchange providers support ZEC (#2802)

* ensure all exchange providers support ZEC

* remove zaddr and tzec currencies and refactor providers

* Update lib/exchange/provider/near_Intents_exchange_provider.dart

* add back old zcash currencies for backward compatibility, thanks Czarek

* minor fix [skip ci]

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update android/app/src/main/AndroidManifestBase.xml [skip ci]

* Update ios/Runner/InfoBase.plist [skip ci]

* fix: remove unused code fix: macos fix: disable on linux and macos

* update build numbers [skip ci]

* Add zcash to birdpay

---------

Co-authored-by: Czarek Nakamoto <cyjan@mrcyjanek.net>
Co-authored-by: Serhii <borodenko.sv@gmail.com>
This commit is contained in:
Omar Hatem
2026-01-16 03:16:41 +01:00
committed by GitHub
co-authored by Czarek Nakamoto Serhii
parent 2a77d359f1
commit ef9b0fbae6
141 changed files with 5664 additions and 284 deletions
@@ -4,7 +4,6 @@ on:
pull_request:
jobs:
# -----------------------------------------
# PATH A: Internal PRs
internal-build:
+22 -8
View File
@@ -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.
+10 -4
View File
@@ -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
done
+7 -2
View File
@@ -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
fiat-check-output.txt
scripts/zcash_lib
scripts/android/yttrium
scripts/bitbox_flutter
scripts/reown_flutter
-6
View File
@@ -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
+4 -2
View File
@@ -39,7 +39,7 @@
android:versionName="__versionName__"
android:requestLegacyExternalStorage="true"
android:extractNativeLibs="true">
<service
<service
android:name="androidx.work.impl.foreground.SystemForegroundService"
android:foregroundServiceType="dataSync"
android:exported="false" />
@@ -112,6 +112,8 @@
<data android:scheme="arbitrum" />
<data android:scheme="arbitrum-wallet" />
<data android:scheme="arbitrum_wallet" />
<data android:scheme="zcash" />
<data android:scheme="zcash-wallet" />
</intent-filter>
<!-- nano-gpt link scheme -->
<intent-filter android:autoVerify="true">
@@ -139,7 +141,7 @@
android:resource="@xml/provider_paths" />
</provider>
</application>
<queries>
<intent>
<action android:name="android.intent.action.VIEW" />
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 -960 960 960" width="24px" fill="#e3e3e3"><path d="M480-80q-139-35-229.5-159.5T160-516v-244l320-120 320 120v244q0 152-90.5 276.5T480-80Zm0-84q104-33 172-132t68-220v-189l-240-90-240 90v189q0 121 68 220t172 132Zm0-316Z"/></svg>

After

Width:  |  Height:  |  Size: 290 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.0 KiB

After

Width:  |  Height:  |  Size: 32 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="45" height="45" viewBox="0 0 45 45" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="1" y="1" width="43" height="43" rx="6" fill="#E4A50B"/>
<rect x="1" y="1" width="43" height="43" rx="6" stroke="black" stroke-width="2"/>
<path d="M30.9445 14.7287V11.1141H24.4639V7.13086H20.4807V11.1141H14V15.9084H24.0425L14 29.5436V33.1582H20.4807V37.1309H24.4639V33.1582H30.9445V28.3639H20.8915L30.9445 14.7287Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 443 B

+2
View File
@@ -1 +1,3 @@
Add Zcash wallet
Generic enhancements
Bug fixes
+12
View File
@@ -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
+2 -2
View File
@@ -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"
+11 -7
View File
@@ -62,7 +62,6 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> implemen
CryptoCurrency.hbar,
CryptoCurrency.sc,
CryptoCurrency.usdcsol,
CryptoCurrency.zaddr,
CryptoCurrency.zec,
CryptoCurrency.zen,
CryptoCurrency.xvg,
@@ -140,6 +139,10 @@ class CryptoCurrency extends EnumerableItem<int> with Serializable<int> 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<int> with Serializable<int> 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<int> with Serializable<int> 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<int, CryptoCurrency> _rawCurrencyMap =
[...all, ...havenCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {
[...all, ...havenCurrencies, ...zcashCurrencies].fold<Map<int, CryptoCurrency>>(<int, CryptoCurrency>{}, (acc, item) {
acc.addAll({item.raw: item});
return acc;
});
static final Map<String, CryptoCurrency> _nameCurrencyMap =
[...all, ...havenCurrencies].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
[...all, ...havenCurrencies, ...zcashCurrencies].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
acc.addAll({item.name: item});
return acc;
});
static final Map<String, CryptoCurrency> _fullNameCurrencyMap =
[...all, ...havenCurrencies].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
[...all, ...havenCurrencies, ...zcashCurrencies].fold<Map<String, CryptoCurrency>>(<String, CryptoCurrency>{}, (acc, item) {
if(item.fullName != null){
acc.addAll({item.fullName!.toLowerCase(): item});
}
@@ -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');
+12 -2
View File
@@ -115,16 +115,26 @@ Future<Map<String, dynamic>> dumpDb() async {
}
}
Future<List<String>> _getTableNames() async {
Future<List<String>> _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<Map<String, dynamic>> _dumpDb() async {
final tableNames = await _getTableNames();
final tableNames = await _getTableNames(db);
final ret = <String, dynamic>{};
for (final tableName in tableNames) {
ret[tableName] = await db.query(tableName);
}
return ret;
}
Future<Map<String, dynamic>> dumpCustomDb(String path) async {
final db = await openDatabase(path);
final tableNames = await _getTableNames(db);
final ret = <String, dynamic>{};
for (final tableName in tableNames) {
ret[tableName] = await db.query(tableName);
}
return ret;
}
+16
View File
@@ -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<String, Object?> 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();
+13 -3
View File
@@ -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<String> pathForWalletDir({required String name, required WalletType type}) async {
Future<String> 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<String> 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);
+4
View File
@@ -332,6 +332,7 @@ class WalletInfo {
this.hashedWalletIdentifier,
this.isNonSeedWallet,
this.sortOrder,
this.addressPageType,
) : _yatLastUsedAddressController = StreamController<String>.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<String, dynamic> 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,
);
}
+1
View File
@@ -270,6 +270,7 @@ class WalletInfo extends HiveObject {
hashedWalletIdentifier,
isNonSeedWallet,
0,
addressPageType,
);
final wiId = await walletInfo.save();
for (final address in usedAddresses ?? <String>[]) {
+14
View File
@@ -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;
}
+1 -2
View File
@@ -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
+7
View File
@@ -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
+3
View File
@@ -0,0 +1,3 @@
## 1.0.0
- Initial version.
+1
View File
@@ -0,0 +1 @@
# cw_zcash
+31
View File
@@ -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
+15
View File
@@ -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';
+41
View File
@@ -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;
}
@@ -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<bool>(false, (final a, final b) => a || (b.sendAll));
if (isAll) {
return availableBalance;
}
return credentials.outputs.fold<int>(
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<void> 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<Map<String, String>> commitUR() {
throw UnimplementedError('UR not supported for Zcash');
}
@override
bool shouldCommitUR() => false;
}
+296
View File
@@ -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,
];
}
+30
View File
@@ -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));
}
}
@@ -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.';
}
@@ -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;
}
}
@@ -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<int, List<Account>> rotationAccounts = {};
static Map<int, List<Account>> rotationAccountsUsable = {};
static Map<int, List<ShieldedTx>> shieldedAccountsTx = {};
static Future<void> 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<void> 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<String> 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<dynamic, dynamic> data = jsonDecode(utf8.decode(bytes));
rotationAccounts = (data["rotationAccounts"] as Map<String, dynamic>).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<String, dynamic>).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<String, dynamic>).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<int>.from(base64.decode(value)));
static Future<void> 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<int, List<ShieldedTx>> newShieldedAccountsTx = {};
final accounts = WarpApi.getAccountList(coin);
if (accounts.isEmpty) return;
final Map<int, String> 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<void> _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<String>? 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<String>? 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();
}
}
@@ -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<OutputInfo> outputs;
final MoneroTransactionPriority priority;
final CryptoCurrency currency;
}
@@ -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<String, String> _destinationAddressMap = {};
static String? getCachedDestinationAddress(final String txId) {
printV("$txId -> ${_destinationAddressMap.keys.join(",")}");
return _destinationAddressMap[txId] ??
_destinationAddressMap['"$txId"'] ??
_destinationAddressMap[txId.replaceAll('"', '')];
}
static Future<void> 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<void> 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");
}
}
}
+24
View File
@@ -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<TransactionInfo> {
ZcashTransactionHistory() {
transactions = ObservableMap<String, TransactionInfo>();
}
@override
void addMany(final Map<String, TransactionInfo> txs) {
transactions.addAll(txs);
}
@override
void addOne(final TransactionInfo txs) {
transactions[txs.txHash] = txs;
}
@override
Future<void> save() async {
// No need to save anything, backend is taking care of that
}
}
+976
View File
@@ -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<ZcashBalance, ZcashTransactionHistory, ZcashTransactionInfo>
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<CryptoCurrency, ZcashBalance> 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<void> changePassword(final String password) async {
// throw UnimplementedError();
}
static bool isNodeWorking = true;
@override
Future<bool> checkNodeHealth() {
return Future.value(isNodeWorking);
}
@override
Future<void> close({final bool shouldCleanup = false}) async {
_stopSyncStatusUpdates();
_stopPeriodicSync();
await ZcashWalletService.runInDbMutex(() async => WarpApi.cancelSync());
}
@override
@action
Future<void> 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<PendingTransaction> createTransaction(final Object credentials) async {
final creds = credentials as ZcashTransactionCredentials;
await updateBalance();
final zcashBalance = balance[CryptoCurrency.zec];
final availableBalance = zcashBalance?.confirmed ?? 0;
final recipients = <Recipient>[];
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<List<ShieldedTx>> getShieldTxForUi() async {
final tx = (ZcashTaddressRotation.shieldedAccountsTx[accountId] ?? <ShieldedTx>[])
.map((final v) {
final unpacked = v.unpack();
unpacked.memo ??= "";
unpacked.memo = "${unpacked.memo}\n$_dispPhrase".trim();
final List<int> buff = base64.decode(
ZcashTaddressRotation.flatBuffersPack(unpacked.pack),
);
return ShieldedTx(buff);
})
.where((final t) => t.value > 0);
return tx.toList();
}
static Map<int, List<ShieldedTx>> 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<Map<String, ZcashTransactionInfo>> 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<String, ZcashTransactionInfo> 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<void> 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<void> 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<void> 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<void> updateTransactionsHistory() => updateTransactions();
@override
Future<void> save() async {}
Future<void> 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<String> signMessage(final String message, {final String? address = null}) {
throw UnimplementedError();
}
@override
@action
Future<void> 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<void> 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<void> _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<void> _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<void> _autoShield() async {
try {
await autoShieldMutex.acquire();
await _$autoShield();
} catch (e) {
printV("shielding failed: $e");
await Future.delayed(Duration(seconds: 30));
} finally {
autoShieldMutex.release();
}
}
Future<void> _$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<void> 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<bool> verifyMessage(
final String message,
final String signature, {
final String? address = null,
}) {
throw UnimplementedError();
}
@observable
late WalletAddresses walletAddresses = ZcashWalletAddresses(accountId, walletInfo);
static Future<ZcashWallet> 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<ZcashWallet> 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<ZcashWallet> 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<int> _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<int?> 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<void> _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<String> 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<String> 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<String> 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<void> _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<int> getHeightByDate(final DateTime date) {
return WarpApi.getBlockHeightByTime(coin, date);
}
}
@@ -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<void> 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<void> 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<void> 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<void> updateAddressesInBox() async {
_transparentAddress = null;
_saplingAddress = null;
_orchardAddress = null;
_unifiedAddress = null;
}
@override
Map<String, String> get usableAddressesMap => addressesMap;
@override
Map<String, String> get usableAllAddressesMap => allAddressesMap;
List<WalletInfoAddressInfo> getAddressInfos() {
if (addressPageType != ZcashAddressType.transparentRotated) {
return [];
}
final List<WalletInfoAddressInfo> allInfos = [];
for (final entry in addressInfos.entries) {
allInfos.addAll(entry.value);
}
return allInfos;
}
}
+181
View File
@@ -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<T> runInDbMutex<T>(final Future<T> Function() call) async {
try {
printV("dbMutexQueue: ${dbMutexQueue++}");
await dbMutex.acquire();
return await call();
} finally {
dbMutex.release();
dbMutexQueue--;
}
}
static Set<String> autoshieldTx = {};
static Future<void> 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<void> 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<ZcashWallet> create(final WalletCredentials credentials, {final bool? isTestnet}) {
return ZcashWalletBase.create(credentials);
}
@override
Future<bool> isWalletExit(final String name) async {
final path = await pathForWallet(name: name, type: getType());
return File(path).existsSync();
}
@override
Future<ZcashWallet> 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<void> 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<void> 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<ZcashWallet> restoreFromKeys(
final ZcashFromKeysWalletCredentials credentials, {
final bool? isTestnet,
}) async {
throw UnimplementedError();
}
@override
Future<ZcashWallet> 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<ZcashWallet> restoreFromHardwareWallet(final ZcashNewWalletCredentials credentials) {
throw UnimplementedError(
"Restoring a Zcash wallet from a hardware wallet is not yet supported!",
);
}
}
+29
View File
@@ -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
@@ -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:
+6
View File
@@ -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
+6
View File
@@ -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 = "<group>"; };
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 = "<group>"; };
CE918BF72D533ECE007F186E /* MoneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = MoneroWallet.xcframework; sourceTree = "<group>"; };
CE918BF92D533ED4007F186E /* WowneroWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = WowneroWallet.xcframework; sourceTree = "<group>"; };
CE918BFB2D533ED8007F186E /* ZanoWallet.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; path = ZanoWallet.xcframework; sourceTree = "<group>"; };
CEFC07A12EF2EEEB0012DB29 /* WarpApiFFI.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = WarpApiFFI.xcframework; path = ../../zcash_lib/WarpApiFFI.xcframework; sourceTree = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
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 = "<group>"; };
@@ -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 */,
+20
View File
@@ -342,6 +342,26 @@
<string>arbitrum-wallet</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>zcash</string>
<key>CFBundleURLSchemes</key>
<array>
<string>zcash</string>
</array>
</dict>
<dict>
<key>CFBundleTypeRole</key>
<string>Viewer</string>
<key>CFBundleURLName</key>
<string>zcash-wallet</string>
<key>CFBundleURLSchemes</key>
<array>
<string>zcash-wallet</string>
</array>
</dict>
</array>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
+1 -1
View File
@@ -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<String, dynamic>;
final status = responseJSON['Status'] as String;
final fiatAmount = responseJSON['Fiat_Amount'] as double?;
@@ -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");
}
+9 -2
View File
@@ -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<String, dynamic>) : null,
ltc: map['LTC'] != null ? CryptoPaymentData.fromMap(map['LTC'] as Map<String, dynamic>) : null,
ltc_mweb: map['LTC_MWEB'] != null ? CryptoPaymentData.fromMap(map['LTC_MWEB'] as Map<String, dynamic>) : null,
zec: map['ZEC_CHAIN'] != null ? CryptoPaymentData.fromMap(map['ZEC_CHAIN'] as Map<String, dynamic>) : null,
invoiceTime: DateTime.fromMillisecondsSinceEpoch(map['invoice_time'] as int),
expirationTime: DateTime.fromMillisecondsSinceEpoch(map['expiration_time'] as int),
commission: map['commission'] as int?,
+17 -5
View File
@@ -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]+';
+15
View File
@@ -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;
+3
View File
@@ -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<MnemonicItem> {
@@ -52,6 +53,8 @@ class SeedValidator extends Validator<MnemonicItem> {
return zano!.getWordList(language);
case WalletType.decred:
return decred!.getDecredWordList();
case WalletType.zcash:
return zcash!.getZcashWordList(language);
case WalletType.none:
case WalletType.haven:
return [];
+1
View File
@@ -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:
+4 -1
View File
@@ -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<void> setup({
getIt.registerFactory<MoonPayProvider>(() => MoonPayProvider(
appStore: getIt.get<AppStore>(),
wallet: getIt.get<AppStore>().wallet!,
isTestEnvironment: kDebugMode,
isTestEnvironment: kDebugMode || kProfileMode,
));
getIt.registerFactory<OnRamperBuyProvider>(() => OnRamperBuyProvider(
@@ -1224,6 +1225,8 @@ Future<void> 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');
}
+20 -1
View File
@@ -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<void> defaultSettingsMigration(
{required int version,
@@ -559,7 +560,14 @@ Future<void> 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<void> 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<void> 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<void> 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<void> resetBitcoinElectrumServer(
+4 -1
View File
@@ -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();
}
+6 -1
View File
@@ -56,6 +56,9 @@ Future<List<Node>> 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<void> resetToDefault(Box<Node> 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<void> resetToDefault(Box<Node> nodeSource) async {
decredNodes +
dogecoinElectrumServerList +
baseNodes +
arbitrumNodes;
arbitrumNodes +
zcashNodes;
await nodeSource.clear();
await nodeSource.addAll(nodes);
+1 -1
View File
@@ -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)
+1
View File
@@ -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';
@@ -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<TransactionPriority> 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 [];
@@ -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<CryptoCurrency> _notSupported = [];
static final List<CryptoCurrency> _supported = [
CryptoCurrency.btc,
CryptoCurrency.eth,
@@ -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<CryptoCurrency> _notSupported = [
CryptoCurrency.zaddr,
CryptoCurrency.xhv,
];
static final apiKey =
isMoneroOnly ? secrets.changeNowMoneroApiKey : secrets.changeNowCakeWalletApiKey;
+1 -3
View File
@@ -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<ExchangePair> pairList;
ExchangeProviderDescription get description;
bool get isAvailable;
@@ -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<CryptoCurrency> _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';
@@ -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<CryptoCurrency> _notSupported = [];
LetsExchangeExchangeProvider();
static const apiKey = secrets.letsExchangeBearerToken;
static const _baseUrl = 'api.letsexchange.io';
@@ -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<CryptoCurrency> _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<dynamic>;
@@ -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<Token> 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)
@@ -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<CryptoCurrency> _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';
@@ -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<CryptoCurrency> _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:
@@ -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<CryptoCurrency> _notSupported = [];
StealthExExchangeProvider();
static final apiKey = secrets.stealthExBearerToken;
static final _additionalFeePercent = double.tryParse(secrets.stealthExAdditionalFeePercent);
@@ -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<CryptoCurrency> _notSupported = [];
SwapsXyzExchangeProvider();
static final _apiKey = secrets.swapsXyzApiKey;
static const _baseUrl = 'api-v2.swaps.xyz';
@@ -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<CryptoCurrency> _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<String, dynamic>;
if (response.statusCode == 400 || responseBody["success"] == false) {
final error = responseBody['errors'][0]['msg'] as String;
final List<dynamic> errorsList = responseBody['errors'] as List? ?? [];
final error = errorsList.isNotEmpty
? (errorsList[0]['msg'] as String?) ?? responseBody.toString()
: responseBody.toString();
ExchangeProviderLogger.logError(
provider: description,
function: 'createTrade',
@@ -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<CryptoCurrency> _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];
@@ -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<String, bool> providerStates;
@@ -48,11 +47,6 @@ class TrocadorExchangeProvider extends ExchangeProvider {
'SwapSpace',
];
static const List<CryptoCurrency> _notSupported = [
CryptoCurrency.stx,
CryptoCurrency.zaddr,
];
static final apiKey = isMoneroOnly ? secrets.trocadorMoneroApiKey : secrets.trocadorApiKey;
static const clearNetAuthority = 'api.trocador.app';
static const onionApiAuthority = clearNetAuthority;
@@ -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<CryptoCurrency> _notSupported = [];
XOSwapExchangeProvider();
static const _apiAuthority = 'exchange.exodus.io';
static const _apiPath = '/v3';
+9 -1
View File
@@ -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<void> 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<void> initializeAppConfigs({bool loadWallet = true}) async {
payjoinSessionSource: payjoinSessionSource,
anonpayInvoiceInfo: anonpayInvoiceInfo,
havenSeedStore: havenSeedStore,
initialMigrationVersion: 54,
initialMigrationVersion: 55,
);
}
@@ -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';
+1
View File
@@ -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:
@@ -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<DesktopWalletSelectionD
final zanoIcon = Image.asset('assets/images/crypto/zano.webp', height: 24, width: 24);
final decredIcon = Image.asset('assets/images/crypto/decred.webp', height: 24, width: 24);
final dogeIcon = Image.asset('assets/images/crypto/dogecoin.webp', height: 24, width: 24);
final zcashIcon = Image.asset('assets/images/crypto/zcash.webp', height: 24, width: 24);
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
Image _newWalletImage(BuildContext context) => Image.asset(
@@ -153,7 +155,7 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
});
}
Image _imageFor({required WalletType type, bool? isTestnet}) {
Widget _imageFor({required WalletType type, bool? isTestnet}) {
switch (type) {
case WalletType.bitcoin:
if (isTestnet == true) return tBitcoinIcon;
@@ -190,6 +192,8 @@ class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionD
return baseIcon;
case WalletType.arbitrum:
return arbitrumIcon;
case WalletType.zcash:
return zcashIcon;
case WalletType.none:
return nonWalletTypeIcon;
}
@@ -3,7 +3,7 @@ import 'package:flutter/material.dart';
class DropDownItemWidget extends StatelessWidget {
const DropDownItemWidget({super.key, required this.title, required this.image});
final double tileHeight = 60;
final Image image;
final Widget image;
final String title;
@override
@@ -6,6 +6,7 @@ import 'package:cake_wallet/anonpay/anonpay_donation_link_info.dart';
import 'package:cake_wallet/entities/preferences_key.dart';
import 'package:cake_wallet/src/screens/receive/anonpay_receive_page.dart';
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
import 'package:cake_wallet/zcash/zcash.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
import 'package:cake_wallet/src/widgets/gradient_background.dart';
@@ -14,6 +15,7 @@ import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/utils/share_util.dart';
import 'package:cake_wallet/view_model/dashboard/receive_option_view_model.dart';
import 'package:cake_wallet/view_model/dashboard/dashboard_view_model.dart';
import 'package:cw_core/utils/print_verbose.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
@@ -269,6 +271,10 @@ class AddressPage extends BasePage {
addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
return;
}
if (dashboardViewModel.type == WalletType.zcash) {
addressListViewModel.setAddressType(zcash!.getOptionToType(option));
return;
}
switch (option) {
case ReceivePageOption.anonPayInvoice:
@@ -313,6 +319,10 @@ class AddressPage extends BasePage {
addressListViewModel.type == WalletType.litecoin) {
addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
}
if (addressListViewModel.type == WalletType.zcash) {
printV("help me i'll kms if that wont work: ${zcash!.getZcashAddressType(option)}");
addressListViewModel.setAddressType(zcash!.getZcashAddressType(option));
}
}
});
@@ -104,12 +104,12 @@ class TransactionsPage extends StatelessWidget {
tags.add("MWEB");
}
}
return Observer(
builder: (_) => 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,
@@ -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<MenuWidget> {
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<MenuWidget> {
Image zanoIcon;
Image decredIcon;
Image dogecoinIcon;
Image zcashIcon;
@override
void initState() {
@@ -229,7 +232,7 @@ class MenuWidgetState extends State<MenuWidget> {
);
}
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<MenuWidget> {
return decredIcon;
case WalletType.dogecoin:
return dogecoinIcon;
case WalletType.zcash:
return zcashIcon;
default:
throw Exception('No icon for ${type.toString()}');
}
@@ -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<String> 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'),
),
@@ -286,7 +286,7 @@ class WalletRestoreFromSeedFormState extends State<WalletRestoreFromSeedForm> {
'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,
@@ -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,
@@ -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) {
@@ -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<BlockchainHeightWidget> {
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) {
+23 -1
View File
@@ -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;
+3 -2
View File
@@ -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<void> _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) =>
+2 -2
View File
@@ -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;
+2
View File
@@ -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:
@@ -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
@@ -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<void> 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(
@@ -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;
@@ -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 '';
}
@@ -231,6 +231,7 @@ abstract class HomeSettingsViewModelBase with Store {
case WalletType.bitcoinCash:
case WalletType.decred:
case WalletType.dogecoin:
case WalletType.zcash:
return false;
}
@@ -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;
}
@@ -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:
@@ -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;
}
@@ -97,6 +97,7 @@ abstract class NodeCreateOrEditViewModelBase with Store {
case WalletType.dogecoin:
case WalletType.zano:
case WalletType.decred:
case WalletType.zcash:
return false;
}
}
+5
View File
@@ -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;

Some files were not shown because too many files have changed in this diff Show More